Repository: Systems-Theory-in-Systems-Biology/EPI (language: python)

Issue:
Erroneous definition of bound and limit arrays in stock.py
There is a minor bug in the definition of the dataBounds, paramBounds and paramSamplingLimits for the stock example. 

For example for the limits, the code in EPIC/epi/examples/stock/stock.py currently states

    def getParamSamplingLimits(self):
        return np.array([[-10.0, 10.0] * self.paramDim])

However, we would probably like to have an array definition like

    def getParamSamplingLimits(self):
        return np.array([[-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0]])

We implicitly assume the parameter dimension to be 6 but this is similar for the other examples. 
I checked the other examples and from my understanding, this is the only example with this problem. 
It therefore should be done by simply replacing the three array definitions. 

Thanks in advance, 
Vincent
Erroneous definition of bound and limit arrays in stock.py
There is a minor bug in the definition of the dataBounds, paramBounds and paramSamplingLimits for the stock example. 

For example for the limits, the code in EPIC/epi/examples/stock/stock.py currently states

    def getParamSamplingLimits(self):
        return np.array([[-10.0, 10.0] * self.paramDim])

However, we would probably like to have an array definition like

    def getParamSamplingLimits(self):
        return np.array([[-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0], [-10.0, 10.0]])

We implicitly assume the parameter dimension to be 6 but this is similar for the other examples. 
I checked the other examples and from my understanding, this is the only example with this problem. 
It therefore should be done by simply replacing the three array definitions. 

Thanks in advance, 
Vincent
Better sampling limits for corona example
The corona example does currently not pass its test due to ODE solver troubles. 

This can be solved by replacing the parameter sampling limits in file EPIC/epi/examples/corona/corona.py with the following, new limits:

    def getParamSamplingLimits(self):
        return np.array([[-3.5, -0.5], [-1.0, 2.0], [-1.0, 2.0]])

Using these limits, the test passes with up to 50.000 samples.
Thank you in advance, 

Vincent


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d0519e5b..7dcef109 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,6 +47,11 @@ jobs:
           installer-parallel: true
       - name: Verify poetry installation
         run: poetry --version
+      - name: Install amici dependencies
+        run: |
+          sudo apt install -y swig
+          sudo apt install -y libblas-dev
+          sudo apt install -y libatlas-base-dev
       - name: Load cached venv
         id: cached-poetry-dependencies
         uses: actions/cache@v3
@@ -72,8 +77,9 @@ jobs:
         run: |
           source $VENV
           pytest -v
-      - name: Run coverage
-        run: |
-          source $VENV
-          coverage run -m pytest -v
-          coverage report
+      # TODO: coverage run creates segmentation fault on Github ci, but not locally. Debug it
+      # - name: Run coverage
+      #   run: |
+      #     source $VENV
+      #     coverage run -m pytest -v
+      #     coverage report
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 00000000..cf45e226
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,44 @@
+name: Publish to PyPI
+on:
+  push:
+    tags:
+      - '*'
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
+          cache: pip
+      - name: Install Poetry
+        uses: snok/install-poetry@v1
+        with:
+          #version: 1.2.2
+          virtualenvs-create: true
+          virtualenvs-in-project: true
+          installer-parallel: true
+      - name: Verify poetry installation
+        run: poetry --version
+      - name: Install amici dependencies
+        run: |
+          sudo apt install -y swig
+          sudo apt install -y libblas-dev
+          sudo apt install -y libatlas-base-dev
+      - name: Load cached venv
+        id: cached-poetry-dependencies
+        uses: actions/cache@v3
+        with:
+          path: .venv
+          key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
+      - name: Install dependencies
+        run: poetry install --no-interaction --with=dev
+        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
+      - name: Publish to PyPI
+        env:
+          PYPI_USERNAME: __token__
+          PYPI_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
+        run: |
+          poetry publish --build -u $PYPY_USERNAME -p $PYPI_PASSWORD
diff --git a/.gitignore b/.gitignore
index 8317a664..f3926a03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -131,6 +131,9 @@ dmypy.json
 
 # End of https://www.gitignore.io/api/python
 
-# Custom
+# Project specific
 Data
 Applications
+amici
+swig
+generated_sbml_model
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2052a194..ceb265dc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,7 +18,7 @@ repos:
         entry: pflake8
         additional_dependencies: [pyproject-flake8]
 -   repo: https://github.com/PyCQA/isort
-    rev: 5.10.1
+    rev: 5.12.0
     hooks:
     -   id: isort
         args: [--settings-path=pyproject.toml]
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..d931b3bd
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+### Added
+
+- Feature X
+- Feature Y
+
+### Changed
+
+- Refactored module A to improve performance
+
+### Fixed
+
+- Bug in module B that caused a crash
+
+## [0.1.0] - 2022-03-xx
+
+### Added
+
+- Initial release
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index d07b181b..d971e80a 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -22,6 +22,24 @@
   curl -sSL https://install.python-poetry.org | python3 -
   ```
   
+- Install dependencies:
+  
+  - For amici (sbml):
+
+    ```bash
+    sudo apt install swig
+    sudo apt install libblas-dev
+    sudo apt install libatlas-base-dev
+    ```
+  
+  - For cpp:
+
+    ```bash
+    sudo apt install cmake
+    sudo apt install libeigen3-dev
+    sudo apt install pybind11-dev
+    ```
+
 - Install epi:
 
   ```bash
@@ -40,53 +58,59 @@
 
 Here are the most important infos on how to maintain this repository.
 
-- **Dependency Management with Poetry**: \
-  We use poetry as build system, for the dependency management and the virtual environment. During the [Quickstart](#quickstart) we installed all dependencies into the virtual environment, therefore:
+### Dependency Management with Poetry
 
-  ---
-  **IMPORTANT**
+We use poetry as build system, for the dependency management and the virtual environment. During the [Quickstart](#quickstart) we installed all dependencies into the virtual environment, therefore:
 
-  Run all commands in the next section in the poetry shell. It can be started with `poetry shell`. Alternatively you can run commands with `poetry run <yourcommand>`.
+---
+**IMPORTANT**
 
-  ---
+Run all commands in the next section in the poetry shell. It can be started with `poetry shell`. Alternatively you can run commands with `poetry run <yourcommand>`.
 
-  Run ```poetry add package_name``` to add the library/package with the name ```package_name``` as dependencie to your project. Use ```poetry add --group dev package_name``` to add ```package_name``` to your ```dev``` dependencies. You can have arbitrary group names.
-  
-  For more information read the [Poetry Documentation](https://python-poetry.org/docs/basic-usage/#initialising-a-pre-existing-project).
+---
 
-- **Code quality checks**: \
-  We use black, flake8, isort to maintain a common style and check the code. Please check your code install the pre-commit hook:
+Run ```poetry add package_name``` to add the library/package with the name ```package_name``` as dependencie to your project. Use ```poetry add --group dev package_name``` to add ```package_name``` to your ```dev``` dependencies. You can have arbitrary group names.
+  
+For more information read the [Poetry Documentation](https://python-poetry.org/docs/basic-usage/#initialising-a-pre-existing-project).
 
-    ``` bash
-    pre-commit install
-    ```
+### Code quality checks
 
-    You can also check your changes manually:
+We use black, flake8, isort to maintain a common style and check the code. Please check your code install the pre-commit hook:
 
-    ``` bash
-    pre-commit run --all-files
-    ```
+  ``` bash
+  pre-commit install
+  ```
 
-- **Testing with pytest**:
+  You can also check your changes manually:
 
-  ```bash
-  pytest
+  ``` bash
+  pre-commit run --all-files
   ```
 
-  You can generate a coverage report by running the following code block in your terminal. Please be aware that it might take a long time, think about lowering the number of steps in the sampling.
+Testing with pytest
 
-  ```bash
-  coverage run -m pytest -v
-  coverage report
-  coverage html
-  ```
+```bash
+pytest
+```
 
-<!--  TODO:
-- **Profiling with scalene**:
+You can generate a coverage report by running the following code block in your terminal. Please be aware that it might take a long time, think about lowering the number of steps in the sampling.
 
-  You can profile epi with scalene (or gprofile) using the commands
+```bash
+coverage run -m pytest -v
+coverage report
+coverage html
+```
+
+### Profiling with scalene
 
-   -->
+You can profile epi with scalene (or gprofile) using the commands:
+
+```bash
+python3 -m pip install -U scalene
+scalene tests/profiling.py
+```
+
+This will create a `profile.html` file, which you can open using your browser. Do not rely on the OPENAI optimization proposals. They are often plain wrong in scalene.
 
 <!-- TODO: Add a docker development environment -->
 <!-- - **Working with docker**:
@@ -98,65 +122,86 @@ Here are the most important infos on how to maintain this repository.
   sudo service docker stop
   ``` -->
 
-- **Documentation with Sphinx**:
+### Documentation with Sphinx
 
-  ``` bash
-  cd docs
-  sphinx-apidoc -f -o source/ ../
-  make html
-  ```
+``` bash
+cd docs
+sphinx-apidoc -e -f -o source/ ../
+make html
+```
 
-  All extensions of sphinx which are used to create this documentation and further settings are stored in the file `docs/source/conf.py`.
-  If you add extensions to `conf.py` which are not part of sphinx, add them to the `docs/source/requirement.txt` file to allow github action `mmaraskar/sphinx-action@master` to still build the documentation.
+All extensions of sphinx which are used to create this documentation and further settings are stored in the file `docs/source/conf.py`.
+If you add extensions to `conf.py` which are not part of sphinx, add them to the `docs/source/requirement.txt` file to allow github action `mmaraskar/sphinx-action@master` to still build the documentation.
 
-- **Hosting with GitHub Pages**: \
-  To publish the documentation on github pages you probably have to change some settings in the [GitHub Repository](https://github.com/Systems-Theory-in-Systems-Biology/EPI)
+A [cheatsheet](https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/WritingReST/CheatSheet.html) for reStructuredText with Sphinx.
 
-  ``` text
-  Settings -> Code and automation -> Pages -> Build and Deployment:
-  - Source: Deploy from a branch
-  - Branch: gh-pages && /(root)
-  ```
+### Hosting with GitHub Pages
 
-- **Test Deployment with TestPyPi**: \
-    You have to setup testpypi once:
+To publish the documentation on github pages you probably have to change some settings in the [GitHub Repository](https://github.com/Systems-Theory-in-Systems-Biology/EPI)
 
-    ```bash
-    poetry config repositories.testpypi https://test.pypi.org/legacy/
-    poetry config http-basic.testpypi __token__ pypi-your-api-token-here
-    ```
+``` text
+Settings -> Code and automation -> Pages -> Build and Deployment:
+- Source: Deploy from a branch
+- Branch: gh-pages && /(root)
+```
 
-    Build and deploy:
+### Changelog
 
-    ```bash
-    poetry build
-    poetry publish -r testpypi
-    ```
+We use the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format for the changelog. It should be updated with every pull request.
 
-    Test this with
+### Versioning
 
-    ```bash
-    python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps epi
-    ```
+We use [Semantic Versioning](https://semver.org/). A version number is composed of three parts: major.minor.patch
 
-- **Deployment with PyPi**: \
-    You have to setup pypi once:
+1. The major version should be incremented when you make incompatible changes.
+2. The minor version should be incremented when you add new functionality in a backwards-compatible manner.
+3. The patch version should be incremented when you make backwards-compatible bug fixes.
 
-    ```bash
-    poetry config pypi-token.pypi pypi-your-token-here
-    ```
+Every time a new version is tagged, a GitHub Action workflow is trigger which builds and uploads the version to pypi.
 
-    Build and deploy:
+Please update the version number in the `pyproject.toml` file before tagging the version.
 
-    ```bash
-    poetry publish --build
-    ```
+### Test Deployment with TestPyPi
 
-    Test this with
+You have to setup testpypi once:
 
-    ```bash
-    pip install epi
-    ```
+```bash
+poetry config repositories.testpypi https://test.pypi.org/legacy/
+poetry config http-basic.testpypi __token__ pypi-your-api-token-here
+```
+
+Build and deploy:
+
+```bash
+poetry build
+poetry publish -r testpypi
+```
+
+Test this with
+
+```bash
+python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps epi
+```
+
+### Deployment with PyPi
+  
+You have to setup pypi once:
+
+```bash
+poetry config pypi-token.pypi pypi-your-token-here
+```
+
+Build and deploy:
+
+```bash
+poetry publish --build
+```
+
+Test this with
+
+```bash
+pip install epi
+```
 
 ## Jax with CUDA
 
diff --git a/README.md b/README.md
index 5cae3692..45603e52 100644
--- a/README.md
+++ b/README.md
@@ -7,37 +7,40 @@
 [![pages-build-deployment](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/pages/pages-build-deployment)
 [![Build & Publish Documentation](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/sphinx.yml/badge.svg)](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/sphinx.yml)
 [![CI](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/ci.yml/badge.svg)](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/ci.yml)
+[![pytest](https://img.shields.io/github/actions/workflow/status/Systems-Theory-in-Systems-Biology/EPI/ci.yml?label=pytest&logo=pytest)](https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/ci.yml)
 
-The Euler Parameter Inference (EPI) is a python package for inverse parameter inference.
+[![flake8](https://img.shields.io/badge/flake8-checked-blue.svg)](https://flake8.pycqa.org/)
+[![black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE.md)
+[![Python](https://img.shields.io/badge/python-3.10-purple.svg)](https://www.python.org/)
+
+Euler Parameter Inference (EPI) is a Python package for inverse parameter inference. It provides an implementation of the EPI algorithm, which takes observed data and a model as input and returns a parameter distribution consistent with the observed data by solving the inverse problem directly. In the case of a one-to-one mapping, this is the true underlying distribution.
 
 ## Documentation
 
 The full documentation to this software, including a detailed tutorial on how to use EPI and the api documentation, can be found under [Documentation](https://Systems-Theory-in-Systems-Biology.github.io/EPI/).
 
-## About
+## Features
 
-The EPI algorithm takes observed data and a model as input and returns a parameter distribution, which is consistent with the observed data by solving the inverse problem directly. In the case of a one-to-one mapping, this is the true underlying distribution.
+EPI supports
 
-We support SBML ode models and user provided models.
+- SBML ode models
+- User provided models
+- Models with automatic differentation using jax
 
 ## Installation
 
-  ---
-  **IMPORTANT**
-
-  The package is not yet available on pypi.
+**IMPORTANT**: The package is not yet available on pypi.
 
-  <!-- ```text
-  pip install epi
-  ``` -->
+<!-- ```text
+pip install epi
+``` -->
 
-  ---
-
-You can build the library from the newest source code by following the [Development Quickstart Guide](./DEVELOPMENT.md#quickstart).
+You can build the library from the latest source code by following the [Development Quickstart Guide](./DEVELOPMENT.md#quickstart).
 
 ## Using the library
 
-Derive your model from ```Model``` class and implement the abstract functions.
+To use EPI, derive your model from the `Model` class and implement the abstract functions. Here's an example code snippet:
 
 ```python
 import jax.numpy as jnp
@@ -46,18 +49,12 @@ from epi.core.model import Model
 
 class MyModel(Model):
 
-    paramDim = N # The dimension of a parameter point
-    dataDim = M # The dimension of a data point
+    param_dim = N # The dimension of a parameter point
+    data_dim = M # The dimension of a data point
 
     def forward(self, param):
         return jnp.array(...)
 
-    def getParamSamplingLimits(self):
-        return jnp.array([[-1.,1.], [-101.1, 13.4],...]) # [[UpperBound_dim1,LowerBound_dim1],...]
-
-    def getCentralParam(self):
-        return jnp.array([0.5, -30.0,...])
-
     def jacobian(self, param):
         return jnp.array(...)
 ```
@@ -69,11 +66,14 @@ from epi.sampling import inference
 
 from my_model import MyModel
 
-model = MyModel()
-inference(model=model, dataPath="my_data.csv", numRuns=1, numWalkers=10, numSteps=2500, numProcesses=4)
+central_param = np.array([0.5, -1.5, ...])
+param_limits = np.array([[0.0, 1.0], [-3.0, 0.0], ...])
+
+model = MyModel(central_param, param_limits)
+inference(model=model, data="my_data.csv")
 ```
 
-The file `my_data.csv` has to contain the data in csv format with `seperator=","` in the format
+The `data` argument can be a numpy-2d-array or a PathLike object that points to a CSV file. In the example shown above, the CSV file `my_data.csv` should contain the data in the following format:
 
 ```text
 datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
@@ -83,11 +83,12 @@ datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
 datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
 ```
 
-which corresponds to a matrix with the shape `nSamples x dataDim`.
-The parameter dataPath defaults to `Data/<ModelName>/<ModelName>Data.csv`. The other parameters `numRuns`, `numWalkers`, `numSteps`, `numProcesses` have fixed defaults. The results are written to three files:
+This corresponds to a matrix with the shape `nSamples x data_dim`. For more available options and parameters for the `inference` method, please refer to the [api documentation](https://systems-theory-in-systems-biology.github.io/EPI/epi.core.html#module-epi.core.inference). Note that the inference can be done with grid-based methods (dense grids, sparse grids) or sampling methods (mcmc).
+
+The results are stored in the following location:
 
-* `./Applications/<ModelName>/OverallParams.csv`
-* `./Applications/<ModelName>/OverallSimResults.csv`
-* `./Applications/<ModelName>/OverallDensityEvals.csv`
+* `./Applications/<ModelName>/.../OverallParams.csv`
+* `./Applications/<ModelName>/.../OverallSimResults.csv`
+* `./Applications/<ModelName>/.../OverallDensityEvals.csv`
 
-and contain the sampled parameters, the corresponding data points obtained from the model forward pass and the corresponding density evaluation.
+These files contain the sampled parameters, the corresponding data points obtained from the model forward pass, and the corresponding density evaluation.
diff --git a/TODO.md b/TODO.md
index ac415e5b..cbbe641c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,9 +1,5 @@
 # TODOs
 
-## Redesign
-
-- Change from data "file" flow to data flow?
-
 ## Pickling and jax
 
 - [ ] Document classmethod solution for pickling function, how to use fixed params
@@ -14,31 +10,13 @@
 ## Documentation
 
 - [ ] [Dependabot badge](https://github.com/dependabot/dependabot-core/issues/1912)
-- [ ] Deactivate todos in conf.py
-- [ ] Remove unfinished documentation and references to unfinished code, e.g. temperature model fixed params
 - [ ] How to run jupyternotebook with poetry in vs code and in terminal
 
 ## Deployment
 
 - [ ] Deployment to pypi
 
-## SBML
-
-- Complete sbml class using one of
-  - [ ] [SymbolicSBML](https://gitlab.com/wurssb/Modelling/symbolicsbml)
-  - [ ] [RoadRunner](https://sys-bio.github.io/roadrunner/docs-build/index.html) Could be a good option
-  - [ ] [Sbmltodepy](https://github.com/AnabelSMRuggiero/sbmltoodepy) they dont want users to make issues/ pull requests / .. :(
-
 ## Postponed
 
-- [ ] Fix [Development Quickstart Guide](./DEVELOPMENT.md#quickstart) link in sphinx
 - [ ] create single function ```Model.plot()``` that allows the user to visualize his results
 - [ ] create single function ```Model.test()``` that allows the user to test the inversion for his model on artificial data
-- [ ] Use save, load from numpy and not savetxt, loadtxt
-- [ ] Or maybe better: use hdf5 backend for intermediate results, and inference takes data array as input and returns the three arrays which are currently saved as files
-- [ ] Allow to use the library with own data flow without writing and loading all the files. Return everything we save to files at the moment. So give more control to the user and rely less on the file system and default paths.
-
-## Outlook
-
-- [ ] Testing on other systems than linux on github: Replace apt install and then also test on windows and mac machine using github test matrix.
-- [ ] More / Systematic profiling with scalene
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 353d6bfb..5e13ed03 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,5 +1,6 @@
 myst-parser>=0.18
 sphinx_rtd_theme>=1.2.0
+pydata-sphinx-theme>=0.12.0
 docutils>=0.18
 sphinx_copybutton
 # Install epi and its dependencies so sphinx can generate the docs.
diff --git a/docs/source/MarkdownLinks/changelog.rst b/docs/source/MarkdownLinks/changelog.rst
new file mode 100644
index 00000000..1bb9df33
--- /dev/null
+++ b/docs/source/MarkdownLinks/changelog.rst
@@ -0,0 +1,6 @@
+.. ------------
+.. Contributing
+.. ------------
+
+.. include:: ../../../CHANGELOG.md
+    :parser: myst_parser.sphinx_
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index bec0c950..085f9d40 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -7,7 +7,9 @@ button.copybtn {
     clear: both;
 }
 
-.toggle .header {display: inline;}
+.toggle .header {
+    display: inline;
+}
 
 .toggle .header p:after {
     content: " ▶";
@@ -17,6 +19,11 @@ button.copybtn {
     content: " ▼";
 }
 
+/* Set the background color of the sidebar */
+.bd-sidebar-primary {
+    background-color: #ececec;
+}
+
 /*Toggle from: https://stackoverflow.com/questions/2454577/sphinx-restructuredtext-show-hide-code-snippets */
 /* .. container:: toggle
 
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 6c5a20df..789e7745 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -40,18 +40,10 @@
     "sphinx.ext.todo",
     "sphinx.ext.autosectionlabel",
     "sphinx.ext.coverage",
+    "sphinx.ext.napoleon",
     "sphinx_copybutton",
 ]
 
-# TODO: Switch to a nicer docstyle like numpy or google and also maybe change the theme
-#     'sphinxcontrib.napoleon',
-#     'numpydoc',
-# ]
-# Depending on what is chosen:
-#  html_theme = "pydata_sphinx_theme"
-# poetry add pydata-sphinx-theme --group=dev
-# poetry add sphinxcontrib-napoleon --group=dev
-# poetry add numpydoc --group=dev
 
 myst_enable_extensions = [
     "tasklist",
@@ -88,6 +80,7 @@
 # a list of builtin themes.
 #
 html_theme = "sphinx_rtd_theme"
+# html_theme = "sphinx_book_theme"
 
 html_logo = "../../epi.png"
 
@@ -97,3 +90,33 @@
 html_static_path = ["_static"]
 
 todo_include_todos = 1
+
+html_theme_options = {
+    "navigation_depth": -1,
+}
+
+if html_theme == "sphinx_book_theme":
+    # Add searchbar to sidebar in book theme
+    html_sidebars = {
+        "**": [
+            "navbar-logo.html",
+            "search-field.html",
+            "sbt-sidebar-nav.html",
+        ]  # "sidebar-nav-bs"]
+    }
+    # Set book theme options
+    html_theme_options = {
+        "repository_url": "https://github.com/Systems-Theory-in-Systems-Biology/EPI",
+        "use_repository_button": True,
+        "show_navbar_depth": 1,
+        "show_toc_level": 2,
+        "use_download_button": True,
+        "home_page_in_toc": True,
+        # "use_source_button": True,
+        # "repository_branch": "gh-pages",
+        # "use_edit_page_button": True,
+    }
+
+# Configurate autodoc
+autodoc_member_order = "groupwise"
+add_module_names = False
diff --git a/docs/source/epi.core.dense_grid.rst b/docs/source/epi.core.dense_grid.rst
new file mode 100644
index 00000000..615d6055
--- /dev/null
+++ b/docs/source/epi.core.dense_grid.rst
@@ -0,0 +1,7 @@
+epi.core.dense\_grid module
+===========================
+
+.. automodule:: epi.core.dense_grid
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.inference.rst b/docs/source/epi.core.inference.rst
new file mode 100644
index 00000000..e554e1f2
--- /dev/null
+++ b/docs/source/epi.core.inference.rst
@@ -0,0 +1,7 @@
+epi.core.inference module
+=========================
+
+.. automodule:: epi.core.inference
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.kde.rst b/docs/source/epi.core.kde.rst
new file mode 100644
index 00000000..9bbe8e84
--- /dev/null
+++ b/docs/source/epi.core.kde.rst
@@ -0,0 +1,7 @@
+epi.core.kde module
+===================
+
+.. automodule:: epi.core.kde
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.model.rst b/docs/source/epi.core.model.rst
new file mode 100644
index 00000000..236bdb7e
--- /dev/null
+++ b/docs/source/epi.core.model.rst
@@ -0,0 +1,7 @@
+epi.core.model module
+=====================
+
+.. automodule:: epi.core.model
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.result_manager.rst b/docs/source/epi.core.result_manager.rst
new file mode 100644
index 00000000..a2a82997
--- /dev/null
+++ b/docs/source/epi.core.result_manager.rst
@@ -0,0 +1,7 @@
+epi.core.result\_manager module
+===============================
+
+.. automodule:: epi.core.result_manager
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.rst b/docs/source/epi.core.rst
index 65ab75f5..107b01bc 100644
--- a/docs/source/epi.core.rst
+++ b/docs/source/epi.core.rst
@@ -4,45 +4,17 @@ epi.core package
 Submodules
 ----------
 
-epi.core.functions module
--------------------------
-
-.. automodule:: epi.core.functions
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-epi.core.kde module
--------------------
-
-.. automodule:: epi.core.kde
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-epi.core.model module
----------------------
-
-.. automodule:: epi.core.model
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-epi.core.sampling module
-------------------------
-
-.. automodule:: epi.core.sampling
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-epi.core.sparsegrid module
---------------------------
-
-.. automodule:: epi.core.sparsegrid
-   :members:
-   :undoc-members:
-   :show-inheritance:
+.. toctree::
+   :maxdepth: 4
+
+   Dense Grid<epi.core.dense_grid>
+   Inference<epi.core.inference>
+   KDE<epi.core.kde>
+   Model<epi.core.model>
+   Result Manager<epi.core.result_manager>
+   Sampling<epi.core.sampling>
+   Sparse Grid<epi.core.sparsegrid>
+   Transformations<epi.core.transformations>
 
 Module contents
 ---------------
diff --git a/docs/source/epi.core.sampling.rst b/docs/source/epi.core.sampling.rst
new file mode 100644
index 00000000..37793725
--- /dev/null
+++ b/docs/source/epi.core.sampling.rst
@@ -0,0 +1,7 @@
+epi.core.sampling module
+========================
+
+.. automodule:: epi.core.sampling
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.sparsegrid.rst b/docs/source/epi.core.sparsegrid.rst
new file mode 100644
index 00000000..b5897c34
--- /dev/null
+++ b/docs/source/epi.core.sparsegrid.rst
@@ -0,0 +1,7 @@
+epi.core.sparsegrid module
+==========================
+
+.. automodule:: epi.core.sparsegrid
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.core.transformations.rst b/docs/source/epi.core.transformations.rst
new file mode 100644
index 00000000..8b17e8de
--- /dev/null
+++ b/docs/source/epi.core.transformations.rst
@@ -0,0 +1,7 @@
+epi.core.transformations module
+===============================
+
+.. automodule:: epi.core.transformations
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.jax_extension.rst b/docs/source/epi.jax_extension.rst
new file mode 100644
index 00000000..80a1ac94
--- /dev/null
+++ b/docs/source/epi.jax_extension.rst
@@ -0,0 +1,7 @@
+epi.jax\_extension module
+=========================
+
+.. automodule:: epi.jax_extension
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.plotting.marginals.rst b/docs/source/epi.plotting.marginals.rst
new file mode 100644
index 00000000..8b2ee3fd
--- /dev/null
+++ b/docs/source/epi.plotting.marginals.rst
@@ -0,0 +1,7 @@
+epi.plotting.marginals module
+=============================
+
+.. automodule:: epi.plotting.marginals
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.plotting.plots.rst b/docs/source/epi.plotting.plots.rst
new file mode 100644
index 00000000..01bf52e3
--- /dev/null
+++ b/docs/source/epi.plotting.plots.rst
@@ -0,0 +1,7 @@
+epi.plotting.plots module
+=========================
+
+.. automodule:: epi.plotting.plots
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.plotting.plotter.rst b/docs/source/epi.plotting.plotter.rst
new file mode 100644
index 00000000..c737e2c7
--- /dev/null
+++ b/docs/source/epi.plotting.plotter.rst
@@ -0,0 +1,7 @@
+epi.plotting.plotter module
+===========================
+
+.. automodule:: epi.plotting.plotter
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/epi.plotting.rst b/docs/source/epi.plotting.rst
index fda47a9d..19d1ba58 100644
--- a/docs/source/epi.plotting.rst
+++ b/docs/source/epi.plotting.rst
@@ -4,29 +4,12 @@ epi.plotting package
 Submodules
 ----------
 
-epi.plotting.marginals module
------------------------------
+.. toctree::
+   :maxdepth: 4
 
-.. automodule:: epi.plotting.marginals
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-epi.plotting.plots module
--------------------------
-
-.. automodule:: epi.plotting.plots
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-epi.plotting.plotter module
----------------------------
-
-.. automodule:: epi.plotting.plotter
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   Marginals<epi.plotting.marginals>
+   Plots<epi.plotting.plots>
+   Plotting<epi.plotting.plotter>
 
 Module contents
 ---------------
diff --git a/docs/source/epi.rst b/docs/source/epi.rst
index bc81d851..a665870e 100644
--- a/docs/source/epi.rst
+++ b/docs/source/epi.rst
@@ -7,19 +7,16 @@ Subpackages
 .. toctree::
    :maxdepth: 4
 
-   epi.core
-   epi.plotting
+   Core<epi.core>
+   Plotting<epi.plotting>
 
 Submodules
 ----------
 
-epi.jax\_extension module
--------------------------
+.. toctree::
+   :maxdepth: 4
 
-.. automodule:: epi.jax_extension
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   Jax Extension<epi.jax_extension>
 
 Module contents
 ---------------
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
index 4f9d3bf9..1f45915b 100644
--- a/docs/source/examples.rst
+++ b/docs/source/examples.rst
@@ -15,7 +15,7 @@ a sbml model; and a problem defined through external C++ code.
    External C++ Model <examples/cpp_example>
 
 .. * :ref:`1D-Temperature model from the tutorial<Temperature Model>`
-.. * :ref:`High-Dimensional Stock Data<StockData Model>`
+.. * :ref:`High-Dimensional Stock Data<stock_data Model>`
 .. * :ref:`Corona ODE Model<Corona ODE Model>`
 .. * :ref:`SBML Model<SBML Model>`
 .. * :ref:`External C++ Model<C++ Model>`
diff --git a/docs/source/examples/corona_example.rst b/docs/source/examples/corona_example.rst
index ad63071a..ceb2f5a5 100644
--- a/docs/source/examples/corona_example.rst
+++ b/docs/source/examples/corona_example.rst
@@ -1,24 +1,15 @@
 Corona ODE Model
 ----------------
-The corona ode model is contained in :file:`epi/examples/corona/corona.py`. 
-
-.. TODO::
-
-    The model implements the function :math:`y_i(q_i)=???`.
+A corona ode model is contained in :file:`epi/examples/corona/corona.py`. 
 
 Specialities
 ____________
 
 * ODE solver: To solve the ODE problem the jax based ode solver library :code:`diffrax` is used: https://github.com/patrick-kidger/diffrax.
 * Automatic Differentiation: The derivatives are calculated automatically with jax by deriving from the class :py:class:`~epi.core.model.JaxModel`,
-  which automatically calculates the :py:meth:`epi.core.model.jacobian`.
+  which automatically calculates sets :py:meth:`~epi.core.model.Model.jacobian`.
 * JIT compilation: Inheriting from :py:class:`~epi.core.model.JaxModel` also enables jit compilation / optimization for the forward and jacobian method.
   This usually results in a significant execution speedup. It also allows to run your model on the gpu.
-* vectorization of model calls using :code:`jax.vmap`:
-
-  .. code-block:: python
-
-    artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
 
 .. literalinclude:: ../../../epi/examples/corona/corona.py
   :language: python
diff --git a/docs/source/examples/cpp_example.rst b/docs/source/examples/cpp_example.rst
index 54c209e5..b8c61f31 100644
--- a/docs/source/examples/cpp_example.rst
+++ b/docs/source/examples/cpp_example.rst
@@ -9,7 +9,7 @@ Specialities
 ____________
 
 * Calling C++ Code: Calls external c++ code using pybind11
-* Performance Comparison: The file :file:`python_reference_plants.py` includes
+* Performance Comparison: The file :file:`epi/examples/cpp/python_reference_plants.py` includes
   python models implementing the same mapping. You can compare the performance of the different approaches.
 
 Preparation
@@ -25,12 +25,14 @@ ___________
 C++ Model Definition
 ____________________
 
-.. literalinclude:: ../../../epi/examples/cpp/cpp_plant.py
+.. literalinclude:: ../../../epi/examples/cpp/cpp_model.hpp
   :language: c++
 
-.. TODO::
+Wrapping the C++ Code
+_____________________
 
-    Why is pygments not parsing the c++ code?
+.. literalinclude:: ../../../epi/examples/cpp/wrapper.cpp
+  :language: c++
 
 The example code is inconsistent in the following way:
 It uses a normal array for the forward method,
@@ -39,15 +41,11 @@ for the jacobian method. This allows to show us how to write wrapper code
 for normal arrays as well as for eigen objects. On the python side exclusively
 numpy 1d/2d arrays will be used.
 
-.. TODO::
-
-    What about vectorization? Jax will likely not help with speeding up.
-
 .. note::
 
-    For more information on how to use pybind11 see:
-    PyBind11 Documentation: https://pybind11.readthedocs.io/en/stable/
-    PyBind11 Eigen Notes: https://pybind11.readthedocs.io/en/stable/advanced/cast/eigen.html
+    The C++ code is wrapped using pybind11. For more information on how to use pybind11 see:
+    * PyBind11 Documentation: https://pybind11.readthedocs.io/en/stable/
+    * PyBind11 Eigen Notes: https://pybind11.readthedocs.io/en/stable/advanced/cast/eigen.html
 
 
 Compilation
@@ -66,4 +64,4 @@ _________________
 .. literalinclude:: ../../../epi/examples/cpp/cpp_plant.py
     :language: python
 
-You can use the example model as template for your own C++ Model.
+You can use the cpp example model as template for your own C++ Model.
diff --git a/docs/source/examples/sbml_example.rst b/docs/source/examples/sbml_example.rst
index 5a88a53f..1628878d 100644
--- a/docs/source/examples/sbml_example.rst
+++ b/docs/source/examples/sbml_example.rst
@@ -1,14 +1,33 @@
-SBML Model
+SBML Models
 ----------
-The sbml model loads the biological model from an external file using the sbml standard and generates the forward and jacobian method automatically.
+The :py:class:`~epi.core.model.SBMLModel` loads a biological model from an external file written in the sbml standard format.
+It generates the forward and jacobian method automatically and derives the parameter and data dimension from the sbml model.
 
-.. TODO::
+The two example models are included in :file:`epi/examples/sbml/`.
 
-    * The sbml model is not implemented yet. We are working on it.
 
 Specialities
 ____________
-* You can visualize sbml files with https://sbml4humans.de/.
+
+* Super simple setup
+* No need to write any model code
+
+Here's a code snippet to load your own sbml model and to do the parameter inference:
+
+.. code-block:: python
+
+    from epi.core.model import SBMLModel
+    from epi.core.inference import inference
+
+    model = SBMLModel('model.xml', central_param=[1.0, 1.0], param_limits=[[0.0, 2.0], [0.0, 2.0]], param_names=['k1', 'k2'])
+    model.inference(model, 'data.csv')
+
+The attribute :py:attr:`~epi.core.model.SBMLModel.param_names` contains the names of the parameters in the sbml model, for which the inference should be performed.
+Per default it contains all parameters from the sbml model file.
+
+.. note::
+    For the SBML Standard see https://sbml.org/.
+    You can visualize sbml files with https://sbml4humans.de/.
 
 .. .. literalinclude:: ../../../epi/examples/sbml/sbml_model.py
 ..   :language: python
diff --git a/docs/source/examples/stock_example.rst b/docs/source/examples/stock_example.rst
index 1cbee54f..40ae329a 100644
--- a/docs/source/examples/stock_example.rst
+++ b/docs/source/examples/stock_example.rst
@@ -1,22 +1,16 @@
-StockData Model
----------------
-The high-dimensional stock data model is contained in :file:`epi/examples/stock/stock.py`.
-
-.. TODO::
-
-    The model implements the function :math:`y_i(q_i)`
+Stock Data Model
+----------------
+A high-dimensional stock data model is contained in :file:`epi/examples/stock/stock.py`.
 
 Specialities
 ____________
 
-* External Data Source: The model shows how to use an external data source in the workflow by overwriting the method :py:meth:`epi.core.model.Model.dataLoader`.
-* High-Dimensional: The model has a high number of dimensions: DataDim = 19, ParamDim = 6
-  * Large Number of Walkers in MCMC Sampling
-  * Visualization: The visualization can be done for each dimension separately, for two selected dimensions or using spider web plots.
-
-.. TODO::
-
-    Visualization
+* External Data Source: The model loads stock data from the web.
+* High-Dimensional: The model has a high number of dimensions: data_dim = 19, param_dim = 6. The samples emcee strongly recommended to use at least 12 walkers for this model. 
+* Automatic Differentiation: The derivatives are calculated automatically with jax by deriving from the class :py:class:`~epi.core.model.JaxModel`,
+  which automatically calculates sets :py:meth:`~epi.core.model.Model.jacobian`.
+* JIT compilation: Inheriting from :py:class:`~epi.core.model.JaxModel` also enables jit compilation / optimization for the forward and jacobian method.
+  This usually results in a significant execution speedup. It also allows to run your model on the gpu.
 
 .. literalinclude:: ../../../epi/examples/stock/stock.py
   :language: python
diff --git a/docs/source/examples/temperature_example.rst b/docs/source/examples/temperature_example.rst
index a6c51217..b5dd0dff 100644
--- a/docs/source/examples/temperature_example.rst
+++ b/docs/source/examples/temperature_example.rst
@@ -1,21 +1,32 @@
 Temperature Model
 -----------------
 The temperature model is contained in :file:`epi/examples/temperature/temperature.py`.
-The model :math:`y_i(q_i)=60 \cos(q_i)-30=s(q_i)` describes the temperature for a place on the earth :math:`y_i` by using the latitude coordinates :math:`q_i`.
-The jacobian :math:`{\frac{dy}{dq}]_i(q_i)=-30 \sin(q_i)` can be calculated analytically.
+The model :math:`y(q)=60 \cos(q)-30=s(q)` describes the temperature for a place on the earth :math:`y` by using the latitude coordinates :math:`q`.
+The jacobian :math:`{\frac{dy}{dq}}(q_i)=-60 \sin(q_i)` can be calculated analytically.
+
+.. literalinclude:: ../../../epi/examples/temperature/temperature.py
+  :language: python
+  :pyobject: Temperature
 
 Specialities
 ____________
 
-* Additional fixed parameters: The model includes fixed parameters :code:`self.lowT=30.0` and :code:`self.highT=30.0`.
+* Additional fixed parameters: The model includes fixed parameters :code:`self.low_T=30.0` and :code:`self.high_T=30.0`.
   These fixed parameters are passed to the forward function separately. You can create models with different parameters by
-  creating several model objects. However you should think about overwriting the method :py:meth:`epi.core.model.Model.getModelName()`
-  to include the fixed parameters of the model object. Else your results for different fixed parameter sets will be mixed.
-
-.. TODO::
+  creating several model objects.
 
-    Fix _forward, call, ... and maybe adapt this documentation part then
+  The best way to seperate the outputs for the parametrized models is to pass a string based on the fixed_params to the attribute :py:attr:`run_name` of the :py:func:`~epi.core.inference.inference` function.
 
 .. literalinclude:: ../../../epi/examples/temperature/temperature.py
   :language: python
-  :pyobject: Temperature
+  :pyobject: TemperatureWithFixedParams
+
+.. literalinclude:: ../../../tests/test_fixed_params.py
+  :language: python
+  :pyobject: test_fixed_params
+  :lines: 9-
+
+.. note::
+
+  The functions :py:meth:`~epi.examples.temperature.temperature.TemperatureWithFixedParams.calc_forward` is not strictly necessary.
+  However it can help to make it work with jax.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 45bb31fa..046e9712 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -8,11 +8,11 @@ Welcome to EPI's documentation!
 .. role:: python(code)
    :language: python
    :class: highlight
-
+ 
 .. include:: welcome.rst
 
 .. toctree::
-   :maxdepth: 1
+   :maxdepth: 2
    :caption: Contents:
 
    API DOC<modules>
@@ -22,6 +22,7 @@ Welcome to EPI's documentation!
    MarkdownLinks/contributing
    MarkdownLinks/license
    MarkdownLinks/citation
+   MarkdownLinks/changelog
 
 ..
    Indices and tables
diff --git a/docs/source/tutorial_material/tutorial.ipynb b/docs/source/tutorial_material/tutorial.ipynb
index f849731b..a320d5e9 100644
--- a/docs/source/tutorial_material/tutorial.ipynb
+++ b/docs/source/tutorial_material/tutorial.ipynb
@@ -92,21 +92,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "ename": "ModuleNotFoundError",
-     "evalue": "No module named 'epi'",
-     "output_type": "error",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mModuleNotFoundError\u001b[0m                       Traceback (most recent call last)",
-      "\u001b[0;32m/tmp/ipykernel_20770/3791781029.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mjax\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnumpy\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mjnp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      3\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnumpy\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mfrom\u001b[0m \u001b[0mepi\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodel\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mModel\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
-      "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'epi'"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "import importlib\n",
     "import jax.numpy as jnp\n",
@@ -124,7 +112,7 @@
     "- `jacobian`\n",
     "\n",
     "In addition it must implement the methods\n",
-    "- `getCentralParam`\n",
+    "- `getcentral_param`\n",
     "- `getParamSamplingLimits`\n",
     "This provides the sampling algorithm with sensible starting values and boundary values.\n",
     "\n",
@@ -138,27 +126,21 @@
    "outputs": [],
    "source": [
     "class Temperature(Model):\n",
-    "    def __init__(self, delete: bool = False, create: bool = True) -> None:\n",
-    "        super().__init__(delete, create)\n",
     "\n",
-    "        self.dataPath = importlib.resources.path(\n",
-    "            \"epi.examples.temperature\", \"TemperatureData.csv\"\n",
-    "        )\n",
+    "    param_dim = 1\n",
+    "    data_dim = 1\n",
+    "\n",
+    "    defaultcentral_param = np.array([np.pi / 4.0])\n",
+    "    defaultParamSamplingLimits = np.array([[0, np.pi / 2]])\n",
     "\n",
     "    def forward(self, param):\n",
-    "        lowT = -30.0\n",
-    "        highT = 30.0\n",
-    "        res = jnp.array([lowT + (highT - lowT) * jnp.cos(jnp.abs(param[0]))])\n",
+    "        low_T = -30.0\n",
+    "        high_T = 30.0\n",
+    "        res = jnp.array([low_T + (high_T - low_T) * jnp.cos(jnp.abs(param[0]))])\n",
     "        return res\n",
     "\n",
     "    def jacobian(self, param):\n",
-    "        return jnp.array([60.0 * jnp.sin(jnp.abs(param[0]))])\n",
-    "\n",
-    "    def getCentralParam(self):\n",
-    "        return np.array([np.pi / 4.0])\n",
-    "\n",
-    "    def getParamSamplingLimits(self):\n",
-    "        return np.array([[0, np.pi / 2]])"
+    "        return jnp.array([60.0 * jnp.sin(jnp.abs(param[0]))])"
    ]
   },
   {
@@ -177,10 +159,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from epi.core.sampling import inference\n",
+    "from epi.core.inference import inference\n",
     "\n",
     "model = Temperature()\n",
-    "inference(model, dataPath = \"TemperatureData.csv\")"
+    "inference(model, data = \"TemperatureData.csv\")"
    ]
   },
   {
@@ -188,7 +170,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Depending on the complexity of your model the sampling can take a long time. Due to this reason, not only the final results but also intermediate sampling results are saved. You can find them in the folder `Applications/Temperature/`. The final results are stored in the file `Applications/Temperature/OverallSimResults.csv`."
+    "Depending on the complexity of your model the sampling can take a long time. Due to this reason, not only the final results but also intermediate sampling results are saved. You can find them in the folder `Applications/Temperature/`."
    ]
   }
  ],
diff --git a/docs/source/tutorial_material/tutorial.rst b/docs/source/tutorial_material/tutorial.rst
index 7c9aba21..d460607e 100644
--- a/docs/source/tutorial_material/tutorial.rst
+++ b/docs/source/tutorial_material/tutorial.rst
@@ -90,7 +90,7 @@ Of course, you also need the imports:
     from epi.core.model import Model
 
 A model inhereting from :py:class:`~epi.core.model.Model` must implement the methods :py:meth:`~epi.core.model.Model.forward` and :py:meth:`~epi.core.model.Model.jacobian`.
-In addition it must provide the methods :py:meth:`~epi.core.model.Model.getCentralParam` and :py:meth:`~epi.core.model.Model.getParamSamplingLimits` to provide the sampling algorithm with sensible starting values and boundary values.
+In addition it must provide the methods :py:meth:`~epi.core.model.Model.getcentral_param` and :py:meth:`~epi.core.model.Model.getParamSamplingLimits` to provide the sampling algorithm with sensible starting values and boundary values.
 The jacobian is derived analytically here and implemented explicitly.
 
 .. important::
@@ -123,14 +123,15 @@ Now we can now use EPI to infer the parameter distribution from the data.
 
 .. code-block:: python
 
-    from epic.sampling import inference
+    from epi.core.inference import inference
 
     model = Temperature()
-    inference(model, dataPath = "TemperatureData.csv")
+    inference(model, data = "TemperatureData.csv")
 
 Depending on the complexity of your model the sampling can take a long time.
 Due to this reason, not only the final results but also intermediate sampling results are saved.
-You can find them in the folder :file:`Applications/Temperature/`. The final results are stored in the file :file:`Applications/Temperature/OverallSimResults.csv`.
+You can find them in the folder :file:`Applications/Temperature/`. The final results are stored in the file :file:`Applications/Temperature/<run_nam>/<slice_name>/OverallSimResults.csv`.
+The ``slice_name`` results from the optional parameter :py:attr:`slice` of the :py:func:`~epi.core.inference.inference` function.
 
 .. .. code-block:: python
 
diff --git a/docs/source/welcome.rst b/docs/source/welcome.rst
index e9eb40cb..07b25c52 100644
--- a/docs/source/welcome.rst
+++ b/docs/source/welcome.rst
@@ -1,108 +1,116 @@
 .. image:: images/epi.png
-   :width: 200pt
+   :width: 100pt
 
 -------------------------------
 EPI - Euler Parameter Inference
 -------------------------------
 
-
-The Euler Parameter Inference Codebase (EPI) is a python package for inverse parameter inference.
-The EPI algorithm takes observed data and a model as input and returns a parameter distribution, which is consistent with the observed data by solving the inverse problem directly. In the case of a one-to-one mapping, this is the true underlying distribution.
-We support SBML ode models and user provided models.
+Euler Parameter Inference (EPI) is a Python package for inverse parameter inference. It provides an implementation of the EPI algorithm, which takes observed data and a model as input and returns a parameter distribution consistent with the observed data by solving the inverse problem directly. In the case of a one-to-one mapping, this is the true underlying distribution.
 
 .. Put the badges here?
+.. image:: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/pages/pages-build-deployment/badge.svg
+    :target: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/pages/pages-build-deployment
+.. image:: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/sphinx.yml/badge.svg
+    :target: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/sphinx.yml
+.. image:: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/ci.yml/badge.svg
+    :target: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/ci.yml
+.. image:: https://img.shields.io/github/actions/workflow/status/Systems-Theory-in-Systems-Biology/EPI/ci.yml?label=pytest&logo=pytest
+    :target: https://github.com/Systems-Theory-in-Systems-Biology/EPI/actions/workflows/ci.yml
+    :alt: pytest
+
+.. image:: https://img.shields.io/badge/flake8-checked-blue.svg
+    :target: https://flake8.pycqa.org/
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+    :target: https://github.com/psf/black
+.. image:: https://img.shields.io/badge/License-MIT-yellow.svg
+    :target: ./LICENSE.md
+.. image:: https://img.shields.io/badge/python-3.10-purple.svg
+    :target: https://www.python.org/
+
+--------
+Features
+--------
+
+EPI supports
+
+* SBML ode models
+* User provided models
+* Models with automatic differentation using jax
 
 ------------
 Installation
 ------------
 
-The package is no yet available on pypi.
+**Important**: The package is no yet available on pypi.
+
 ..  and can be installed with: 
 
 .. .. code-block:: bash
    
 ..    pip install epi
 
-You can build the library from the newest source code by following the :doc:`Development Quickstart Guide </MarkdownLinks/development>`.
+You can build the library from the latest source code by following the :doc:`Development Quickstart Guide </MarkdownLinks/development>`.
 
 ------------
 How to start
 ------------
 
-| Derive your model from :py:class:`epi.core.model.Model` and implement the abstract functions :py:meth:`~epi.core.model.Model.forward` and :py:meth:`~epi.core.model.Model.jacobian`.
-| You also need to define the parameter sampling limits and the central parameter. This is done by implementing the functions :py:meth:`~epi.core.model.Model.getParamSamplingLimits` and :py:meth:`~epi.core.model.Model.getCentralParam`.
-| The last requirement is to define the data and parameter Dimension, dataDim and paramDim.
+.. To use EPI, derive your model from :py:class:`epi.core.model.Model` and implement the abstract functions :py:meth:`~epi.core.model.Model.forward` and :py:meth:`~epi.core.model.Model.jacobian`. You also need to define the data and parameter dimension, :py:attr:`~epi.core.model.Model.data_dim` and :py:attr:`~epi.core.model.Model.param_dim` of your model.
+To use EPI, derive your model from the Model class and implement the abstract functions. Here's an example code snippet:
 
 .. code-block:: python
-   
-   # my_model.py
-
-   import jax.numpy as jnp
-   from epi.core.model import Model
+    
+    # my_model.py
+    import jax.numpy as jnp
 
-   class MyModel(Model):
+    from epi.core.model import Model
 
-      paramDim = N # The dimension of a parameter point
-      dataDim = M # The dimension of a data point
+    class MyModel(Model):
 
-      def forward(self, param):
-         return jnp.array(...)
+        param_dim = N # The dimension of a parameter point
+        data_dim = M # The dimension of a data point
 
-      def getParamSamplingLimits(self):
-         return jnp.array([[-1.,1.], [-101.1, 13.4],...]) # [[UpperBound_dim1,LowerBound_dim1],...]
+        def forward(self, param):
+            return jnp.array(...)
 
-      def getCentralParam(self):
-         return jnp.array([0.5, -30.0,...])
-
-      def jacobian(self, param):
-         return jnp.array(...)
+        def jacobian(self, param):
+            return jnp.array(...)
 
 To evaluate the model and infer the parameter distribution, call:
 
 .. code-block:: python
 
-   from epi.core.sampling import inference
-   from my_model import MyModel
-
-   model = MyModel()
-   inference(model=model, dataPath="my_data.csv")
+    from epi.core.inference import inference
 
-The file :file:`my_data.csv` has to contain the data in csv format with :code:`seperator=,` in the format
-
-.. code-block:: text
-   
-   # my_data.csv
+    from my_model import MyModel
 
-   datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
-   datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
-   datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
-   ...
-   datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
+    central_param = np.array([0.5, -1.5, ...])
+    param_limits = np.array([[0.0, 1.0], [-3.0, 0.0], ...])
 
-which corresponds to a matrix with the shape `nSamples x dataDim`.
-The parameter dataPath defaults to `Data/<ModelName>/<ModelName>Data.csv`. The other parameters `numRuns`, `numWalkers`, `numSteps`, `numProcesses` have fixed defaults. The results are written to three files:
+    model = MyModel(central_param, param_limits)
+    inference(model=model, data="my_data.csv")
 
-* `./Applications/<ModelName>/OverallParams.csv`
-* `./Applications/<ModelName>/OverallSimResults.csv`
-* `./Applications/<ModelName>/OverallDensityEvals.csv`
+The parameter :py:attr:`data` can be a numpy-2d-array or a PathLike object that points to a CSV file. In the example shown above, the CSV file :file:`my_data.csv` should contain the data in the following format:
 
-and contain the sampled parameters, the corresponding data points obtained from the model forward pass and the corresponding density evaluation.
+.. code-block:: text
 
-.. note::
-   
-   Please read the documentation for our :doc:`Examples </examples>`.
+    datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
+    datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
+    datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
+    ...
+    datapoint_dim1, datapoint_dim2, datapoint_dim3, ..., datapoint_dimN
 
-.. TODO: move this ?
+This corresponds to a matrix with the shape :py:attr:`nSamples` x :py:attr:`data_dim`. For more available options and parameters for the :py:mod:`~epi.core.inference` method, please refer to the API documentation.
+Note that the inference can be done with grid-based methods (dense grids, sparse grids) or sampling methods (mcmc).
 
-.. You can also derive your model from
+The results are stored in the following locations
 
-.. * :py:class:`~epi.core.model.JaxModel`: The jacobian of your forward method is automatically calculated. Use jax.numpy instead of numpy for the forward method implementation!
-.. * :py:class:`~epi.core.model.SBMLModel`: The complete model is derived from the given sbml file. You don't need to define the Model manually.
+* :file:`./Applications/<ModelName>/.../OverallParams.csv`
+* :file:`./Applications/<ModelName>/.../OverallSimResults.csv`
+* :file:`./Applications/<ModelName>/.../OverallDensityEvals.csv`
 
-.. Optionally you can also inherit, and implement the abstract functions from
+These files contain the sampled parameters, the corresponding data points obtained from the model forward pass, and the corresponding density evaluation.
 
-.. * :py:class:`~epi.core.model.ArtificialModelInterface`: This allows you to check if the inversion algorithm is working for your model using the function :py:meth:`~epi.core.model.Model.test`.
+.. note::
    
-.. * :py:class:`~epi.core.model.VisualizationModelInterface`: This allows you to plot the results of the data inference using the function :py:meth:`~epi.core.model.Model.plot`.
-
-.. .. warning:: TODO: The functions plot and test may not exist yet!!!
+   Please read the documentation for our :doc:`Examples </examples>`.
diff --git a/epi/core/dense_grid.py b/epi/core/dense_grid.py
new file mode 100644
index 00000000..db0df59a
--- /dev/null
+++ b/epi/core/dense_grid.py
@@ -0,0 +1,213 @@
+from enum import Enum
+from multiprocessing import Pool
+from typing import Union
+
+import numpy as np
+from numpy.polynomial.chebyshev import chebpts1
+
+from epi.core.model import Model
+from epi.core.result_manager import ResultManager
+from epi.core.sampling import calc_kernel_width
+from epi.core.transformations import evaluate_density
+
+
+class DenseGridType(Enum):
+    """The type of grid to be used."""
+
+    EQUIDISTANT = 0  #: The equidistant grid has the same distance between two grid points in each dimension.
+    CHEBYSHEV = 1  #: The Chebyshev grid is a tensor product of Chebyshev polynomial roots. They are optimal for polynomial interpolation and quadrature.
+
+
+def generate_chebyshev_grid(
+    num_grid_points: np.ndarray, limits: np.ndarray, flatten=False
+) -> Union[np.ndarray, list[np.ndarray]]:
+    """Generate a grid with the given number of grid points for each dimension.
+
+    Args:
+        num_grid_points(np.ndarray): The number of grid points for each dimension.
+        limits(np.ndarray): The limits for each dimension.
+        flatten(bool): If True, the grid is returned as a flatten array. If False, the grid is returned as a list of arrays, one for each dimension. (Default value = False)
+
+    Returns:
+        np.ndarray: The grid containing the grid points.
+
+    """
+    ndim = num_grid_points.size
+    axes = [
+        chebpts1(num_grid_points[i]) * (limits[i][1] - limits[i][0]) / 2
+        + (limits[i][1] + limits[i][0]) / 2
+        for i in range(ndim)
+    ]
+    mesh = np.meshgrid(*axes, indexing="ij")
+    if flatten:
+        return np.array(mesh).reshape(ndim, -1).T
+    else:
+        return mesh
+
+
+def generate_regular_grid(
+    num_grid_points: np.ndarray, limits: np.ndarray, flatten=False
+) -> Union[np.ndarray, list[np.ndarray]]:
+    """Generate a grid with the given number of grid points for each dimension.
+
+    Args:
+        num_grid_points(np.ndarray): The number of grid points for each dimension.
+        limits(np.ndarray): The limits for each dimension.
+        flatten(bool): If True, the grid is returned as a flatten array. If False, the grid is returned as a list of arrays, one for each dimension. (Default value = False)
+
+    Returns:
+        np.ndarray: The grid containing the grid points.
+
+    """
+    ndim = num_grid_points.size
+    axes = [
+        np.linspace(limits[i][0], limits[i][1], num=num_grid_points[i])
+        for i in range(ndim)
+    ]
+    mesh = np.meshgrid(*axes, indexing="ij")
+    if flatten:
+        return np.array(mesh).reshape(ndim, -1).T
+    else:
+        return mesh
+
+
+def run_dense_grid_evaluation(
+    model: Model,
+    data: np.ndarray,
+    slice: np.ndarray,
+    result_manager: ResultManager,
+    num_grid_points: np.ndarray,
+    dense_grid_type: DenseGridType,
+    num_processes: int,
+    load_balancing_safety_faktor: int,
+) -> None:
+    """This function runs a dense grid evaluation for the given model and data.
+
+    Args:
+        model(Model): The model for which the evaluation should be performed.
+        data(np.ndarray): The data for which the evaluation should be performed.
+        slice(np.ndarray): The slice for which the evaluation should be performed.
+        result_manager(ResultManager): The result manager that should be used to save the results.
+        num_grid_points(np.ndarray): The number of grid points for each dimension.
+        dense_grid_type(DenseGridType): The type of grid that should be used. (Default value = DenseGridType.EQUIDISTANT)
+        num_processes(int): The number of processes that should be used for the evaluation. (Default value = NUM_PROCESSES)
+        load_balancing_safety_faktor(int): Split the grid into num_processes * load_balancing_safety_faktor chunks.
+            This will ensure that each process can be loaded with a similar amount of work if the run time difference between the evaluations
+            does not exceed the load_balancing_safety_faktor. (Default value = 4)
+
+    Raises:
+        ValueError: If the dimension of the numbers of grid points does not match the number of parameters in the slice.
+        ValueError: If the grid type is unknown.
+
+    """
+
+    if slice.shape[0] != num_grid_points.shape[0]:
+        raise ValueError(
+            f"The dimension of the numbers of grid points {num_grid_points} does not match the number of parameters in the slice {slice}"
+        )
+    limits = model.param_limits
+    data_stdevs = calc_kernel_width(data)
+
+    if dense_grid_type == DenseGridType.CHEBYSHEV:
+        grid = generate_chebyshev_grid(num_grid_points, limits, flatten=True)
+    elif dense_grid_type == DenseGridType.EQUIDISTANT:
+        grid = generate_regular_grid(num_grid_points, limits, flatten=True)
+    else:
+        raise ValueError(f"Unknown grid type: {dense_grid_type}")
+
+    # Split the grid into chunks that can be evaluated by each process
+    grid_chunks = np.array_split(
+        grid, num_processes * load_balancing_safety_faktor
+    )
+    # Calc cumsum for indexing
+    grid_chunks_cumsum = np.cumsum(
+        [0] + [grid_chunk.shape[0] for grid_chunk in grid_chunks]
+    )
+    # Define a function which evaluates the density for a given grid chunk
+    global evaluate_on_grid_chunk  # Needed to make this function pickleable
+
+    def evaluate_on_grid_chunk(args):
+        grid_chunk, model, data, data_stdevs, slice = args
+        # Init the result array
+        evaluation_results = np.zeros(
+            (grid_chunk.shape[0], data.shape[1] + slice.shape[0] + 1)
+        )
+        # Evaluate the grid points
+        for i, gridPoint in enumerate(grid_chunk):
+            density, param_sim_res_density = evaluate_density(
+                gridPoint, model, data, data_stdevs, slice
+            )
+            evaluation_results[i] = param_sim_res_density
+        return evaluation_results
+
+    pool = Pool(processes=num_processes)
+    results = np.zeros((grid.shape[0], data.shape[1] + slice.shape[0] + 1))
+    for i, result in enumerate(
+        pool.imap(
+            evaluate_on_grid_chunk,
+            [
+                (grid_chunks[i], model, data, data_stdevs, slice)
+                for i in range(len(grid_chunks))
+            ],
+        )
+    ):
+        results[grid_chunks_cumsum[i] : grid_chunks_cumsum[i + 1]] = result
+
+    pool.close()
+    pool.join()
+
+    result_manager.save_overall(
+        slice,
+        results[:, 0 : data.shape[1]],
+        results[:, data.shape[1] : data.shape[1] + slice.shape[0]],
+        results[:, data.shape[1] + slice.shape[0] :],
+    )
+
+
+def inference_dense_grid(
+    model: Model,
+    data: np.ndarray,
+    result_manager: ResultManager,
+    slices: list[np.ndarray],
+    num_processes: int,
+    num_grid_points: Union[int, list[np.ndarray]] = 10,
+    dense_grid_type: DenseGridType = DenseGridType.EQUIDISTANT,
+    load_balancing_safety_faktor: int = 4,
+) -> None:
+    """This function runs a dense grid evaluation for the given model and data. The grid points are distributed evenly over the parameter space.
+
+    Args:
+        model (Model): The model describing the mapping from parameters to data.
+        data (np.ndarray): The data to be used for the inference.
+        result_manager (ResultManager): The result manager to be used for the inference.
+        slices (np.ndarray): A list of slices to be used for the inference.
+        num_processes (int): The number of processes to be used for the inference.
+        num_grid_points (Union[int, list[np.ndarray]], optional): The number of grid points to be used for each parameter. If an int is given, it is assumed to be the same for all parameters. Defaults to 10.
+        load_balancing_safety_faktor (int, optional): Split the grid into num_processes * load_balancing_safety_faktor chunks. Defaults to 4.
+
+    Raises:
+        TypeError: If the num_grid_points argument has the wrong type.
+    """
+
+    # If the number of grid points is given as an int, we construct a list of arrays with the same number of grid points for each parameter in the slice
+    if isinstance(num_grid_points, int):
+        num_grid_points = [
+            np.full(len(slice), num_grid_points) for slice in slices
+        ]
+    elif isinstance(num_grid_points, list[np.ndarray]):
+        pass
+    else:
+        raise TypeError(
+            f"The num_grid_points argument has to be either an int or a list of arrays. The passed argument was of type {type(num_grid_points)}"
+        )
+    for slice, n_points in zip(slices, num_grid_points):
+        run_dense_grid_evaluation(
+            model=model,
+            data=data,
+            slice=slice,
+            result_manager=result_manager,
+            num_grid_points=n_points,
+            dense_grid_type=dense_grid_type,
+            num_processes=num_processes,
+            load_balancing_safety_faktor=load_balancing_safety_faktor,
+        )
diff --git a/epi/core/functions.py b/epi/core/functions.py
deleted file mode 100644
index 4b3aa720..00000000
--- a/epi/core/functions.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from typing import Tuple
-
-import jax.numpy as jnp
-import numpy as np
-from jax import jit
-
-from epi import logger
-from epi.core.kde import evalKDEGauss
-from epi.core.model import Model
-
-
-def evalLogTransformedDensity(
-    param: np.ndarray, model: Model, data: np.ndarray, dataStdevs: np.ndarray
-) -> Tuple[np.double, np.ndarray]:
-    """Given a simulation model, its derivative and corresponding data, evaluate the natural log of the parameter density that is the backtransformed data distribution.
-        This function is intended to be used with the emcee sampler and can be implemented more efficiently at some points.
-
-    Input: param (parameter for which the transformed density shall be evaluated)
-           model
-           data (data for the model: 2D array with shape (#numDataPoints, #dataDim))
-           dataStdevs (array of suitable kernel standard deviations for each data dimension)
-    Output: logTransformedDensity (natural log of parameter density at the point param)
-          : allRes (array concatenation of parameters, simulation results and evaluated density, stored as "blob" by the emcee sampler)
-    """
-    limits = model.getParamSamplingLimits()
-
-    # Check if the tried parameter is within the just-defined bounds and return the lowest possible log density if not.
-    if np.any((param < limits[:, 0]) | (param > limits[:, 1])):
-        logger.info(
-            "Parameters outside of predefined range"
-        )  # Slows down the sampling to much? -> Change logger level to warning or even error
-        return -np.inf, np.zeros(model.paramDim + model.dataDim + 1)
-
-    # If the parameter is within the valid ranges...
-    else:
-        # Evaluating the model and the jacobian for the specified parameter simultaneously provide a little speedup over calculating it separately in some cases.
-        simRes, jac = model.valjac(param)
-
-        # Evaluate the data density in the simulation result.
-        densityEvaluation = evalKDEGauss(data, simRes, dataStdevs)
-
-        # Calculate the simulation model's pseudo-determinant in the parameter point (also called the correction factor).
-        correction = calcGramDeterminant(jac)
-
-        # Multiply data density and correction factor.
-        trafoDensityEvaluation = densityEvaluation * correction
-
-        # Use the log of the transformed density because emcee requires this.
-        logTransformedDensity = np.log(trafoDensityEvaluation)
-
-        # Store the current parameter, its simulation result as well as its density in a large vector that is stored separately by emcee.
-        allRes = np.concatenate(
-            (param, simRes, np.array([trafoDensityEvaluation]))
-        )
-
-        return logTransformedDensity, allRes
-
-
-def calcGramDeterminant(jac: jnp.ndarray) -> jnp.double:
-    """Evaluate the pseudo-determinant of the jacobian (that serves as a correction term) in one specific parameter point.
-    Returns 0 if the correction factor is not finite.
-
-    :param jac: The jacobian for which the pseudo determinant shall be calculated
-    :type jac: jnp.ndarray
-    """
-    correction = _calcGramDeterminant(jac)
-    # If the correction factor is not finite, return 0 instead to not affect the sampling.
-    if not jnp.isfinite(correction):
-        correction = 0.0
-        logger.warning("Invalid value encountered for correction factor")
-    return correction
-
-
-@jit
-def _calcGramDeterminant(jac: jnp.ndarray) -> jnp.double:
-    """Jitted calculation of the pseudo-determinant of the jacobian. This function is called by calcGramDeterminant() and should not be called directly.
-    It does not check if the correction factor is finite.
-
-    Not much faster than a similar numpy version. However it can run on gpu and is maybe a bit faster because we can jit compile the sequence of operations.
-    """
-
-    jac = jnp.atleast_2d(jac)
-
-    if jac.shape[0] == jac.shape[1]:
-        return jnp.abs(jnp.linalg.det(jac))
-    else:
-        jacT = jnp.transpose(jac)
-        # The pseudo-determinant is calculated as the square root of the determinant of the matrix-product of the Jacobian and its transpose.
-        # For numerical reasons, one can regularize the matrix product by adding a diagonal matrix of ones before calculating the determinant.
-        # correction = np.sqrt(np.linalg.det(np.matmul(jacT,jac) + np.eye(param.shape[0])))
-        correction = jnp.sqrt(jnp.linalg.det(jnp.matmul(jacT, jac)))
-        return correction
diff --git a/epi/core/inference.py b/epi/core/inference.py
new file mode 100644
index 00000000..f7d2fd9b
--- /dev/null
+++ b/epi/core/inference.py
@@ -0,0 +1,102 @@
+import os
+import pathlib
+from enum import Enum
+from typing import Optional, Union
+
+import jax.numpy as jnp
+import numpy as np
+
+from epi.core.dense_grid import inference_dense_grid
+from epi.core.model import Model
+from epi.core.result_manager import ResultManager
+from epi.core.sampling import inference_mcmc
+from epi.core.sparsegrid import inference_sparse_grid
+
+
+# Define an enum for the inference types: DenseGrid, MCMC
+class InferenceType(Enum):
+    """The type of inference to be used."""
+
+    DENSE_GRID = 0  #: The dense grid inference uses a dense grid to evaluate the joint distribution.
+    MCMC = 1  #: The MCMC inference uses a Markov Chain Monte Carlo sampler to sample from the joint distribution.
+    SPARSE_GRID = 2  #: The sparse grid inference uses a sparse grid to evaluate the joint distribution. It is not tested and not recommended.
+
+
+def inference(
+    model: Model,
+    data: Union[str, os.PathLike, np.ndarray],
+    inference_type: InferenceType = InferenceType.MCMC,
+    slices: Optional[list[np.ndarray]] = None,
+    num_processes: int = 4,
+    run_name: str = "default_run",
+    result_manager: ResultManager = None,
+    continue_sampling: bool = False,
+    **kwargs,
+) -> None:
+    """Starts the parameter inference for the given model. If a data path is given, it is used to load the data for the model. Else, the default data path of the model is used.
+
+    Args:
+        model(Model): The model describing the mapping from parameters to data.
+        data(Union[str, os.PathLike, np.ndarray]): The data to be used for the inference. If a string is given, it is assumed to be a path to a file containing the data.
+        inference_type(InferenceType, optional): The type of inference to be used. (Default value = InferenceType.MCMC)
+        slices(list[np.ndarray], optional): A list of slices to be used for the inference. If None, the full joint distribution is computed. (Default value = None)
+        num_processes(int, optional): The number of processes to be used for the inference. (Default value = 4)
+        run_name(str): The name of the run. (Default value = "default_run")
+        result_manager(ResultManager, optional): The result manager to be used for the inference. If None, a new result manager is created with default paths and saving methods. (Default value = None)
+        continue_sampling(bool, optional): If True, the inference will continue sampling from the last saved point. (Default value = False)
+        **kwargs: Additional keyword arguments to be passed to the inference function. The possible parameters depend on the inference type.
+
+    Returns:
+
+    """
+
+    # Load data from file if necessary
+    if isinstance(data, (str, os.PathLike, pathlib.Path)):
+        data = np.loadtxt(data, delimiter=",", ndmin=2)
+    elif not isinstance(data, (np.ndarray, jnp.ndarray)):
+        raise TypeError(
+            f"The data argument must be a path to a file or a numpy array. The argument passed was of type {type(data)}."
+        )
+
+    slices = slices or [
+        np.arange(model.param_dim)
+    ]  # If no slice is given, compute full joint distribution, i.e. a slice with all parameters
+    result_manager = result_manager or ResultManager(
+        model.name, run_name
+    )  # If no result_manager is given, create one with default paths
+
+    if not continue_sampling:
+        result_manager.delete_application_folder_structure(model, slices)
+    result_manager.create_application_folder_structure(model, slices)
+
+    if inference_type == InferenceType.DENSE_GRID:
+        inference_dense_grid(
+            model=model,
+            data=data,
+            result_manager=result_manager,
+            slices=slices,
+            num_processes=num_processes,
+            **kwargs,
+        )
+    elif inference_type == InferenceType.MCMC:
+        inference_mcmc(
+            model=model,
+            data=data,
+            result_manager=result_manager,
+            slices=slices,
+            num_processes=num_processes,
+            **kwargs,
+        )
+    elif inference_type == InferenceType.SPARSE_GRID:
+        inference_sparse_grid(
+            model=model,
+            data=data,
+            result_manager=result_manager,
+            slices=slices,
+            num_processes=num_processes,
+            **kwargs,
+        )
+    else:
+        raise NotImplementedError(
+            f"The inference type {inference_type} is not implemented yet."
+        )
diff --git a/epi/core/kde.py b/epi/core/kde.py
index c30dab39..dc0bd89a 100644
--- a/epi/core/kde.py
+++ b/epi/core/kde.py
@@ -4,39 +4,41 @@
 .. _KDE: https://en.wikipedia.org/wiki/Kernel_density_estimation
 """
 
+import typing
+
 import jax.numpy as jnp
 from jax import jit
 from jax.scipy.stats import cauchy, norm
 
 
 @jit
-def evalKDECauchy(
-    data: jnp.ndarray, simRes: jnp.ndarray, scales: jnp.ndarray
-) -> jnp.double:
+def eval_kde_cauchy(
+    data: jnp.ndarray, sim_res: jnp.ndarray, scales: jnp.ndarray
+) -> typing.Union[jnp.double, jnp.ndarray]:
     r"""Evaluates a Cauchy Kernel Density estimator in one or several simulation results.
         Assumes that each data point is a potentially high-dimensional sample
         from a joint data distribution.
         This is for example given for time-series data, where each evaluation
         time is one dimension of the data point.
-        In the following formula x are the evaluation points (simRes) and y is the data.
+        In the following formula x are the evaluation points (sim_res) and y is the data.
 
         .. math::
             density_{i} = \frac{1}{samples} \sum_{s=1}^{samples} \prod_{d=1}^{dims} \frac{1}{(\frac{x_{i,d} - y_{s,d}}{scales_d})^2 \; \pi \; scales_d}
 
-    :param data: data for the model: 2D array with shape (#Samples, #MeasurementDimensions)
-    :type data: jnp.ndarray
-    :param simRes: evaluation coordinates array of shape (#nEvals, #MeasurementDimensions) or (#MeasurementDimensions,)
-    :type simRes: jnp.ndarray
-    :param scales: one scale for each dimension
-    :type scales: jnp.ndarray
-    :return: estimated kernel density evaluated at the simulation result(s), shape: (#nEvals,) or ()
-    :rtype: jnp.double
+    Args:
+      data(jnp.ndarray): data for the model: 2D array with shape (#Samples, #MeasurementDimensions)
+      sim_res(jnp.ndarray): evaluation coordinates array of shape (#nEvals, #MeasurementDimensions) or (#MeasurementDimensions,)
+      scales(jnp.ndarray): one scale for each dimension
+
+    Returns:
+        typing.Union[jnp.double, jnp.ndarray]: estimated kernel density evaluated at the simulation result(s), shape: (#nEvals,) or ()
+
     """
 
     return (
         jnp.sum(
             jnp.prod(
-                cauchy.pdf(simRes[..., jnp.newaxis, :], data, scales),
+                cauchy.pdf(sim_res[..., jnp.newaxis, :], data, scales),
                 axis=-1,  # prod over #measurementDimensions
             ),
             axis=-1,  # sum over sampleDim
@@ -46,28 +48,27 @@ def evalKDECauchy(
 
 
 @jit
-def evalKDEGauss(
-    data: jnp.ndarray, simRes: jnp.ndarray, scales: jnp.ndarray
-) -> jnp.double:
+def eval_kde_gauss(
+    data: jnp.ndarray, sim_res: jnp.ndarray, scales: jnp.ndarray
+) -> typing.Union[jnp.double, jnp.ndarray]:
     """Evaluates a Gaussian Kernel Density estimator in one or severalsimulation result.
     Assumes that each data point is a potentially high-dimensional sample from a joint data distribution.
     This is for example given for time-series data, where each evaluation time is one dimension of the data point.
     While it is possible to define different standard deviations for different measurement dimensions, it is so far not possible to define covariances.
 
-    :param data: data for the model: 2D array with shape (#Samples, #MeasurementDimensions)
-    :type data: jnp.ndarray
-    :param simRes: evaluation coordinates array of shape (#nEvals, #MeasurementDimensions) or (#MeasurementDimensions,)
-    :type simRes: jnp.ndarray
-    :param scales: one scale for each dimension
-    :type scales: jnp.ndarray
-    :return: estimated kernel density evaluated at the simulation result(s), shape: (#nEvals,) or ()
-    :rtype: jnp.double
+    Args:
+        data(jnp.ndarray): data for the model: 2D array with shape (#Samples, #MeasurementDimensions)
+        sim_res(jnp.ndarray): evaluation coordinates array of shape (#nEvals, #MeasurementDimensions) or (#MeasurementDimensions,)
+        scales(jnp.ndarray): one scale for each dimension
+
+    Returns:
+        typing.Union[jnp.double, jnp.ndarray]: estimated kernel density evaluated at the simulation result(s), shape: (#nEvals,) or ()
     """
 
     return (
         jnp.sum(
             jnp.prod(
-                norm.pdf(simRes[..., jnp.newaxis, :], data, scales),
+                norm.pdf(sim_res[..., jnp.newaxis, :], data, scales),
                 axis=-1,  # prod over #measurementDimensions
             ),
             axis=-1,  # sum over sampleDim
@@ -77,17 +78,20 @@ def evalKDEGauss(
 
 
 @jit
-def calcKernelWidth(data: jnp.ndarray) -> jnp.double:
+def calc_kernel_width(data: jnp.ndarray) -> jnp.ndarray:
     """Sets the width of the kernels used for density estimation of the data according to the Silverman rule
 
-    Input: data: 2d array with shape (#Samples, #MeasurementDimensions): data for the model
+    Args:
+        data(jnp.ndarray): data for the model: 2D array with shape (#Samples, #MeasurementDimensions)
+
+    Returns:
+        jnp.ndarray: kernel width for each data dimension, shape: (#MeasurementDimensions,)
 
-    Output: stdevs: array with shape (#MeasurementDimensions): suitable kernel standard deviations for each measurement dimension
     """
-    numDataPoints, dataDim = data.shape
+    num_data_points, data_dim = data.shape
     stdevs = jnp.std(data, axis=0, ddof=1)
 
     # Silvermans rule
-    return stdevs * (numDataPoints * (dataDim + 2) / 4.0) ** (
-        -1.0 / (dataDim + 4)
+    return stdevs * (num_data_points * (data_dim + 2) / 4.0) ** (
+        -1.0 / (data_dim + 4)
     )
diff --git a/epi/core/model.py b/epi/core/model.py
index 3e678620..7a60a177 100644
--- a/epi/core/model.py
+++ b/epi/core/model.py
@@ -1,34 +1,39 @@
 import inspect
 import os
-import shutil
 from abc import ABC, abstractmethod
 from functools import partial
-from typing import Tuple
+from typing import Optional, Tuple, Union
 
+import jax.numpy as jnp
 import numpy as np
-import seedir
-from jax import jacrev, jit
-from seedir import FakeDir, FakeFile
+from jax import jacrev, jit, vmap
 
-from epi import logger
-from epi.core.kde import calcKernelWidth
+import amici
 from epi.jax_extension import value_and_jacrev
 
 
 class Model(ABC):
     """The base class for all models using the EPI algorithm.
 
-    It contains three abstract methods which need to be implemented by subclasses
+    Args:
+        central_param(np.ndarray): The central parameter for the model. (Default value = None)
+        param_limits(np.ndarray): The limits for the parameters. The limits are given as a 2D array with shape (param_dim, 2). (Default value = None)
+        name(str): The name of the model. The class name is used if no name is given. (Default value = None)
     """
 
-    paramDim = None
-    dataDim = None
+    param_dim: Optional[
+        np.ndarray
+    ] = None  #: The dimension of the parameter space of the model. It must be defined in the subclass.
+    data_dim: Optional[
+        np.ndarray
+    ] = None  #: The dimension of the data space of the model. It must be defined in the subclass.
 
     def __init_subclass__(cls, **kwargs):
+        """Check if the required attributes are set."""
         if not inspect.isabstract(cls):
             for required in (
-                "paramDim",
-                "dataDim",
+                "param_dim",
+                "data_dim",
             ):
                 if not getattr(cls, required):
                     raise AttributeError(
@@ -36,333 +41,137 @@ def __init_subclass__(cls, **kwargs):
                     )
         return cls
 
-    def __init__(self, delete: bool = False, create: bool = True) -> None:
-        if delete:
-            self.deleteApplicationFolderStructure()
-            self.createApplicationFolderStructure()
-        elif create:
-            self.createApplicationFolderStructure()
+    def __init__(
+        self,
+        central_param: np.ndarray,
+        param_limits: np.ndarray,
+        name: Optional[str] = None,
+    ) -> None:
+        self.central_param = central_param
+        self.param_limits = param_limits
 
-        self.setDataPath(
-            "Data/" + self.getModelName() + "Data.csv"
-        )  # Set default data path
+        self.name = name or self.__class__.__name__
 
     @abstractmethod
-    def getParamSamplingLimits(self) -> np.ndarray:
-        """Define model-specific lower and upper limits for the sampling
-        to avoid parameter regions where the evaluation of the model is instable.
+    def forward(self, param: np.ndarray) -> np.ndarray:
+        """Executed the forward pass of the model to obtain data from a parameter. You can also do equivalently :code:`model(param)`.
 
-        :raises NotImplementedError: Implement this method allow the mcmc sampler to work stably
-        :return: The limits in the format np.array([lower_dim1, upper_dim1], [lower_dim2, upper_dim2], ...)
-        :rtype: np.ndarray
-        """
-        raise NotImplementedError
+        Args:
+            param(np.ndarray): The parameter for which the data should be generated.
 
-    @abstractmethod
-    def getCentralParam(self) -> np.ndarray:
-        """Define a model-specific central parameter point, which will be used as starting point for the mcmc sampler.
+        Returns:
+            np.ndarray: The data generated from the parameter.
 
-        :raises NotImplementedError: Implement this method to provide a good starting point for the mcmc sampler.
-        :return: A single parameter point in the format np.array([p_dim1, p_dim2, ...])
-        :rtype: np.ndarray
         """
         raise NotImplementedError
 
     @abstractmethod
-    def forward(self, param: np.ndarray):
-        """Executed the forward pass of the model to obtain data from a parameter. You can also do equivalently :code:`model(param)`.
+    def jacobian(self, param: np.ndarray) -> np.ndarray:
+        """Evaluates the jacobian of the :func:`~epi.core.model.Model.forward` method.
 
-        :param param: The parameter(set) for which the model should be evaluated.
-        :raises NotImplementedError: Implement this method to make you model callable.
-        """
-        raise NotImplementedError
+        Args:
+            param(np.ndarray): The parameter for which the jacobian should be evaluated.
 
-    @abstractmethod
-    def jacobian(self, param: np.ndarray):
-        """Evaluates the jacobian of the :func:`~epic.core.model.Model.forward` method.
+        Returns:
+            np.ndarray: The jacobian for the variables returned by the :func:`~epi.core.model.Model.forward` method with respect to the parameters.
 
-        :param param: The parameter(set) for which the jacobian of your model should be evaluated.
-        :type param: np.ndarray
-        :return: The jacobian for the variables returned by the :func:`~epic.core.model.Model.forward` method with respect to the parameters.
-        :rtype: np.ndarray
         """
         raise NotImplementedError
 
-    def valjac(self, param: np.ndarray):
+    def forward_and_jacobian(
+        self, param: np.ndarray
+    ) -> Tuple[np.ndarray, np.ndarray]:
         """Evaluates the jacobian and the forward pass of the model at the same time. If the method is not overwritten in a subclass it,
-        it simply calls :func:`~epic.core.model.Model.forward` and :func:`~epic.core.model.Model.jacobian`.
-
-        :param param: The parameter(set) for which the model and the jacobian should be evaluated.
-        :type param: np.ndarray
-        """
+        it simply calls :func:`~epi.core.model.Model.forward` and :func:`~epi.core.model.Model.jacobian`.
 
-        return self.forward(param), self.jacobian(param)
-
-    def setDataPath(self, path: str) -> None:
-        """Set the path to the data file which shall be used from now on.
-
-        :param path: The path to the data file.
-        :type path: str
-        """
-        self.dataPath = path
-
-    def dataLoader(
-        self,
-    ) -> Tuple[int, np.ndarray, np.ndarray, np.ndarray]:
-        """Load the data from the data file found under the models current data path and calculate several properties of the data.
-
-        :return: The dimension of the data space, the data and the estimated optimal kernel width for each dimension of the data.
-        """
-
-        data = np.loadtxt(self.dataPath, delimiter=",", ndmin=2)
-        dataStdevs = calcKernelWidth(data)
-
-        return (
-            self.dataDim,
-            data,
-            dataStdevs,
-        )
-
-    def loadSimResults(self, numBurnSamples: int, occurrence: int):
-        """Load the files generated by the EPI algorithm through sampling
-
-        :param model: Model from which the results will be loaded
-        :type model: Model
-        :param numBurnSamples: Ignore the first samples of each chain
-        :type numBurnSamples: int
-        :param occurrence: step of sampling from chains
-        :type occurrence: int
-        :return: _description_
-        :rtype: _type_
-        """
-        densityEvals = np.loadtxt(
-            self.getApplicationPath() + "/OverallDensityEvals.csv",
-            delimiter=",",
-        )[numBurnSamples::occurrence]
-        simResults = np.loadtxt(
-            self.getApplicationPath() + "/OverallSimResults.csv",
-            delimiter=",",
-            ndmin=2,
-        )[numBurnSamples::occurrence, :]
-        paramChain = np.loadtxt(
-            self.getApplicationPath() + "/OverallParams.csv",
-            delimiter=",",
-            ndmin=2,
-        )[numBurnSamples::occurrence, :]
-        return densityEvals, simResults, paramChain
-
-    def getModelName(self) -> str:
-        """Returns the name of the class to which the object belongs. Overwrite it if you want to
-        give your model a custom name, e. g. depending on the name of your parameters.
-
-        :return: The class name of the calling object.
-        :rtype: str
-        """
-        return self.__class__.__name__
+        Args:
+            param(np.ndarray): The parameter for which the jacobian should be evaluated.
 
-    def getApplicationPath(self) -> str:
-        """Returns the path to the simulation results folder, containing also intermediate results
-
-        :return: path as string to the simulation folder
-        :rtype: str
-        """
-        path = "Applications/" + self.getModelName()
-        return path
+        Returns:
+            typing.Tuple[np.ndarray, np.ndarray]: The data generated from the parameter and the jacobian for the variables returned by the :func:`~epi.core.model.Model.forward` method with respect to the parameters.
 
-    def createApplicationFolderStructure(self) -> None:
-        """Creates the `Application` folder including subfolder where all simulation results
-        are stored for this model. No files are deleted during this action.
         """
-        indent = 4
-        plotFolderTree = (
-            (" " * indent + "- SpiderWebs/ \n" + " " * indent + "- Plots/")
-            if self.isVisualizable()
-            else ""
-        )
 
-        os.makedirs("Data", exist_ok=True)
-        applicationFolderStructure = (
-            "Applications/ \n"
-            "  - {modelName}/ \n"
-            "    - DensityEvals/ \n"
-            "    - Params/ \n"
-            "    - SimResults/ \n"
-        )
-        path = "."
-        structure = applicationFolderStructure + plotFolderTree
-
-        def create(f, root):
-            fpath = f.get_path()
-            joined = os.path.join(root, fpath)
-            if isinstance(f, FakeDir):
-                try:
-                    os.mkdir(joined)
-                except FileExistsError:
-                    logger.info(f"Directory `{joined}` already exists")
-            elif isinstance(f, FakeFile):
-                try:
-                    with open(joined, "w"):
-                        pass
-                except FileExistsError:
-                    logger.info(f"File `{joined}` already exists")
-
-        fakeStructure = seedir.fakedir_fromstring(
-            structure.format(modelName=self.getModelName())
-        )
-        fakeStructure.realize = lambda path_arg: fakeStructure.walk_apply(
-            create, root=path_arg
-        )
-        fakeStructure.realize(path)
-
-    def deleteApplicationFolderStructure(self):
-        """Deletes the models `Applications` subfolder"""
-        try:
-            shutil.rmtree(self.getApplicationPath())
-        except FileNotFoundError:
-            logger.info(
-                f"Directory `{self.getApplicationPath()}` can't be deleted, "
-                "because it does not exist."
-            )
+        return self.forward(param), self.jacobian(param)
 
-    def __call__(self, param):
-        return self.forward(param)
+    def is_artificial(self) -> bool:
+        """Determines whether the model provides artificial parameter and data sets.
 
-    def isArtificial(self) -> bool:
-        """Determines whether the model provides artificial data
+        Returns:
+            bool: True if the model inherits from the ArtificialModelInterface
 
-        :return: True if the model inherits from the ArtificialModelInterface
-        :rtype: bool
         """
         return issubclass(self.__class__, ArtificialModelInterface)
 
-    def isVisualizable(self) -> bool:
-        """Determines whether the model provides bounds for the visualization grids
-
-        :return: True if the model inherits from the VisualizationModelInterface
-        :rtype: bool
-        """
-        return issubclass(self.__class__, VisualizationModelInterface)
-
 
 class ArtificialModelInterface(ABC):
     """By inheriting from this interface you indicate that you are providing an artificial parameter dataset,
     and the corresponding artificial data dataset, which can be used to compare the results from epi with the ground truth.
     The comparison can be done using the plotEmceeResults.
 
-    :raises NotImplementedError: Implement the generateArtificialData function to implement this interface.
     """
 
-    NUM_ARTIFICIAL_SAMPLES = 1000
-
     @abstractmethod
-    def generateArtificialData(
-        self, numSamples: int = NUM_ARTIFICIAL_SAMPLES
-    ) -> None:
-        """
-        .. note::
+    def generate_artificial_params(self, num_samples: int) -> np.ndarray:
+        """This method must be overwritten an return an numpy array of num_samples parameters.
 
-            This method returns None. You have to do the following:
+        Args:
+            num_samples(int): The number of parameters to generate.
 
-            .. code-block:: python
-
-                np.savetxt(trueParams,
-                    "Data/" + self.getModelName() + "Params.csv", delimiter=","
-                )
-                np.savetxt(trueData,
-                    "Data/" + self.getModelName() + "Data.csv", delimiter=","
-                )
+        Returns:
+            np.ndarray: The generated parameters.
 
-            To create the true data from the true params, you can simply call your model.
+        Raises:
+            NotImplementedError: If the method is not overwritten in a subclass.
 
-        :raises NotImplementedError: Generating the artificial data is up to the user
         """
         raise NotImplementedError
 
-    def paramLoader(self) -> Tuple[np.ndarray, np.ndarray]:
-        """Load and return all parameters for artificial set ups
-
-        :return: Loaded parameters and the optimal kernel width for each parameter
-        :rtype: Tuple[np.ndarray, np.ndarray]
-        """
-        trueParams = np.loadtxt(
-            "Data/" + self.getModelName() + "Params.csv",
-            delimiter=",",
-            ndmin=2,
-        )
-        paramStdevs = calcKernelWidth(trueParams)
-
-        return trueParams, paramStdevs
-
-
-class VisualizationModelInterface(ABC):
-    """Provides the function for the generation of the dataGrid and paramGrid for the visualization of the distributions.
-    It forces subclasses to implement the abstract methods  getParamBounds: and getDataBounds:.
-    """
+    def generate_artificial_data(
+        self,
+        params: Union[os.PathLike, str, np.ndarray],
+    ) -> np.ndarray:
+        """This method is called when the user wants to generate artificial data from the model.
 
-    @abstractmethod
-    def getParamBounds(self) -> np.ndarray:
-        """Returns the bounds on the parameters used to visualize the parameter distribution.
+        Args:
+            params: typing.Union[os.PathLike, str, np.ndarray]: The parameters for which the data should be generated. Can be either a path to a file, a numpy array or a string.
 
         Returns:
-              np.array: An array of the form [[lowerLimit_dim1, upperLimit_dim1], [lowerLimit_dim2, upperLimit_dim2],...]
+            np.ndarray: The data generated from the parameters.
 
         Raises:
-            NotImplementedError: If the method is not implemented in the subclass
+            TypeError: If the params argument is not a path to a file, a numpy array or a string.
+
         """
-        raise NotImplementedError
+        if isinstance(params, str) or isinstance(params, os.PathLike):
+            params = np.loadtxt(params, delimiter=",", ndmin=2)
+        elif isinstance(params, np.ndarray) or isinstance(params, jnp.ndarray):
+            pass
+        else:
+            raise TypeError(
+                f"The params argument has to be either a path to a file or a numpy array. The passed argument was of type {type(params)}"
+            )
 
-    @abstractmethod
-    def getDataBounds(self) -> np.ndarray:
-        """Returns the bounds on the data used to visualize the data distribution.
+        # try to use jax vmap to perform the forward pass on multiple parameters at once
+        if isinstance(self, JaxModel):
+            return vmap(self.forward, in_axes=0)(params)
+        else:
+            return np.vectorize(self.forward, signature="(n)->(m)")(params)
 
-        Returns:
-              np.array: An array of the form [[lowerLimit_dim1, upperLimit_dim1], [lowerLimit_dim2, upperLimit_dim2],...]
 
-        Raises:
-            NotImplementedError: If the method is not implemented in the subclass
-        """
-        raise NotImplementedError
-
-    def scale(interval: np.array, scale: float):
-        middle = (interval[1, :] - interval[0, :]) / 2.0
-        return (interval - middle) * scale + middle
+def add_autodiff(_cls):
+    """
+    Decorator to automatically create the jacobian method based on the forward method.
+    Additionally it jit compiles the forward and jacobian method with jax.
 
-    def generateVisualizationGrid(
-        self, resolution: int
-    ) -> Tuple[np.ndarray, np.ndarray]:
-        """This function creates a grid for the data as well as the parameters with a
-        constant number of points in each dimension. It saves the grids as csv files in the `Plots/*grid.csv`
-        in your Application folder.
-
-        :param resolution: The number of grid points in each dimension
-        :type resolution: int
-        :return: The dataGrid and teh paramGrid.
-        :rtype: Tuple[np.ndarray, np.ndarray]
-        """
-        # allocate storage for the parameter and data plotting grid
-        paramGrid = np.zeros((resolution, self.paramDim))
-        dataGrid = np.zeros((resolution, self.dataDim))
-        for d in range(self.dataDim):
-            dataGrid[:, d] = np.linspace(*self.dataBounds[d, :], resolution)
-        for d in range(self.paramDim):
-            paramGrid[:, d] = np.linspace(*self.paramBounds[d, :], resolution)
-
-        # store both grids as csv files into the model-specific plot directory
-        np.savetxt(
-            "Applications/" + self.getModelName() + "/Plots/dataGrid.csv",
-            dataGrid,
-            delimiter=",",
-        )
-        np.savetxt(
-            "Applications/" + self.getModelName() + "/Plots/paramGrid.csv",
-            paramGrid,
-            delimiter=",",
-        )
-        return dataGrid, paramGrid
+    Args:
+        _cls: The class to decorate.
 
+    Returns:
+        The decorated class with the jacobian method and the forward and jacobian method jit compiled with jax.
 
-def autodiff(_cls):
-    _cls.initFwAndBw()
+    """
+    _cls.init_fw_and_bw()
     return _cls
 
 
@@ -373,33 +182,157 @@ class JaxModel(Model):
     Dont overwrite the __init__ method of JaxModel without calling the super constructor.
     Else your forward method wont be jitted.
 
-    :param Model: Abstract parent class
-    :type Model: Model
     """
 
-    def __init__(self, delete: bool = False, create: bool = True) -> None:
-        super().__init__(delete, create)
+    def __init__(
+        self,
+        central_param: np.ndarray,
+        param_limits: np.ndarray,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        """Constructor of the JaxModel class.
+
+        Args:
+            name: str: The name of the model. If None the name of the class is used.
+        """
+        super().__init__(
+            central_param=central_param,
+            param_limits=param_limits,
+            name=name,
+            **kwargs,
+        )
         # TODO: Check performance implications of not setting this at the class level but for each instance.
         type(self).forward = partial(JaxModel.forward_method, self)
 
     def __init_subclass__(cls, **kwargs):
-        return autodiff(super().__init_subclass__(**kwargs))
+        """Automatically create the jacobian method based on the forward method for the subclass."""
+        return add_autodiff(super().__init_subclass__(**kwargs))
 
     @classmethod
-    def initFwAndBw(cls):
-        # Calculate jitted methods for the subclass(es)
-        # It is an unintended sideeffect that this happens for all intermediate classes also.
-        # E.g. for: class CoronaArtificial(Corona)
+    def init_fw_and_bw(cls):
+        """Calculates the jitted methods for the subclass(es).
+        It is an unintended sideeffect that this happens for all intermediate classes also.
+        E.g. for: class CoronaArtificial(Corona)
+        """
         cls.fw = jit(cls.forward)
         cls.bw = jit(jacrev(cls.forward))
         cls.vj = jit(value_and_jacrev(cls.forward))
 
     @staticmethod
-    def forward_method(self, param):
+    def forward_method(self, param: np.ndarray) -> np.ndarray:
+        """This method is called by the jitted forward method. It is not intended to be called directly.
+
+        Args:
+            param(np.ndarray): The parameter for which the data should be generated.
+
+        Returns:
+            np.ndarray: The data generated from the parameter.
+
+        """
         return type(self).fw(param)
 
-    def jacobian(self, param):
+    def jacobian(self, param: np.ndarray) -> np.ndarray:
+        """Jacobian of the forward pass with respect to the parameters.
+
+        Args:
+            param(np.ndarray): The parameter for which the jacobian should be evaluated.
+
+        Returns:
+            np.ndarray: The jacobian for the variables returned by the :func:`~epi.core.model.Model.forward` method with respect to the parameters.
+
+        """
         return type(self).bw(param)
 
-    def valjac(self, param):
+    def forward_and_jacobian(
+        self, param: np.ndarray
+    ) -> Tuple[np.ndarray, np.ndarray]:
+        """Evaluates the jacobian and the forward pass of the model at the same time. This can be more efficient than calling the :func:`~epi.core.model.Model.forward` and :func:`~epi.core.model.Model.jacobian` methods separately.
+
+        Args:
+            param(np.ndarray): The parameter for which the jacobian should be evaluated.
+
+        Returns:
+            typing.Tuple[np.ndarray, np.ndarray]: The data and the jacobian for a given parameter.
+
+        """
         return type(self).vj(param)
+
+
+class SBMLModel(Model):
+    """The SBMLModel class is a wrapper for the AMICI python interface to simulate SBML models using this package.
+
+    Args:
+        sbml_file(str): The path to the SBML model file.
+        param_names(list): A list of parameter names. If None the parameter names are extracted from the SBML model.
+        tEnd(float): The end time of the simulation. (Default value = 1.0)
+        skip_creation(bool): If True the model is not created againg based on the SBML file. Instead the model is loaded from a previously created model. (Default value = False)
+        central_param(np.ndarray): The central parameter for the model
+        param_limits(np.ndarray): The parameter limits for the model
+    """
+
+    @property
+    def param_dim(self):
+        """The number of parameters of the model."""
+        return len(self.model.getParameterIds())
+
+    @property
+    def data_dim(self):
+        """The number of observables of the model."""
+        return len(self.model.getObservableIds())
+
+    def __init__(
+        self,
+        sbml_file: str,
+        central_param: np.ndarray,
+        param_limits: np.ndarray,
+        param_names=None,
+        tEnd=1.0,
+        skip_creation: bool = False,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(central_param, param_limits, name, **kwargs)
+
+        model_name = self.name
+        model_dir = "./amici"
+
+        # Generate python code
+        if not skip_creation:
+            sbml_importer = amici.SbmlImporter(sbml_file)
+            sbml_importer.sbml2amici(model_name, model_dir)
+
+        # Load the generated model
+        model_module = amici.import_model_module(model_name, model_dir)
+        model = model_module.getModel()
+        solver = model.getSolver()
+
+        model.setTimepoints([tEnd])
+        model.requireSensitivitiesForAllParameters()
+        solver.setSensitivityMethod(amici.SensitivityMethod.forward)
+        solver.setSensitivityOrder(amici.SensitivityOrder.first)
+
+        self.param_names = param_names or model.getParameterNames()
+
+        self.model = model
+        self.solver = solver
+
+    def forward(self, params):
+        for i, param in enumerate(params):
+            self.model.setParameterByName(self.param_names[i], param)
+        rdata = amici.runAmiciSimulation(self.model, self.solver)
+        return rdata.x[-1]
+
+    def jacobian(self, params):
+        for i, param in enumerate(params):
+            self.model.setParameterByName(self.param_names[i], param)
+        rdata = amici.runAmiciSimulation(self.model, self.solver)
+        return rdata.sx[-1].T
+
+    def forward_and_jacobian(
+        self, params: np.ndarray
+    ) -> Tuple[np.ndarray, np.ndarray]:
+        for i, param in enumerate(params):
+            self.model.setParameterByName(self.param_names[i], param)
+        rdata = amici.runAmiciSimulation(self.model, self.solver)
+        return rdata.x[-1], rdata.sx[-1].T
diff --git a/epi/core/result_manager.py b/epi/core/result_manager.py
new file mode 100644
index 00000000..79552aeb
--- /dev/null
+++ b/epi/core/result_manager.py
@@ -0,0 +1,304 @@
+# TODO: Import Path from pathlib?
+import os
+import shutil
+from os import path
+from typing import Optional, Tuple
+
+import numpy as np
+import seedir
+from seedir import FakeDir, FakeFile
+
+from epi import logger
+from epi.core.model import Model
+
+
+class ResultManager:
+    """The result manager is responsible for saving the results of the inference and loading them again.
+
+    Attributes:
+        model_name(str): The name of the model (e.g. "temperature"). It is used to create the folder structure.
+        run_name(str): The name of the run which shall be saved. It is used to create subfolders for different runs.
+    """
+
+    def __init__(self, model_name: str, run_name: str) -> None:
+        self.model_name = model_name
+        self.run_name = run_name
+
+    def count_emcee_sub_runs(self, slice: np.ndarray) -> int:
+        """This data organization function counts how many sub runs are saved for the specified scenario.
+
+        Args:
+            slice(np.ndarray): The slice for which the number of sub runs should be counted.
+
+        Returns:
+          num_existing_files(int): The number of completed sub runs of the emcee particle swarm sampler.
+
+        """
+        # Initialize the number of existing files to be 0
+        num_existing_files = 0
+
+        # Increase the just defined number until no corresponding file is found anymore ...
+        while path.isfile(
+            self.get_slice_path(slice)
+            + "/SimResults/"
+            + "sim_results_"
+            + str(num_existing_files)
+            + ".csv"
+        ):
+            num_existing_files += 1
+
+        return num_existing_files
+
+    def get_slice_name(self, slice: np.ndarray) -> str:
+        """This organization function returns the name of the folder for the current slice.
+
+        Args:
+            slice(np.ndarray): The slice for which the name of the folder will be returned.
+
+        Returns:
+            str: The name of the folder for the current slice.
+
+        """
+
+        return "Slice_" + "Q".join([str(i) for i in slice])
+
+    def get_slice_path(self, slice: np.ndarray) -> str:
+        """Returns the path to the folder where the results for the given slice are stored.
+
+        Args:
+            slice(np.ndarray): The slice for which the path will be returned.
+
+        Returns:
+            str: The path to the folder where the results for the given slice are stored.
+
+        """
+        sliceName = self.get_slice_name(slice)
+        return os.path.join(
+            "Applications", self.model_name, self.run_name, sliceName
+        )
+
+    def create_application_folder_structure(
+        self, model: Model, slices: Optional[list[np.ndarray]] = None
+    ) -> None:
+        """Creates the `Application` folder including subfolder where all simulation results
+        are stored for this model. No files are deleted during this action.
+
+        Args:
+            model(Model): The model for which the folder structure will be created
+            slices(list[np.ndarray]): The slices for which the folder structure will be created
+
+        """
+
+        if slices is None:
+            slices = [np.arange(model.param_dim)]
+        for slice in slices:
+            self.create_slice_folder_structure(slice)
+
+    def create_slice_folder_structure(self, slice: np.ndarray) -> None:
+        """Creates the subfolders in `Aplication` for the given slice where all simulation results
+        are stored for this model and slice. No files are deleted during this action.
+
+        Args:
+            slice(np.ndarray): The slice for which the folder structure will be created
+
+        """
+        applicationFolderStructure = (
+            "Applications/ \n"
+            "  - {modelName}/ \n"
+            "    - {runName}/ \n"
+            "       - {sliceName}/ \n"
+            "           - DensityEvals/ \n"
+            "           - Params/ \n"
+            "           - SimResults/ \n"
+        )
+        path = "."
+        structure = applicationFolderStructure
+
+        def create(f, root):
+            """
+
+            Args:
+              f:
+              root:
+
+            Returns:
+
+            """
+            fpath = f.get_path()
+            joined = os.path.join(root, fpath)
+            if isinstance(f, FakeDir):
+                try:
+                    os.mkdir(joined)
+                except FileExistsError:
+                    logger.info(f"Directory `{joined}` already exists")
+            elif isinstance(f, FakeFile):
+                try:
+                    with open(joined, "w"):
+                        pass
+                except FileExistsError:
+                    logger.info(f"File `{joined}` already exists")
+
+        sliceName = self.get_slice_name(slice)
+        fakeStructure = seedir.fakedir_fromstring(
+            structure.format(
+                modelName=self.model_name,
+                runName=self.run_name,
+                sliceName=sliceName,
+            )
+        )
+        fakeStructure.realize = lambda path_arg: fakeStructure.walk_apply(
+            create, root=path_arg
+        )
+        fakeStructure.realize(path)
+
+    def delete_application_folder_structure(self, model, slices) -> None:
+        """Deletes the `Applications` subfolder
+
+        Args:
+            slices(list[np.ndarray]): The slices for which the folder structure will be deleted
+            model(Model): The model for which the folder structure will be deleted
+
+        """
+        for slice in slices:
+            try:
+                self.delete_slice_folder_structure(slice)
+            except FileNotFoundError:
+                logger.info(
+                    f"Folder structure for slice {slice} does not exist"
+                )
+
+    def delete_slice_folder_structure(self, slice: np.ndarray) -> None:
+        """Deletes the `Applications/[slice]` subfolder
+
+        Args:
+            slice(np.ndarray): The slice for which the folder structure will be deleted
+
+        """
+        path = self.get_slice_path(slice)
+        shutil.rmtree(path)
+
+    def get_application_path(self) -> str:
+        """Returns the path to the simulation results folder, containing also intermediate results.
+
+        Returns:
+            str: The path to the simulation results folder, containing also intermediate results.
+
+        """
+        path = "Applications/" + self.model_name
+        return path
+
+    def save_run(
+        self,
+        model: Model,
+        slice: np.ndarray,
+        run,
+        sampler_results: np.ndarray,
+        final_walker_positions: np.ndarray,
+    ) -> None:
+        """Saves the results of a single run of the emcee particle swarm sampler.
+        sampler_results has the shape (num_walkers * num_steps, sampling_dim + data_dim + 1), we save them
+        as seperate files in the folders 'Params' and'SimResults' and 'DensityEvals'.
+
+        Args:
+            model(Model): The model for which the results will be saved
+            slice(np.ndarray): The slice for which the results will be saved
+            run(int): The run for which the results will be saved
+            sampler_results(np.ndarray): The results of the sampler, expects an np.array with shape (num_walkers * num_steps, sampling_dim + data_dim + 1)
+            final_walker_positions(np.ndarray): The final positions of the walkers, expects an np.array with shape (num_walkers, sampling_dim)
+
+        """
+
+        sampling_dim = final_walker_positions.shape[1]
+
+        results_path = self.get_slice_path(slice)
+
+        # Save the parameters
+        np.savetxt(
+            results_path + "/Params/params_" + str(run) + ".csv",
+            sampler_results[:, :sampling_dim],
+            delimiter=",",
+        )
+
+        # Save the density evaluations
+        np.savetxt(
+            results_path + "/DensityEvals/density_evals_" + str(run) + ".csv",
+            sampler_results[:, -1],
+            delimiter=",",
+        )
+
+        # Save the simulation results
+        np.savetxt(
+            results_path + "/SimResults/sim_results_" + str(run) + ".csv",
+            sampler_results[
+                :, sampling_dim : model.param_dim + model.data_dim
+            ],
+            delimiter=",",
+        )
+
+        # Save the final walker positions
+        np.savetxt(
+            results_path + "/final_walker_positions_" + str(run) + ".csv",
+            final_walker_positions,
+            delimiter=",",
+        )
+
+    def save_overall(
+        self, slice, overall_params, overall_sim_results, overall_density_evals
+    ):
+        """Saves the results of all runs of the emcee particle swarm sampler for the given slice.
+
+        Args:
+            slice(np.ndarray): The slice for which the results will be saved. TODO document dimensions of overall_params, overall_sim_results, overall_density_evals
+            overall_params(np.ndarray): The results of the sampler.
+            overall_sim_results(np.ndarray): The results of the sampler.
+            overall_density_evals(np.ndarray): The results of the sampler.
+
+        """
+        # Save the three just-created files.
+        np.savetxt(
+            self.get_slice_path(slice) + "/overall_density_evals.csv",
+            overall_density_evals,
+            delimiter=",",
+        )
+        np.savetxt(
+            self.get_slice_path(slice) + "/overall_sim_results.csv",
+            overall_sim_results,
+            delimiter=",",
+        )
+        np.savetxt(
+            self.get_slice_path(slice) + "/overall_params.csv",
+            overall_params,
+            delimiter=",",
+        )
+
+    def load_sim_results(
+        self, slice: np.ndarray, num_burn_samples: int, occurrence: int
+    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
+        """Load the files generated by the EPI algorithm through sampling
+
+        Args:
+            slice(np.ndarray): Slice for which the results will be loaded
+            num_burn_samples(int): Ignore the first samples of each chain
+            occurrence(int): step of sampling from chains
+
+        Returns:
+            typing.Tuple[np.ndarray, np.ndarray, np.ndarray]: The density evaluations, the simulation results and the parameter chain
+
+        """
+        results_path = self.get_slice_path(slice)
+
+        density_evals = np.loadtxt(
+            results_path + "/overall_density_evals.csv",
+            delimiter=",",
+        )[num_burn_samples::occurrence]
+        sim_results = np.loadtxt(
+            results_path + "/overall_sim_results.csv",
+            delimiter=",",
+            ndmin=2,
+        )[num_burn_samples::occurrence, :]
+        param_chain = np.loadtxt(
+            results_path + "/overall_params.csv",
+            delimiter=",",
+            ndmin=2,
+        )[num_burn_samples::occurrence, :]
+        return density_evals, sim_results, param_chain
diff --git a/epi/core/sampling.py b/epi/core/sampling.py
index 9eeb04e6..771646e9 100644
--- a/epi/core/sampling.py
+++ b/epi/core/sampling.py
@@ -1,281 +1,317 @@
-from multiprocessing import Pool
+"""Sampling methods for the EPI package.
 
-# from pathos.multiprocessing import Pool
+This module provides functions to handle the sampling in EPI. It is based on the emcee package.
+
+.. _emcee: https://emcee.readthedocs.io/en/stable/
+
+Attributes:
+    NUM_RUNS (int): Default number of runs of the emcee sampler.
+    NUM_WALKERS (int): Default number of walkers in the emcee sampler.
+    NUM_STEPS (int): Default number of steps each walker performs before storing the sub run.
+    NUM_PROCESSES (int): Default number of parallel threads.
+
+"""
+
+import typing
 from os import path
 
 import emcee
 import numpy as np
+from schwimmbad import MultiPool
 
 from epi import logger
-from epi.core.functions import evalLogTransformedDensity
+from epi.core.kde import calc_kernel_width
 from epi.core.model import Model
+from epi.core.result_manager import ResultManager
+from epi.core.transformations import eval_log_transformed_density
 
-NUM_RUNS = 2
-NUM_WALKERS = 10
-NUM_STEPS = 2500
-NUM_PROCESSES = 4
+# TODO: This works on the blob
+# Return the samples.
+# return sampler.get_chain(discard=0, thin=1, flat=True)
+# TODO: This stores the sample as 2d array in the format walker1_step1, walker2_step1, walker3_step1, walker1_step2, walker2_step2, walker3_step2, ...
+# sampler_results = samplerBlob.reshape(
+#     num_walkers * num_steps, sampling_dim + data_dim + 1
+# )
 
 
-def countEmceeSubRuns(model: Model) -> int:
-    """This data organization function counts how many sub runs are saved for the specified scenario.
+def run_emcee_once(
+    model: Model,
+    data: np.ndarray,
+    data_stdevs: np.ndarray,
+    slice: np.ndarray,
+    initial_walker_positions: np.ndarray,
+    num_walkers: int,
+    num_steps: int,
+    num_processes: int,
+) -> np.ndarray:
+    """Run the emcee particle swarm sampler once.
+
+    Args:
+        model (Model): The model which will be sampled
+        data (np.ndarray): data
+        data_stdevs (np.ndarray): kernel width for the data
+        slice (np.ndarray): slice of the parameter space which will be sampled
+        initial_walker_positions (np.ndarray): initial parameter values for the walkers
+        num_walkers (int): number of particles in the particle swarm sampler
+        num_steps (int): number of samples each particle performs before storing the sub run
+        num_processes (int): number of parallel threads
+
+    Returns:
+        np.ndarray: samples from the transformed parameter density
 
-    :param model: The model for which the files will be counted
-    :return: numExistingFiles (number of completed sub runs of the emcee particle swarm sampler)
     """
-    # Initialize the number of existing files to be 0
-    numExistingFiles = 0
 
-    # Increase the just defined number until no corresponding file is found anymore ...
-    while path.isfile(
-        model.getApplicationPath()
-        + "/DensityEvals/"
-        + str(numExistingFiles)
-        + ".csv"
-    ):
-        numExistingFiles += 1
+    global work
+
+    def work(params):
+        s = eval_log_transformed_density(
+            params, model, data, data_stdevs, slice
+        )
+        return s
+
+    pool = MultiPool(processes=num_processes)
+
+    # define a custom move policy
+    movePolicy = [
+        (emcee.moves.WalkMove(), 0.1),
+        (emcee.moves.StretchMove(), 0.1),
+        (
+            emcee.moves.GaussianMove(0.00001, mode="sequential", factor=None),
+            0.8,
+        ),
+    ]
+    # movePolicy = [(emcee.moves.GaussianMove(0.00001, mode='sequential', factor=None), 1.0)]
+    sampling_dim = slice.shape[0]
+
+    # Call the sampler for all parallel workers (possibly use arg moves = movePolicy)
+    try:
+        sampler = emcee.EnsembleSampler(
+            num_walkers,
+            sampling_dim,
+            # eval_log_transformed_density,
+            work,
+            pool=pool,
+            moves=movePolicy,
+            # args=[model, data, data_stdevs, slice],
+        )
+        # Extract the final walker position and close the pool of worker processes.
+        final_walker_positions, _, _, _ = sampler.run_mcmc(
+            initial_walker_positions, num_steps, tune=True, progress=True
+        )
+    except ValueError as e:
+        # If the message equals "Probability function returned NaN."
+        if str(e) == "Probability function returned NaN.":
+            raise ValueError(
+                "Probability function returned NaN. "
+                "You possibly have to exclude data dimensions which do not depend on the paramaters. "
+                "In addition your parameters should not be linearly dependent."
+            )
+        else:
+            raise e
+
+    if pool is not None:
+        pool.close()
+        pool.join()
+
+    # TODO: Keep as 3d array?
+    # Should have shape (num_steps, num_walkers, param_dim+data_dim+1)
+    sampler_results = sampler.get_blobs()
+    data_dim = data.shape[1]
+    sampler_results = sampler_results.reshape(
+        num_steps * num_walkers, sampling_dim + data_dim + 1
+    )
+    sampler_results = sampler_results.reshape(
+        num_walkers * num_steps, sampling_dim + data_dim + 1
+    )
+
+    logger.info(
+        f"The acceptance fractions of the emcee sampler per walker are: {np.round(sampler.acceptance_fraction, 2)}"
+    )
+    try:
+        corrTimes = sampler.get_autocorr_time()
+        logger.info(f"autocorrelation time: {corrTimes[0]}")
+    except emcee.autocorr.AutocorrError as e:
+        logger.warning(
+            "The autocorrelation time could not be calculate reliable"
+        )
 
-    return numExistingFiles
+    return sampler_results, final_walker_positions
 
 
-def runEmceeSampling(
+def run_emcee_sampling(
     model: Model,
-    numRuns: int = NUM_RUNS,
-    numWalkers: int = NUM_WALKERS,
-    numSteps: int = NUM_STEPS,
-    numProcesses: int = NUM_PROCESSES,
-) -> None:
+    data: np.ndarray,
+    slice: np.ndarray,
+    result_manager: ResultManager,
+    num_processes: int,
+    num_runs: int,
+    num_walkers: int,
+    num_steps: int,
+) -> typing.Tuple[np.ndarray, np.ndarray, np.ndarray]:
     """Create a representative sample from the transformed parameter density using the emcee particle swarm sampler.
-        Inital values are not stored in the chain and each file contains <numSteps> blocks of size numWalkers.
-
-    :param model: The model which will be sampled
-    :param numRuns: (number of stored sub runs)
-    :param numWalkers: (number of particles in the particle swarm sampler)
-    :param numSteps: (number of samples each particle performs before storing the sub run)
-    :param numProcesses: (number of parallel threads)
-    :return: None, except for stored files
+       Inital values are not stored in the chain and each file contains <num_steps> blocks of size num_walkers.
+
+    Args:
+        model (Model): The model which will be sampled
+        data (np.ndarray): data
+        slice (np.ndarray): slice of the parameter space which will be sampled
+        result_manager (ResultManager): ResultManager which will store the results
+        num_processes (int): number of parallel threads.
+        num_runs (int): number of stored sub runs.
+        num_walkers (int): number of particles in the particle swarm sampler.
+        num_steps (int): number of samples each particle performs before storing the sub run.
+
+    Returns:
+        typing.Tuple[np.ndarray, np.ndarray, np.ndarray]: Array with all params, array with all data, array with all log probabilities
+
     """
 
-    # Load data, data standard deviations and model characteristics for the specified model.
-    (
-        dataDim,
-        data,
-        dataStdevs,
-    ) = model.dataLoader()
+    data_stdevs = calc_kernel_width(data)
+    sampling_dim = slice.shape[0]
+    central_param = model.central_param
 
     # Initialize each walker at a Gaussian-drawn random, slightly different parameter close to the central parameter.
-    walkerInitParams = model.getCentralParam() + 0.002 * (
-        np.random.rand(numWalkers, model.paramDim) - 0.5
+    # TODO Make random variation of initial walker positions dependent on sampling limits?
+    initial_walker_positions = central_param[slice] + 0.002 * (
+        np.random.rand(num_walkers, sampling_dim) - 0.5
     )
 
     # Count and print how many runs have already been performed for this model
-    numExistingFiles = countEmceeSubRuns(model)
-    logger.debug(f"{numExistingFiles} existing files found")
+    num_existing_files = result_manager.count_emcee_sub_runs(slice)
+    logger.debug(f"{num_existing_files} existing files found")
 
     # Loop over the remaining sub runs and contiune the counter where it ended.
-    for run in range(numExistingFiles, numExistingFiles + numRuns):
-        logger.info(f"Run {run} of {numRuns}")
+    for run in range(num_existing_files, num_existing_files + num_runs):
+        logger.info(f"Run {run} of {num_runs}")
 
         # If there are current walker positions defined by runs before this one, use them.
-        positionPath = model.getApplicationPath() + "/currentPos.csv"
-        if path.isfile(positionPath):
-            walkerInitParams = np.loadtxt(
-                positionPath,
+        position_path = (
+            result_manager.get_slice_path(slice) + "/currentPos.csv"
+        )
+        if path.isfile(position_path):
+            initial_walker_positions = np.loadtxt(
+                position_path,
                 delimiter=",",
                 ndmin=2,
             )
             logger.info(
-                f"Continue sampling from saved sampler position in {positionPath}"
+                f"Continue sampling from saved sampler position in {position_path}"
             )
 
-        else:
-            logger.info("Start sampling from start")
-
-        # Create a pool of worker processes.
-        pool = Pool(processes=numProcesses)
-
-        # define a custom move policy
-        movePolicy = [
-            (emcee.moves.WalkMove(), 0.1),
-            (emcee.moves.StretchMove(), 0.1),
-            (
-                emcee.moves.GaussianMove(
-                    0.00001, mode="sequential", factor=None
-                ),
-                0.8,
-            ),
-        ]
-        # movePolicy = [(emcee.moves.GaussianMove(0.00001, mode='sequential', factor=None), 1.0)]
-
-        # Call the sampler for all parallel workers (possibly use arg moves = movePolicy)
-        sampler = emcee.EnsembleSampler(
-            numWalkers,
-            model.paramDim,
-            evalLogTransformedDensity,
-            pool=pool,
-            moves=movePolicy,
-            args=[model, data, dataStdevs],
-        )
-
-        # Extract the final walker position and close the pool of worker processes.
-        finalPos, _, _, _ = sampler.run_mcmc(
-            walkerInitParams, numSteps, tune=True, progress=True
-        )
-        pool.close()
-        pool.join()
-
-        # Save the current walker positions as initial values for the next run.
-        np.savetxt(
-            positionPath,
-            finalPos,
-            delimiter=",",
+        # Run the sampler.
+        sampler_results, final_walker_positions = run_emcee_once(
+            model,
+            data,
+            data_stdevs,
+            slice,
+            initial_walker_positions,
+            num_walkers,
+            num_steps,
+            num_processes,
         )
 
-        # Should have shape (numSteps, numWalkers, paramDim+dataDim+1)
-        samplerBlob = sampler.get_blobs()
-
-        # Create a large container for all sampling results (sampled parameters, corresponding simulation results and parameter densities) and fill it using the emcee blob option.
-        allRes = samplerBlob.reshape(numWalkers * numSteps, -1)
-
-        # Save all sampling results in .csv files.
-        np.savetxt(
-            "Applications/"
-            + model.getModelName()
-            + "/Params/"
-            + str(run)
-            + ".csv",
-            allRes[:, 0 : model.paramDim],
-            delimiter=",",
-        )
-        np.savetxt(
-            "Applications/"
-            + model.getModelName()
-            + "/SimResults/"
-            + str(run)
-            + ".csv",
-            allRes[:, model.paramDim : model.paramDim + dataDim],
-            delimiter=",",
-        )
-        np.savetxt(
-            "Applications/"
-            + model.getModelName()
-            + "/DensityEvals/"
-            + str(run)
-            + ".csv",
-            allRes[:, -1],
-            delimiter=",",
+        result_manager.save_run(
+            model, slice, run, sampler_results, final_walker_positions
         )
 
-        logger.info(
-            f"The acceptance fractions of the emcee sampler per walker are: {np.round(sampler.acceptance_fraction, 2)}"
-        )
-        try:
-            corrTimes = sampler.get_autocorr_time()
-            logger.info(f"autocorrelation time: {corrTimes[0]}")
-        except emcee.autocorr.AutocorrError as e:
-            logger.warning(
-                "The autocorrelation time could not be calculate reliable"
-            )
+    (
+        overall_params,
+        overall_sim_results,
+        overall_density_evals,
+    ) = concatenate_emcee_sampling_results(model, result_manager, slice)
+    result_manager.save_overall(
+        slice, overall_params, overall_sim_results, overall_density_evals
+    )
+    return overall_params, overall_sim_results, overall_density_evals
 
 
-def concatenateEmceeSamplingResults(model: Model):
+# TODO: Make this a method of the ResultManager? It uses the ResultManager to load the results and many hard coded paths.
+def concatenate_emcee_sampling_results(
+    model: Model, result_manager: ResultManager, slice: np.ndarray
+) -> typing.Tuple[np.ndarray, np.ndarray, np.ndarray]:
     """Concatenate many sub runs of the emcee sampler to create 3 large files for sampled parameters, corresponding simulation results and density evaluations.
         These files are later used for result visualization.
 
-    Input: model
-    Output: <none except for stored files>
-    """
+    Args:
+        model (Model): The model for which the results should be concatenated
+        result_manager (ResultManager): ResultManager to load the results from
+        slice (np.ndarray): slice for which the results should be concatenated
 
-    # Count and print how many sub runs are ready to be merged.
-    numExistingFiles = countEmceeSubRuns(model)
-    logger.info(f"{numExistingFiles} existing files found for concatenation")
+    Returns:
+        typing.Tuple[np.ndarray, np.ndarray, np.ndarray]: Array with all params, array with all data, array with all log probabilities
 
-    # Load one example file and use it to extract how many samples are stored per file.
-    numSamplesPerFile = np.loadtxt(
-        model.getApplicationPath() + "/Params/0.csv",
-        delimiter=",",
-        ndmin=2,
-    ).shape[0]
-
-    # The overall number of sampled is the number of sub runs multiplied with the number of samples per file.
-    numSamples = numExistingFiles * numSamplesPerFile
+    """
 
-    # Create containers large enough to store all sampling information.
-    overallDensityEvals = np.zeros(numSamples)
-    overallSimResults = np.zeros((numSamples, model.dataDim))
-    overallParams = np.zeros((numSamples, model.paramDim))
+    # Count and print how many sub runs are ready to be merged.
+    num_existing_files = result_manager.count_emcee_sub_runs(slice)
+    logger.info(f"{num_existing_files} existing files found for concatenation")
 
     densityFiles = (
-        "Applications/" + model.getModelName() + "/DensityEvals/{}.csv"
+        result_manager.get_slice_path(slice)
+        + "/DensityEvals/density_evals_{}.csv"
     )
-    simResultsFiles = (
-        "Applications/" + model.getModelName() + "/SimResults/{}.csv"
+    sim_result_files = (
+        result_manager.get_slice_path(slice) + "/SimResults/sim_results_{}.csv"
     )
-    paramFiles = "Applications/" + model.getModelName() + "/Params/{}.csv"
-    # Loop over all sub runs, load the respective sample files and store them at their respective places in the overall containers.
-    for i in range(numExistingFiles):
-        overallDensityEvals[
-            i * numSamplesPerFile : (i + 1) * numSamplesPerFile
-        ] = np.loadtxt(
-            densityFiles.format(i),
-            delimiter=",",
-        )
-        overallSimResults[
-            i * numSamplesPerFile : (i + 1) * numSamplesPerFile, :
-        ] = np.loadtxt(
-            simResultsFiles.format(i),
-            delimiter=",",
-            ndmin=2,
-        )
-        overallParams[
-            i * numSamplesPerFile : (i + 1) * numSamplesPerFile, :
-        ] = np.loadtxt(
-            paramFiles.format(i),
-            delimiter=",",
-            ndmin=2,
-        )
+    paramFiles = result_manager.get_slice_path(slice) + "/Params/params_{}.csv"
 
-    # Save the three just-created files.
-    np.savetxt(
-        model.getApplicationPath() + "/OverallDensityEvals.csv",
-        overallDensityEvals,
-        delimiter=",",
+    overall_params = np.vstack(
+        [
+            np.loadtxt(paramFiles.format(i), delimiter=",", ndmin=2)
+            for i in range(num_existing_files)
+        ]
     )
-    np.savetxt(
-        model.getApplicationPath() + "/OverallSimResults.csv",
-        overallSimResults,
-        delimiter=",",
+    overall_sim_results = np.vstack(
+        [
+            np.loadtxt(sim_result_files.format(i), delimiter=",", ndmin=2)
+            for i in range(num_existing_files)
+        ]
     )
-    np.savetxt(
-        model.getApplicationPath() + "/OverallParams.csv",
-        overallParams,
-        delimiter=",",
+    overall_density_evals = np.hstack(
+        [
+            np.loadtxt(densityFiles.format(i), delimiter=",")
+            for i in range(num_existing_files)
+        ]
     )
 
+    return overall_params, overall_sim_results, overall_density_evals
+
 
-def calcWalkerAcceptance(model: Model, numWalkers: int, numBurnSamples: int):
+def calc_walker_acceptance(
+    model: Model,
+    slice: np.ndarray,
+    num_walkers: int,
+    num_burn_samples: int,
+    result_manager: ResultManager,
+):
     """Calculate the acceptance ratio for each individual walker of the emcee chain.
-        This is especially important to find "zombie" walkers, that are never moving.
+    This is especially important to find "zombie" walkers, that are never moving.
+
+    Args:
+        model (Model): The model for which the acceptance ratio should be calculated
+        slice (np.ndarray): slice for which the acceptance ratio should be calculated
+        num_walkers (int): number of walkers in the emcee chain
+        num_burn_samples (int): number of samples that were ignored at the beginning of each chain
+        result_manager (ResultManager): ResultManager to load the results from
 
-    Input: model
-           numBurnSamples (integer number of ignored first samples of each chain)
-           numWalkers (integer number of emcee walkers) that were used for the emcee chain which is analyzed here
+    Returns:
+        np.ndarray: Array with the acceptance ratio for each walker
 
-    Output: acceptanceRatios (np.array of size numWalkers)
     """
 
     # load the emcee parameter chain
     params = np.loadtxt(
-        model.getApplicationPath() + "/OverallParams.csv",
+        result_manager.get_slice_path(slice) + "/overall_params.csv",
         delimiter=",",
         ndmin=2,
-    )[numBurnSamples:, :]
+    )[num_burn_samples:, :]
 
     # calculate the number of steps each walker walked
     # subtract 1 because we count the steps between the parameters
-    numSteps = int(params.shape[0] / numWalkers) - 1
+    num_steps = int(params.shape[0] / num_walkers) - 1
 
     # Unflatten the parameter chain and count the number of accepted steps for each walker
-    params = params.reshape(numSteps + 1, numWalkers, model.paramDim)
+    params = params.reshape(num_steps + 1, num_walkers, model.param_dim)
 
     # Build a boolean array that is true if the parameters of the current step are the same as the parameters of the next step and sum over it
     # If the parameters are the same, the step is not accepted and we add 0 to the number of accepted steps
@@ -286,43 +322,51 @@ def calcWalkerAcceptance(model: Model, numWalkers: int, numBurnSamples: int):
     )
 
     # calculate the acceptance ratio by dividing the number of accepted steps by the overall number of steps
-    acceptanceRatios = numAcceptedSteps / numSteps
+    acceptanceRatios = numAcceptedSteps / num_steps
 
     return acceptanceRatios
 
 
-def inference(
+def inference_mcmc(
     model: Model,
-    dataPath: str = None,
-    numRuns: int = NUM_RUNS,
-    numWalkers: int = NUM_WALKERS,
-    numSteps: int = NUM_STEPS,
-    numProcesses: int = NUM_PROCESSES,
-):
-    """Starts the parameter inference for the given model. If a data path is given, it is used to load the data for the model. Else, the default data path of the model is used.
-
-
-    :param model: The model describing the mapping from parameters to data.
-    :type model: Model
-    :param dataPath: path to the data relative to the current working directory.
-                      If None, the default path defined in the Model class initializer is used, defaults to None
-    :type dataPath: str, optional
-    :param numRuns: Number of independent runs, defaults to NUM_RUNS
-    :type numRuns: int, optional
-    :param numWalkers: Number of walkers for each run, influencing each other, defaults to NUM_WALKERS
-    :type numWalkers: int, optional
-    :param numSteps: Number of steps each walker does in each run, defaults to NUM_STEPS
-    :type numSteps: int, optional
-    :param numProcesses: number of processes to use, defaults to NUM_PROCESSES
-    :type numProcesses: int, optional
+    data: np.ndarray,
+    result_manager: ResultManager,
+    slices: list[np.ndarray],
+    num_processes: int,
+    num_runs: int = 2,
+    num_walkers: int = 10,
+    num_steps: int = 2500,
+    calc_walker_acceptance_bool: bool = False,
+) -> None:
+    """This function runs a MCMC sampling for the given model and data.
+
+    Args:
+        model (Model): The model describing the mapping from parameters to data.
+        data (np.ndarray): The data to be used for the inference.
+        result_manager (ResultManager): The result manager to be used for the inference.
+        slices (np.ndarray): A list of slices to be used for the inference.
+        num_processes (int): The number of processes to be used for the inference.
+        num_runs (int, optional): The number of runs to be used for the inference. Defaults to 2.
+        num_walkers (int, optional): The number of walkers to be used for the inference. Defaults to 10.
+        num_steps (int, optional): The number of steps to be used for the inference. Defaults to 2500.
+        calc_walker_acceptance_bool (bool, optional): If True, the acceptance rate of the walkers is calculated and printed. Defaults to False.
+
     """
 
-    if dataPath is not None:
-        model.setDataPath(dataPath)
-    else:
-        logger.warning(
-            f"No data path provided for this inference call. Using the data path of the model: {model.dataPath}"
+    for slice in slices:
+        run_emcee_sampling(
+            model=model,
+            data=data,
+            slice=slice,
+            result_manager=result_manager,
+            num_runs=num_runs,
+            num_walkers=num_walkers,
+            num_steps=num_steps,
+            num_processes=num_processes,
         )
-
-    runEmceeSampling(model, numRuns, numWalkers, numSteps, numProcesses)
-    concatenateEmceeSamplingResults(model)
+        if calc_walker_acceptance_bool:
+            num_burn_in_steps = int(num_steps * 0.01)
+            acceptance = calc_walker_acceptance(
+                model, slice, num_walkers, num_burn_in_steps, result_manager
+            )
+            logger.info(f"Acceptance rate for slice {slice}: {acceptance}")
diff --git a/epi/core/sparsegrid.py b/epi/core/sparsegrid.py
index ce8e418f..2f05845f 100644
--- a/epi/core/sparsegrid.py
+++ b/epi/core/sparsegrid.py
@@ -1,35 +1,39 @@
-"""This module provides functions to handle create Sparse Grids (SGs_) and work with them.
-   All pure SG functions are defined on the unit hypercube $[0,1]^d$.
+""" This module provides functions to handle create Sparse Grids (SGs_) and work with them.
+    All pure SG functions are defined on the unit hypercube $[0,1]^d$.
 
+    .. warning::
+
+        The inference with this class is not tested and not recommended for use!
 
 .. _SGs: https://en.wikipedia.org/wiki/Sparse_grid
 """
 
+import typing
 from functools import partial
 from multiprocessing import Pool
 
 import numpy as np
 
 from epi import logger
-from epi.core.functions import evalLogTransformedDensity
+from epi.core.kde import calc_kernel_width
 from epi.core.model import Model
-
-NUM_LEVELS = 5
-NUM_PROCESSES = 4
+from epi.core.result_manager import ResultManager
+from epi.core.transformations import eval_log_transformed_density
 
 
-def basis1D(
+def basis_1d(
     points1D: np.ndarray, centre1D: np.double, level: int
 ) -> np.ndarray:
-
     """Evaluate a 1D hat function in an array of doubles. The hat is centered around centre1D
-       and the hat's level defines its support. The support shrinks exponentially with growing level and a level of 0 is equivalent with full support on [0,1].
+    and the hat's level defines its support. The support shrinks exponentially with growing level and a level of 0 is equivalent with full support on [0,1].
 
-    Input: points1D (np.1darray of 1D evaluation coordinate doubles)
-           centre1D (np.double indicating the centre of the hat within the interval [0,1])
-           level (int specifying the size/extend/support of the hat function)
+    Args:
+        points1D(np.ndarray): The points at which the hat function should be evaluated.
+        centre1D(np.double): The centre of the hat function.
+        level(int): The level of the hat function. The level defines the support of the hat function.
 
-    Output: (np.1darray (size equivalent to size of points1D) of hat function evaluations)
+    Returns:
+        np.ndarray: The hat function evaluated at the given points.
 
     """
 
@@ -38,17 +42,19 @@ def basis1D(
     )
 
 
-def basisnD(
+def basis_nd(
     points: np.ndarray, centre: np.ndarray, levels: np.ndarray
 ) -> np.ndarray:
-
     """Use a tensor product to generalise the 1D basis function to arbitrarily high dimensions.
 
-    Input: points (np.2darray of shape #Points x #Dims indicating the basis evaluation coordinates in nD)
-           centre (np.1darray of shape #Dims defining the nD centre of an nD basis function)
-           levels (np.1darray of type int and shape #Dims defining one basis function level per dimension)
+    Args:
+        points(np.ndarray): The points at which the basis function should be evaluated. Shape: (numPoints, numDims)
+        centre(np.ndarray): The centre of the basis function. Shape: (numDims,)
+        levels(np.ndarray): The levels of the basis function. Shape: (numDims,)
+
+    Returns:
+        np.ndarray: The basis function evaluated at the given points. Shape: (numPoints,)
 
-    Output: basisEval (np.1darray of shape #Points returning one nD basis evaluation per specified evaluation point)
     """
 
     # Initialize the basis evaluation of each point as 1
@@ -57,31 +63,34 @@ def basisnD(
     # loop over all dimensions
     for d in range(points.shape[1]):
         # Multipy the current basis evaluation with the evaluation result of the current dimension
-        basisEval *= basis1D(points[:, d], centre[d], levels[d])
+        basisEval *= basis_1d(points[:, d], centre[d], levels[d])
 
     return basisEval
 
 
-def meshgrid2Matrix(meshgrid: list) -> np.ndarray:
+def meshgrid2matrix(meshgrid: list) -> np.ndarray:
     """Convert a np.meshgrid into a np.2darray of grid points.
     The function is mainly used when assigning grid points to Smolnyak-Subspaces.
 
-    Input: meshgrid (list of np.arrays returned by np.meshgrid)
+    Args:
+        meshgrid(list): A list of np.arrays returned by np.meshgrid
+
+    Returns:
+        np.ndarray: A matrix of shape #Points x #Dims defining all grid points
 
-    Output: matrix (np.2darray of shape #Points x #Dims defining all grid points)
     """
 
     # calculate the shape of the matrix and initialize with 0s
     dim = len(meshgrid)
-    nPoints = np.prod(meshgrid[0].shape)
+    n_points = np.prod(meshgrid[0].shape)
 
-    matrix = np.zeros((nPoints, dim))
+    matrix = np.zeros((n_points, dim))
 
     # read out the respective meshgrid entry for each matrix entry
     for d in range(dim):
-        linearMeshSlice = np.reshape(meshgrid[d], -1)
-        for p in range(nPoints):
-            matrix[p, d] = linearMeshSlice[p]
+        linear_mesh_slice = np.reshape(meshgrid[d], -1)
+        for p in range(n_points):
+            matrix[p, d] = linear_mesh_slice[p]
 
     return matrix
 
@@ -90,169 +99,191 @@ class SparseGrid(object):
     """Each object of this class respresents a sparse grid.
     In this implementation, a sparse grid is a list of Smolnyak-subspaces.
     Each subspace is in principle a regular grid of a certain grid width but every second grid point is negelcted.
+
+    Attributes:
+        dim (int): The dimension of the sparse grid. This is the same as the dimension of the parameter space.
+        max_level_sum (int): The maximum sum of all levels of the subspaces. This is the same as the maximum level of the sparse grid.
+        subspace_list (list): A list of all subspaces that are part of the sparse grid.
+        levels2index (dict): A dictionary that maps the level combination of a subspace to its index in the subspace_list.
+        nSubspaces (int): The number of subspaces in the sparse grid.
+        n_points (int): The number of grid points in the sparse grid.
+        index_list4top_down_sparse_grid_traverse[ (list): A list of indices that defines an ordering of subspaces where low-level subspaces come before high-level ones.
+        allPoints (np.ndarray): A matrix of shape #Points x #Dims defining all grid points in the sparse grid.
+
     """
 
-    def __init__(self, dim: int, maxLevelSum: int) -> None:
+    def __init__(self, dim: int, max_level_sum: int) -> None:
         """Constructor for a sparse grid.
         A sparse grid is uniquely defined by its dimension and a level sum that must not be exceeded by any of the Smolnyak subspaces.
         A subspace's levels define how fine the grid is resolved in each of the respective dimensions.
         The position of a certain subspace within the list of subspaces can be tracked using the levels2index dictionary.
         As we only limit the sum of all levels, the sparse grids implemented here are not refined in a dimension-dependent way.
 
-        Input: dim (int specifying the (parameter) dimension)
-               maxLevelSum (int specifying how refined the sparse grid shall be)
+        Args:
+            dim (int): The dimension of the sparse grid. This is the same as the dimension of the parameter space.
+            max_level_sum (int): The maximum sum of all levels of the subspaces. This is the same as the maximum level of the sparse grid.
+
         """
 
         self.dim = dim
-        self.maxLevelSum = maxLevelSum
+        self.max_level_sum = max_level_sum
 
         # initiation of the root, list of subspaces and dictionary that maps the level-combination to the list-index
         root = Subspace(np.zeros(dim, dtype="int"), self)
-        self.subspaceList = [root]
+        self.subspace_list = [root]
         self.levels2index = {}
         self.levels2index[tuple(np.zeros(dim, dtype="int"))] = 0
 
-        # refine root by calling the recursive function refineSubspace and count resulting subspaces and grid points
-        self.refineSubspace(np.zeros(dim, dtype="int"), 0)
-        self.nSubspaces = len(self.subspaceList)
-        self.computeNPoints()
+        # refine root by calling the recursive function refine_subspace and count resulting subspaces and grid points
+        self.refine_subspace(np.zeros(dim, dtype="int"), 0)
+        self.nSubspaces = len(self.subspace_list)
+        self.compute_n_points()
 
         # create an ordering of subspaces where low-level subspaces come before high-level ones
-        self.computeIndexList4TopDownSparseGridTraverse()
+        self.compute_index_list4top_down_sparse_grid_traverse()
 
         # collect all points from all subspaces
-        self.computeAllPoints()
+        self.compute_all_points()
 
-    def refineSubspace(
-        self, currentLevels: np.ndarray, indexRefinedLevel: int
+    def refine_subspace(
+        self, current_levels: np.ndarray, indexRefinedLevel: int
     ) -> None:
         """Recursive function used to accumulate all subspaces up to a specified level sum in the form of a list
         It returns the list itself together with a dictionary that maps the level-combination of each subspace onto its index inside the list.
         This function only lists each subspace once.
 
-        Input: currentLevels (np.1darray of type int and shape #Dims that specifies the subspace we are currently considering)
-               indexRefinedLevel (int that stores the index that got altered to form the current subspace)
+        Args:
+            current_levels (np.ndarray): The level combination of the subspace that is currently being refined. Shape (dim,)
+            indexRefinedLevel (int): The index of the level that was refined to form the current subspace.
+
         """
 
         # This loop makes sure that each subspace is only counted once.
         # Achieved by storing the index that got altered to form the current subspace and letting the current
         # ... subspace only refine level indices with similar or higher entry number in the levels array.
         for i in range(indexRefinedLevel, self.dim):
-
             # derive the level increment array and calculate new level
-            levelsIncrement = np.zeros(self.dim, dtype="int")
-            levelsIncrement[i] = 1
+            levels_increment = np.zeros(self.dim, dtype="int")
+            levels_increment[i] = 1
 
-            newLevels = currentLevels + levelsIncrement
+            new_levels = current_levels + levels_increment
 
             # kill-condition for recursion if max level is reached
-            if np.sum(newLevels) <= self.maxLevelSum:
-
+            if np.sum(new_levels) <= self.max_level_sum:
                 # store refined subspace in list and dictionary
-                self.levels2index[tuple(newLevels)] = len(self.subspaceList)
-                self.subspaceList.append(Subspace(newLevels, self))
+                self.levels2index[tuple(new_levels)] = len(self.subspace_list)
+                self.subspace_list.append(Subspace(new_levels, self))
 
                 # recursive call to refine refined subspace
-                self.refineSubspace(newLevels, i)
+                self.refine_subspace(new_levels, i)
 
-    def computeNPoints(self):
+    def compute_n_points(self):
         """Iterates over all subspaces of the sparse grid and accumulates the total number of gridpoints."""
 
         # initiate the counter to be 0
-        self.nPoints = 0
+        self.n_points = 0
 
         # loop over all subspaces
         for s in range(self.nSubspaces):
-
             # get current subspace
-            currentSubspace = self.subspaceList[s]
+            current_subspace = self.subspace_list[s]
             # add the number of points in the current subspace
-            self.nPoints += currentSubspace.nPoints
+            self.n_points += current_subspace.n_points
 
-    def computeAllPoints(self):
+    def compute_all_points(self):
         """Collect all SG points in one array by iterating over all subspaces."""
 
         # allocate enough storage for all points
-        self.points = np.zeros((self.nPoints, self.dim))
+        self.points = np.zeros((self.n_points, self.dim))
 
         # initiate a counter for the number of already counted points
-        numIncludedPoints = 0
+        num_included_points = 0
 
         # loop over all subspaces of the SG
         for i in range(self.nSubspaces):
             # traverse the SG in a top-down manner
-            currentSubspace = self.subspaceList[
-                self.indexList4TopDownSparseGridTraverse[i]
+            current_subspace = self.subspace_list[
+                self.index_list4top_down_sparse_grid_traverse[i]
             ]
 
             # copy the points from the subspace into the array of the SG
             self.points[
-                numIncludedPoints : numIncludedPoints
-                + currentSubspace.nPoints,
+                num_included_points : num_included_points
+                + current_subspace.n_points,
                 :,
-            ] = currentSubspace.points
+            ] = current_subspace.points
 
             # increase the counter accordingly
-            numIncludedPoints += currentSubspace.nPoints
+            num_included_points += current_subspace.n_points
 
-    def computeIndexList4TopDownSparseGridTraverse(self):
+    def compute_index_list4top_down_sparse_grid_traverse(self):
         """Create an ordering of subspaces where low-level subspaces come before high-level ones."""
+
         # allocate storage to count the sum of levels of each subspace
-        levelSums = np.zeros(self.nSubspaces, dtype="int")
+        level_sums = np.zeros(self.nSubspaces, dtype="int")
 
         # loop over all subspaces and sum over their levels array
         for i in range(self.nSubspaces):
-            levelSums[i] = np.sum(list(self.levels2index)[i])
+            level_sums[i] = np.sum(list(self.levels2index)[i])
 
         # argument sort by the just-calculated level-sum
-        self.indexList4TopDownSparseGridTraverse = np.argsort(levelSums)
+        self.index_list4top_down_sparse_grid_traverse = np.argsort(level_sums)
 
-    def evalFunctionSG(self, function):
-        """Evaluate the provided function for all subspaces of a sparse grid by using Subspace.evalFunction
+    # TODO: Shouldn't an eval function return something?
+    def eval_function_sg(self, function: typing.Callable):
+        """Evaluate the provided function for all subspaces of a sparse grid by using Subspace.eval_function
 
-        Input: function (python function that can be evaluated in one sparse grid point)
+        Args:
+            function (typing.Callable): The function that is to be evaluated. It must be possible to evaluate the function in a single sparse grid point.
         """
 
         # loop over all subspaces
         for s in range(self.nSubspaces):
-            # call evalFunction for the current subspace
-            self.subspaceList[s].evalFunction(function)
+            # call eval_function for the current subspace
+            self.subspace_list[s].eval_function(function)
 
-    def computeCoefficients(self):
+    def compute_coefficients(self):
         """When using sparse grids for function interpolation (and quadrature),
         this function computes the coefficients of all basis function of the whole sparse grid.
+
+        Args:
+
+        Returns:
+
         """
 
         # loop over all smolnyak subspaces in a low to high level order
         for s in range(self.nSubspaces):
-            currentSubspace = self.subspaceList[
-                self.indexList4TopDownSparseGridTraverse[s]
+            current_subspace = self.subspace_list[
+                self.index_list4top_down_sparse_grid_traverse[s]
             ]
 
             # calculate coefficients for the current subspace (consider contributions from "larger" basis functions)
-            currentSubspace.coeffs = (
-                currentSubspace.fEval - currentSubspace.lowerLevelContributions
+            current_subspace.coeffs = (
+                current_subspace.f_eval
+                - current_subspace.lower_level_contributions
             )
 
             # pass up contributions arising from the just-computed coefficients to
             # ... all higher levels if there are any
-            if np.sum(currentSubspace.levels) < self.maxLevelSum:
-                currentSubspace.passContributions2HigherLevels()
+            if np.sum(current_subspace.levels) < self.max_level_sum:
+                current_subspace.pass_contributions2higher_levels()
 
-    def computeIntegral(self):
-        """Perform sparse grid integration over whole Sparse Grid using the computed coefficients (coeffs) and the volume of each basis function (basisFuncVol)"""
+    def compute_integral(self):
+        """Perform sparse grid integration over whole Sparse Grid using the computed coefficients (coeffs) and the volume of each basis function (basis_func_vol)"""
         # initialise the integral to be 0
         self.integral = 0
 
         # loop over all subspaces
         for s in range(self.nSubspaces):
             # exrtact the current subspace
-            currentSubspace = self.subspaceList[s]
+            current_subspace = self.subspace_list[s]
 
             # multiply the volume of each basis function with the sum of all coefficients of this subspace and add the result to the integral
             # (this implicitely uses that all basis functions of a given subspace have the same volume)
             self.integral += (
-                np.sum(currentSubspace.coeffs) * currentSubspace.basisFuncVol
+                np.sum(current_subspace.coeffs)
+                * current_subspace.basis_func_vol
             )
 
 
@@ -262,29 +293,30 @@ class Subspace(object):
     def __init__(self, levels: np.ndarray, SG: SparseGrid) -> None:
         """Initialize the subspace by assigning a level, dimension, number of points and the actual points themselves.
 
-        Input: levels (np.1darray of type int and shape #Dims that specifies the refinement of the current subspace in each dimension)
-               SG: (SparseGrid of which the current subspace is a part)
+        Args:
+            levels (np.ndarray): The level of the subspace in each dimension. Shape: (#Dims, )
+            SG (SparseGrid): The sparse grid of which the current subspace is a part.
+
         """
 
         # fill all known information into the class variables
         self.SG = SG
         self.levels = levels
         self.dim = levels.shape[0]
-        self.basisFuncVol = np.power(0.5, np.sum(self.levels + 1))
-        self.nPoints = np.prod(np.power(2, levels))
+        self.basis_func_vol = np.power(0.5, np.sum(self.levels + 1))
+        self.n_points = np.prod(np.power(2, levels))
 
         # this variable is created without being directly filled -> Caution when using it; Check for reliable data
-        self.lowerLevelContributions = np.zeros(self.nPoints)
+        self.lower_level_contributions = np.zeros(self.n_points)
 
         # Create all points of the current subspace and fill self.points
         # Start by creating an empty list to store the coordinates of all single dimensions
-        singleDimPoints = []
+        single_dim_points = []
 
         # loop over all dimensions
         for d in range(self.dim):
-
             # append a list of 1d coordinates for each dimension
-            singleDimPoints.append(
+            single_dim_points.append(
                 np.linspace(
                     1 / np.power(2, levels[d] + 1),
                     1 - 1 / np.power(2, levels[d] + 1),
@@ -293,39 +325,40 @@ def __init__(self, levels: np.ndarray, SG: SparseGrid) -> None:
             )
 
         # create all possible combinations from the 1d coordinate arrays
-        meshgrid = np.meshgrid(*singleDimPoints)
+        meshgrid = np.meshgrid(*single_dim_points)
 
-        # convert the numpy meshgrid to a matrix of all points with shape (nPoints,dim)
-        self.points = meshgrid2Matrix(meshgrid)
+        # convert the numpy meshgrid to a matrix of all points with shape (n_points,dim)
+        self.points = meshgrid2matrix(meshgrid)
 
-    def evalFunction(self, function):
+    def eval_function(self, function: typing.Callable):
         """Evaluate a function in all points of the respective subspace.
-        This function is typically called by SparseGrid.evalFunctionSG.
+        This function is typically called by SparseGrid.eval_function_sg.
+
+        Args:
+            function (typing.Callable): The function that is to be evaluated. It must be possible to evaluate the function in a single sparse grid point.
 
-        Input: function (python function that can be evaluated in one sparse grid point)
         """
         # create an empty array of size #Points
-        self.fEval = np.zeros(self.nPoints)
+        self.f_eval = np.zeros(self.n_points)
 
         # loop over all grid points of the subspace
-        for i in range(self.nPoints):
+        for i in range(self.n_points):
             # evaluate the provided function in the current grid point
-            self.fEval[i] = function(self.points[i, :])
+            self.f_eval[i] = function(self.points[i, :])
 
-    def passContributions2HigherLevels(self):
+    def pass_contributions2higher_levels(self):
         """During sparse grid interpolation, this function passes contributions to all subspaces with higher level."""
 
         # loop over all subspaces of the SG (this can be made more efficient)
         for s in range(self.SG.nSubspaces):
-            higherLevelSubspace = self.SG.subspaceList[s]
+            higherLevelSubspace = self.SG.subspace_list[s]
 
             # check if the higherLevelSubspace indeed has a higher level
             if np.sum(higherLevelSubspace.levels) > np.sum(self.levels):
-
                 # loop over all points in the mother subspace and add contributions to lower levels
-                for p in range(self.nPoints):
-                    higherLevelSubspace.lowerLevelContributions += (
-                        basisnD(
+                for p in range(self.n_points):
+                    higherLevelSubspace.lower_level_contributions += (
+                        basis_nd(
                             higherLevelSubspace.points,
                             self.points[p, :],
                             self.levels,
@@ -334,109 +367,75 @@ def passContributions2HigherLevels(self):
                     )
 
 
-def sparseGridInference(
+def inference_sparse_grid(
     model: Model,
-    dataPath: str = None,
-    numLevels: int = NUM_LEVELS,
-    numProcesses: int = NUM_PROCESSES,
+    data: np.ndarray,
+    result_manager: ResultManager,
+    slices: typing.List[np.ndarray],
+    num_processes: int,
+    numLevels: int = 5,
 ):
     """Evaluates the transformed parameter density over a set of points resembling a sparse grid, thereby attempting parameter inference. If a data path is given, it is used to load the data for the model. Else, the default data path of the model is used.
 
+    Args:
+      model(Model): The model describing the mapping from parameters to data.
+      data(np.ndarray): The data to be used for inference.
+      num_processes(int): number of processes to use for parallel evaluation of the model.
+      numLevels(int, optional): Maximum sparse grid level depth that mainly defines the number of points. Defaults to 5.
 
-    :param model: The model describing the mapping from parameters to data.
-    :type model: Model
-    :param dataPath: path to the data relative to the current working directory.
-                      If None, the default path defined in the Model class initializer is used, defaults to None
-    :type dataPath: str, optional
-    :param numLevels: Maximum sparse grid level depth that mainly defines the number of points, defaults to NUM_LEVELS
-    :type numLevels: int, optional
-    :param numProcesses: number of processes to use, defaults to NUM_PROCESSES
-    :type numProcesses: int, optional
     """
 
-    # check if a data path is specified
-    if dataPath is not None:
-        model.setDataPath(dataPath)
-    # default to the path defined in the model if no other indication is given.
-    else:
-        logger.warning(
-            f"No data path provided for this inference call. Using the data path of the model: {model.dataPath}"
-        )
+    logger.warning(
+        "The inference_sparse_grid function is not tested and not recommended for use."
+    )
 
     # Load data, data standard deviations and model characteristics for the specified model.
-    (
-        dataDim,
-        data,
-        dataStdevs,
-    ) = model.dataLoader()
+    data_stdevs = calc_kernel_width(data)
 
-    paramDim = model.paramDim
+    for slice in slices:
+        # build the sparse grid over [0,1]^param_dim
+        grid = SparseGrid(slice.shape[0], numLevels)
 
-    # build the sparse grid over [0,1]^paramDim
-    grid = SparseGrid(paramDim, numLevels)
+        # get the model's parameter limits
+        param_limits = model.param_limits
 
-    # get the model's parameter limits
-    parameterLimits = model.getParamSamplingLimits()
+        # scale the sparse grid points from [0,1]^param_dim to the scaled parameter space
+        scaledSparseGridPoints = param_limits[slice, 0] + grid.points * (
+            param_limits[slice, 1] - param_limits[slice, 0]
+        )
 
-    # scale the sparse grid points from [0,1]^paramDim to the scaled parameter space
-    scaledSparseGridPoints = parameterLimits[:, 0] + grid.points * (
-        parameterLimits[:, 1] - parameterLimits[:, 0]
-    )
+        # allocate Memory for the parameters, their simulation evaluation and their probability density
+        results = np.zeros(
+            (grid.n_points, slice.shape[0] + model.data_dim + 1)
+        )
 
-    # allocate Memory for the parameters, their simulation evaluation and their probability density
-    allRes = np.zeros((grid.nPoints, paramDim + model.dataDim + 1))
-
-    # Create a pool of worker processes
-    pool = Pool(processes=numProcesses)
-
-    # evaluate the probability density transformation for all sparse grid points in parallel
-    parResults = pool.map(
-        partial(
-            evalLogTransformedDensity,
-            model=model,
-            data=data,
-            dataStdevs=dataStdevs,
-        ),
-        scaledSparseGridPoints,
-    )
+        # Create a pool of worker processes
+        pool = Pool(processes=num_processes)
+
+        # evaluate the probability density transformation for all sparse grid points in parallel
+        parResults = pool.map(
+            partial(
+                eval_log_transformed_density,
+                model=model,
+                data=data,
+                data_stdevs=data_stdevs,
+                slice=slice,
+            ),
+            scaledSparseGridPoints,
+        )
 
-    # close the worker pool
-    pool.close()
-    pool.join()
-
-    # extract the parameter, simulation result and transformed density evaluation
-    for i in range(grid.nPoints):
-        allRes[i, :] = parResults[i][1]
-
-    # Save all sparse grid evaluation results in separate .csv files that also indicate the sparse grid level.
-    np.savetxt(
-        "Applications/"
-        + model.getModelName()
-        + "/Params/SG"
-        + str(numLevels)
-        + "Levels.csv",
-        allRes[:, 0:paramDim],
-        delimiter=",",
-    )
-    np.savetxt(
-        "Applications/"
-        + model.getModelName()
-        + "/SimResults/SG"
-        + str(numLevels)
-        + "Levels.csv",
-        allRes[:, paramDim : paramDim + dataDim],
-        delimiter=",",
-    )
-    np.savetxt(
-        "Applications/"
-        + model.getModelName()
-        + "/DensityEvals/SG"
-        + str(numLevels)
-        + "Levels.csv",
-        allRes[:, -1],
-        delimiter=",",
-    )
+        # close the worker pool
+        pool.close()
+        pool.join()
 
+        # extract the parameter, simulation result and transformed density evaluation
+        for i in range(grid.n_points):
+            results[i, :] = parResults[i][1]
 
-# TODO: Use maybe only one function for storing allRes
-# TODO: Plotting for general dimension
+        # save the results
+        result_manager.save_overall(
+            slice,
+            results[:, 0 : data.shape[1]],
+            results[:, data.shape[1] : data.shape[1] + slice.shape[0]],
+            results[:, data.shape[1] + slice.shape[0] :],
+        )
diff --git a/epi/core/transformations.py b/epi/core/transformations.py
new file mode 100644
index 00000000..e3dc2000
--- /dev/null
+++ b/epi/core/transformations.py
@@ -0,0 +1,144 @@
+from typing import Tuple
+
+import jax.numpy as jnp
+import numpy as np
+from jax import jit
+
+from epi import logger
+from epi.core.kde import eval_kde_gauss
+from epi.core.model import Model
+
+
+def evaluate_density(
+    param: np.ndarray,
+    model: Model,
+    data: np.ndarray,
+    data_stdevs: np.ndarray,
+    slice: np.ndarray,
+) -> Tuple[np.double, np.ndarray]:
+    """Given a simulation model, its derivative and corresponding data, evaluate the parameter density that is the backtransformed data distribution.
+
+    Args:
+        param (np.ndarray): parameter for which the transformed density shall be evaluated
+        model (Model): model to be evaluated
+        data (np.ndarray): data for the model. 2D array with shape (#num_data_points, #data_dim)
+        data_stdevs (np.ndarray): array of suitable kernel width for each data dimension
+        slice (np.ndarray): slice of the parameter vector that is to be evaluated
+
+    Returns:
+        Tuple[np.double, np.ndarray]:
+            : parameter density at the point param
+            : vector containing the parameter, the simulation result and the density
+    """
+
+    limits = model.param_limits
+
+    # Build the full parameter vector for evaluation based on the passed param slice and the constant central points
+    fullParam = model.central_param
+    fullParam[slice] = param
+
+    # Check if the tried parameter is within the just-defined bounds and return the lowest possible density if not.
+    if np.any((param < limits[slice, 0]) | (param > limits[slice, 1])):
+        logger.info(
+            "Parameters outside of predefined range"
+        )  # Slows down the sampling to much? -> Change logger level to warning or even error
+        return 0, np.zeros(slice.shape[0] + model.data_dim + 1)
+
+    # If the parameter is within the valid ranges...
+    else:
+        # Evaluating the model and the jacobian for the specified parameter simultaneously provide a little speedup over calculating it separately in some cases.
+        sim_res, jac = model.forward_and_jacobian(fullParam)
+
+        # Evaluate the data density in the simulation result.
+        densityEvaluation = eval_kde_gauss(data, sim_res, data_stdevs)
+
+        # Calculate the simulation model's pseudo-determinant in the parameter point (also called the correction factor).
+        correction = calc_gram_determinant(jac)
+
+        # Multiply data density and correction factor.
+        trafo_density_evaluation = densityEvaluation * correction
+
+        # Store the current parameter, its simulation result as well as its density in a large vector that is stored separately by emcee.
+        evaluation_results = np.concatenate(
+            (param, sim_res, np.array([trafo_density_evaluation]))
+        )
+
+        return trafo_density_evaluation, evaluation_results
+
+
+def eval_log_transformed_density(
+    param: np.ndarray,
+    model: Model,
+    data: np.ndarray,
+    data_stdevs: np.ndarray,
+    slice: np.ndarray,
+) -> Tuple[np.double, np.ndarray]:
+    """Given a simulation model, its derivative and corresponding data, evaluate the natural log of the parameter density that is the backtransformed data distribution.
+        This function is intended to be used with the emcee sampler and can be implemented more efficiently at some points.
+
+    Args:
+        param (np.ndarray): parameter for which the transformed density shall be evaluated
+        model (Model): model to be evaluated
+        data (np.ndarray): data for the model. 2D array with shape (#num_data_points, #data_dim)
+        data_stdevs (np.ndarray): array of suitable kernel width for each data dimension
+        slice (np.ndarray): slice of the parameter vector that is to be evaluated
+
+    Returns:
+        Tuple[np.double, np.ndarray]:
+            : natural log of the parameter density at the point param
+            : sampler_results (array concatenation of parameters, simulation results and evaluated density, stored as "blob" by the emcee sampler)
+
+    """
+    trafo_density_evaluation, evaluation_results = evaluate_density(
+        param, model, data, data_stdevs, slice
+    )
+    if trafo_density_evaluation == 0:
+        return -np.inf, evaluation_results
+    return np.log(trafo_density_evaluation), evaluation_results
+
+
+def calc_gram_determinant(jac: jnp.ndarray) -> jnp.double:
+    """Evaluate the pseudo-determinant of the jacobian (that serves as a correction term) in one specific parameter point.
+    Returns 0 if the correction factor is not finite.
+
+    Args:
+      jac(jnp.ndarray): The jacobian for which the pseudo determinant shall be calculated
+
+    Returns:
+        jnp.double: The pseudo-determinant of the jacobian
+
+    """
+    correction = _calc_gram_determinant(jac)
+    # If the correction factor is not finite, return 0 instead to not affect the sampling.
+    if not jnp.isfinite(correction):
+        correction = 0.0
+        logger.warning("Invalid value encountered for correction factor")
+    return correction
+
+
+@jit
+def _calc_gram_determinant(jac: jnp.ndarray) -> jnp.double:
+    """Jitted calculation of the pseudo-determinant of the jacobian. This function is called by calc_gram_determinant() and should not be called directly.
+    It does not check if the correction factor is finite.
+
+    Not much faster than a similar numpy version. However it can run on gpu and is maybe a bit faster because we can jit compile the sequence of operations.
+
+    Args:
+        jac (jnp.ndarray): The jacobian for which the pseudo determinant shall be calculated
+
+    Returns:
+        jnp.double: The pseudo-determinant of the jacobian
+
+    """
+
+    jac = jnp.atleast_2d(jac)
+
+    if jac.shape[0] == jac.shape[1]:
+        return jnp.abs(jnp.linalg.det(jac))
+    else:
+        jacT = jnp.transpose(jac)
+        # The pseudo-determinant is calculated as the square root of the determinant of the matrix-product of the Jacobian and its transpose.
+        # For numerical reasons, one can regularize the matrix product by adding a diagonal matrix of ones before calculating the determinant.
+        # correction = np.sqrt(np.linalg.det(np.matmul(jacT,jac) + np.eye(param.shape[0])))
+        correction = jnp.sqrt(jnp.linalg.det(jnp.matmul(jacT, jac)))
+        return correction
diff --git a/epi/examples/corona/corona.py b/epi/examples/corona/corona.py
index b7e0ab66..2b9a9b9e 100644
--- a/epi/examples/corona/corona.py
+++ b/epi/examples/corona/corona.py
@@ -1,44 +1,40 @@
-import importlib.resources
+from typing import Optional
 
 import diffrax as dx
 import jax.numpy as jnp
 import numpy as np
-from jax import vmap
 
 from epi import logger
-from epi.core.model import (
-    ArtificialModelInterface,
-    JaxModel,
-    VisualizationModelInterface,
-)
+from epi.core.model import ArtificialModelInterface, JaxModel
 
 
-class Corona(JaxModel, VisualizationModelInterface):
+class Corona(JaxModel):
+    """ """
 
-    paramDim = 3
-    dataDim = 4
+    param_dim = 3
+    data_dim = 4
 
-    def __init__(self, delete=False, create=False):
-        super().__init__(delete, create)
-        self.dataPath = importlib.resources.path(
-            "epi.examples.corona", "CoronaData.csv"
-        )
+    PARAM_LIMITS = np.array([[-4.5, 0.5], [-2.0, 3.0], [-2.0, 3.0]])
+    CENTRAL_PARAM = np.array([-1.8, 0.0, 0.7])
 
-    def getDataBounds(self):
-        return np.array([[0.0, 4.0], [0.0, 40.0], [0.0, 80.0], [0.0, 3.5]])
-
-    def getParamBounds(self):
-        return np.array([[-4.0, 0.0], [-2.0, 2.0], [-1.0, 3.0]])
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(central_param, param_limits, name=name, **kwargs)
 
-    def getParamSamplingLimits(self):
-        return np.array([[-4.5, 0.5], [-2.0, 3.0], [-2.0, 3.0]])
+    def get_data_bounds(self):
+        return np.array([[0.0, 4.0], [0.0, 40.0], [0.0, 80.0], [0.0, 3.5]])
 
-    def getCentralParam(self):
-        return np.array([-1.8, 0.0, 0.7])
+    def get_param_bounds(self):
+        return np.array([[-3.5, -0.5], [-1.0, 2.0], [-1.0, 2.0]])
 
     @classmethod
-    def forward(cls, logParam):
-        param = jnp.power(10, logParam)
+    def forward(cls, log_param):
+        param = jnp.power(10, log_param)
         xInit = jnp.array([999.0, 0.0, 1.0, 0.0])
 
         def rhs(t, x, param):
@@ -57,7 +53,7 @@ def rhs(t, x, param):
         stepsize_controller = dx.PIDController(rtol=1e-5, atol=1e-5)
 
         try:
-            odeSol = dx.diffeqsolve(
+            ode_sol = dx.diffeqsolve(
                 term,
                 solver,
                 t0=0.0,
@@ -68,7 +64,7 @@ def rhs(t, x, param):
                 saveat=saveat,
                 stepsize_controller=stepsize_controller,
             )
-            return odeSol.ys[1:5, 2]
+            return ode_sol.ys[1:5, 2]
 
         except Exception as e:
             logger.warning("ODE solution not possible!", exc_info=e)
@@ -76,28 +72,14 @@ def rhs(t, x, param):
 
 
 class CoronaArtificial(Corona, ArtificialModelInterface):
-    def generateArtificialData(
-        self, numSamples=ArtificialModelInterface.NUM_ARTIFICIAL_SAMPLES
-    ):
-        lowerBound = np.array([-1.9, -0.1, 0.6])
-        upperBound = np.array([-1.7, 0.1, 0.8])
-
-        trueParamSample = lowerBound + (
-            upperBound - lowerBound
-        ) * np.random.rand(numSamples, 3)
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
-
-    def getParamSamplingLimits(self):
-        return np.array([[-2.5, -1.0], [-0.75, 0.75], [0.0, 1.5]])
+    param_limits = np.array([[-2.5, -1.0], [-0.75, 0.75], [0.0, 1.5]])
+
+    def generate_artificial_params(self, num_samples):
+        lower_bound = np.array([-1.9, -0.1, 0.6])
+        upper_bound = np.array([-1.7, 0.1, 0.8])
+
+        true_param_sample = lower_bound + (
+            upper_bound - lower_bound
+        ) * np.random.rand(num_samples, 3)
+
+        return true_param_sample
diff --git a/epi/examples/cpp/__init__.py b/epi/examples/cpp/__init__.py
index 27c31675..e69de29b 100644
--- a/epi/examples/cpp/__init__.py
+++ b/epi/examples/cpp/__init__.py
@@ -1,3 +0,0 @@
-from .cpp_plant import CppPlant as CppPlant
-from .python_reference_plants import ExternalPlant as ExternalPlant
-from .python_reference_plants import JaxPlant as JaxPlant
diff --git a/epi/examples/cpp/cpp_plant.py b/epi/examples/cpp/cpp_plant.py
index 1aaaf79e..8a9cd264 100644
--- a/epi/examples/cpp/cpp_plant.py
+++ b/epi/examples/cpp/cpp_plant.py
@@ -11,10 +11,28 @@ class CppPlant(Model, ArtificialModelInterface):
     Data0: Size [0,2] # the more water and sun the better
     Data1: Health [0,1], to much water is not good, too much sun is not good
     Data2: Sciarid ;)
+
     """
 
-    paramDim = 2
-    dataDim = 3
+    param_dim = 2
+    data_dim = 3
+
+    PARAM_LIMITS = np.array([[0, 1], [0, 1]])
+    CENTRAL_PARAM = np.array([0.5, 0.5])
+
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: str = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(
+            central_param,
+            param_limits,
+            name,
+            **kwargs,
+        )
 
     def forward(self, param):
         return cpp_model.forward(param)
@@ -22,28 +40,5 @@ def forward(self, param):
     def jacobian(self, param):
         return cpp_model.jacobian(param)
 
-    def getCentralParam(self) -> np.ndarray:
-        return np.array([0.5, 0.5])
-
-    def getParamSamplingLimits(self) -> np.ndarray:
-        return np.array([[0.0, 1.0], [0.0, 1.0]])
-
-    def generateArtificialData(
-        self, numSamples=ArtificialModelInterface.NUM_ARTIFICIAL_SAMPLES
-    ):
-        # randomly create true parameters in [0,1]x[0,1]
-        trueParamSample = np.random.rand(numSamples, 2)
-
-        artificialData = np.zeros((trueParamSample.shape[0], 3))
-        for i in range(trueParamSample.shape[0]):
-            artificialData[i, :] = self.forward(trueParamSample[i, :])
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
+    def generate_artificial_params(self, num_samples: int):
+        return np.random.rand(num_samples, 2)
diff --git a/epi/examples/cpp/python_reference_plants.py b/epi/examples/cpp/python_reference_plants.py
index 097e572b..89ff3386 100644
--- a/epi/examples/cpp/python_reference_plants.py
+++ b/epi/examples/cpp/python_reference_plants.py
@@ -1,6 +1,6 @@
 import jax.numpy as jnp
 import numpy as np
-from jax import jacrev, jit, vmap
+from jax import jacrev, jit
 
 from epi.core.model import ArtificialModelInterface, JaxModel, Model
 
@@ -12,10 +12,28 @@ class JaxPlant(JaxModel, ArtificialModelInterface):
     Data0: Size [0,2] # the more water and sun the better
     Data1: Health [0,1], to much water is not good, too much sun is not good
     Data2: Sciarid :P
+
     """
 
-    paramDim = 2
-    dataDim = 3
+    param_dim = 2
+    data_dim = 3
+
+    PARAM_LIMITS = np.array([[0, 1], [0, 1]])
+    CENTRAL_PARAM = np.array([0.5, 0.5])
+
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: str = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(
+            central_param,
+            param_limits,
+            name,
+            **kwargs,
+        )
 
     @classmethod
     def forward(cls, param):
@@ -27,28 +45,8 @@ def forward(cls, param):
             ]
         )
 
-    def getCentralParam(self) -> np.ndarray:
-        return np.array([0.5, 0.5])
-
-    def getParamSamplingLimits(self) -> np.ndarray:
-        return np.array([[0.0, 1.0], [0.0, 1.0]])
-
-    def generateArtificialData(self, numSamples=1000):
-        # randomly create true parameters in [0,1]x[0,1]
-        trueParamSample = np.random.rand(numSamples, 2)
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
+    def generate_artificial_params(self, num_samples: int):
+        return np.random.rand(num_samples, 2)
 
 
 @jit
@@ -79,8 +77,23 @@ class ExternalPlant(Model, ArtificialModelInterface):
     Data2: Trauerfliegen :P
     """
 
-    paramDim = 2
-    dataDim = 3
+    param_dim = 2
+    data_dim = 3
+
+    PARAM_LIMITS = np.array([[0, 1], [0, 1]])
+    CENTRAL_PARAM = np.array([0.5, 0.5])
+
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: str = None,
+    ) -> None:
+        super().__init__(
+            central_param,
+            param_limits,
+            name,
+        )
 
     def forward(self, param):
         return fw(param)
@@ -88,27 +101,5 @@ def forward(self, param):
     def jacobian(self, param):
         return bw(param)
 
-    def getCentralParam(self) -> np.ndarray:
-        return np.array([0.5, 0.5])
-
-    def getParamSamplingLimits(self) -> np.ndarray:
-        return np.array([[0.0, 1.0], [0.0, 1.0]])
-
-    def generateArtificialData(
-        self, numSamples=ArtificialModelInterface.NUM_ARTIFICIAL_SAMPLES
-    ):
-        # randomly create true parameters in [0,1]x[0,1]
-        trueParamSample = np.random.rand(numSamples, 2)
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
+    def generate_artificial_params(self, num_samples: int):
+        return np.random.rand(num_samples, 2)
diff --git a/epi/examples/sbml/Caffeine_2Wks_Exponential_decay.xml b/epi/examples/sbml/Caffeine_2Wks_Exponential_decay.xml
new file mode 100644
index 00000000..0fc46bc5
--- /dev/null
+++ b/epi/examples/sbml/Caffeine_2Wks_Exponential_decay.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" level="3" version="1">
+  <model id="Caffeine_2Wks_Exponential_decay" name="Caffeine_2Wks_Exponential_decay">
+    <listOfCompartments>
+      <compartment id="Compartment" size="1" constant="true"/>
+    </listOfCompartments>
+    <listOfSpecies>
+      <species id="y" compartment="Compartment" initialAmount="0" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
+    </listOfSpecies>
+    <listOfParameters>
+      <parameter id="t" name="t" constant="false"/>
+      <parameter id="A" name="A" value="1" constant="true"/>
+      <parameter id="B" name="B" value="1" constant="true"/>
+      <parameter id="sd_y_obs" name="sd_y_obs" value="0.1" constant="true"/>
+    </listOfParameters>
+    <listOfRules>
+      <rateRule variable="y">
+        <math xmlns="http://www.w3.org/1998/Math/MathML">
+          <apply>
+            <times/>
+            <apply>
+              <minus/>
+              <cn type="integer"> 1 </cn>
+              <apply>
+                <times/>
+                <ci> A </ci>
+                <ci> t </ci>
+              </apply>
+            </apply>
+            <apply>
+              <exp/>
+              <apply>
+                <minus/>
+                <ci> B </ci>
+                <apply>
+                  <times/>
+                  <ci> A </ci>
+                  <ci> t </ci>
+                </apply>
+              </apply>
+            </apply>
+          </apply>
+        </math>
+      </rateRule>
+      <assignmentRule variable="t">
+        <math xmlns="http://www.w3.org/1998/Math/MathML">
+          <csymbol encoding="text" definitionURL="http://www.sbml.org/sbml/symbols/time"> time </csymbol>
+        </math>
+      </assignmentRule>
+    </listOfRules>
+  </model>
+</sbml>
diff --git a/epi/examples/sbml/sbml_caffeine_model.py b/epi/examples/sbml/sbml_caffeine_model.py
new file mode 100644
index 00000000..5c625eea
--- /dev/null
+++ b/epi/examples/sbml/sbml_caffeine_model.py
@@ -0,0 +1,48 @@
+import importlib
+from typing import Optional
+
+import numpy as np
+
+from epi.core.model import ArtificialModelInterface, SBMLModel
+
+
+class CaffeineSBMLModel(SBMLModel, ArtificialModelInterface):
+
+    param_dim = 2
+    data_dim = 1
+
+    CENTRAL_PARAM = np.array([1.0, 1.0])
+    PARAM_LIMITS = np.array([[0.0, 2.0], [0.0, 2.0]])
+
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        sbml_file = importlib.resources.path(
+            "epi.examples.sbml", "Caffeine_2Wks_Exponential_decay.xml"
+        )
+        param_names = ["A", "B"]
+        super().__init__(
+            sbml_file,
+            central_param,
+            param_limits,
+            param_names,
+            1.0,
+            False,
+            name,
+            **kwargs,
+        )
+
+    def generate_artificial_params(self, num_samples: int) -> np.ndarray:
+        diff0 = 0.2
+        diff1 = 0.2
+        params = np.random.rand(num_samples, self.param_dim)
+        params[:, 0] *= diff0
+        params[:, 0] += 1.0 - diff0 / 2.0
+
+        params[:, 1] *= diff1
+        params[:, 1] += 1.0 - diff1 / 2.0
+        return params
diff --git a/epi/examples/sbml/sbml_menten_model.py b/epi/examples/sbml/sbml_menten_model.py
new file mode 100644
index 00000000..8e337a81
--- /dev/null
+++ b/epi/examples/sbml/sbml_menten_model.py
@@ -0,0 +1,56 @@
+import importlib
+from typing import Optional
+
+import numpy as np
+
+from epi.core.model import ArtificialModelInterface, SBMLModel
+
+
+class MentenSBMLModel(SBMLModel, ArtificialModelInterface):
+
+    CENTRAL_PARAM = np.array([50.0, 1.0])
+    PARAM_LIMITS = np.array([[0.0, 100.0], [0.0, 2.0]])
+
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        sbml_file = importlib.resources.path(
+            "epi.examples.sbml", "sbml_menten_model.xml"
+        )
+        param_names = ["Km", "kcat"]
+        super().__init__(
+            sbml_file,
+            central_param,
+            param_limits,
+            param_names,
+            1.0,
+            False,
+            name,
+            **kwargs,
+        )
+
+    # Overwrite the forward, jacobian, and forward_and_jacobian methods to remove the first variable which is not dependent on the parameters
+    def forward(self, params) -> np.ndarray:
+        return super().forward(params)[1:]
+
+    def jacobian(self, params) -> np.ndarray:
+        return super().jacobian(params)[1:, :]
+
+    def forward_and_jacobian(self, params) -> np.ndarray:
+        val, jac = super().forward_and_jacobian(params)
+        return val[1:], jac[1:, :]
+
+    def generate_artificial_params(self, num_samples: int) -> np.ndarray:
+        diff0 = 5.0
+        diff1 = 0.2
+        params = np.random.rand(num_samples, self.param_dim)
+        params[:, 0] *= diff0
+        params[:, 0] += 50.0 - diff0 / 2.0
+
+        params[:, 1] *= diff1
+        params[:, 1] += 1.0 - diff1 / 2.0
+        return params
diff --git a/epi/examples/sbml/sbml_menten_model.xml b/epi/examples/sbml/sbml_menten_model.xml
new file mode 100644
index 00000000..38859edd
--- /dev/null
+++ b/epi/examples/sbml/sbml_menten_model.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<sbml xmlns="http://www.sbml.org/sbml/level3/version2/core" level="3" version="2">
+  <model id="Simple_Menten" name="Simple_Menten">
+    <listOfUnitDefinitions>
+      <unitDefinition metaid="METAID_U0" id="u0" name="ml">
+        <listOfUnits>
+          <unit kind="litre" exponent="1" scale="-3" multiplier="1"/>
+        </listOfUnits>
+      </unitDefinition>
+      <unitDefinition metaid="METAID_U1" id="u1" name="mmole / l">
+        <listOfUnits>
+          <unit kind="litre" exponent="-1" scale="1" multiplier="1"/>
+          <unit kind="mole" exponent="1" scale="-3" multiplier="1"/>
+        </listOfUnits>
+      </unitDefinition>
+      <unitDefinition metaid="METAID_U2" id="u2" name="umole / l">
+        <listOfUnits>
+          <unit kind="litre" exponent="-1" scale="1" multiplier="1"/>
+          <unit kind="mole" exponent="1" scale="-6" multiplier="1"/>
+        </listOfUnits>
+      </unitDefinition>
+      <unitDefinition metaid="METAID_U3" id="u3" name="1 / s">
+        <listOfUnits>
+          <unit kind="second" exponent="-1" scale="1" multiplier="1"/>
+        </listOfUnits>
+      </unitDefinition>
+      <unitDefinition metaid="METAID_U4" id="u4" name="s">
+        <listOfUnits>
+          <unit kind="second" exponent="1" scale="1" multiplier="1"/>
+        </listOfUnits>
+      </unitDefinition>
+    </listOfUnitDefinitions>
+    <listOfCompartments>
+      <compartment id="v0" name="Tube" spatialDimensions="3" size="10" units="u0" constant="true"/>
+    </listOfCompartments>
+    <listOfSpecies>
+      <species metaid="METAID_P0" sboTerm="SBO:0000252" id="p0" name="Protein" compartment="v0" initialConcentration="100" substanceUnits="u2" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="true"/>
+      <species metaid="METAID_S0" sboTerm="SBO:0000247" id="s0" name="Substrate" compartment="v0" initialConcentration="10" substanceUnits="u1" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
+      <species metaid="METAID_S1" sboTerm="SBO:0000247" id="s1" name="Product" compartment="v0" initialConcentration="0" substanceUnits="u1" hasOnlySubstanceUnits="false" boundaryCondition="false" constant="false"/>
+    </listOfSpecies>
+    <listOfParameters>
+      <parameter id="Km" value="50" units="u1" constant="false"/>
+      <parameter id="kcat" value="1" units="u3" constant="false"/>
+    </listOfParameters>
+    <listOfReactions>
+      <annotation>
+        <enzymeml:data xmlns:enzymeml="http://sbml.org/enzymeml/version2">
+          <enzymeml:formats>
+            <enzymeml:format id="format0">
+              <enzymeml:column type="time" unit="u4" index="0"/>
+              <enzymeml:column replica="simu_simulation1.yaml_s0_25.0" species="s0" type="conc" unit="u1" index="1" isCalculated="True"/>
+              <enzymeml:column replica="simu_simulation1.yaml_s1_0.0" species="s1" type="conc" unit="u1" index="2" isCalculated="True"/>
+            </enzymeml:format>
+            <enzymeml:format id="format1">
+              <enzymeml:column type="time" unit="u4" index="0"/>
+              <enzymeml:column replica="simu_simulation2.yaml_s0_100.0" species="s0" type="conc" unit="u1" index="1" isCalculated="True"/>
+              <enzymeml:column replica="simu_simulation2.yaml_s1_0.0" species="s1" type="conc" unit="u1" index="2" isCalculated="True"/>
+            </enzymeml:format>
+          </enzymeml:formats>
+          <enzymeml:listOfMeasurements>
+            <enzymeml:measurement file="file0" id="m0" name="simulation1.yaml">
+              <enzymeml:initConc reactant="s0" value="25.0" unit="u1"/>
+              <enzymeml:initConc reactant="s1" value="0.0" unit="u1"/>
+              <enzymeml:initConc reactant="p0" value="1.0" unit="u2"/>
+            </enzymeml:measurement>
+            <enzymeml:measurement file="file1" id="m1" name="simulation2.yaml">
+              <enzymeml:initConc reactant="s0" value="100.0" unit="u1"/>
+              <enzymeml:initConc reactant="s1" value="0.0" unit="u1"/>
+              <enzymeml:initConc reactant="p0" value="1.0" unit="u2"/>
+            </enzymeml:measurement>
+          </enzymeml:listOfMeasurements>
+          <enzymeml:files>
+            <enzymeml:file file="./data/m0.csv" format="format0" id="file0"/>
+            <enzymeml:file file="./data/m1.csv" format="format1" id="file1"/>
+          </enzymeml:files>
+        </enzymeml:data>
+      </annotation>
+      <reaction metaid="METAID_R0" sboTerm="SBO:0000176" id="r0" name="Reaction" reversible="false">
+        <listOfReactants>
+          <speciesReference sboTerm="SBO:0000015" species="s0" stoichiometry="1" constant="false"/>
+        </listOfReactants>
+        <listOfProducts>
+          <speciesReference sboTerm="SBO:0000011" species="s1" stoichiometry="1" constant="false"/>
+        </listOfProducts>
+        <listOfModifiers>
+          <modifierSpeciesReference sboTerm="SBO:0000013" species="p0"/>
+        </listOfModifiers>
+        <kineticLaw name="Menten-Model">
+          <math xmlns="http://www.w3.org/1998/Math/MathML">
+            <apply>
+              <divide/>
+              <apply>
+                <times/>
+                <ci> kcat </ci>
+                <ci> p0 </ci>
+                <ci> s0 </ci>
+              </apply>
+              <apply>
+                <plus/>
+                <ci> Km </ci>
+                <ci> s0 </ci>
+              </apply>
+            </apply>
+          </math>
+        </kineticLaw>
+      </reaction>
+    </listOfReactions>
+  </model>
+</sbml>
diff --git a/epi/examples/simple_models.py b/epi/examples/simple_models.py
index 2a8651c6..2566592a 100644
--- a/epi/examples/simple_models.py
+++ b/epi/examples/simple_models.py
@@ -1,86 +1,73 @@
+from typing import Optional
+
 import jax.numpy as jnp
 import numpy as np
-from jax import vmap
 
-from epi.core.model import (
-    ArtificialModelInterface,
-    JaxModel,
-    VisualizationModelInterface,
-)
+from epi.core.model import ArtificialModelInterface, JaxModel
+
 
+class Linear(JaxModel, ArtificialModelInterface):
+    param_dim = 2
+    data_dim = 2
 
-class Linear(JaxModel, ArtificialModelInterface, VisualizationModelInterface):
+    PARAM_LIMITS = np.array([[-0.2, 1.2], [-0.2, 1.2]])
+    CENTRAL_PARAM = np.array([0.5, 0.5])
 
-    paramDim = 2
-    dataDim = 2
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(central_param, param_limits, name=name, **kwargs)
 
     @classmethod
     def forward(cls, param):
         return jnp.array([param[0] * 10, (-2.0) * param[1] - 2.0])
 
-    def getCentralParam(self):
-        return np.array([0.5, 0.5])
-
-    def getParamSamplingLimits(self):
-        return np.array([[-10.0, 11.0], [-10.0, 11.0]])
-
-    def generateArtificialData(
-        self, numSamples=ArtificialModelInterface.NUM_ARTIFICIAL_SAMPLES
-    ):
-
-        # randomly create true parameters in [0,1]^2
-        trueParamSample = np.random.rand(numSamples, 2)
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
-
-    def getParamBounds(self, scale=1.0) -> np.ndarray:
-        return np.array([[-0.2, 1.2], [-0.2, 1.2]])
+    def generate_artificial_params(self, num_samples: int):
+        return np.random.rand(num_samples, self.param_dim)
 
-    def getDataBounds(self, scale=1.0) -> np.ndarray:
-        return np.array([[-2.0, 12.0], [-4.4, -1.6]])
 
+class Exponential(JaxModel):
+    param_dim = 2
+    data_dim = 2
 
-class Exponential(JaxModel, VisualizationModelInterface):
+    PARAM_LIMITS = np.array([[1.0, 2.0], [1.0, 2.0]])
+    CENTRAL_PARAM = np.array([1.0, 1.0])
 
-    paramDim = 2
-    dataDim = 2
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(central_param, param_limits, name=name, **kwargs)
 
     @classmethod
     def forward(cls, param):
         return jnp.array([param[0] * jnp.exp(1), jnp.exp(param[1])])
 
-    def getCentralParam(self) -> np.ndarray:
-        return np.array([1.0, 1.0])
 
-    def getParamSamplingLimits(self) -> np.ndarray:
-        return np.array([[1.0, 2.0], [1.0, 2.0]])
-
-    def getParamBounds(self) -> np.ndarray:
-        return np.array([0.8, 2.2], [0.8, 2.2])
+class LinearODE(JaxModel, ArtificialModelInterface):
+    param_dim = 2
+    data_dim = 2
 
-    # TODO: ???
-    # KDExGrid = np.linspace(0.8 * np.exp(1), 2.2 * np.exp(1), KDEresolution)
-    # KDEyGrid = np.linspace(np.exp(0.8), np.exp(2.2), KDEresolution)
-    # KDExMesh, KDEyMesh = np.meshgrid(KDExGrid, KDEyGrid)
-    def getDataBounds(self) -> np.ndarray:
-        return self.forward(self.getParamBounds())
+    PARAM_LIMITS = np.array([[-2.0, 4.0], [-2.0, 4.0]])
+    CENTRAL_PARAM = np.array([1.5, 1.5])
 
+    TRUE_PARAM_LIMITS = np.array([[1.0, 2.0], [1.0, 2.0]])
 
-class LinearODE(JaxModel, ArtificialModelInterface):
-
-    paramDim = 2
-    dataDim = 2
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(central_param, param_limits, name=name, **kwargs)
 
     @classmethod
     def forward(cls, param):
@@ -91,25 +78,5 @@ def forward(cls, param):
             ]
         )
 
-    def getParamSamplingLimits(self):
-        return np.array([[-10.0, 23.0], [-10.0, 23.0]])
-
-    def getCentralParam(self):
-        return np.array([1.5, 1.5])
-
-    def generateArtificialData(self, numSamples=1000):
-        # randomly create true parameters in [1,2]^2
-        trueParamSample = np.random.rand(numSamples, 2) + 1
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
+    def generate_artificial_params(self, num_samples: int):
+        return np.random.rand(num_samples, 2) + 1
diff --git a/epi/examples/stock/stock.py b/epi/examples/stock/stock.py
index c3ac60f8..3d2b560c 100644
--- a/epi/examples/stock/stock.py
+++ b/epi/examples/stock/stock.py
@@ -1,18 +1,11 @@
-import importlib
-import os
+from typing import Optional
 
 import jax.numpy as jnp
 import numpy as np
-import pandas as pd
 import yfinance as yf
-from jax import vmap
 
 from epi import logger
-from epi.core.model import (
-    ArtificialModelInterface,
-    JaxModel,
-    VisualizationModelInterface,
-)
+from epi.core.model import ArtificialModelInterface, JaxModel
 
 # Ticker source: https://investexcel.net/all-yahoo-finance-stock-tickers/#google_vignette, Date:27.10.2022
 TICKERS = [
@@ -27,91 +20,51 @@
 ]
 
 
-class Stock(JaxModel, VisualizationModelInterface):
+class Stock(JaxModel):
     """Model simulating stock data."""
 
-    dataDim = 19
-    paramDim = 6
+    data_dim = 19
+    param_dim = 6
+
+    PARAM_LIMITS = np.array([[-10.0, 10.0] for _ in range(param_dim)])
+    CENTRAL_PARAM = np.array(
+        [
+            0.41406223,
+            1.04680993,
+            1.21173553,
+            0.8078955,
+            1.07772437,
+            0.64869251,
+        ]
+    )
 
     def __init__(
-        self, delete: bool = False, create: bool = True, ticker="ETF50"
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
     ) -> None:
-        """Initialize the model and set a ticker. Can be chosen from the list of available tickers TICKERS.
-        Possibly outdated list: [ETF, Index1, Index2, Mutual, Stocks1, Stocks2, Stocks3]
+        super().__init__(central_param, param_limits, name=name, **kwargs)
 
-        :param ticker: The ticker from which the data should be used, defaults to "ETF"
-        :type ticker: str, optional
-        """
-        super().__init__(delete, create)
-        self.dataPath = f"Data/{self.getModelName()}/{ticker}Data.csv"
-
-        # Check if data for the given ticker exists
-        if not os.path.isfile(self.dataPath):
-            logger.warning("Ticker data not found. Downloading data...")
-            tickerPath = importlib.resources.path(
-                "epi.examples.stock", f"{ticker}.csv"
-            )
-            self.downloadData(tickerPath)
-
-    def getDataBounds(self):
-        return np.array([[-7.5, 7.5] * self.dataDim])
-
-    def getParamBounds(self):
-        return np.array([[-2.0, 2.0] * self.paramDim])
-
-    def getParamSamplingLimits(self):
-        return np.array([[-10.0, 10.0] * self.paramDim])
+    def download_data(self, ticker_list_path: str):
+        """Download stock data for a ticker list from yahoo finance.
 
-    def getCentralParam(self):
-        return np.array(
-            [
-                0.41406223,
-                1.04680993,
-                1.21173553,
-                0.8078955,
-                1.07772437,
-                0.64869251,
-            ]
-        )
+        Args:
+          ticker_list_path(str): path to the ticker list csv file
 
-    def downloadData(self, tickerListPath: str):
-        """Download stock data for a ticker list from yahoo finance.
+        Returns:
+            stock_data, stock_ids and the name of the tickerList
 
-        :param tickerListPath: path to the ticker list csv file
-        :type tickerListPath: str
         """
         logger.info("Downloading stock data...")
         start = "2022-01-31"
         end = "2022-03-01"
-        dates = np.array(
-            [
-                "2022-01-31",
-                "2022-02-01",
-                "2022-02-02",
-                "2022-02-03",
-                "2022-02-04",
-                "2022-02-07",
-                "2022-02-08",
-                "2022-02-09",
-                "2022-02-10",
-                "2022-02-11",
-                "2022-02-14",
-                "2022-02-15",
-                "2022-02-16",
-                "2022-02-17",
-                "2022-02-18",
-                "2022-02-22",
-                "2022-02-23",
-                "2022-02-24",
-                "2022-02-25",
-                "2022-02-28",
-            ]
-        )
 
-        stocks = np.loadtxt(tickerListPath, dtype="str")
+        stocks = np.loadtxt(ticker_list_path, dtype="str")
 
         try:
-            df: pd.DataFrame = yf.download(
+            df = yf.download(
                 stocks.tolist(), start, end, interval="1d", repair=True
             )
         except Exception as e:
@@ -137,29 +90,17 @@ def downloadData(self, tickerListPath: str):
         # Drop the row of the first day
         df = df.iloc[1:, :]
 
-        # get the remaining stockIDs and create a numpy array from the dataframe
-        stockIDs = df.columns.get_level_values(1)  # .unique()
-        stockData = df.to_numpy()
+        # get the remaining stock_ids and create a numpy array from the dataframe
+        stock_ids = df.columns.get_level_values(1)  # .unique()
+        stock_data = df.to_numpy()
 
         # get the name of the tickerList
-        if type(tickerListPath) != str:
-            tickerListName = tickerListPath.name.split(".")[0]
+        if type(ticker_list_path) != str:
+            ticker_list_name = ticker_list_path.name.split(".")[0]
         else:
-            tickerListName = tickerListPath.split("/")[-1].split(".")[0]
-
-        # save all time points except for the first
-        os.makedirs(f"Data/{self.getModelName()}", exist_ok=True)
-        np.savetxt(
-            f"Data/{self.getModelName()}/{tickerListName}Data.csv",
-            stockData.T,
-            delimiter=",",
-        )
-        np.savetxt(
-            f"Data/{self.getModelName()}/{tickerListName}IDs.csv",
-            stockIDs,
-            delimiter=",",
-            fmt="% s",
-        )
+            ticker_list_name = ticker_list_path.split("/")[-1].split(".")[0]
+
+        return stock_data.T, stock_ids, ticker_list_name
 
     @classmethod
     def forward(cls, param):
@@ -173,72 +114,36 @@ def iteration(x, param):
                 ]
             )
 
-        def repetition(x, param, numRepetitions):
-            for i in range(numRepetitions):
+        def repetition(x, param, num_repetitions):
+            for i in range(num_repetitions):
                 x = iteration(x, param)
-
             return x
 
-        x0 = jnp.zeros(2)
-        x1 = repetition(x0, param, 1)
-        x2 = repetition(x1, param, 1)
-        x3 = repetition(x2, param, 1)
-        x4 = repetition(x3, param, 1)
-        x5 = repetition(x4, param, 3)
-        x6 = repetition(x5, param, 1)
-        x7 = repetition(x6, param, 1)
-        x8 = repetition(x7, param, 1)
-        x9 = repetition(x8, param, 1)
-        x10 = repetition(x9, param, 3)
-        x11 = repetition(x10, param, 1)
-        x12 = repetition(x11, param, 1)
-        x13 = repetition(x12, param, 1)
-        x14 = repetition(x13, param, 1)
-        x15 = repetition(x14, param, 4)
-        x16 = repetition(x15, param, 1)
-        x17 = repetition(x16, param, 1)
-        x18 = repetition(x17, param, 1)
-        x19 = repetition(x18, param, 3)
-
-        timeCourse = jnp.array(
-            [
-                x1,
-                x2,
-                x3,
-                x4,
-                x5,
-                x6,
-                x7,
-                x8,
-                x9,
-                x10,
-                x11,
-                x12,
-                x13,
-                x14,
-                x15,
-                x16,
-                x17,
-                x18,
-                x19,
-            ]
-        )
-
-        return timeCourse[:, 0]
+        x = jnp.zeros(2)
+        time_course = [
+            repetition(x, param, n)
+            for n in [1, 1, 1, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 1, 4, 1, 1, 1, 3]
+        ]
+        return jnp.array([x[0] for x in time_course])
 
 
 class StockArtificial(Stock, ArtificialModelInterface):
-    def __init__(self, *args, **kwargs):
-        super(Stock, self).__init__(*args, **kwargs)
-
-    def generateArtificialData(
-        self, numSamples=ArtificialModelInterface.NUM_ARTIFICIAL_SAMPLES
-    ):
-        logger.info(
-            f"Generating {numSamples} data samples by evaluating the model. "
-            "This might take a very long time!"
+    """ """
+
+    PARAM_LIMITS = np.array([[-1.0, 3.0] for _ in range(Stock.param_dim)])
+
+    def __init__(
+        self,
+        central_param: np.ndarray = Stock.CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super(Stock, self).__init__(
+            central_param, param_limits, name=name, **kwargs
         )
 
+    def generate_artificial_params(self, num_samples):
         mean = np.array(
             [
                 0.41406223,
@@ -251,23 +156,8 @@ def generateArtificialData(
         )
         stdevs = np.array([0.005, 0.01, 0.05, 0.005, 0.01, 0.05])
 
-        trueParamSample = np.random.randn(numSamples, self.paramDim)
-        trueParamSample *= stdevs
-        trueParamSample += mean
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Params.csv",
-            trueParamSample,
-            delimiter=",",
-        )
+        true_param_sample = np.random.randn(num_samples, self.param_dim)
+        true_param_sample *= stdevs
+        true_param_sample += mean
 
-    def getParamSamplingLimits(self):
-        return np.array([[-1.0, 3.0] * self.paramDim])
+        return true_param_sample
diff --git a/epi/examples/temperature/temperature.py b/epi/examples/temperature/temperature.py
index 7c9dc738..221607cb 100644
--- a/epi/examples/temperature/temperature.py
+++ b/epi/examples/temperature/temperature.py
@@ -1,8 +1,8 @@
 import importlib
+from typing import Optional
 
 import jax.numpy as jnp
 import numpy as np
-from jax import vmap
 
 from epi.core.model import ArtificialModelInterface, Model
 
@@ -11,63 +11,67 @@
 
 
 class Temperature(Model):
+    """ """
 
-    paramDim = 1
-    dataDim = 1
+    param_dim = 1
+    data_dim = 1
 
-    def __init__(self, delete: bool = False, create: bool = True) -> None:
-        super().__init__(delete, create)
+    PARAM_LIMITS = np.array([[0, np.pi / 2]])
+    CENTRAL_PARAM = np.array([np.pi / 4.0])
 
-        self.dataPath = importlib.resources.path(
-            "epi.examples.temperature", "TemperatureData.csv"
-        )
+    def __init__(
+        self,
+        central_param: np.ndarray = CENTRAL_PARAM,
+        param_limits: np.ndarray = PARAM_LIMITS,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(central_param, param_limits, name=name, **kwargs)
 
     def forward(self, param):
-        lowT = -30.0
-        highT = 30.0
-        res = jnp.array([lowT + (highT - lowT) * jnp.cos(jnp.abs(param[0]))])
+        low_T = -30.0
+        high_T = 30.0
+        res = jnp.array(
+            [low_T + (high_T - low_T) * jnp.cos(jnp.abs(param[0]))]
+        )
         return res
 
     def jacobian(self, param):
         return jnp.array([60.0 * jnp.sin(jnp.abs(param[0]))])
 
-    def getCentralParam(self):
-        return np.array([np.pi / 4.0])
-
-    def getParamSamplingLimits(self):
-        return np.array([[0, np.pi / 2]])
-
 
 class TemperatureArtificial(Temperature, ArtificialModelInterface):
-    def generateArtificialData(self):
+    def generate_artificial_params(self, num_data_points: int = -1):
         paramPath = importlib.resources.path(
             "epi.examples.temperature", "TemperatureArtificialParams.csv"
         )
-        trueParamSample = np.loadtxt(paramPath, delimiter=",", ndmin=2)
-
-        artificialData = vmap(self.forward, in_axes=0)(trueParamSample)
-
-        np.savetxt(
-            f"Data/{self.getModelName()}Data.csv",
-            artificialData,
-            delimiter=",",
-        )
+        true_param_sample = np.loadtxt(paramPath, delimiter=",", ndmin=2)
+        return true_param_sample
 
 
 class TemperatureWithFixedParams(Temperature):
-    def __init__(self, delete: bool = False, create: bool = True) -> None:
-        super().__init__(delete, create)
-        self.lowT = -30.0
-        self.highT = 30.0
+    def __init__(
+        self,
+        low_T: np.double = -30.0,
+        high_T: np.double = 30.0,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(name=name, **kwargs)
+        self.low_T = low_T
+        self.high_T = high_T
 
     def forward(self, param):
-        return self.calcForward(param, self.highT, self.lowT)
+        return self.calc_forward(param, self.high_T, self.low_T)
 
-    def calcForward(self, param, highT, lowT):
-        res = jnp.array([lowT + (highT - lowT) * jnp.cos(jnp.abs(param[0]))])
+    def calc_forward(self, param, high_T, low_T):
+        res = jnp.array(
+            [low_T + (high_T - low_T) * jnp.cos(jnp.abs(param[0]))]
+        )
         return res
 
     def jacobian(self, param):
-        return jnp.array(
-            [(self.highT - self.lowT) * jnp.sin(jnp.abs(param[0]))]
-        )
+        return self.calc_jacobian(param, self.high_T, self.low_T)
+
+    def calc_jacobian(self, param, high_T, low_T):
+        return jnp.array([(high_T - low_T) * jnp.sin(jnp.abs(param[0]))])
diff --git a/epi/jax_extension.py b/epi/jax_extension.py
index 84a103fc..e70d2d27 100644
--- a/epi/jax_extension.py
+++ b/epi/jax_extension.py
@@ -5,14 +5,31 @@
 import jax.numpy as jnp
 
 
-def value_and_jacfwd(fun: Callable[[jnp.ndarray], jnp.ndarray]):
+def value_and_jacfwd(
+    fun: Callable[[jnp.ndarray], jnp.ndarray]
+) -> Callable[[jnp.ndarray], Callable[[jnp.ndarray], jnp.ndarray]]:
     """Returns a function that computes the value and the jacobian of the passed function using forward mode AD.
 
-    :param fun: The function to supplement with the jacobian
-    :type fun: Callable[[jnp.ndarray], jnp.ndarray]
+    Args:
+      fun(Callable[[jnp.ndarray], jnp.ndarray]): The function to supplement with the jacobian
+
+    Returns:
+      typing.Callable[[jnp.ndarray], typing.Tuple[jnp.ndarray, jnp.ndarray]]: A function that computes the value and the jacobian of the passed function using forward mode AD.
+
     """
 
-    def value_and_jacfwd_fun(x: jnp.ndarray):
+    def value_and_jacfwd_fun(
+        x: jnp.ndarray,
+    ) -> Callable[[jnp.ndarray], jnp.ndarray]:
+        """
+
+        Args:
+          x(jnp.ndarray): The input to the function
+
+        Returns:
+          typing.Tuple[jnp.ndarray, jnp.ndarray]: The value and the jacobian of the passed function using forward mode AD.
+
+        """
         pushfwd = partial(jax.jvp, fun, (x,))
         y, jac = jax.vmap(pushfwd, out_axes=(None, -1))(
             (jnp.eye(x.shape[0], dtype=x.dtype),)
@@ -22,14 +39,29 @@ def value_and_jacfwd_fun(x: jnp.ndarray):
     return value_and_jacfwd_fun
 
 
-def value_and_jacrev(fun: Callable[..., jnp.ndarray]):
+def value_and_jacrev(
+    fun: Callable[..., jnp.ndarray]
+) -> Callable[[jnp.ndarray], Callable[[jnp.ndarray], jnp.ndarray]]:
     """Returns a function that computes the value and the jacobian of the passed function using reverse mode AD.
 
-    :param fun: The function to supplement with the jacobian
-    :type fun: Callable[..., jnp.ndarray]
+    Args:
+      fun(Callable[..., jnp.ndarray]): The function to supplement with the jacobian
+
+    Returns:
+      typing.Callable[[jnp.ndarray], typing.Tuple[jnp.ndarray, jnp.ndarray]]: A function that computes the value and the jacobian of the passed function using reverse mode AD.
+
     """
 
     def value_and_jacrev_fun(x):
+        """
+
+        Args:
+          x(jnp.ndarray): The input to the function
+
+        Returns:
+          typing.Tuple[jnp.ndarray, jnp.ndarray]: The value and the jacobian of the passed function using reverse mode AD.
+
+        """
         y, pullback = jax.vjp(fun, x)
         jac = jax.vmap(pullback)(jnp.eye(y.shape[0], dtype=y.dtype))[0]
         return y, jac
diff --git a/epi/plotting/__init__.py b/epi/plotting/__init__.py
index 392e687a..2d93c6b6 100644
--- a/epi/plotting/__init__.py
+++ b/epi/plotting/__init__.py
@@ -1,7 +1,7 @@
 # In the whole plotting module:
 # TODO: Extract duplicate code into functions
 # TODO: Provide some "goto" plotting functions, which are applicable to all models / choose an adequate plotting
-# function based on model attributes like dataDim, ...
+# function based on model attributes like data_dim, ...
 # TODO: Set default values for resolution everywhere or nowhere
 # TODO: If accessing original data: respect model.dataPath attribute. E.g. by using model.dataLoader() to load the data
 # or change the implementation of model
diff --git a/epi/plotting/marginals.py b/epi/plotting/marginals.py
index c04ee4d1..1a205246 100644
--- a/epi/plotting/marginals.py
+++ b/epi/plotting/marginals.py
@@ -1,6 +1,6 @@
 import numpy as np
 
-from epi.core.kde import calcKernelWidth, evalKDEGauss
+from epi.core.kde import calc_kernel_width, eval_kde_gauss
 from epi.core.model import Model
 
 
@@ -8,152 +8,173 @@ def calcDataMarginals(model: Model, resolution: int) -> None:
     """Evaluate the one-dimensional marginals of the original data over equi-distant grids.
         The stored evaluations can then be used for result visualization.
 
-    :parameter model: The model which manages the data and provides the visualization grid
-    :parameter resolution: defines the number of grid points for each marginal evaluation is directly proportional to the runtime
-    :return: None, but stores results as files
+    Args:
+      eter: model: The model which manages the data and provides the visualization grid
+      eter: resolution: defines the number of grid points for each marginal evaluation is directly proportional to the runtime
+      model: Model:
+      resolution: int:
+
+    Returns:
+      None, but stores results as files
+
     """
 
     # Load data, data standard deviations and model characteristics for the specified model.
     (
-        dataDim,
+        data_dim,
         data,
-        dataStdevs,
+        data_stdevs,
     ) = model.dataLoader()
 
     # Create containers for the data marginal evaluations.
-    trueDataMarginals = np.zeros((resolution, dataDim))
+    trueDataMarginals = np.zeros((resolution, data_dim))
 
     # Load the grid over which the data marginal will be evaluated
     dataGrid, _ = model.generateVisualizationGrid(resolution)
 
     # Loop over each simulation result dimension and marginalize over the rest.
-    for dim in range(dataDim):
+    for dim in range(data_dim):
         # The 1D-arrays of true data have to be casted to 2D-arrays, as this format is obligatory for kernel density estimation.
         marginalData = np.zeros((data.shape[0], 1))
         marginalData[:, 0] = data[:, dim]
 
         # Loop over all grid points and evaluate the 1D kernel marginal density estimation of the data sample.
         for i in range(resolution):
-            trueDataMarginals[i, dim] = evalKDEGauss(
+            trueDataMarginals[i, dim] = eval_kde_gauss(
                 marginalData,
                 np.array([dataGrid[i, dim]]),
-                np.array([dataStdevs[dim]]),
+                np.array([data_stdevs[dim]]),
             )
 
     # Store the marginal KDE approximation of the data
     np.savetxt(
-        model.getApplicationPath() + "/Plots/trueDataMarginals.csv",
+        model.get_application_path() + "/Plots/trueDataMarginals.csv",
         trueDataMarginals,
         delimiter=",",
     )
 
 
 def calcEmceeSimResultsMarginals(
-    model: Model, numBurnSamples: int, occurrence: int, resolution: int
+    model: Model, num_burn_samples: int, occurrence: int, resolution: int
 ) -> None:
     """Evaluate the one-dimensional marginals of the emcee sampling simulation results over equi-distant grids.
         The stores evaluations can then be used for result visualization.
 
-    :param model:
-    :param numBurnSamples: (Number of ignored first samples of each chain), defaults to 20% of all samples
-    :param occurence: (step of sampling from chains), defaults to numWalkers+1 (ensures that the chosen samples are nearly uncorrelated)
-    :param resolution: (defines the number of grid points for each marginal evaluation is directly proportional to the runtime), defaults to 100
-    :return: None, except for stored files
+    Args:
+      model: param num_burn_samples: (Number of ignored first samples of each chain), defaults to 20% of all samples
+      occurence: step of sampling from chains), defaults to num_walkers+1 (ensures that the chosen samples are nearly uncorrelated)
+      resolution: defines the number of grid points for each marginal evaluation is directly proportional to the runtime), defaults to 100
+      model: Model:
+      num_burn_samples: int:
+      occurrence: int:
+      resolution: int:
+
+    Returns:
+      None, except for stored files
+
     """
 
     # Load the emcee simulation results chain
-    simResults = np.loadtxt(
-        model.getApplicationPath() + "/OverallSimResults.csv",
+    sim_results = np.loadtxt(
+        model.get_application_path() + "/OverallSimResults.csv",
         delimiter=",",
         ndmin=2,
-    )[numBurnSamples::occurrence, :]
+    )[num_burn_samples::occurrence, :]
 
     # Load data, data standard deviations and model characteristics for the specified model.
     (
-        dataDim,
+        data_dim,
         data,
-        dataStdevs,
+        data_stdevs,
     ) = model.dataLoader()
 
     # Create containers for the simulation results marginal evaluations.
-    inferredDataMarginals = np.zeros((resolution, dataDim))
+    inferredDataMarginals = np.zeros((resolution, data_dim))
 
     # Load the grid over which the simulation results marginal will be evaluated
     dataGrid, _ = model.generateVisualizationGrid(resolution)
 
     # Loop over each data dimension and marginalize over the rest.
-    for dim in range(dataDim):
+    for dim in range(data_dim):
         # The 1D-arrays of simulation resultshave to be casted to 2D-arrays, as this format is obligatory for kernel density estimation.
-        marginalSimResults = np.zeros((simResults.shape[0], 1))
-        marginalSimResults[:, 0] = simResults[:, dim]
+        marginalSimResults = np.zeros((sim_results.shape[0], 1))
+        marginalSimResults[:, 0] = sim_results[:, dim]
 
         # Loop over all grid points and evaluate the 1D kernel marginal density estimation of the emcee simulation results.
         for i in range(resolution):
-            inferredDataMarginals[i, dim] = evalKDEGauss(
+            inferredDataMarginals[i, dim] = eval_kde_gauss(
                 marginalSimResults,
                 np.array([dataGrid[i, dim]]),
-                np.array([dataStdevs[dim]]),
+                np.array([data_stdevs[dim]]),
             )
 
     # Store the marginal KDE approximation of the simulation results emcee sample
     np.savetxt(
-        model.getApplicationPath() + "/Plots/inferredDataMarginals.csv",
+        model.get_application_path() + "/Plots/inferredDataMarginals.csv",
         inferredDataMarginals,
         delimiter=",",
     )
 
 
 def calcParamMarginals(
-    model: Model, numBurnSamples: int, occurrence: int, resolution: int
+    model: Model, num_burn_samples: int, occurrence: int, resolution: int
 ) -> None:
     """Evaluate the one-dimensional marginals of the emcee sampling parameters (and potentially true parameters) over equi-distant grids.
         The stores evaluations can then be used for result visualization.
 
-    :param model:(model ID)
-    :param numBurnSamples: (Number of ignored first samples of each chain), defaults to 20% of all samples
-    :param occurrence: (step of sampling from chains), defaults to numWalkers+1 (ensures that the chosen samples are nearly uncorrelated)
-    :param resolution: (defines the number of grid points for each marginal evaluation is directly proportional to the runtime), defaults to 100
-    :return: None, except for stored files
+    Args:
+      model: model ID)
+      num_burn_samples: Number of ignored first samples of each chain), defaults to 20% of all samples
+      occurrence: step of sampling from chains), defaults to num_walkers+1 (ensures that the chosen samples are nearly uncorrelated)
+      resolution: defines the number of grid points for each marginal evaluation is directly proportional to the runtime), defaults to 100
+      model: Model:
+      num_burn_samples: int:
+      occurrence: int:
+      resolution: int:
+
+    Returns:
+      None, except for stored files
+
     """
 
     # If the model name indicates an artificial setting, indicate that true parameter information is available
-    artificialModel = model.isArtificial()
+    artificialModel = model.is_artificial()
 
     # Load the emcee parameter chain
-    paramChain = np.loadtxt(
-        model.getApplicationPath() + "/OverallParams.csv",
+    param_chain = np.loadtxt(
+        model.get_application_path() + "/OverallParams.csv",
         delimiter=",",
         ndmin=2,
-    )[numBurnSamples::occurrence, :]
+    )[num_burn_samples::occurrence, :]
 
-    paramDim = model.paramDim
+    param_dim = model.param_dim
 
     # Define the standard deviation for plotting the parameters based on the sampled parameters and not the true ones.
-    paramStdevs = calcKernelWidth(paramChain)
+    paramStdevs = calc_kernel_width(param_chain)
 
     # Create containers for the parameter marginal evaluations and the underlying grid.
     _, paramGrid = model.generateVisualizationGrid(resolution)
-    inferredParamMarginals = np.zeros((resolution, paramDim))
+    inferredParamMarginals = np.zeros((resolution, param_dim))
 
     # If there are true parameter values available, load them and allocate storage similar to the just-defined one.
     if artificialModel:
-        trueParamSample, _ = model.paramLoader()
-        trueParamMarginals = np.zeros((resolution, paramDim))
+        true_param_sample, _ = model.paramLoader()
+        trueParamMarginals = np.zeros((resolution, param_dim))
 
     # Loop over each parameter dimension and marginalize over the rest.
-    for dim in range(paramDim):
+    for dim in range(param_dim):
         # As the kernel density estimators only work for 2D-arrays of data, we have to cast the column of parameter samples into a 1-column matrix (or 2D-array).
-        marginalParamChain = np.zeros((paramChain.shape[0], 1))
-        marginalParamChain[:, 0] = paramChain[:, dim]
+        marginalParamChain = np.zeros((param_chain.shape[0], 1))
+        marginalParamChain[:, 0] = param_chain[:, dim]
 
         # If there is true parameter information available, we have to do the same type cast for the true parameter samples.
         if artificialModel:
-            trueMarginalParamSample = np.zeros((trueParamSample.shape[0], 1))
-            trueMarginalParamSample[:, 0] = trueParamSample[:, dim]
+            trueMarginalParamSample = np.zeros((true_param_sample.shape[0], 1))
+            trueMarginalParamSample[:, 0] = true_param_sample[:, dim]
 
         # Loop over all grid points and evaluate the 1D kernel density estimation of the reconstructed marginal parameter distribution.
         for i in range(resolution):
-            inferredParamMarginals[i, dim] = evalKDEGauss(
+            inferredParamMarginals[i, dim] = eval_kde_gauss(
                 marginalParamChain,
                 np.array([paramGrid[i, dim]]),
                 np.array(paramStdevs[dim]),
@@ -161,7 +182,7 @@ def calcParamMarginals(
 
             # If true parameter information is available, evaluate a similar 1D marginal distribution based on the true parameter samples.
             if artificialModel:
-                trueParamMarginals[i, dim] = evalKDEGauss(
+                trueParamMarginals[i, dim] = eval_kde_gauss(
                     trueMarginalParamSample,
                     np.array([paramGrid[i, dim]]),
                     np.array(paramStdevs[dim]),
@@ -169,16 +190,14 @@ def calcParamMarginals(
 
     # Store the (potentially 2) marginal distribution(s) for later plotting
     np.savetxt(
-        model.getApplicationPath() + "/Plots/inferredParamMarginals.csv",
+        model.get_application_path() + "/Plots/inferredParamMarginals.csv",
         inferredParamMarginals,
         delimiter=",",
     )
 
     if artificialModel:
         np.savetxt(
-            "Applications/"
-            + model.getModelName()
-            + "/Plots/trueParamMarginals.csv",
+            "Applications/" + model.name + "/Plots/trueParamMarginals.csv",
             trueParamMarginals,
             delimiter=",",
         )
diff --git a/epi/plotting/plots.py b/epi/plotting/plots.py
index 5f224c60..4d51086d 100644
--- a/epi/plotting/plots.py
+++ b/epi/plotting/plots.py
@@ -2,9 +2,9 @@
 import numpy as np
 from matplotlib import cm
 
-from epi.core.functions import evalLogTransformedDensity
-from epi.core.kde import calcKernelWidth, evalKDEGauss
+from epi.core.kde import calc_kernel_width, eval_kde_gauss
 from epi.core.model import Model
+from epi.core.transformations import eval_log_transformed_density
 
 colorQ = np.array([255.0, 147.0, 79.0]) / 255.0
 colorQApprox = np.array([204.0, 45.0, 53.0]) / 255.0
@@ -17,44 +17,47 @@
 # TODO: Fix np.loadtxt(..., ndmin=2) in all functions
 
 
-def plotEmceeResults(model: Model, numBurnSamples, occurrence, resolution=100):
+def plotEmceeResults(
+    model: Model, num_burn_samples, occurrence, resolution=100
+):
     """Plot sampling results in comparison to true results
 
-    :param model: Model from which the results will be plotted
-    :type model: Model
-    :param numBurnSamples: Ignore the first samples of each chain
-    :type numBurnSamples: _type_
-    :param occurrence: step of sampling from chains
-    :type occurrence: _type_
-    :param resolution: number of points on the plotting grid?, defaults to 100
-    :type resolution: int, optional
+    Args:
+      model(Model): Model from which the results will be plotted
+      num_burn_samples(_type_): Ignore the first samples of each chain
+      occurrence(_type_): step of sampling from chains
+      resolution(int, optional, optional): number of points on the plotting grid?, defaults to 100
+      model: Model:
+
+    Returns:
+
     """
-    artificialModel = model.isArtificial()
+    artificialModel = model.is_artificial()
 
     (
-        dataDim,
+        data_dim,
         data,
-        dataStdevs,
+        data_stdevs,
     ) = model.dataLoader()
 
-    densityEvals, simResults, paramChain = model.loadSimResults(
-        numBurnSamples, occurrence
+    density_evals, sim_results, param_chain = model.load_sim_results(
+        num_burn_samples, occurrence
     )
 
     if artificialModel:
         trueParams, paramStdevs = model.paramLoader()
     else:
-        paramStdevs = calcKernelWidth(paramChain)
+        paramStdevs = calc_kernel_width(param_chain)
 
     # plot sampled parameters in comparison to true ones
-    for dim in range(model.paramDim):
+    for dim in range(model.param_dim):
         evaluations = np.zeros(resolution)
-        singleParamChain = np.zeros((paramChain.shape[0], 1))
-        singleParamChain[:, 0] = paramChain[:, dim]
+        singleParamChain = np.zeros((param_chain.shape[0], 1))
+        singleParamChain[:, 0] = param_chain[:, dim]
 
         paramGrid = np.linspace(
-            np.amin(paramChain[:, dim]),
-            np.amax(paramChain[:, dim]),
+            np.amin(param_chain[:, dim]),
+            np.amax(param_chain[:, dim]),
             resolution,
         )
 
@@ -64,14 +67,14 @@ def plotEmceeResults(model: Model, numBurnSamples, occurrence, resolution=100):
             trueSingleParamSample[:, 0] = trueParams[:, dim]
 
         for i in range(resolution):
-            evaluations[i] = evalKDEGauss(
+            evaluations[i] = eval_kde_gauss(
                 singleParamChain,
                 np.array([paramGrid[i]]),
                 np.array(paramStdevs[dim]),
             )
 
             if artificialModel:
-                trueEvaluations[i] = evalKDEGauss(
+                trueEvaluations[i] = eval_kde_gauss(
                     trueSingleParamSample,
                     np.array([paramGrid[i]]),
                     np.array(paramStdevs[dim]),
@@ -94,12 +97,12 @@ def plotEmceeResults(model: Model, numBurnSamples, occurrence, resolution=100):
         plt.plot()
 
     # plot sampled simulation results in comparison to original data
-    for dim in range(dataDim):
+    for dim in range(data_dim):
         simResEvaluations = np.zeros(resolution)
         dataEvaluations = np.zeros(resolution)
 
-        singleSimResults = np.zeros((simResults.shape[0], 1))
-        singleSimResults[:, 0] = simResults[:, dim]
+        singleSimResults = np.zeros((sim_results.shape[0], 1))
+        singleSimResults[:, 0] = sim_results[:, dim]
         singleData = np.zeros((data.shape[0], 1))
         singleData[:, 0] = data[:, dim]
 
@@ -113,15 +116,15 @@ def plotEmceeResults(model: Model, numBurnSamples, occurrence, resolution=100):
         evalGrid = np.linspace(globalMin, globalMax, resolution)
 
         for i in range(resolution):
-            simResEvaluations[i] = evalKDEGauss(
+            simResEvaluations[i] = eval_kde_gauss(
                 singleSimResults,
                 np.array([evalGrid[i]]),
-                np.array([dataStdevs[dim]]),
+                np.array([data_stdevs[dim]]),
             )
-            dataEvaluations[i] = evalKDEGauss(
+            dataEvaluations[i] = eval_kde_gauss(
                 singleData,
                 np.array([evalGrid[i]]),
-                np.array([dataStdevs[dim]]),
+                np.array([data_stdevs[dim]]),
             )
 
         plt.figure()
@@ -137,25 +140,33 @@ def plotEmceeResults(model: Model, numBurnSamples, occurrence, resolution=100):
 
 
 def plotDataMarginals(model: Model):
+    """
+
+    Args:
+      model: Model:
+
+    Returns:
+
+    """
     (
-        paramDim,
-        dataDim,
-        numDataPoints,
-        centralParam,
+        param_dim,
+        data_dim,
+        num_data_points,
+        central_param,
         data,
-        dataStdevs,
+        data_stdevs,
     ) = model.loadData()
 
     dataGrid = np.loadtxt(
-        model.getApplicationPath() + "/Plots/dataGrid.csv",
+        model.get_application_path() + "/Plots/dataGrid.csv",
         delimiter=",",
     )
     trueDataMarginals = np.loadtxt(
-        model.getApplicationPath() + "/Plots/trueDataMarginals.csv",
+        model.get_application_path() + "/Plots/trueDataMarginals.csv",
         delimiter=",",
     )
 
-    for dim in range(dataDim):
+    for dim in range(data_dim):
         plt.figure()
         plt.plot(
             dataGrid[:, dim],
@@ -174,50 +185,58 @@ def plotDataMarginals(model: Model):
         plt.show()
 
 
-def plotMarginals(model: Model, numBurnSamples, occurrence):
-    artificialModel = model.isArtificial()
+def plotMarginals(model: Model, num_burn_samples, occurrence):
+    """
+
+    Args:
+      model: Model:
+      num_burn_samples:
+      occurrence:
+
+    Returns:
+
+    """
+    artificialModel = model.is_artificial()
 
-    simResults = np.loadtxt(
-        model.getApplicationPath() + "/OverallSimResults.csv",
+    sim_results = np.loadtxt(
+        model.get_application_path() + "/OverallSimResults.csv",
         delimiter=",",
-    )[numBurnSamples::occurrence, :]
-    paramChain = np.loadtxt(
-        model.getApplicationPath() + "/OverallParams.csv",
+    )[num_burn_samples::occurrence, :]
+    param_chain = np.loadtxt(
+        model.get_application_path() + "/OverallParams.csv",
         delimiter=",",
-    )[numBurnSamples::occurrence, :]
+    )[num_burn_samples::occurrence, :]
 
     paramGrid = np.loadtxt(
-        model.getApplicationPath() + "/Plots/paramGrid.csv",
+        model.get_application_path() + "/Plots/paramGrid.csv",
         delimiter=",",
     )
     inferredParamMarginals = np.loadtxt(
-        model.getApplicationPath() + "/Plots/inferredParamMarginals.csv",
+        model.get_application_path() + "/Plots/inferredParamMarginals.csv",
         delimiter=",",
     )
 
     if artificialModel:
         trueParams, paramStdevs = model.paramLoader()
         trueParamMarginals = np.loadtxt(
-            "Applications/"
-            + model.getModelName()
-            + "/Plots/trueParamMarginals.csv",
+            "Applications/" + model.name + "/Plots/trueParamMarginals.csv",
             delimiter=",",
         )
 
     dataGrid = np.loadtxt(
-        model.getApplicationPath() + "/Plots/dataGrid.csv",
+        model.get_application_path() + "/Plots/dataGrid.csv",
         delimiter=",",
     )
     trueDataMarginals = np.loadtxt(
-        model.getApplicationPath() + "/Plots/trueDataMarginals.csv",
+        model.get_application_path() + "/Plots/trueDataMarginals.csv",
         delimiter=",",
     )
     inferredDataMarginals = np.loadtxt(
-        model.getApplicationPath() + "/Plots/inferredDataMarginals.csv",
+        model.get_application_path() + "/Plots/inferredDataMarginals.csv",
         delimiter=",",
     )
 
-    for dim in range(model.paramDim):
+    for dim in range(model.param_dim):
         plt.figure()
         plt.plot(
             paramGrid[:, dim],
@@ -225,7 +244,7 @@ def plotMarginals(model: Model, numBurnSamples, occurrence):
             c=colorQApprox,
             label="inferred param. marg. KDE",
         )
-        # plt.hist(paramChain[:,dim], bins = paramGrid[:,dim], color = np.concatenate((colorQApprox, np.array([0.5]))), label = "inferred param. marg. hist.", density = True)
+        # plt.hist(param_chain[:,dim], bins = paramGrid[:,dim], color = np.concatenate((colorQApprox, np.array([0.5]))), label = "inferred param. marg. hist.", density = True)
 
         if artificialModel:
             plt.plot(
@@ -239,7 +258,7 @@ def plotMarginals(model: Model, numBurnSamples, occurrence):
         plt.legend()
         plt.show()
 
-    for dim in range(model.dataDim):
+    for dim in range(model.data_dim):
         plt.figure()
         plt.plot(
             dataGrid[:, dim],
@@ -247,7 +266,7 @@ def plotMarginals(model: Model, numBurnSamples, occurrence):
             c=colorYApprox,
             label="reconstr. data marg. KDE",
         )
-        # plt.hist(simResults[:,dim], bins = dataGrid[:,dim], color = np.concatenate((colorYApprox, np.array([0.5]))), label = "recontr. data. marg. hist.", density = True)
+        # plt.hist(sim_results[:,dim], bins = dataGrid[:,dim], color = np.concatenate((colorYApprox, np.array([0.5]))), label = "recontr. data. marg. hist.", density = True)
 
         plt.plot(
             dataGrid[:, dim],
@@ -260,48 +279,52 @@ def plotMarginals(model: Model, numBurnSamples, occurrence):
         plt.show()
 
 
-def plotSpiderWebs(model: Model, numBurnSamples, occurrence):
+def plotSpiderWebs(model: Model, num_burn_samples, occurrence):
     """Draw each sample (row of the matrix) as a circle of linear interpolations of its dimensions.
     Loads all necessary data and subsequently calls the method singleWeb 3 or 4 times
 
-    :param model: (model ID)
-    :param numBurnSamples: (Number of ignored first samples of each chain)
-    :param occurence: (step of sampling from chains)
-    :return: None, shows a plot
+    Args:
+      model: model ID)
+      num_burn_samples: Number of ignored first samples of each chain)
+      occurence: step of sampling from chains)
+      model: Model:
+      occurrence:
+
+    Returns:
+      None, shows a plot
+
     """
 
     # If the model name indicates an artificial setting, indicate that true parameter information is available
-    artificialModel = model.isArtificial()
+    artificialModel = model.is_artificial()
 
     # load emcee parameter samples and corresponding simulation results
     emceeParams = np.loadtxt(
-        model.getApplicationPath() + "/OverallParams.csv",
+        model.get_application_path() + "/OverallParams.csv",
         delimiter=",",
-    )[numBurnSamples::occurrence, :]
+    )[num_burn_samples::occurrence, :]
     emceeSimResults = np.loadtxt(
-        model.getApplicationPath() + "/OverallSimResults.csv",
+        model.get_application_path() + "/OverallSimResults.csv",
         delimiter=",",
-    )[numBurnSamples::occurrence, :]
+    )[num_burn_samples::occurrence, :]
 
     # load underlying data
-    trueData = np.loadtxt(
-        "Data/" + model.getModelName() + "Data.csv", delimiter=","
-    )
+    trueData = np.loadtxt("Data/" + model.name + "Data.csv", delimiter=",")
 
     # if available, load also the true parameter values
     if artificialModel:
         trueParams = np.loadtxt(
-            "Data/" + model.getModelName() + "Params.csv", delimiter=","
+            "Data/" + model.name + "Params.csv", delimiter=","
         )
 
     # compute the upper and lower bound of each data dimension
     # this serves as the identical scaling of every data plot
-    upperBoundsSimResults = np.max(emceeSimResults, 0)
-    lowerBoundsSimResults = np.min(emceeSimResults, 0)
+    upper_boundsSimResults = np.max(emceeSimResults, 0)
+    lower_boundsSimResults = np.min(emceeSimResults, 0)
 
     # do the same for the parameters
-    upperBoundsParams = np.max(emceeParams, 0)
-    lowerBoundsParams = np.min(emceeParams, 0)
+    upper_boundsParams = np.max(emceeParams, 0)
+    lower_boundsParams = np.min(emceeParams, 0)
 
     # set the image quality by defining dots per inch
     dpi = 1000
@@ -309,65 +332,73 @@ def plotSpiderWebs(model: Model, numBurnSamples, occurrence):
     # create all web figures
     emceeSimResultsWeb = singleWeb(
         emceeSimResults,
-        lowerBoundsSimResults,
-        upperBoundsSimResults,
+        lower_boundsSimResults,
+        upper_boundsSimResults,
         colorYApprox,
         dpi,
     )
     plt.savefig(
-        model.getApplicationPath() + "/SpiderWebs/emceeSimResults.png",
+        model.get_application_path() + "/SpiderWebs/emceeSimResults.png",
         dpi=dpi,
     )
 
     trueDataWeb = singleWeb(
-        trueData, lowerBoundsSimResults, upperBoundsSimResults, colorY, dpi
+        trueData, lower_boundsSimResults, upper_boundsSimResults, colorY, dpi
     )
     plt.savefig(
-        model.getApplicationPath() + "/SpiderWebs/trueData.png",
+        model.get_application_path() + "/SpiderWebs/trueData.png",
         dpi=dpi,
     )
 
     emceeParamsWeb = singleWeb(
-        emceeParams, lowerBoundsParams, upperBoundsParams, colorQApprox, dpi
+        emceeParams, lower_boundsParams, upper_boundsParams, colorQApprox, dpi
     )
     plt.savefig(
-        model.getApplicationPath() + "/SpiderWebs/emceeParams.png",
+        model.get_application_path() + "/SpiderWebs/emceeParams.png",
         dpi=dpi,
     )
 
     if artificialModel:
         trueParamsWeb = singleWeb(
-            trueParams, lowerBoundsParams, upperBoundsParams, colorQ, dpi
+            trueParams, lower_boundsParams, upper_boundsParams, colorQ, dpi
         )
         plt.savefig(
-            "Applications/"
-            + model.getModelName()
-            + "/SpiderWebs/trueParams.png",
+            "Applications/" + model.name + "/SpiderWebs/trueParams.png",
             dpi=dpi,
         )
 
 
-def singleWeb(matrix, lowerBounds, upperBounds, color, dpi):
+def singleWeb(matrix, lower_bounds, upper_bounds, color, dpi):
     """Create a single spider web plot for one data or parameter matrix and given bounds
 
     Input: matrix (2D np.array of size #samples x #dimensions)
-           lowerBounds (np.arraay of size #dimensions that defines the lower bound of all regarded values)
-           upperBounds (np.arraay of size #dimensions that defines the upper bound of all regarded values)
+           lower_bounds (np.arraay of size #dimensions that defines the lower bound of all regarded values)
+           upper_bounds (np.arraay of size #dimensions that defines the upper bound of all regarded values)
            color (np.array with 3 entries indicating RGB values of the plot)
            dpi (int that defines the quality of the image)
 
     Output: web (matplotlib figure representing the single spider web)
+
+    Args:
+      matrix:
+      lower_bounds:
+      upper_bounds:
+      color:
+      dpi:
+
+    Returns:
+
     """
 
     # extract matrix dimensions
-    numSamples, numDims = matrix.shape
+    num_samples, numDims = matrix.shape
 
     # create a normalized matrix where each column aka. dimension is scaled to [0, 1]
-    normMatrix = (matrix - lowerBounds) / (upperBounds - lowerBounds)
+    normMatrix = (matrix - lower_bounds) / (upper_bounds - lower_bounds)
 
     # create an augmented matrix that is identical to the normalized matrix
     # except for an additional last column that is obtained by copying the first one
-    augMatrix = np.zeros((numSamples, numDims + 1))
+    augMatrix = np.zeros((num_samples, numDims + 1))
     augMatrix[:, 0:numDims] = normMatrix
 
     # copy the first column to the end
@@ -396,13 +427,13 @@ def singleWeb(matrix, lowerBounds, upperBounds, color, dpi):
 
     plt.xticks([])
     plt.yticks([])
-    for i in range(numSamples):
+    for i in range(num_samples):
         # modColor = color*(1 - 0.3*np.random.rand(3))
         plt.plot(
             xProjCoords[i, :],
             yProjCoords[i, :],
             color=color,
-            linewidth=1000.0 / numSamples,
+            linewidth=1000.0 / num_samples,
             alpha=0.05,
         )
 
@@ -423,20 +454,24 @@ def plotTest(
     """Visualize the results of EPI applied to a model with 2 parameters and
         2 output dimensions as surface plots.
 
-    :param model: model from which the results shall be loaded
-    :type model: Model
-    :param dataPlotResolution: number of grid points per data dimension, defaults to 25
-    :type dataPlotResolution: int, optional
-    :param paramPlotResolution: number of grid points per parameter dimension
-    :type dataPlotResolution: int, optional
+    Args:
+      model(Model): model from which the results shall be loaded
+      dataPlotResolution(int, optional): number of grid points per data dimension, defaults to 25
+      paramPlotResolution: number of grid points per parameter dimension
+      model: Model:
+      dataPlotResolution: int:  (Default value = 25)
+      paramPlotResolution: int:  (Default value = 25)
+
+    Returns:
+
     """
 
     # ---------------------------------------------------------------------------
     # First, we load and visualize the underlying data
     (
-        dataDim,
+        data_dim,
         data,
-        dataStdevs,
+        data_stdevs,
     ) = model.dataLoader()
 
     # create the grid over which the data KDE will be evaluated
@@ -457,7 +492,7 @@ def plotTest(
             # define the evaluation grid point
             dataEvalPoint = np.array([dataxMesh[i, j], datayMesh[i, j]])
             # evaluate the data density at the defined grid point
-            dataEvals[i, j] = evalKDEGauss(data, dataEvalPoint, dataStdevs)
+            dataEvals[i, j] = eval_kde_gauss(data, dataEvalPoint, data_stdevs)
 
     # plot the data KDE using a surface plot
     fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
@@ -475,13 +510,13 @@ def plotTest(
 
     # ---------------------------------------------------------------------------
     # Second, we load the emcee parameter sampling results and als visualize them
-    paramChain = np.loadtxt(
-        model.getApplicationPath() + "/OverallParams.csv",
+    param_chain = np.loadtxt(
+        model.get_application_path() + "/OverallParams.csv",
         delimiter=",",
     )
 
     # calculate reasonable standard deviations for the KDE
-    paramChainStdevs = calcKernelWidth(paramChain)
+    paramChainStdevs = calc_kernel_width(param_chain)
 
     # define the grid over which the inferred parameter density will be evaluated
     paramxGrid = np.linspace(0.8, 2.2, paramPlotResolution)
@@ -498,8 +533,8 @@ def plotTest(
             # define the evaluation parameter point
             paramEvalPoint = np.array([paramxMesh[i, j], paramyMesh[i, j]])
             # evaluate the parameter KDE at the defined grid point
-            paramEvals[i, j] = evalKDEGauss(
-                paramChain, paramEvalPoint, paramChainStdevs
+            paramEvals[i, j] = eval_kde_gauss(
+                param_chain, paramEvalPoint, paramChainStdevs
             )
 
     # plot the inferred parameter distribution
@@ -525,12 +560,12 @@ def plotTest(
 
     # Load the MCMC simulation results
     simResultsChain = np.loadtxt(
-        model.getApplicationPath() + "/OverallSimResults.csv",
+        model.get_application_path() + "/OverallSimResults.csv",
         delimiter=",",
     )
 
     # calculate reasonable standard deviations for the KDE
-    simResultsChainStdevs = calcKernelWidth(simResultsChain)
+    simResultsChainStdevs = calc_kernel_width(simResultsChain)
 
     # allocate storage for the data density evaluation
     simResultsEvals = np.zeros((dataPlotResolution, dataPlotResolution))
@@ -541,7 +576,7 @@ def plotTest(
             # define the evaluation grid point
             simResultsEvalPoint = np.array([dataxMesh[i, j], datayMesh[i, j]])
             # evaluate the data density at the defined grid point
-            simResultsEvals[i, j] = evalKDEGauss(
+            simResultsEvals[i, j] = eval_kde_gauss(
                 simResultsChain, simResultsEvalPoint, simResultsChainStdevs
             )
 
@@ -563,12 +598,16 @@ def plotTest(
 def plotGridResults(model: Model) -> None:
     """The temperature model for artificial and real data is evaluated over a grid and plotted
 
-    :param model: _description_
-    :type model: Model
+    Args:
+      model(Model): _description_
+      model: Model:
+
+    Returns:
+
     """
 
     (
-        dataDim,
+        data_dim,
         data,
         stdevs,
     ) = model.dataLoader()
@@ -577,7 +616,7 @@ def plotGridResults(model: Model) -> None:
 
     # TODO: Fix var names etc.
     trueParams = paramStdevs = None
-    artificialModel = model.isArtificial()
+    artificialModel = model.is_artificial()
     if artificialModel:
         trueParams, paramStdevs = model.paramLoader()
 
@@ -585,7 +624,7 @@ def plotGridResults(model: Model) -> None:
         "Applications/Temperature/Latitudes.csv", delimiter=","
     )
     trueLatitudes = rawTrueLatitudes[..., np.newaxis]
-    trueLatitudesStdevs = calcKernelWidth(trueLatitudes)
+    trueLatitudesStdevs = calc_kernel_width(trueLatitudes)
 
     resolution = -1
     if artificialModel:
@@ -598,35 +637,35 @@ def plotGridResults(model: Model) -> None:
     trueDensity = np.zeros(resolution)
     trafoDensity = np.zeros(resolution)
 
-    if model.getModelName() == "TemperatureArtificial":
+    if model.name == "TemperatureArtificial":
         simulatedTemperatures = np.zeros(resolution)
 
         for i in range(resolution):
-            trueDensity[i] = evalKDEGauss(
+            trueDensity[i] = eval_kde_gauss(
                 trueLatitudes,
                 np.array([latitudesGrid[i]]),
                 trueLatitudesStdevs,
             )
-            trafoDensity[i], _ = evalLogTransformedDensity(
+            trafoDensity[i], _ = eval_log_transformed_density(
                 model, np.array([latitudesGrid[i]]), data, stdevs
             )
-            simulatedTemperatures[i] = evalKDEGauss(
+            simulatedTemperatures[i] = eval_kde_gauss(
                 data, np.array([temperaturesGrid[i]]), stdevs
             )
 
-    elif model.getModelName() == "Temperature":
+    elif model.name == "Temperature":
         measuredTemperatures = np.zeros(resolution)
 
         for i in range(resolution):
-            trueDensity[i] = evalKDEGauss(
+            trueDensity[i] = eval_kde_gauss(
                 trueLatitudes,
                 np.array([latitudesGrid[i]]),
                 trueLatitudesStdevs,
             )
-            trafoDensity[i], _ = evalLogTransformedDensity(
+            trafoDensity[i], _ = eval_log_transformed_density(
                 model, np.array([latitudesGrid[i]]), data, stdevs
             )
-            measuredTemperatures[i] = evalKDEGauss(
+            measuredTemperatures[i] = eval_kde_gauss(
                 data, np.array([temperaturesGrid[i]]), stdevs
             )
 
@@ -646,7 +685,7 @@ def plotGridResults(model: Model) -> None:
 
         inferredTemperatures = np.zeros(resolution)
         for i in range(resolution):
-            inferredTemperatures[i] = evalKDEGauss(
+            inferredTemperatures[i] = eval_kde_gauss(
                 inferredTemperaturesSample,
                 np.array([temperaturesGrid[i]]),
                 stdevs,
@@ -671,7 +710,7 @@ def plotGridResults(model: Model) -> None:
     )
     plt.legend()
     plt.savefig(
-        "Images/" + model.getModelName() + "/TrueLatSample.svg",
+        "Images/" + model.name + "/TrueLatSample.svg",
         bbox_inches="tight",
     )
     plt.show()
@@ -689,9 +728,7 @@ def plotGridResults(model: Model) -> None:
         label="True Latitudes (KDE)",
     )
     plt.legend()
-    plt.savefig(
-        "Images/model.getModelName()/TrueLatKDE.svg", bbox_inches="tight"
-    )
+    plt.savefig("Images/model.name/TrueLatKDE.svg", bbox_inches="tight")
     plt.show()
 
     sim_measure_temp = (
@@ -718,7 +755,7 @@ def plotGridResults(model: Model) -> None:
     )
     plt.legend()
     plt.savefig(
-        "Images/" + model.getModelName() + "/" + name + "TempSample.svg",
+        "Images/" + model.name + "/" + name + "TempSample.svg",
         bbox_inches="tight",
     )
     plt.show()
@@ -737,7 +774,7 @@ def plotGridResults(model: Model) -> None:
     plt.legend()
 
     plt.savefig(
-        "Images/" + model.getModelName() + "/" + name + "TempKDE.svg",
+        "Images/" + model.name + "/" + name + "TempKDE.svg",
         bbox_inches="tight",
     )
     plt.show()
@@ -763,7 +800,7 @@ def plotGridResults(model: Model) -> None:
     )
     plt.legend()
     plt.savefig(
-        "Images/" + model.getModelName() + "/TrueLatVsITAKDE.svg",
+        "Images/" + model.name + "/TrueLatVsITAKDE.svg",
         bbox_inches="tight",
     )
     plt.show()
@@ -825,7 +862,7 @@ def plotGridResults(model: Model) -> None:
         )
         plt.show()
 
-    if model.getModelName() == "Temperature":
+    if model.name == "Temperature":
         # plot ITA inferred latitude density alone
         plt.figure(figsize=(6, 4))
         plt.axis([-0.05, np.pi / 2.0 + 0.05, 0.0, 1.6])
diff --git a/epi/plotting/plotter.py b/epi/plotting/plotter.py
index 2072e8ab..32825ca9 100644
--- a/epi/plotting/plotter.py
+++ b/epi/plotting/plotter.py
@@ -8,7 +8,7 @@
 import matplotlib.pyplot as plt
 import numpy as np
 
-from epi.core.kde import evalKDEGauss
+from epi.core.kde import eval_kde_gauss
 from epi.core.model import Model
 
 # Colors
@@ -26,12 +26,16 @@ def plotKDEoverGrid(
 ) -> None:
     """Plot the 1D kernel density estimation of different data sets over a grid
 
-    :param data: array of array of samples
-    :type data: np.ndarray
-    :param stdevs: one kernel standard deviation for each dimension
-    :type stdevs: np.ndarray
-    :param resolution: _description_, defaults to 101
-    :type resolution: int, optional
+    Args:
+      data(np.ndarray): array of array of samples
+      stdevs(np.ndarray): one kernel standard deviation for each dimension
+      resolution(int, optional): _description_, defaults to 101
+      data: np.ndarray:
+      stdevs: np.ndarray:
+      resolution: int:  (Default value = 101)
+
+    Returns:
+
     """
 
     for dim in range(data.shape[1]):
@@ -45,7 +49,7 @@ def plotKDEoverGrid(
         evaluations = np.zeros(resolution)
 
         for i in range(resolution):
-            evaluations[i] = evalKDEGauss(
+            evaluations[i] = eval_kde_gauss(
                 np.transpose(np.array([data[:, dim]])),
                 np.array([evalPoints[i]]),
                 np.array([stdevs[dim]]),
@@ -58,6 +62,8 @@ def plotKDEoverGrid(
 
 
 class DataParamEnum(Enum):
+    """ """
+
     Data = (0,)
     Params = 1
 
@@ -66,11 +72,15 @@ class DataParamEnum(Enum):
 def plotDataSamples(model: Model):
     """Scatter plot of data samples?
 
-    :param model: _description_
-    :type model: Model
+    Args:
+      model(Model): _description_
+      model: Model:
+
+    Returns:
+
     """
 
-    artificialModel = model.isArtificial()
+    artificialModel = model.is_artificial()
     name = "Sim" if artificialModel else "Meas"
     sim_measure_label = (
         "Simulation data" if artificialModel else "Measured data"
@@ -92,10 +102,10 @@ def plotDataSamples(model: Model):
         # if not artificial: plot inferred data samples from result param samples
         # if not artificialModel:
         #     # Second, we load the emcee parameter sampling results and als visualize them
-        #     simResults = model.loadSimResults(0,1)[1] # Picking simResults
+        #     sim_results = model.load_sim_results(0,1)[1] # Picking sim_results
         #     plt.scatter(
-        #         simResults[:,dim],
-        #         np.zeros(simResults.shape[0]),
+        #         sim_results[:,dim],
+        #         np.zeros(sim_results.shape[0]),
         #         color=colorYApprox,
         #         alpha=0.1,
         #         label="Inferred Data samples"
@@ -106,8 +116,12 @@ def plotDataSamples(model: Model):
 def plotDataKDE(model: Model):
     """Continuos plot of data kde?
 
-    :param model: _description_
-    :type model: Model
+    Args:
+      model(Model): _description_
+      model: Model:
+
+    Returns:
+
     """
     # plot data kde
     # if not artificial: plot inferred data kde from inferred data samples
@@ -117,8 +131,12 @@ def plotDataKDE(model: Model):
 def plotParamSamples(model: Model):
     """Scatter plot of param samples?
 
-    :param model: _description_
-    :type model: Model
+    Args:
+      model(Model): _description_
+      model: Model:
+
+    Returns:
+
     """
     # if artificial: plot param samples from file
     # else: sample from inferred param distr to get samples
@@ -128,11 +146,15 @@ def plotParamSamples(model: Model):
 def plotParamKDE(model: Model):
     """Continuos plot of param kde?
 
-    :param model: _description_
-    :type model: Model
+    Args:
+      model(Model): _description_
+      model: Model:
+
+    Returns:
+
     """
     # if artificial: plot param kde from param samples file
-    # plot inferred param kde from paramChain sim results
+    # plot inferred param kde from param_chain sim results
     pass
 
 
@@ -140,9 +162,13 @@ def sampleFromResults(model: Model, sampleSize: int = 1000):
     """Samples from the calculated param distribution
     and calculates the data points for these parameters
 
-    :param model: The model from which the results shall be loaded
-    :type model: Model
-    :param sampleSize: number of drawn samples, defaults to 1000
-    :type sampleSize: int, optional
+    Args:
+      model(Model): The model from which the results shall be loaded
+      sampleSize(int, optional): number of drawn samples, defaults to 1000
+      model: Model:
+      sampleSize: int:  (Default value = 1000)
+
+    Returns:
+
     """
     pass
diff --git a/poetry.lock b/poetry.lock
index 854e82cf..8f0731e9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,5 +1,20 @@
 # This file is automatically @generated by Poetry and should not be changed by hand.
 
+[[package]]
+name = "accessible-pygments"
+version = "0.0.3"
+description = "A collection of accessible pygments styles"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "accessible-pygments-0.0.3.tar.gz", hash = "sha256:0917b507646a6b3393eb49091a62506f6b2a3935f5d95e4d1249dc505f8a4eae"},
+    {file = "accessible_pygments-0.0.3-py2.py3-none-any.whl", hash = "sha256:6a643a2979b1f14fead79a8bbf51a6ae09e9df70d5ed60bb61a6eb9092cdcb81"},
+]
+
+[package.dependencies]
+pygments = ">=1.5"
+
 [[package]]
 name = "alabaster"
 version = "0.7.13"
@@ -12,6 +27,38 @@ files = [
     {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"},
 ]
 
+[[package]]
+name = "amici"
+version = "0.16.1"
+description = "Advanced multi-language Interface to CVODES and IDAS"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "amici-0.16.1.tar.gz", hash = "sha256:aec799184cc96fcbda075ab2a4599890ab42474145e064cc57da995e38a49b28"},
+]
+
+[package.dependencies]
+h5py = "*"
+mpmath = "*"
+numpy = [
+    {version = ">=1.17.5", markers = "python_version == \"3.8\""},
+    {version = ">=1.19.3", markers = "python_version == \"3.9\""},
+    {version = ">=1.21.4", markers = "python_version >= \"3.10\""},
+]
+pandas = "*"
+pkgconfig = "*"
+python-libsbml = "*"
+setuptools = ">=48"
+sympy = ">=1.9"
+toposort = "*"
+wurlitzer = "*"
+
+[package.extras]
+petab = ["petab (>=0.1.27)"]
+pysb = ["pysb (>=1.13.1)"]
+test = ["coverage", "pytest", "pytest-cov", "pytest-rerunfailures", "shyaml"]
+
 [[package]]
 name = "appdirs"
 version = "1.4.4"
@@ -24,6 +71,36 @@ files = [
     {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
 ]
 
+[[package]]
+name = "appnope"
+version = "0.1.3"
+description = "Disable App Nap on macOS >= 10.9"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
+    {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
+]
+
+[[package]]
+name = "asttokens"
+version = "2.2.1"
+description = "Annotate AST trees with source code positions"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"},
+    {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"},
+]
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+test = ["astroid", "pytest"]
+
 [[package]]
 name = "attrs"
 version = "22.2.0"
@@ -45,18 +122,49 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy
 
 [[package]]
 name = "babel"
-version = "2.11.0"
+version = "2.12.1"
 description = "Internationalization utilities"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"},
-    {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"},
+    {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"},
+    {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
 ]
 
 [package.dependencies]
-pytz = ">=2015.7"
+pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
+
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
+    {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.11.2"
+description = "Screen-scraping library"
+category = "dev"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+    {file = "beautifulsoup4-4.11.2-py3-none-any.whl", hash = "sha256:0e79446b10b3ecb499c1556f7e228a53e64a2bfcebd455f370d8927cb5b59e39"},
+    {file = "beautifulsoup4-4.11.2.tar.gz", hash = "sha256:bc4bdda6717de5a2987436fb8d72f45dc90dd856bdfd512a1314ce90349a0106"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
 
 [[package]]
 name = "black"
@@ -197,100 +305,87 @@ files = [
 
 [[package]]
 name = "charset-normalizer"
-version = "3.0.1"
+version = "3.1.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
-python-versions = "*"
-files = [
-    {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"},
-    {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"},
-    {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"},
-    {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"},
-    {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"},
-    {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"},
-    {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"},
-    {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"},
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
+    {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
 ]
 
 [[package]]
@@ -320,6 +415,24 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "comm"
+version = "0.1.2"
+description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "comm-0.1.2-py3-none-any.whl", hash = "sha256:9f3abf3515112fa7c55a42a6a5ab358735c9dccc8b5910a9d8e3ef5998130666"},
+    {file = "comm-0.1.2.tar.gz", hash = "sha256:3e2f5826578e683999b93716285b3b1f344f157bf75fa9ce0a797564e742f062"},
+]
+
+[package.dependencies]
+traitlets = ">=5.3"
+
+[package.extras]
+test = ["pytest"]
+
 [[package]]
 name = "contourpy"
 version = "1.0.7"
@@ -397,63 +510,63 @@ test-no-images = ["pytest"]
 
 [[package]]
 name = "coverage"
-version = "7.1.0"
+version = "7.2.1"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"},
-    {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"},
-    {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"},
-    {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"},
-    {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"},
-    {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"},
-    {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"},
-    {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"},
-    {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"},
-    {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"},
-    {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"},
-    {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"},
-    {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"},
-    {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"},
-    {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"},
-    {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"},
-    {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"},
-    {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"},
-    {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"},
-    {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"},
-    {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"},
-    {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"},
-    {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"},
-    {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"},
-    {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"},
-    {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"},
-    {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"},
-    {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"},
-    {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"},
-    {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"},
-    {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"},
-    {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"},
-    {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"},
-    {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"},
-    {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"},
-    {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"},
-    {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"},
-    {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"},
-    {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"},
-    {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"},
-    {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"},
-    {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"},
-    {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"},
-    {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"},
-    {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"},
-    {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"},
-    {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"},
-    {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"},
-    {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"},
-    {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"},
-    {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"},
+    {file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"},
+    {file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"},
+    {file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"},
+    {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"},
+    {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"},
+    {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"},
+    {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"},
+    {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"},
+    {file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"},
+    {file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"},
+    {file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"},
+    {file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"},
+    {file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"},
+    {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"},
+    {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"},
+    {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"},
+    {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"},
+    {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"},
+    {file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"},
+    {file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"},
+    {file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"},
+    {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"},
+    {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"},
+    {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"},
+    {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"},
+    {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"},
+    {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"},
+    {file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"},
+    {file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"},
+    {file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"},
+    {file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"},
+    {file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"},
+    {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"},
+    {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"},
+    {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"},
+    {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"},
+    {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"},
+    {file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"},
+    {file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"},
+    {file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"},
+    {file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"},
+    {file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"},
+    {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"},
+    {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"},
+    {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"},
+    {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"},
+    {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"},
+    {file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"},
+    {file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"},
+    {file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"},
+    {file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"},
 ]
 
 [package.extras]
@@ -461,35 +574,35 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "39.0.1"
+version = "39.0.2"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"},
-    {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"},
-    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"},
-    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"},
-    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"},
-    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"},
-    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"},
-    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"},
-    {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"},
-    {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"},
-    {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"},
-    {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"},
-    {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"},
-    {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"},
-    {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"},
-    {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"},
-    {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"},
-    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"},
-    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"},
-    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"},
-    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"},
-    {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"},
-    {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"},
+    {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06"},
+    {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7"},
+    {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612"},
+    {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a"},
+    {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97"},
+    {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828"},
+    {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011"},
+    {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536"},
+    {file = "cryptography-39.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5"},
+    {file = "cryptography-39.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0"},
+    {file = "cryptography-39.0.2-cp36-abi3-win32.whl", hash = "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480"},
+    {file = "cryptography-39.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9"},
+    {file = "cryptography-39.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac"},
+    {file = "cryptography-39.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074"},
+    {file = "cryptography-39.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1"},
+    {file = "cryptography-39.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3"},
+    {file = "cryptography-39.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354"},
+    {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915"},
+    {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84"},
+    {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108"},
+    {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3"},
+    {file = "cryptography-39.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3"},
+    {file = "cryptography-39.0.2.tar.gz", hash = "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f"},
 ]
 
 [package.dependencies]
@@ -517,6 +630,45 @@ files = [
     {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
 ]
 
+[[package]]
+name = "debugpy"
+version = "1.6.6"
+description = "An implementation of the Debug Adapter Protocol for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "debugpy-1.6.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:0ea1011e94416e90fb3598cc3ef5e08b0a4dd6ce6b9b33ccd436c1dffc8cd664"},
+    {file = "debugpy-1.6.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dff595686178b0e75580c24d316aa45a8f4d56e2418063865c114eef651a982e"},
+    {file = "debugpy-1.6.6-cp310-cp310-win32.whl", hash = "sha256:87755e173fcf2ec45f584bb9d61aa7686bb665d861b81faa366d59808bbd3494"},
+    {file = "debugpy-1.6.6-cp310-cp310-win_amd64.whl", hash = "sha256:72687b62a54d9d9e3fb85e7a37ea67f0e803aaa31be700e61d2f3742a5683917"},
+    {file = "debugpy-1.6.6-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:78739f77c58048ec006e2b3eb2e0cd5a06d5f48c915e2fc7911a337354508110"},
+    {file = "debugpy-1.6.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23c29e40e39ad7d869d408ded414f6d46d82f8a93b5857ac3ac1e915893139ca"},
+    {file = "debugpy-1.6.6-cp37-cp37m-win32.whl", hash = "sha256:7aa7e103610e5867d19a7d069e02e72eb2b3045b124d051cfd1538f1d8832d1b"},
+    {file = "debugpy-1.6.6-cp37-cp37m-win_amd64.whl", hash = "sha256:f6383c29e796203a0bba74a250615ad262c4279d398e89d895a69d3069498305"},
+    {file = "debugpy-1.6.6-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:23363e6d2a04d726bbc1400bd4e9898d54419b36b2cdf7020e3e215e1dcd0f8e"},
+    {file = "debugpy-1.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c"},
+    {file = "debugpy-1.6.6-cp38-cp38-win32.whl", hash = "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32"},
+    {file = "debugpy-1.6.6-cp38-cp38-win_amd64.whl", hash = "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225"},
+    {file = "debugpy-1.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6"},
+    {file = "debugpy-1.6.6-cp39-cp39-win32.whl", hash = "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe"},
+    {file = "debugpy-1.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1"},
+    {file = "debugpy-1.6.6-py2.py3-none-any.whl", hash = "sha256:be596b44448aac14eb3614248c91586e2bc1728e020e82ef3197189aae556115"},
+    {file = "debugpy-1.6.6.zip", hash = "sha256:b9c2130e1c632540fbf9c2c88341493797ddf58016e7cba02e311de9b0a96b67"},
+]
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+    {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+
 [[package]]
 name = "diffrax"
 version = "0.2.2"
@@ -607,6 +759,21 @@ files = [
 [package.extras]
 test = ["pytest (>=6)"]
 
+[[package]]
+name = "executing"
+version = "1.2.0"
+description = "Get the currently executing AST node of a frame, and other information"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"},
+    {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"},
+]
+
+[package.extras]
+tests = ["asttokens", "littleutils", "pytest", "rich"]
+
 [[package]]
 name = "filelock"
 version = "3.9.0"
@@ -642,18 +809,18 @@ pyflakes = ">=3.0.0,<3.1.0"
 
 [[package]]
 name = "fonttools"
-version = "4.38.0"
+version = "4.39.0"
 description = "Tools to manipulate font files"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "fonttools-4.38.0-py3-none-any.whl", hash = "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb"},
-    {file = "fonttools-4.38.0.zip", hash = "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1"},
+    {file = "fonttools-4.39.0-py3-none-any.whl", hash = "sha256:f5e764e1fd6ad54dfc201ff32af0ba111bcfbe0d05b24540af74c63db4ed6390"},
+    {file = "fonttools-4.39.0.zip", hash = "sha256:909c104558835eac27faeb56be5a4c32694192dca123d073bf746ce9254054af"},
 ]
 
 [package.extras]
-all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=14.0.0)", "xattr", "zopfli (>=0.1.4)"]
+all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"]
 graphite = ["lz4 (>=1.7.4.2)"]
 interpolatable = ["munkres", "scipy"]
 lxml = ["lxml (>=4.0,<5)"]
@@ -663,19 +830,52 @@ repacker = ["uharfbuzz (>=0.23.0)"]
 symfont = ["sympy"]
 type1 = ["xattr"]
 ufo = ["fs (>=2.2.0,<3)"]
-unicode = ["unicodedata2 (>=14.0.0)"]
+unicode = ["unicodedata2 (>=15.0.0)"]
 woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
 
+[[package]]
+name = "h5py"
+version = "3.8.0"
+description = "Read and write HDF5 files from Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "h5py-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:533d7dad466ddb7e3b30af274b630eb7c1a6e4ddf01d1c373a0334dc2152110a"},
+    {file = "h5py-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c873ba9fd4fa875ad62ce0e4891725e257a8fe7f5abdbc17e51a5d54819be55c"},
+    {file = "h5py-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3389b63222b1c7a158bb7fe69d11ca00066740ec5574596d47a2fe5317f563a"},
+    {file = "h5py-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f3350fc0a8407d668b13247861c2acd23f7f5fe7d060a3ad9b0820f5fcbcae0"},
+    {file = "h5py-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db03e3f2c716205fbdabb34d0848459840585225eb97b4f08998c743821ca323"},
+    {file = "h5py-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36761693efbe53df179627a775476dcbc37727d6e920958277a7efbc18f1fb73"},
+    {file = "h5py-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33b15aae79e9147aebe1d0e54099cbcde8d65e3e227cd5b59e49b1272aa0e09d"},
+    {file = "h5py-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f6f6ffadd6bfa9b2c5b334805eb4b19ca0a5620433659d8f7fb86692c40a359"},
+    {file = "h5py-3.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8f55d9c6c84d7d09c79fb85979e97b81ec6071cc776a97eb6b96f8f6ec767323"},
+    {file = "h5py-3.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:377865821fe80ad984d003723d6f8890bd54ceeb5981b43c0313b9df95411b30"},
+    {file = "h5py-3.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0fef76e10b9216657fa37e7edff6d8be0709b25bd5066474c229b56cf0098df9"},
+    {file = "h5py-3.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ffc344ec9984d2cd3ca0265007299a8bac8d85c1ad48f4639d8d3aed2af171"},
+    {file = "h5py-3.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bacaa1c16810dd2b3e4417f8e730971b7c4d53d234de61fe4a918db78e80e1e4"},
+    {file = "h5py-3.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47f757d1b76f0ecb8aa0508ec8d1b390df67a8b67ee2515dc1b046f3a1596ea"},
+    {file = "h5py-3.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f891b17e3a3e974e93f9e34e7cca9f530806543571ce078998676a555837d91d"},
+    {file = "h5py-3.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:290e00fa2de74a10688d1bac98d5a9cdd43f14f58e562c580b5b3dfbd358ecae"},
+    {file = "h5py-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:03890b1c123d024fb0239a3279737d5432498c1901c354f8b10d8221d1d16235"},
+    {file = "h5py-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49bc857635f935fa30e92e61ac1e87496df8f260a6945a3235e43a9890426866"},
+    {file = "h5py-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5fd2252d1fc364ba0e93dd0b7089f4906b66805cb4e6aca7fa8874ac08649647"},
+    {file = "h5py-3.8.0.tar.gz", hash = "sha256:6fead82f0c4000cf38d53f9c030780d81bfa0220218aee13b90b7701c937d95f"},
+]
+
+[package.dependencies]
+numpy = ">=1.14.5"
+
 [[package]]
 name = "identify"
-version = "2.5.18"
+version = "2.5.19"
 description = "File identification library for Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"},
-    {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"},
+    {file = "identify-2.5.19-py2.py3-none-any.whl", hash = "sha256:3ee3533e7f6f5023157fbebbd5687bb4b698ce6f305259e0d24b2d7d9efb72bc"},
+    {file = "identify-2.5.19.tar.gz", hash = "sha256:4102ecd051f6884449e7359e55b38ba6cd7aafb6ef27b8e2b38495a5723ea106"},
 ]
 
 [package.extras]
@@ -756,6 +956,79 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "ipykernel"
+version = "6.21.3"
+description = "IPython Kernel for Jupyter"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "ipykernel-6.21.3-py3-none-any.whl", hash = "sha256:24ebd9715e317c185e37156ab3a87382410185230dde7aeffce389d6c7d4428a"},
+    {file = "ipykernel-6.21.3.tar.gz", hash = "sha256:c8ff581905d70e7299bc1473a2f7c113bec1744fb3746d58e5b4b93bd8ee7001"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+comm = ">=0.1.1"
+debugpy = ">=1.6.5"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=20"
+tornado = ">=6.1"
+traitlets = ">=5.4.0"
+
+[package.extras]
+cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"]
+pyqt5 = ["pyqt5"]
+pyside6 = ["pyside6"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "ipython"
+version = "8.11.0"
+description = "IPython: Productive Interactive Computing"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "ipython-8.11.0-py3-none-any.whl", hash = "sha256:5b54478e459155a326bf5f42ee4f29df76258c0279c36f21d71ddb560f88b156"},
+    {file = "ipython-8.11.0.tar.gz", hash = "sha256:735cede4099dbc903ee540307b9171fbfef4aa75cfcacc5a273b2cda2f02be04"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "sys_platform == \"darwin\""}
+backcall = "*"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+jedi = ">=0.16"
+matplotlib-inline = "*"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+pickleshare = "*"
+prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+
+[package.extras]
+all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
+
 [[package]]
 name = "isort"
 version = "5.12.0"
@@ -831,14 +1104,14 @@ scipy = ">=1.5"
 
 [[package]]
 name = "jaxtyping"
-version = "0.2.12"
+version = "0.2.14"
 description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees."
 category = "main"
 optional = false
-python-versions = "~=3.7"
+python-versions = "~=3.8"
 files = [
-    {file = "jaxtyping-0.2.12-py3-none-any.whl", hash = "sha256:ae17bb386d3f0b7af8306d910bec85be793bdad427e28b4ea7a23d409488fb70"},
-    {file = "jaxtyping-0.2.12.tar.gz", hash = "sha256:7d3bca2bdbcb2f74d2f54d18d1957a2836a11dff09e44c2c023da254ddaf20e8"},
+    {file = "jaxtyping-0.2.14-py3-none-any.whl", hash = "sha256:91de0ef79a7ee497c4b5c08f096edbcfc7627bec71e865caaf6748e8175e8627"},
+    {file = "jaxtyping-0.2.14.tar.gz", hash = "sha256:5b78abbba18e0ca5b4c0870c965e204e0cdfadc1d385c24a0e1fcc15d6b3680d"},
 ]
 
 [package.dependencies]
@@ -846,6 +1119,26 @@ numpy = ">=1.20.0"
 typeguard = ">=2.13.3"
 typing-extensions = ">=3.7.4.1"
 
+[[package]]
+name = "jedi"
+version = "0.18.2"
+description = "An autocompletion tool for Python that can be used for text editors."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"},
+    {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"},
+]
+
+[package.dependencies]
+parso = ">=0.8.0,<0.9.0"
+
+[package.extras]
+docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+
 [[package]]
 name = "jinja2"
 version = "3.1.2"
@@ -864,6 +1157,51 @@ MarkupSafe = ">=2.0"
 [package.extras]
 i18n = ["Babel (>=2.7)"]
 
+[[package]]
+name = "jupyter-client"
+version = "8.0.3"
+description = "Jupyter protocol implementation and client libraries"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "jupyter_client-8.0.3-py3-none-any.whl", hash = "sha256:be48ac6bd659cbbddb7a674cf06b3b8afbf53f228253cf58bde604c03bd487b0"},
+    {file = "jupyter_client-8.0.3.tar.gz", hash = "sha256:ed65498bea6d876ef9d8da3e0db3dd33c5d129f5b2645f56ae03993782966bd0"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""}
+jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["codecov", "coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-core"
+version = "5.2.0"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "jupyter_core-5.2.0-py3-none-any.whl", hash = "sha256:4bdc2928c37f6917130c667d8b8708f20aee539d8283c6be72aabd2a4b4c83b0"},
+    {file = "jupyter_core-5.2.0.tar.gz", hash = "sha256:1407cdb4c79ee467696c04b76633fc1884015fa109323365a6372c8e890cc83f"},
+]
+
+[package.dependencies]
+platformdirs = ">=2.5"
+pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["myst-parser", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"]
+test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
+
 [[package]]
 name = "kiwisolver"
 version = "1.4.4"
@@ -1122,53 +1460,53 @@ files = [
 
 [[package]]
 name = "matplotlib"
-version = "3.7.0"
+version = "3.7.1"
 description = "Python plotting package"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "matplotlib-3.7.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:3da8b9618188346239e51f1ea6c0f8f05c6e218cfcc30b399dd7dd7f52e8bceb"},
-    {file = "matplotlib-3.7.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c0592ba57217c22987b7322df10f75ef95bc44dce781692b4b7524085de66019"},
-    {file = "matplotlib-3.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:21269450243d6928da81a9bed201f0909432a74e7d0d65db5545b9fa8a0d0223"},
-    {file = "matplotlib-3.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb2e76cd429058d8954121c334dddfcd11a6186c6975bca61f3f248c99031b05"},
-    {file = "matplotlib-3.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de20eb1247725a2f889173d391a6d9e7e0f2540feda24030748283108b0478ec"},
-    {file = "matplotlib-3.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5465735eaaafd1cfaec3fed60aee776aeb3fd3992aa2e49f4635339c931d443"},
-    {file = "matplotlib-3.7.0-cp310-cp310-win32.whl", hash = "sha256:092e6abc80cdf8a95f7d1813e16c0e99ceda8d5b195a3ab859c680f3487b80a2"},
-    {file = "matplotlib-3.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:4f640534ec2760e270801056bc0d8a10777c48b30966eef78a7c35d8590915ba"},
-    {file = "matplotlib-3.7.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f336e7014889c38c59029ebacc35c59236a852e4b23836708cfd3f43d1eaeed5"},
-    {file = "matplotlib-3.7.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a10428d4f8d1a478ceabd652e61a175b2fdeed4175ab48da4a7b8deb561e3fa"},
-    {file = "matplotlib-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46ca923e980f76d34c1c633343a72bb042d6ba690ecc649aababf5317997171d"},
-    {file = "matplotlib-3.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c849aa94ff2a70fb71f318f48a61076d1205c6013b9d3885ade7f992093ac434"},
-    {file = "matplotlib-3.7.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827e78239292e561cfb70abf356a9d7eaf5bf6a85c97877f254009f20b892f89"},
-    {file = "matplotlib-3.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:691ef1f15360e439886186d0db77b5345b24da12cbc4fc57b26c4826db4d6cab"},
-    {file = "matplotlib-3.7.0-cp311-cp311-win32.whl", hash = "sha256:21a8aeac39b4a795e697265d800ce52ab59bdeb6bb23082e2d971f3041074f02"},
-    {file = "matplotlib-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:01681566e95b9423021b49dea6a2395c16fa054604eacb87f0f4c439750f9114"},
-    {file = "matplotlib-3.7.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cf119eee4e57389fba5ac8b816934e95c256535e55f0b21628b4205737d1de85"},
-    {file = "matplotlib-3.7.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:21bd4033c40b95abd5b8453f036ed5aa70856e56ecbd887705c37dce007a4c21"},
-    {file = "matplotlib-3.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:111ef351f28fd823ed7177632070a6badd6f475607122bc9002a526f2502a0b5"},
-    {file = "matplotlib-3.7.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f91d35b3ef51d29d9c661069b9e4ba431ce283ffc533b981506889e144b5b40e"},
-    {file = "matplotlib-3.7.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a776462a4a63c0bfc9df106c15a0897aa2dbab6795c693aa366e8e283958854"},
-    {file = "matplotlib-3.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dfd4a0cbd151f6439e6d7f8dca5292839ca311e7e650596d073774847ca2e4f"},
-    {file = "matplotlib-3.7.0-cp38-cp38-win32.whl", hash = "sha256:56b7b79488209041a9bf7ddc34f1b069274489ce69e34dc63ae241d0d6b4b736"},
-    {file = "matplotlib-3.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:8665855f3919c80551f377bc16df618ceabf3ef65270bc14b60302dce88ca9ab"},
-    {file = "matplotlib-3.7.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:f910d924da8b9fb066b5beae0b85e34ed1b6293014892baadcf2a51da1c65807"},
-    {file = "matplotlib-3.7.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cf6346644e8fe234dc847e6232145dac199a650d3d8025b3ef65107221584ba4"},
-    {file = "matplotlib-3.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d1e52365d8d5af699f04581ca191112e1d1220a9ce4386b57d807124d8b55e6"},
-    {file = "matplotlib-3.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c869b646489c6a94375714032e5cec08e3aa8d3f7d4e8ef2b0fb50a52b317ce6"},
-    {file = "matplotlib-3.7.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ddac5f59e78d04b20469bc43853a8e619bb6505c7eac8ffb343ff2c516d72f"},
-    {file = "matplotlib-3.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0304c1cd802e9a25743414c887e8a7cd51d96c9ec96d388625d2cd1c137ae3"},
-    {file = "matplotlib-3.7.0-cp39-cp39-win32.whl", hash = "sha256:a06a6c9822e80f323549c6bc9da96d4f233178212ad9a5f4ab87fd153077a507"},
-    {file = "matplotlib-3.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb52aa97b92acdee090edfb65d1cb84ea60ab38e871ba8321a10bbcebc2a3540"},
-    {file = "matplotlib-3.7.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3493b48e56468c39bd9c1532566dff3b8062952721b7521e1f394eb6791495f4"},
-    {file = "matplotlib-3.7.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d0dcd1a0bf8d56551e8617d6dc3881d8a1c7fb37d14e5ec12cbb293f3e6170a"},
-    {file = "matplotlib-3.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51fb664c37714cbaac69c16d6b3719f517a13c96c3f76f4caadd5a0aa7ed0329"},
-    {file = "matplotlib-3.7.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4497d88c559b76da320b7759d64db442178beeea06a52dc0c629086982082dcd"},
-    {file = "matplotlib-3.7.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9d85355c48ef8b9994293eb7c00f44aa8a43cad7a297fbf0770a25cdb2244b91"},
-    {file = "matplotlib-3.7.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03eb2c8ff8d85da679b71e14c7c95d16d014c48e0c0bfa14db85f6cdc5c92aad"},
-    {file = "matplotlib-3.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71b751d06b2ed1fd017de512d7439c0259822864ea16731522b251a27c0b2ede"},
-    {file = "matplotlib-3.7.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b51ab8a5d5d3bbd4527af633a638325f492e09e45e78afdf816ef55217a09664"},
-    {file = "matplotlib-3.7.0.tar.gz", hash = "sha256:8f6efd313430d7ef70a38a3276281cb2e8646b3a22b3b21eb227da20e15e6813"},
+    {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"},
+    {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"},
+    {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"},
+    {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"},
+    {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"},
+    {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"},
+    {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"},
+    {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"},
+    {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"},
+    {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"},
+    {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"},
+    {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"},
+    {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"},
+    {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"},
+    {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"},
+    {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"},
+    {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"},
+    {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"},
+    {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"},
+    {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"},
+    {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"},
+    {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"},
+    {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"},
+    {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"},
+    {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"},
+    {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"},
+    {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"},
+    {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"},
+    {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"},
+    {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"},
+    {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"},
+    {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"},
+    {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"},
 ]
 
 [package.dependencies]
@@ -1183,6 +1521,21 @@ pillow = ">=6.2.0"
 pyparsing = ">=2.3.1"
 python-dateutil = ">=2.7"
 
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.6"
+description = "Inline Matplotlib backend for Jupyter"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
+    {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
+]
+
+[package.dependencies]
+traitlets = "*"
+
 [[package]]
 name = "mccabe"
 version = "0.7.0"
@@ -1197,14 +1550,14 @@ files = [
 
 [[package]]
 name = "mdit-py-plugins"
-version = "0.3.4"
+version = "0.3.5"
 description = "Collection of plugins for markdown-it-py"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "mdit-py-plugins-0.3.4.tar.gz", hash = "sha256:3278aab2e2b692539082f05e1243f24742194ffd92481f48844f057b51971283"},
-    {file = "mdit_py_plugins-0.3.4-py3-none-any.whl", hash = "sha256:4f1441264ac5cb39fa40a5901921c2acf314ea098d75629750c138f80d552cdf"},
+    {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"},
+    {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"},
 ]
 
 [package.dependencies]
@@ -1227,6 +1580,24 @@ files = [
     {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
 ]
 
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+description = "Python library for arbitrary-precision floating-point arithmetic"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
+    {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
+]
+
+[package.extras]
+develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"]
+docs = ["sphinx"]
+gmpy = ["gmpy2 (>=2.1.0a4)"]
+tests = ["pytest (>=4.6)"]
+
 [[package]]
 name = "multitasking"
 version = "0.0.11"
@@ -1280,20 +1651,32 @@ testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov",
 
 [[package]]
 name = "natsort"
-version = "8.2.0"
+version = "8.3.1"
 description = "Simple yet flexible natural sorting in Python."
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "natsort-8.2.0-py3-none-any.whl", hash = "sha256:04fe18fdd2b9e5957f19f687eb117f102ef8dde6b574764e536e91194bed4f5f"},
-    {file = "natsort-8.2.0.tar.gz", hash = "sha256:57f85b72c688b09e053cdac302dd5b5b53df5f73ae20b4874fcbffd8bf783d11"},
+    {file = "natsort-8.3.1-py3-none-any.whl", hash = "sha256:d583bc9050dd10538de36297c960b93f873f0cd01671a3c50df5bd86dd391dcb"},
+    {file = "natsort-8.3.1.tar.gz", hash = "sha256:517595492dde570a4fd6b6a76f644440c1ba51e2338c8a671d7f0475fda8f9fd"},
 ]
 
 [package.extras]
 fast = ["fastnumbers (>=2.0.0)"]
 icu = ["PyICU (>=1.0.0)"]
 
+[[package]]
+name = "nest-asyncio"
+version = "1.5.6"
+description = "Patch asyncio to allow nested event loops"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "nest_asyncio-1.5.6-py3-none-any.whl", hash = "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8"},
+    {file = "nest_asyncio-1.5.6.tar.gz", hash = "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290"},
+]
+
 [[package]]
 name = "nodeenv"
 version = "1.7.0"
@@ -1309,19 +1692,6 @@ files = [
 [package.dependencies]
 setuptools = "*"
 
-[[package]]
-name = "nose"
-version = "1.3.7"
-description = "nose extends unittest to make testing easier"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
-    {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"},
-    {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"},
-    {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"},
-]
-
 [[package]]
 name = "numpy"
 version = "1.24.2"
@@ -1440,6 +1810,22 @@ pytz = ">=2020.1"
 [package.extras]
 test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
 
+[[package]]
+name = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
+    {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
+]
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["docopt", "pytest (<6.0.0)"]
+
 [[package]]
 name = "pathspec"
 version = "0.11.0"
@@ -1452,6 +1838,33 @@ files = [
     {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
 ]
 
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pickleshare"
+version = "0.7.5"
+description = "Tiny 'shelve'-like database with concurrency support"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
+    {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
+]
+
 [[package]]
 name = "pillow"
 version = "9.4.0"
@@ -1536,16 +1949,28 @@ files = [
 docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
 tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
 
+[[package]]
+name = "pkgconfig"
+version = "1.5.5"
+description = "Interface Python with pkg-config"
+category = "main"
+optional = false
+python-versions = ">=3.3,<4.0"
+files = [
+    {file = "pkgconfig-1.5.5-py3-none-any.whl", hash = "sha256:d20023bbeb42ee6d428a0fac6e0904631f545985a10cdd71a20aa58bc47a4209"},
+    {file = "pkgconfig-1.5.5.tar.gz", hash = "sha256:deb4163ef11f75b520d822d9505c1f462761b4309b1bb713d08689759ea8b899"},
+]
+
 [[package]]
 name = "platformdirs"
-version = "3.0.0"
+version = "3.1.1"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
-    {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
+    {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"},
+    {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"},
 ]
 
 [package.extras]
@@ -1587,6 +2012,75 @@ nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
 virtualenv = ">=20.10.0"
 
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.38"
+description = "Library for building powerful interactive command lines in Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"},
+    {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "psutil"
+version = "5.9.4"
+description = "Cross-platform lib for process and system monitoring in Python."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"},
+    {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"},
+    {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"},
+    {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"},
+    {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"},
+    {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"},
+    {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"},
+    {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"},
+    {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"},
+    {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"},
+    {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"},
+    {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"},
+    {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"},
+    {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"},
+]
+
+[package.extras]
+test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+    {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
+]
+
+[package.extras]
+tests = ["pytest"]
+
 [[package]]
 name = "py"
 version = "1.11.0"
@@ -1623,6 +2117,32 @@ files = [
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
 ]
 
+[[package]]
+name = "pydata-sphinx-theme"
+version = "0.13.1"
+description = "Bootstrap-based Sphinx theme from the PyData community"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pydata_sphinx_theme-0.13.1-py3-none-any.whl", hash = "sha256:ce29c1de7961d616dfa25f4c3a9619818d4bb2d4a9985ed078367af294fbccc2"},
+    {file = "pydata_sphinx_theme-0.13.1.tar.gz", hash = "sha256:a37d967207c2d787cfe5cf74abfc6b3bcaf0606cac6b283a3876e603d67d6a71"},
+]
+
+[package.dependencies]
+accessible-pygments = "*"
+Babel = "*"
+beautifulsoup4 = "*"
+docutils = "!=0.17.0"
+packaging = "*"
+pygments = ">=2.7"
+sphinx = ">=4.2"
+
+[package.extras]
+dev = ["nox", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml"]
+doc = ["ablog (>=0.11.0rc2)", "colorama", "ipyleaflet", "jupyter_sphinx", "linkify-it-py", "matplotlib", "myst-nb", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube", "sphinxext-rediraffe", "xarray"]
+test = ["codecov", "pytest", "pytest-cov", "pytest-regressions"]
+
 [[package]]
 name = "pyflakes"
 version = "3.0.1"
@@ -1681,49 +2201,16 @@ files = [
 flake8 = "6.0.0"
 tomli = {version = "*", markers = "python_version < \"3.11\""}
 
-[[package]]
-name = "pysces"
-version = "1.0.3"
-description = "The Python Simulator for Cellular Systems - simulation and analysis tools for modelling biological systems"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
-    {file = "pysces-1.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a785d2c0f558bdf414cda3375a996f921f2a912422346530560b663cbc793fd3"},
-    {file = "pysces-1.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42daf24b75db2efbce46d062d20cf5b53860e00607cce5094a0ddf7e2a7e0c1"},
-    {file = "pysces-1.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:0903cd136c378ec84dee800fdf29be7619615f86fdc4b16be201955e3f5d692c"},
-    {file = "pysces-1.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cbdaef41bdcc60038ea93815bc3fe456f7fa482d5761699ad76732f265185cc3"},
-    {file = "pysces-1.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e15f486ceb1e43cce58f96197e77933513672d1167e1c92989f0685c4d5b6bb9"},
-    {file = "pysces-1.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:d067e051b8733572cbbe0243eb623a441d78d604a1dcb85a18b5cc681f2d6135"},
-    {file = "pysces-1.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36f7da6e353ae700bc74ab548d5686d8da2824826d0cacdd930ca3feee6be2ba"},
-    {file = "pysces-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d171525222a02b51783e9c754b4e7c53e4d49ada4d0dd171cc39288bb2ae80b3"},
-    {file = "pysces-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:a46064b9a54f145c93ad93e7605d88dc28e1b1467f4cc10d86351cda7b938292"},
-    {file = "pysces-1.0.3.tar.gz", hash = "sha256:19bb2da5a02b9e5b3e4ef77924d4eaa62505e6ac8c4373f9f3704f2118db0059"},
-]
-
-[package.dependencies]
-matplotlib = "*"
-nose = "*"
-numpy = "*"
-python-libsbml = {version = "*", optional = true, markers = "extra == \"sbml\""}
-scipy = "*"
-
-[package.extras]
-all = ["assimulo", "ipyparallel", "python-libsbml"]
-cvode = ["assimulo"]
-parscan = ["ipyparallel"]
-sbml = ["python-libsbml"]
-
 [[package]]
 name = "pytest"
-version = "7.2.1"
+version = "7.2.2"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
-    {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
+    {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
+    {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
 ]
 
 [package.dependencies]
@@ -1840,6 +2327,30 @@ files = [
     {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
 ]
 
+[[package]]
+name = "pywin32"
+version = "305"
+description = "Python for Window Extensions"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pywin32-305-cp310-cp310-win32.whl", hash = "sha256:421f6cd86e84bbb696d54563c48014b12a23ef95a14e0bdba526be756d89f116"},
+    {file = "pywin32-305-cp310-cp310-win_amd64.whl", hash = "sha256:73e819c6bed89f44ff1d690498c0a811948f73777e5f97c494c152b850fad478"},
+    {file = "pywin32-305-cp310-cp310-win_arm64.whl", hash = "sha256:742eb905ce2187133a29365b428e6c3b9001d79accdc30aa8969afba1d8470f4"},
+    {file = "pywin32-305-cp311-cp311-win32.whl", hash = "sha256:19ca459cd2e66c0e2cc9a09d589f71d827f26d47fe4a9d09175f6aa0256b51c2"},
+    {file = "pywin32-305-cp311-cp311-win_amd64.whl", hash = "sha256:326f42ab4cfff56e77e3e595aeaf6c216712bbdd91e464d167c6434b28d65990"},
+    {file = "pywin32-305-cp311-cp311-win_arm64.whl", hash = "sha256:4ecd404b2c6eceaca52f8b2e3e91b2187850a1ad3f8b746d0796a98b4cea04db"},
+    {file = "pywin32-305-cp36-cp36m-win32.whl", hash = "sha256:48d8b1659284f3c17b68587af047d110d8c44837736b8932c034091683e05863"},
+    {file = "pywin32-305-cp36-cp36m-win_amd64.whl", hash = "sha256:13362cc5aa93c2beaf489c9c9017c793722aeb56d3e5166dadd5ef82da021fe1"},
+    {file = "pywin32-305-cp37-cp37m-win32.whl", hash = "sha256:a55db448124d1c1484df22fa8bbcbc45c64da5e6eae74ab095b9ea62e6d00496"},
+    {file = "pywin32-305-cp37-cp37m-win_amd64.whl", hash = "sha256:109f98980bfb27e78f4df8a51a8198e10b0f347257d1e265bb1a32993d0c973d"},
+    {file = "pywin32-305-cp38-cp38-win32.whl", hash = "sha256:9dd98384da775afa009bc04863426cb30596fd78c6f8e4e2e5bbf4edf8029504"},
+    {file = "pywin32-305-cp38-cp38-win_amd64.whl", hash = "sha256:56d7a9c6e1a6835f521788f53b5af7912090674bb84ef5611663ee1595860fc7"},
+    {file = "pywin32-305-cp39-cp39-win32.whl", hash = "sha256:9d968c677ac4d5cbdaa62fd3014ab241718e619d8e36ef8e11fb930515a1e918"},
+    {file = "pywin32-305-cp39-cp39-win_amd64.whl", hash = "sha256:50768c6b7c3f0b38b7fb14dd4104da93ebced5f1a50dc0e834594bff6fbe1271"},
+]
+
 [[package]]
 name = "pyyaml"
 version = "6.0"
@@ -1890,6 +2401,96 @@ files = [
     {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
 ]
 
+[[package]]
+name = "pyzmq"
+version = "25.0.0"
+description = "Python bindings for 0MQ"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "pyzmq-25.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:2d05d904f03ddf1e0d83d97341354dfe52244a619b5a1440a5f47a5b3451e84e"},
+    {file = "pyzmq-25.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a154ef810d44f9d28868be04641f837374a64e7449df98d9208e76c260c7ef1"},
+    {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:487305c2a011fdcf3db1f24e8814bb76d23bc4d2f46e145bc80316a59a9aa07d"},
+    {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e7b87638ee30ab13230e37ce5331b3e730b1e0dda30120b9eeec3540ed292c8"},
+    {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75243e422e85a62f0ab7953dc315452a56b2c6a7e7d1a3c3109ac3cc57ed6b47"},
+    {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:31e523d067ce44a04e876bed3ff9ea1ff8d1b6636d16e5fcace9d22f8c564369"},
+    {file = "pyzmq-25.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8539216173135e9e89f6b1cc392e74e6b935b91e8c76106cf50e7a02ab02efe5"},
+    {file = "pyzmq-25.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2754fa68da08a854f4816e05160137fa938a2347276471103d31e04bcee5365c"},
+    {file = "pyzmq-25.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1bc30f0c18444d51e9b0d0dd39e3a4e7c53ee74190bebef238cd58de577ea9"},
+    {file = "pyzmq-25.0.0-cp310-cp310-win32.whl", hash = "sha256:01d53958c787cfea34091fcb8ef36003dbb7913b8e9f8f62a0715234ebc98b70"},
+    {file = "pyzmq-25.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:58fc3ad5e1cfd2e6d24741fbb1e216b388115d31b0ca6670f894187f280b6ba6"},
+    {file = "pyzmq-25.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e4bba04ea779a3d7ef25a821bb63fd0939142c88e7813e5bd9c6265a20c523a2"},
+    {file = "pyzmq-25.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af1fbfb7ad6ac0009ccee33c90a1d303431c7fb594335eb97760988727a37577"},
+    {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85456f0d8f3268eecd63dede3b99d5bd8d3b306310c37d4c15141111d22baeaf"},
+    {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0645b5a2d2a06fd8eb738018490c514907f7488bf9359c6ee9d92f62e844b76f"},
+    {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f72ea279b2941a5203e935a4588b9ba8a48aeb9a926d9dfa1986278bd362cb8"},
+    {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4e295f7928a31ae0f657e848c5045ba6d693fe8921205f408ca3804b1b236968"},
+    {file = "pyzmq-25.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ac97e7d647d5519bcef48dd8d3d331f72975afa5c4496c95f6e854686f45e2d9"},
+    {file = "pyzmq-25.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:656281d496aaf9ca4fd4cea84e6d893e3361057c4707bd38618f7e811759103c"},
+    {file = "pyzmq-25.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f6116991568aac48b94d6d8aaed6157d407942ea385335a6ed313692777fb9d"},
+    {file = "pyzmq-25.0.0-cp311-cp311-win32.whl", hash = "sha256:0282bba9aee6e0346aa27d6c69b5f7df72b5a964c91958fc9e0c62dcae5fdcdc"},
+    {file = "pyzmq-25.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:526f884a27e8bba62fe1f4e07c62be2cfe492b6d432a8fdc4210397f8cf15331"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ccb3e1a863222afdbda42b7ca8ac8569959593d7abd44f5a709177d6fa27d266"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4046d03100aca266e70d54a35694cb35d6654cfbef633e848b3c4a8d64b9d187"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3100dddcada66ec5940ed6391ebf9d003cc3ede3d320748b2737553019f58230"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7877264aa851c19404b1bb9dbe6eed21ea0c13698be1eda3784aab3036d1c861"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5049e75cc99db65754a3da5f079230fb8889230cf09462ec972d884d1704a3ed"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:81f99fb1224d36eb91557afec8cdc2264e856f3464500b55749020ce4c848ef2"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a1cd4a95f176cdc0ee0a82d49d5830f13ae6015d89decbf834c273bc33eeb3d3"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-win32.whl", hash = "sha256:926236ca003aec70574754f39703528947211a406f5c6c8b3e50eca04a9e87fc"},
+    {file = "pyzmq-25.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:94f0a7289d0f5c80807c37ebb404205e7deb737e8763eb176f4770839ee2a287"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f3f96d452e9580cb961ece2e5a788e64abaecb1232a80e61deffb28e105ff84a"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:930e6ad4f2eaac31a3d0c2130619d25db754b267487ebc186c6ad18af2a74018"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e1081d7030a1229c8ff90120346fb7599b54f552e98fcea5170544e7c6725aab"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:531866c491aee5a1e967c286cfa470dffac1e2a203b1afda52d62b58782651e9"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fc7c1421c5b1c916acf3128bf3cc7ea7f5018b58c69a6866d70c14190e600ce9"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9a2d5e419bd39a1edb6cdd326d831f0120ddb9b1ff397e7d73541bf393294973"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:183e18742be3621acf8908903f689ec520aee3f08449bfd29f583010ca33022b"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-win32.whl", hash = "sha256:02f5cb60a7da1edd5591a15efa654ffe2303297a41e1b40c3c8942f8f11fc17c"},
+    {file = "pyzmq-25.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:cac602e02341eaaf4edfd3e29bd3fdef672e61d4e6dfe5c1d065172aee00acee"},
+    {file = "pyzmq-25.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:e14df47c1265356715d3d66e90282a645ebc077b70b3806cf47efcb7d1d630cb"},
+    {file = "pyzmq-25.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:293a7c2128690f496057f1f1eb6074f8746058d13588389981089ec45d8fdc77"},
+    {file = "pyzmq-25.0.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:731b208bc9412deeb553c9519dca47136b5a01ca66667cafd8733211941b17e4"},
+    {file = "pyzmq-25.0.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b055a1cddf8035966ad13aa51edae5dc8f1bba0b5d5e06f7a843d8b83dc9b66b"},
+    {file = "pyzmq-25.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e1cb97d573ea84d7cd97188b42ca6f611ab3ee600f6a75041294ede58e3d20"},
+    {file = "pyzmq-25.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:60ecbfe7669d3808ffa8a7dd1487d6eb8a4015b07235e3b723d4b2a2d4de7203"},
+    {file = "pyzmq-25.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4c25c95416133942280faaf068d0fddfd642b927fb28aaf4ab201a738e597c1e"},
+    {file = "pyzmq-25.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:be05504af0619d1cffa500af1e0ede69fb683f301003851f5993b5247cc2c576"},
+    {file = "pyzmq-25.0.0-cp38-cp38-win32.whl", hash = "sha256:6bf3842af37af43fa953e96074ebbb5315f6a297198f805d019d788a1021dbc8"},
+    {file = "pyzmq-25.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b90bb8dfbbd138558f1f284fecfe328f7653616ff9a972433a00711d9475d1a9"},
+    {file = "pyzmq-25.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:62b9e80890c0d2408eb42d5d7e1fc62a5ce71be3288684788f74cf3e59ffd6e2"},
+    {file = "pyzmq-25.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484c2c4ee02c1edc07039f42130bd16e804b1fe81c4f428e0042e03967f40c20"},
+    {file = "pyzmq-25.0.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9ca6db34b26c4d3e9b0728841ec9aa39484eee272caa97972ec8c8e231b20c7e"},
+    {file = "pyzmq-25.0.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:610d2d112acd4e5501fac31010064a6c6efd716ceb968e443cae0059eb7b86de"},
+    {file = "pyzmq-25.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3594c0ff604e685d7e907860b61d0e10e46c74a9ffca168f6e9e50ea934ee440"},
+    {file = "pyzmq-25.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c21a5f4e54a807df5afdef52b6d24ec1580153a6bcf0607f70a6e1d9fa74c5c3"},
+    {file = "pyzmq-25.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4725412e27612f0d7d7c2f794d89807ad0227c2fc01dd6146b39ada49c748ef9"},
+    {file = "pyzmq-25.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d3d604fe0a67afd1aff906e54da557a5203368a99dcc50a70eef374f1d2abef"},
+    {file = "pyzmq-25.0.0-cp39-cp39-win32.whl", hash = "sha256:3670e8c5644768f214a3b598fe46378a4a6f096d5fb82a67dfd3440028460565"},
+    {file = "pyzmq-25.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e99629a976809fe102ef73e856cf4b2660acd82a412a51e80ba2215e523dfd0a"},
+    {file = "pyzmq-25.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:66509c48f7446b640eeae24b60c9c1461799a27b1b0754e438582e36b5af3315"},
+    {file = "pyzmq-25.0.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c464cc508177c09a5a6122b67f978f20e2954a21362bf095a0da4647e3e908"},
+    {file = "pyzmq-25.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:28bcb2e66224a7ac2843eb632e4109d6b161479e7a2baf24e37210461485b4f1"},
+    {file = "pyzmq-25.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0e7ef9ac807db50b4eb6f534c5dcc22f998f5dae920cc28873d2c1d080a4fc9"},
+    {file = "pyzmq-25.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5050f5c50b58a6e38ccaf9263a356f74ef1040f5ca4030225d1cb1a858c5b7b6"},
+    {file = "pyzmq-25.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2a73af6504e0d2805e926abf136ebf536735a13c22f709be7113c2ec65b4bec3"},
+    {file = "pyzmq-25.0.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0e8d00228db627ddd1b418c7afd81820b38575f237128c9650365f2dd6ac3443"},
+    {file = "pyzmq-25.0.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5605621f2181f20b71f13f698944deb26a0a71af4aaf435b34dd90146092d530"},
+    {file = "pyzmq-25.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6136bfb0e5a9cf8c60c6ac763eb21f82940a77e6758ea53516c8c7074f4ff948"},
+    {file = "pyzmq-25.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0a90b2480a26aef7c13cff18703ba8d68e181facb40f78873df79e6d42c1facc"},
+    {file = "pyzmq-25.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00c94fd4c9dd3c95aace0c629a7fa713627a5c80c1819326b642adf6c4b8e2a2"},
+    {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20638121b0bdc80777ce0ec8c1f14f1ffec0697a1f88f0b564fa4a23078791c4"},
+    {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f75b4b8574f3a8a0d6b4b52606fc75b82cb4391471be48ab0b8677c82f9ed4"},
+    {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cbb885f347eba7ab7681c450dee5b14aed9f153eec224ec0c3f299273d9241f"},
+    {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c48f257da280b3be6c94e05bd575eddb1373419dbb1a72c3ce64e88f29d1cd6d"},
+    {file = "pyzmq-25.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:866eabf7c1315ef2e93e34230db7cbf672e0d7c626b37c11f7e870c8612c3dcc"},
+    {file = "pyzmq-25.0.0.tar.gz", hash = "sha256:f330a1a2c7f89fd4b0aa4dcb7bf50243bf1c8da9a2f1efc31daf57a2046b31f2"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
 [[package]]
 name = "requests"
 version = "2.28.2"
@@ -1913,20 +2514,19 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
-name = "sbmltoodepy"
-version = "1.0.4"
-description = "A package that creates a python implementation of an SBML model."
+name = "schwimmbad"
+version = "0.3.2"
+description = "A common interface for parallel processing pools."
 category = "main"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 files = [
-    {file = "sbmltoodepy-1.0.4.tar.gz", hash = "sha256:8c212f630580c882dab60b5d548fa331848412d3ae8e0a626319342c7a69dbe0"},
+    {file = "schwimmbad-0.3.2.tar.gz", hash = "sha256:d453b735fcd0d4a5c5a6e18af2f72f678358f7614db3b78b6cd38d6b07debc9f"},
 ]
 
-[package.dependencies]
-numpy = "*"
-python-libsbml = "*"
-scipy = "*"
+[package.extras]
+docs = ["alabaster", "emcee"]
+test = ["dill", "joblib", "mpi4py", "numpy", "pytest", "pytest-astropy", "pytest-cov"]
 
 [[package]]
 name = "scipy"
@@ -1987,14 +2587,14 @@ emoji = ["emoji"]
 
 [[package]]
 name = "setuptools"
-version = "67.4.0"
+version = "67.6.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"},
-    {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"},
+    {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"},
+    {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"},
 ]
 
 [package.extras]
@@ -2026,6 +2626,18 @@ files = [
     {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
 ]
 
+[[package]]
+name = "soupsieve"
+version = "2.4"
+description = "A modern CSS selector implementation for Beautiful Soup."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"},
+    {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"},
+]
+
 [[package]]
 name = "sphinx"
 version = "5.3.0"
@@ -2062,6 +2674,27 @@ docs = ["sphinxcontrib-websupport"]
 lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"]
 test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
 
+[[package]]
+name = "sphinx-book-theme"
+version = "1.0.0"
+description = "A clean book theme for scientific explanations and documentation with Sphinx"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "sphinx_book_theme-1.0.0-py3-none-any.whl", hash = "sha256:f6baba7888d5a63328a210e9960a7a6e951e9632d74edb6fce48c9d5d8c28768"},
+    {file = "sphinx_book_theme-1.0.0.tar.gz", hash = "sha256:7c76ea51347e55d801504bfb1b2433cba49e7212a25fbebd0ce44b3c098ea946"},
+]
+
+[package.dependencies]
+pydata-sphinx-theme = ">=0.13.0"
+sphinx = ">=4,<7"
+
+[package.extras]
+code-style = ["pre-commit"]
+doc = ["ablog", "docutils (==0.17.1)", "folium", "ipywidgets", "matplotlib", "myst-nb", "nbclient", "numpy", "numpydoc", "pandas", "plotly", "sphinx-copybutton", "sphinx-design", "sphinx-examples", "sphinx-tabs (<=3.4.0)", "sphinx-thebe", "sphinx-togglebutton", "sphinxcontrib-bibtex", "sphinxcontrib-youtube", "sphinxext-opengraph"]
+test = ["beautifulsoup4", "coverage", "myst-nb", "pytest", "pytest-cov", "pytest-regressions", "sphinx_thebe"]
+
 [[package]]
 name = "sphinx-copybutton"
 version = "0.5.1"
@@ -2232,6 +2865,41 @@ files = [
 lint = ["docutils-stubs", "flake8", "mypy"]
 test = ["pytest"]
 
+[[package]]
+name = "stack-data"
+version = "0.6.2"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"},
+    {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"},
+]
+
+[package.dependencies]
+asttokens = ">=2.1.0"
+executing = ">=1.2.0"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
+
+[[package]]
+name = "sympy"
+version = "1.11.1"
+description = "Computer algebra system (CAS) in Python"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "sympy-1.11.1-py3-none-any.whl", hash = "sha256:938f984ee2b1e8eae8a07b884c8b7a1146010040fccddc6539c54f401c8f6fcf"},
+    {file = "sympy-1.11.1.tar.gz", hash = "sha256:e32380dce63cb7c0108ed525570092fd45168bdae2faa17e528221ef72e88658"},
+]
+
+[package.dependencies]
+mpmath = ">=0.19"
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
@@ -2244,16 +2912,49 @@ files = [
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
 
+[[package]]
+name = "toposort"
+version = "1.10"
+description = "Implements a topological sort algorithm."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87"},
+    {file = "toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd"},
+]
+
+[[package]]
+name = "tornado"
+version = "6.2"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+category = "dev"
+optional = false
+python-versions = ">= 3.7"
+files = [
+    {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
+    {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},
+    {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"},
+    {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"},
+    {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"},
+    {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"},
+    {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"},
+    {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"},
+    {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"},
+    {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"},
+    {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"},
+]
+
 [[package]]
 name = "tqdm"
-version = "4.64.1"
+version = "4.65.0"
 description = "Fast, Extensible Progress Meter"
 category = "main"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = ">=3.7"
 files = [
-    {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"},
-    {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"},
+    {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"},
+    {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"},
 ]
 
 [package.dependencies]
@@ -2265,6 +2966,22 @@ notebook = ["ipywidgets (>=6)"]
 slack = ["slack-sdk"]
 telegram = ["requests"]
 
+[[package]]
+name = "traitlets"
+version = "5.9.0"
+description = "Traitlets Python configuration system"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"},
+    {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
+
 [[package]]
 name = "typeguard"
 version = "2.13.3"
@@ -2312,14 +3029,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.19.0"
+version = "20.20.0"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"},
-    {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"},
+    {file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"},
+    {file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"},
 ]
 
 [package.dependencies]
@@ -2331,6 +3048,18 @@ platformdirs = ">=2.4,<4"
 docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
 test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
 
+[[package]]
+name = "wcwidth"
+version = "0.2.6"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
+    {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
+]
+
 [[package]]
 name = "wheel"
 version = "0.38.4"
@@ -2346,6 +3075,18 @@ files = [
 [package.extras]
 test = ["pytest (>=3.0.0)"]
 
+[[package]]
+name = "wurlitzer"
+version = "3.0.3"
+description = "Capture C-level output in context managers"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "wurlitzer-3.0.3-py3-none-any.whl", hash = "sha256:ffcc584109f5ecd5244abfde4534f22140f8735a4890ce1abd90b4e503f5f427"},
+    {file = "wurlitzer-3.0.3.tar.gz", hash = "sha256:224f5fe70618be3872c05dfddc8c457191ec1870654596279fcc1edadebe3e5b"},
+]
+
 [[package]]
 name = "yfinance"
 version = "0.1.96"
@@ -2369,21 +3110,21 @@ requests = ">=2.26"
 
 [[package]]
 name = "zipp"
-version = "3.14.0"
+version = "3.15.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "zipp-3.14.0-py3-none-any.whl", hash = "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7"},
-    {file = "zipp-3.14.0.tar.gz", hash = "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb"},
+    {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
+    {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
 ]
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8.1"
-content-hash = "c20cfe4630bff95aba10b218c7af027da20d25c7766599f092174006027914dc"
+content-hash = "91cfccfd68a4f9f761b002293b7c8950c3deb709f39dc3e8f95a7d9b7f685088"
diff --git a/pyproject.toml b/pyproject.toml
index ad8090be..d96c0628 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,34 +20,32 @@ include = []
 python = "^3.8.1"
 jax = "^0.3.25"
 numpy = "^1.23.5"
-scipy = "^1.9.3"
-pandas = "^1.5.2"
 matplotlib = "^3.6.2"
 diffrax = "^0.2.2"
 emcee = "^3.1.3"
 jaxlib = "^0.3.25"
 tqdm = "^4.64.1"
-python-libsbml = "^5.19.7"
 seedir = "^0.4.2"
-sbmltoodepy = "^1.0.4"
-pysces = {extras = ["sbml"], version = "^1.0.3"}
 yfinance = "^0.1.87"
-
+amici = "^0.16.1"
+schwimmbad = "^0.3.2"
 
 [tool.poetry.group.dev.dependencies]
 pre-commit = "^2.20.0"
 black = "^22.10.0"
 flake8 = "^6.0.0"
-isort = "^5.10.1"
+isort = "^5.12.0"
 pytest = "^7.2.0"
 pytest-datafiles = "^2.0.1"
 pyproject-flake8 = "^6.0.0"
 sphinx-copybutton = "^0.5.1"
 sphinx = "^5"
-sphinx-rtd-theme = "^1.2.0"
 myst-parser = "^0.18.1"
 coverage = "^7.0.5"
 sphinx-togglebutton = "^0.3.2"
+ipykernel = "^6.21.2"
+sphinx-book-theme = "^1.0.0"
+sphinx-rtd-theme = "^1.2.0"
 
 [build-system]
 requires = ["poetry-core"]
@@ -61,6 +59,7 @@ profile = "black"
 skip_gitignore = "true"
 float_to_top = "true"
 line_length = 79
+known_first_party = "amici"
 
 [tool.flake8]
 extend-ignore = ["E501", "E203", "F841"] # Length of lines already handled by black, spacing, unused variables
@@ -70,10 +69,15 @@ per-file-ignores = ["__init__.py: F401"]
 testpaths = [
   "tests",
 ]
+norecursedirs = [
+    "swig",
+    "generated_sbml_model",
+    "Data",
+    "Applications",
+    "build",
+]
 addopts = [
     "--import-mode=importlib",
-    "--ignore=tests/test_pysces.py",
-    "--ignore=tests/test_sbml_model.py",
 ]
 pythonpath = ["epi"]
 filterwarnings = [


Test patch:
diff --git a/docs/source/tests.conftest.rst b/docs/source/tests.conftest.rst
new file mode 100644
index 00000000..6230e8e4
--- /dev/null
+++ b/docs/source/tests.conftest.rst
@@ -0,0 +1,7 @@
+tests.conftest module
+=====================
+
+.. automodule:: tests.conftest
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.profiling.rst b/docs/source/tests.profiling.rst
new file mode 100644
index 00000000..fe5759d7
--- /dev/null
+++ b/docs/source/tests.profiling.rst
@@ -0,0 +1,7 @@
+tests.profiling module
+======================
+
+.. automodule:: tests.profiling
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.rst b/docs/source/tests.rst
index 0544e57a..064480e8 100644
--- a/docs/source/tests.rst
+++ b/docs/source/tests.rst
@@ -4,69 +4,21 @@ tests package
 Submodules
 ----------
 
-tests.conftest module
----------------------
-
-.. automodule:: tests.conftest
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.profiling module
-----------------------
-
-.. automodule:: tests.profiling
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.test\_cpp module
-----------------------
-
-.. automodule:: tests.test_cpp
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.test\_examples module
----------------------------
-
-.. automodule:: tests.test_examples
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.test\_jax module
-----------------------
-
-.. automodule:: tests.test_jax
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.test\_kde module
-----------------------
-
-.. automodule:: tests.test_kde
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.test\_plotting module
----------------------------
-
-.. automodule:: tests.test_plotting
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-tests.test\_simple\_models module
----------------------------------
-
-.. automodule:: tests.test_simple_models
-   :members:
-   :undoc-members:
-   :show-inheritance:
+.. toctree::
+   :maxdepth: 4
+
+   configuration<tests.conftest>
+   Profiling<tests.profiling>
+   Test Dense Grid<tests.test_dense_grid>
+   Test Examples<tests.test_examples>
+   Test Fixed Params<tests.test_fixed_params>
+   Test Inference<tests.test_inference>
+   Test Jax<tests.test_jax>
+   Test KDE<tests.test_kde>
+   Test Plotting<tests.test_plotting>
+   Test Simple Models<tests.test_simple_models>
+   Test Slices<tests.test_slices>
+   Test Transformation<tests.test_transformations>
 
 Module contents
 ---------------
diff --git a/docs/source/tests.test_dense_grid.rst b/docs/source/tests.test_dense_grid.rst
new file mode 100644
index 00000000..172827b8
--- /dev/null
+++ b/docs/source/tests.test_dense_grid.rst
@@ -0,0 +1,7 @@
+tests.test\_dense\_grid module
+==============================
+
+.. automodule:: tests.test_dense_grid
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_examples.rst b/docs/source/tests.test_examples.rst
new file mode 100644
index 00000000..95ab02b4
--- /dev/null
+++ b/docs/source/tests.test_examples.rst
@@ -0,0 +1,7 @@
+tests.test\_examples module
+===========================
+
+.. automodule:: tests.test_examples
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_fixed_params.rst b/docs/source/tests.test_fixed_params.rst
new file mode 100644
index 00000000..792e89ec
--- /dev/null
+++ b/docs/source/tests.test_fixed_params.rst
@@ -0,0 +1,7 @@
+tests.test\_fixed\_params module
+================================
+
+.. automodule:: tests.test_fixed_params
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_inference.rst b/docs/source/tests.test_inference.rst
new file mode 100644
index 00000000..7f69553b
--- /dev/null
+++ b/docs/source/tests.test_inference.rst
@@ -0,0 +1,7 @@
+tests.test\_inference module
+============================
+
+.. automodule:: tests.test_inference
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_jax.rst b/docs/source/tests.test_jax.rst
new file mode 100644
index 00000000..d6784e6c
--- /dev/null
+++ b/docs/source/tests.test_jax.rst
@@ -0,0 +1,7 @@
+tests.test\_jax module
+======================
+
+.. automodule:: tests.test_jax
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_kde.rst b/docs/source/tests.test_kde.rst
new file mode 100644
index 00000000..6954db1b
--- /dev/null
+++ b/docs/source/tests.test_kde.rst
@@ -0,0 +1,7 @@
+tests.test\_kde module
+======================
+
+.. automodule:: tests.test_kde
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_plotting.rst b/docs/source/tests.test_plotting.rst
new file mode 100644
index 00000000..ceb6a9be
--- /dev/null
+++ b/docs/source/tests.test_plotting.rst
@@ -0,0 +1,7 @@
+tests.test\_plotting module
+===========================
+
+.. automodule:: tests.test_plotting
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_simple_models.rst b/docs/source/tests.test_simple_models.rst
new file mode 100644
index 00000000..fa992cc1
--- /dev/null
+++ b/docs/source/tests.test_simple_models.rst
@@ -0,0 +1,7 @@
+tests.test\_simple\_models module
+=================================
+
+.. automodule:: tests.test_simple_models
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_slices.rst b/docs/source/tests.test_slices.rst
new file mode 100644
index 00000000..88ab1c68
--- /dev/null
+++ b/docs/source/tests.test_slices.rst
@@ -0,0 +1,7 @@
+tests.test\_slices module
+=========================
+
+.. automodule:: tests.test_slices
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/tests.test_transformations.rst b/docs/source/tests.test_transformations.rst
new file mode 100644
index 00000000..ff3a51c3
--- /dev/null
+++ b/docs/source/tests.test_transformations.rst
@@ -0,0 +1,7 @@
+tests.test\_transformations module
+==================================
+
+.. automodule:: tests.test_transformations
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/tests/conftest.py b/tests/conftest.py
index 730d45ef..36062d71 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,6 +6,12 @@ def pytest_sessionstart(session):
     """Called once at the start of a pytest session
 
     Currently disables the matplotlib output
+
+    Args:
+      session:
+
+    Returns:
+
     """
     import matplotlib
 
diff --git a/tests/profiling.py b/tests/profiling.py
index b6d4be26..8f06c400 100644
--- a/tests/profiling.py
+++ b/tests/profiling.py
@@ -5,16 +5,36 @@
 For profiling / identifying bottlenecks and not just observing the iterations per seconds, run this file with the following command:
 scalene tests/profiling.py from the root directory of the project.
 """
+
+import numpy as np
+
+from epi.core.inference import InferenceType, inference
 from epi.core.model import Model
-from epi.core.sampling import inference
-from epi.examples.corona import CoronaArtificial
+from epi.examples.stock import StockArtificial
 
-if __name__ == "__main__":
-    model: Model = CoronaArtificial(delete=True, create=True)
+
+def profiling_with_slices(inference_type):
+    """ """
+    model: Model = StockArtificial()
 
     # generate artificial data
-    if model.isArtificial():
-        model.generateArtificialData()
+    num_data_points = 1000
+    params = model.generate_artificial_params(num_data_points)
+    data = model.generate_artificial_data(params)
 
-    # run MCMC sampling for EPI
-    inference(model=model)
+    slice1 = np.array([0])
+    slice2 = np.array([1, 2])
+    slice3 = np.array([3, 4, 5])
+    slices = [slice1, slice2, slice3]
+    inference(
+        model,
+        data,
+        inference_type,
+        slices=slices,
+    )
+
+
+if __name__ == "__main__":
+    profiling_with_slices(InferenceType.MCMC)
+    profiling_with_slices(InferenceType.DENSE_GRID)
+    profiling_with_slices(InferenceType.SPARSE_GRID)
diff --git a/tests/test_cpp.py b/tests/test_cpp.py
deleted file mode 100644
index 51f535af..00000000
--- a/tests/test_cpp.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""
-Test the cpp model and its equivalent python implementation. Can be used to compare the performance during the sampling.
-"""
-
-import glob
-import os
-
-import pytest
-
-
-@pytest.mark.xfail(
-    True,
-    reason="XFAIL means that the Cpp Library for the plant model ist not compiled yet",
-)
-def test_cpp_lib_exists():
-    """Test if the cpp library for the plant model exists."""
-    cpp_lib_pattern = "epi/examples/cpp/cpp_model*.so*"
-    file_exists = (
-        len([n for n in glob.glob(cpp_lib_pattern) if os.path.isfile(n)]) > 0
-    )
-    assert file_exists
diff --git a/tests/test_dense_grid.py b/tests/test_dense_grid.py
new file mode 100644
index 00000000..e995b195
--- /dev/null
+++ b/tests/test_dense_grid.py
@@ -0,0 +1,42 @@
+"""
+Test the slices functionality for each of the inference methods.
+"""
+import numpy as np
+import pytest
+
+from epi.core.dense_grid import DenseGridType
+from epi.core.inference import InferenceType, inference
+from epi.core.model import Model
+from epi.examples.stock import StockArtificial
+
+# Parametrize the test to run for each inference type
+
+
+@pytest.mark.parametrize(
+    "dense_grid_type",
+    DenseGridType._member_map_.values(),
+    ids=DenseGridType._member_names_,
+)
+def test_dense_grid(dense_grid_type):
+    """ """
+    model: Model = StockArtificial()
+
+    # generate artificial data
+    if model.is_artificial():
+        num_data_points = 100
+        params = model.generate_artificial_params(num_data_points)
+        data = model.generate_artificial_data(params)
+    else:
+        raise Exception("This test is only for artificial data")
+
+    slice1 = np.array([0])
+    slice2 = np.array([1, 2])
+    slice3 = np.array([3, 4, 5])
+    slices = [slice1, slice2, slice3]
+    inference(
+        model,
+        data,
+        inference_type=InferenceType.DENSE_GRID,
+        slices=slices,
+        dense_grid_type=dense_grid_type,
+    )
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 6287701a..de26c33d 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -6,11 +6,11 @@
 
 import pytest
 
+from epi.core.inference import InferenceType, inference
 from epi.core.model import Model
-from epi.core.sampling import NUM_WALKERS, calcWalkerAcceptance, inference
 
 cpp_plant_example = pytest.param(
-    ("epi.examples.cpp", "CppPlant"),
+    ("epi.examples.cpp.cpp_plant", "CppPlant"),
     marks=pytest.mark.xfail(
         True,
         reason="XFAIL means that the Cpp Library for the plant model ist not compiled yet",
@@ -21,55 +21,85 @@
 def Examples():
     """Provides the list of examples to the parametrized test"""
     for example in [
-        ("epi.examples.stock", "Stock", 12),
-        ("epi.examples.stock", "StockArtificial", 12),
-        # ("epi.examples.corona", "Corona"),
+        ("epi.examples.stock", "Stock", "ETF50.csv"),
+        ("epi.examples.stock", "StockArtificial"),
+        ("epi.examples.corona", "Corona", "CoronaData.csv"),
         ("epi.examples.corona", "CoronaArtificial"),
-        ("epi.examples.temperature", "Temperature"),
+        ("epi.examples.temperature", "Temperature", "TemperatureData.csv"),
         ("epi.examples.temperature", "TemperatureArtificial"),
-        ("epi.examples.temperature", "TemperatureWithFixedParams"),
+        (
+            "epi.examples.temperature",
+            "TemperatureWithFixedParams",
+            "TemperatureData.csv",
+        ),
         cpp_plant_example,
-        ("epi.examples.cpp", "ExternalPlant"),
-        ("epi.examples.cpp", "JaxPlant"),
+        ("epi.examples.cpp.python_reference_plants", "ExternalPlant"),
+        ("epi.examples.cpp.python_reference_plants", "JaxPlant"),
+        ("epi.examples.sbml.sbml_menten_model", "MentenSBMLModel"),
+        ("epi.examples.sbml.sbml_caffeine_model", "CaffeineSBMLModel"),
     ]:
         yield example
 
 
-def getExampleName(example):
-    """Extract the name of the example from the tuple to have nice names in the test report and be able to select the test using -k"""
+def get_example_name(example):
+    """Extract the name of the example from the tuple to have nice names in the test report and be able to select the test using -k
+
+    Args:
+      example:
+
+    Returns:
+
+    """
     return example[1]
 
 
-@pytest.mark.parametrize("example", Examples(), ids=getExampleName)
+@pytest.mark.parametrize("example", Examples(), ids=get_example_name)
 def test_examples(example):
     """
 
-    :param example: The example which should be tested. The tuple contains the module location, the class name and the number of walkers.
-    :type example: Tuple[str, str, int]
+    Args:
+      example(Tuple[str, str, int]): The example which should be tested. The tuple contains the module location, the class name and the number of walkers.
+
+    Returns:
+
     """
     # extract example parameters from tuple
     try:
-        module_location, className, numWalkers = example
+        module_location, className, dataFileName = example
     except ValueError:
         module_location, className = example
-        numWalkers = NUM_WALKERS
+        dataFileName = None
 
     # Import class dynamically to avoid error on imports at the top which cant be tracked back to a specific test
     module = importlib.import_module(module_location)
     ModelClass = getattr(module, className)
-
-    model: Model = ModelClass(
-        delete=True, create=True
-    )  # Delete old results and recreate folder structure
+    model: Model = ModelClass()
 
     # generate artificial data if necessary
-    if model.isArtificial():
-        model.generateArtificialData()
-
+    if model.is_artificial():
+        num_data_points = 100
+        params = model.generate_artificial_params(num_data_points)
+        data = model.generate_artificial_data(params)
+    else:
+        assert dataFileName is not None
+        data = importlib.resources.path(module_location, dataFileName)
+
+        if (
+            className == "Stock"
+        ):  # We check using string comparison because we dont want to statically import the Stock class
+            data, _, _ = model.download_data(
+                data
+            )  # Download the actual stock data from the ticker list data from the internet
+
+    # Run inference
+    num_steps = 100
+    num_walkers = 12  # We choose 12 because then we have enough walkers for all examples. The higher the dimensionality of the model, the more walkers are needed.
     inference(
-        model=model, numWalkers=numWalkers, numSteps=1000
-    )  # using default dataPath of the model
-
-    # Determine walker acceptance rate
-    acceptance = calcWalkerAcceptance(model, numWalkers, numBurnSamples=0)
-    print("Acceptance rate: ", acceptance)
+        model,
+        data,
+        inference_type=InferenceType.MCMC,
+        num_walkers=num_walkers,
+        num_steps=num_steps,
+    )
+
+    # TODO: Check if results are correct / models invertible by comparing them with the artificial data for the artificial models
diff --git a/tests/test_fixed_params.py b/tests/test_fixed_params.py
new file mode 100644
index 00000000..a1380a51
--- /dev/null
+++ b/tests/test_fixed_params.py
@@ -0,0 +1,49 @@
+"""
+Test the slices functionality for each of the inference methods.
+"""
+import importlib
+
+import pytest
+
+from epi.core.inference import inference
+from epi.core.model import Model
+from epi.examples.temperature import TemperatureWithFixedParams
+
+
+def temp_name(low_T, high_T):
+    # Replace the minus sign with a letter to avoid problems with the file name
+    if low_T < 0:
+        lTs = str(low_T).replace("-", "m")
+    else:
+        lTs = str(low_T)
+    if high_T < 0:
+        hTs = str(high_T).replace("-", "m")
+    else:
+        hTs = str(high_T)
+    name = "TemperatureWithFixedParams_" + lTs + "_" + hTs
+    return name
+
+
+@pytest.mark.parametrize(
+    "temperatures",
+    [
+        (-10.0, 10.0),  # 0
+        (-20, 40.0),  # 1
+    ],
+    ids=[0, 1],
+)
+def test_fixed_params(temperatures):
+    """ """
+    model: Model = TemperatureWithFixedParams(*temperatures)
+    data = importlib.resources.path(
+        "epi.examples.temperature", "TemperatureData.csv"
+    )
+
+    run_name = temp_name(*temperatures)
+
+    inference(
+        model,
+        data,
+        num_steps=100,
+        run_name=run_name,
+    )
diff --git a/tests/test_inference.py b/tests/test_inference.py
new file mode 100644
index 00000000..a45cb376
--- /dev/null
+++ b/tests/test_inference.py
@@ -0,0 +1,241 @@
+import jax.scipy.stats as jstats
+import matplotlib.pyplot as plt
+import numpy as np
+
+from epi.core.dense_grid import generate_regular_grid
+from epi.core.inference import InferenceType, inference
+from epi.core.kde import calc_kernel_width, eval_kde_gauss
+from epi.core.result_manager import ResultManager
+from epi.examples.simple_models import LinearODE
+
+
+# WARNING: The following code only works for the simplest case. Equidistant grid, same number of points in each dimension, ...
+def integrate(z, x, y):
+    # Integrate the function over the grid
+    integral = np.trapz(np.trapz(z, y, axis=0), x, axis=0)
+    return integral
+
+
+# TODO: Generalize, currently only works for dense vs mcmc
+def test_inference_mcmc_dense_exact(
+    num_data_points=1000,
+    num_steps=3000,
+    num_grid_points=50,
+):
+    # define the model
+    model = LinearODE()
+
+    # generate artificial data
+    params = model.generate_artificial_params(num_data_points)
+    data = model.generate_artificial_data(params)
+
+    # run EPI with all inference types
+    results = {}
+    full_slice = np.arange(model.param_dim)
+    for inference_type in InferenceType._member_map_.values():
+        result_manager = ResultManager(model.name, str(inference_type))
+        if InferenceType(inference_type) == InferenceType.MCMC:
+            inference(
+                model,
+                data,
+                inference_type,
+                result_manager=result_manager,
+                num_steps=num_steps,
+            )
+            # Take every second sample and skip the first 5% of the chain
+            results[inference_type] = result_manager.load_sim_results(
+                full_slice, num_steps // 20, 2
+            )
+        elif InferenceType(inference_type) == InferenceType.DENSE_GRID:
+            inference(
+                model,
+                data,
+                inference_type,
+                result_manager=result_manager,
+                num_grid_points=num_grid_points,
+            )
+            results[inference_type] = result_manager.load_sim_results(
+                full_slice, 0, 1
+            )
+        else:
+            # skip other inference types
+            continue
+
+    # define true pdf
+    def true_pdf(grid, distribution="uniform"):
+        if distribution == "uniform":
+            val = (
+                1.0
+                / (
+                    LinearODE.TRUE_PARAM_LIMITS[0, 1]
+                    - LinearODE.TRUE_PARAM_LIMITS[0, 0]
+                )
+                / (
+                    LinearODE.TRUE_PARAM_LIMITS[1, 1]
+                    - LinearODE.TRUE_PARAM_LIMITS[1, 0]
+                )
+            )
+            # return val where grid is in the limits and 0 otherwise
+            return np.where(
+                np.logical_and(
+                    np.logical_and(
+                        grid[:, 0] >= LinearODE.TRUE_PARAM_LIMITS[0, 0],
+                        grid[:, 0] <= LinearODE.TRUE_PARAM_LIMITS[0, 1],
+                    ),
+                    np.logical_and(
+                        grid[:, 1] >= LinearODE.TRUE_PARAM_LIMITS[1, 0],
+                        grid[:, 1] <= LinearODE.TRUE_PARAM_LIMITS[1, 1],
+                    ),
+                ),
+                val,
+                0,
+            )
+        elif distribution == "gaussian":
+            mean = np.zeros(model.param_dim) + 1.5
+            cov = np.eye(model.param_dim)
+            return jstats.multivariate_normal.pdf(grid, mean, cov)
+
+    # Extract and process the results
+    lims = LinearODE.PARAM_LIMITS
+    x = np.linspace(lims[0, 0], lims[0, 1], num_grid_points)
+    y = np.linspace(lims[1, 0], lims[1, 1], num_grid_points)
+    grid = generate_regular_grid(
+        np.array([num_grid_points, num_grid_points]), lims, flatten=True
+    )
+    grid_2d = grid.reshape(num_grid_points, num_grid_points, model.param_dim)
+    # grid = results[InferenceType.DENSE_GRID][2]
+
+    mcmc_params = results[InferenceType.MCMC][2]
+    mcmc_params_density = results[InferenceType.MCMC][0]
+    mcmc_kde = eval_kde_gauss(
+        mcmc_params, grid, calc_kernel_width(mcmc_params)
+    )
+
+    dense_grid_pdf = results[InferenceType.DENSE_GRID][0]
+
+    true_pdf_grid = true_pdf(grid)
+    true_kde = eval_kde_gauss(params, grid, calc_kernel_width(params))
+    true_pdf_samples = true_pdf(params)
+
+    def to2d(grid):
+        return grid.reshape(num_grid_points, num_grid_points)
+
+    integral_mcmc_kde = integrate(to2d(mcmc_kde), x, y)
+    integral_dense_grid_pdf = integrate(to2d(dense_grid_pdf), x, y)
+    integral_true_pdf = integrate(to2d(true_pdf_grid), x, y)
+
+    # DEBUGGING
+    print("integral of mcmc kde", integral_mcmc_kde)
+    print("integral of dense grid pdf ", integral_dense_grid_pdf)
+    print("integral of true pdf ", integral_true_pdf)
+
+    # Just a quick check if the integrals are correct and that the range chosen limits are large enough
+    threshold = 0.9  # We want to capture at least 90% of the probability mass
+    # TODO: The threshold should be adapted depending on how hard the problem is
+    # and how many samples / grid points we have
+    assert integral_mcmc_kde > threshold
+    assert integral_dense_grid_pdf > threshold
+    assert integral_true_pdf > threshold
+
+    # Calculate the errors on the grid
+    mcmc_kde_error = np.abs(mcmc_kde - true_pdf_grid)
+    dense_grid_pdf_error = np.abs(dense_grid_pdf - true_pdf_grid)
+
+    # Calculate the integrals of the errors
+    integral_mcmc_kde_error = integrate(to2d(mcmc_kde_error), x, y)
+    integral_dense_grid_pdf_error = integrate(to2d(dense_grid_pdf_error), x, y)
+
+    # Divide the integral through the area of the grid
+    integral_mcmc_kde_error /= (lims[0, 1] - lims[0, 0]) * (
+        lims[1, 1] - lims[1, 0]
+    )
+    integral_dense_grid_pdf_error /= (lims[0, 1] - lims[0, 0]) * (
+        lims[1, 1] - lims[1, 0]
+    )
+
+    # DEBUGGING
+    print("integral of mcmc kde error", integral_mcmc_kde_error)
+    print("integral of dense grid pdf error", integral_dense_grid_pdf_error)
+
+    scatter_mcmc_params = False
+    surf_mcmc_kde = True
+
+    scatter_true_params = False
+    surf_true_pdf_grid = True
+    surf_true_kde = False
+
+    surf_dense_grid_pdf = True
+
+    # Surface plot
+    fig = plt.figure()
+    ax = fig.add_subplot(111, projection="3d")
+    if scatter_mcmc_params:
+        ax.scatter(
+            mcmc_params[:, 0], mcmc_params[:, 1], 0, label="mcmc params"
+        )  # We dont know the normalizing constant, so we cant plot the density
+    if scatter_true_params:
+        ax.scatter(
+            params[:, 0], params[:, 1], true_pdf_samples, label="true params"
+        )
+
+    if surf_mcmc_kde:
+        surf = ax.plot_surface(
+            grid_2d[:, :, 0],
+            grid_2d[:, :, 1],
+            to2d(mcmc_kde),
+            alpha=0.3,
+            label="mcmc kde",
+        )
+        surf._edgecolors2d = surf._edgecolor3d
+        surf._facecolors2d = surf._facecolor3d
+    if surf_true_pdf_grid:
+        surf = ax.plot_surface(
+            grid_2d[:, :, 0],
+            grid_2d[:, :, 1],
+            to2d(true_pdf_grid),
+            alpha=0.3,
+            label="true pdf",
+        )
+        surf._edgecolors2d = surf._edgecolor3d
+        surf._facecolors2d = surf._facecolor3d
+    if surf_true_kde:
+        surf = ax.plot_surface(
+            grid_2d[:, :, 0],
+            grid_2d[:, :, 1],
+            to2d(true_kde),
+            alpha=0.1,
+            label="true kde",
+        )
+        surf._edgecolors2d = surf._edgecolor3d
+        surf._facecolors2d = surf._facecolor3d
+    if surf_dense_grid_pdf:
+        surf = ax.plot_surface(
+            grid_2d[:, :, 0],
+            grid_2d[:, :, 1],
+            to2d(dense_grid_pdf),
+            alpha=0.3,
+            label="dense grid pdf",
+        )
+        surf._edgecolors2d = surf._edgecolor3d
+        surf._facecolors2d = surf._facecolor3d
+
+    ax.set_xlabel("param 1")
+    ax.set_ylabel("param 2")
+    ax.set_zlabel("density")
+
+    ax.legend()
+    plt.show()
+
+    # Assert that the errors are "small"
+    # TODO: Giving a meaning to the hreshold / error should be easy, because we know the true pdf and the pdfs are normalized
+    # TODO: Then evaluate whether the threshold is set reasonable
+    # TODO: The threshold should be adapted depending on how hard the problem is
+    # and how many samples / grid points we have
+    threshold = 0.05
+    assert integral_mcmc_kde_error < threshold
+    assert integral_dense_grid_pdf_error < threshold
+
+
+# Run the inference if main
+if __name__ == "__main__":
+    test_inference_mcmc_dense_exact()
diff --git a/tests/test_jax.py b/tests/test_jax.py
index 39199662..9712202e 100644
--- a/tests/test_jax.py
+++ b/tests/test_jax.py
@@ -22,17 +22,25 @@ def test_jax_cpu():
 def test_jax_gpu():
     """Test wether jax can run on the gpu by executing some jax code.
     The jax gpu test may fail if no nvidia gpu is available or the cuda and cudnn libraries are not installed.
+
+    Args:
+
+    Returns:
+
     """
     jax.default_device = jax.devices("gpu")[0]
     do_matrix_multiplication()
 
 
 # The random part would be different if using numpy
-def do_matrix_multiplication(n: int = 5000):
+def do_matrix_multiplication(n: int = 20):
     """Do a simple matrix-matrix multiplication for two random matrices generated by numpy
 
-    :param n: number of entries per dimension, defaults to 5000
-    :type n: int, optional
+    Args:
+      n(int, optional): number of entries per dimension, defaults to 20
+
+    Returns:
+
     """
     key = random.PRNGKey(
         0
diff --git a/tests/test_kde.py b/tests/test_kde.py
index bb564e06..470245e5 100644
--- a/tests/test_kde.py
+++ b/tests/test_kde.py
@@ -1,18 +1,20 @@
 """Tests for the Kernel Density Estimation module"""
 
 import jax
+import jax.scipy.stats as jstats
 import matplotlib.pyplot as plt
 import numpy as np
 import pytest
 from matplotlib import cm
 
-from epi.core.kde import calcKernelWidth, evalKDECauchy, evalKDEGauss
+from epi.core.dense_grid import generate_regular_grid
+from epi.core.kde import calc_kernel_width, eval_kde_cauchy, eval_kde_gauss
 
 
 def kernel_estimators():
     """Yields the kernel density estimators"""
-    yield evalKDECauchy
-    yield evalKDEGauss
+    yield eval_kde_cauchy
+    yield eval_kde_gauss
 
 
 def kde_data_test_set():
@@ -27,22 +29,49 @@ def kde_data_test_set():
         yield data_list[i], stdev_list[i], grid_bounds_list[i]
 
 
+# Mark the test as expected to fail because the function is not implemented yet, but definitly should be implemented soon!
+@pytest.mark.xfail(reason="Not implemented yet, but is important!")
+def test_calc_kernel_width():
+    data = np.array([[0.0, 1.0], [-1.0, 2.0], [1.0, 3.0]])
+    data_stdevs = calc_kernel_width(data)
+    assert 0 == 1
+
+
+@pytest.mark.xfail(reason="Not implemented yet, contains false values")
+def test_kde_gauss():
+    from epi.core.kde import eval_kde_gauss
+
+    data = np.array([[0.0], [2.0]])
+    data_stdevs = calc_kernel_width(data)
+    grid = np.array([[0.0], [1.0], [2.0]])
+    kde = eval_kde_gauss(data, grid, data_stdevs)
+    assert kde == np.array([0.5, 0.25, 0.5])
+
+
 @pytest.mark.parametrize("evalKDE", kernel_estimators())
 @pytest.mark.parametrize("batch", [False, True])
 def test_KDE_batch(batch, evalKDE):
-    """Test both kernel density estimators by using random data points and evaluating the Kernel Density Estimation at one point"""
+    """Test both kernel density estimators by using random data points and evaluating the Kernel Density Estimation at one point
+
+    Args:
+      batch:
+      evalKDE:
+
+    Returns:
+
+    """
     # Define random data points in 2D
-    dataDim = 2
-    numDataPoints = 3
-    data = np.random.rand(numDataPoints, dataDim)
-    stdevs = calcKernelWidth(data)
+    data_dim = 2
+    num_data_points = 3
+    data = np.random.rand(num_data_points, data_dim)
+    stdevs = calc_kernel_width(data)
 
     # define the evaluation point(s)
     n_samples = 5
     if batch:
-        evalPoint = np.random.rand(n_samples, dataDim)
+        evalPoint = np.random.rand(n_samples, data_dim)
     else:
-        evalPoint = np.array([0.5] * dataDim)
+        evalPoint = np.array([0.5] * data_dim)
 
     # evaluate the KDE
     evaluated = evalKDE(data, evalPoint, stdevs)
@@ -57,7 +86,18 @@ def test_KDE_batch(batch, evalKDE):
 @pytest.mark.parametrize("data, stdevs, grid_bounds", kde_data_test_set())
 @pytest.mark.parametrize("evalKDE", kernel_estimators())
 def test_KDE_data(evalKDE, data, stdevs, grid_bounds, resolution=33):
-    """Test both kernel density estimators by using one data point and evaluating the Kernel Density Estimation over a grid"""
+    """Test both kernel density estimators by using one data point and evaluating the Kernel Density Estimation over a grid
+
+    Args:
+      evalKDE:
+      data:
+      stdevs:
+      grid_bounds:
+      resolution:  (Default value = 33)
+
+    Returns:
+
+    """
 
     xGrid = np.linspace(*(grid_bounds[0]), resolution)
     yGrid = np.linspace(*(grid_bounds[1]), resolution)
@@ -80,3 +120,86 @@ def test_KDE_data(evalKDE, data, stdevs, grid_bounds, resolution=33):
         antialiased=False,
     )
     plt.show()
+
+
+# WARNING: The following code only works for the simplest case. Equidistant grid, same number of points in each dimension, ...
+def integrate(z, x, y):
+    # Integrate the function over the grid
+    integral = np.trapz(np.trapz(z, y, axis=0), x, axis=0)
+    return integral
+
+
+@pytest.mark.parametrize("dim", [1, 2], ids=["1D", "2D"])
+def test_kde_convergence_gauss(
+    dim, num_grid_points=100, num_data_points=10000
+):
+    """Test whether the KDE converges to the true distribution."""
+    # Generate random numbers from a normal distribution
+    data = np.random.randn(num_data_points, dim)
+    stdevs = calc_kernel_width(data)
+
+    # Define the grid
+    num_grid_points = np.array(
+        [num_grid_points for _ in range(dim)], dtype=np.int32
+    )
+    limits = np.array([[-5, 5] for _ in range(dim)])
+    grid = generate_regular_grid(num_grid_points, limits, flatten=True)
+
+    kde_on_grid = eval_kde_gauss(data, grid, stdevs)  # Evaluate the KDE
+    mean = np.zeros(dim)
+    cov = np.eye(dim)
+    exact_on_grid = jstats.multivariate_normal.pdf(
+        grid, mean, cov
+    )  # Evaluate the true distribution
+    diff = np.abs(kde_on_grid - exact_on_grid)  # difference between the two
+
+    # Plot the KDE
+    import matplotlib.pyplot as plt
+
+    if dim == 1:
+        grid = grid[:, 0]
+        error = np.trapz(diff, grid)  # Calculate the error
+        assert error < 0.1  # ~0.06 for 100 grid points, 1000 data points
+
+        plt.plot(grid, kde_on_grid)
+        plt.plot(grid, exact_on_grid)
+
+    elif dim == 2:
+        # Calculate the error
+        diff = diff.reshape(num_grid_points[0], num_grid_points[1])
+        x = np.linspace(limits[0, 0], limits[0, 1], num_grid_points[0])
+        y = np.linspace(limits[1, 0], limits[1, 1], num_grid_points[1])
+        error = integrate(diff, x, y)
+        assert error < 0.15  # ~0.13 for 100 grid points, 1000 data points
+        # Surface plot
+        fig = plt.figure()
+        ax = fig.add_subplot(111, projection="3d")
+        grid_2d = grid.reshape(num_grid_points[0], num_grid_points[1], dim)
+
+        exact_on_grid_2d = exact_on_grid.reshape(
+            num_grid_points[0], num_grid_points[1]
+        )
+        surf = ax.plot_surface(
+            grid_2d[:, :, 0],
+            grid_2d[:, :, 1],
+            exact_on_grid_2d,
+            alpha=0.7,
+            label="exact",
+        )
+        surf._edgecolors2d = surf._edgecolor3d
+        surf._facecolors2d = surf._facecolor3d
+
+        kde_on_grid_2d = kde_on_grid.reshape(
+            num_grid_points[0], num_grid_points[1]
+        )
+        surf = ax.plot_surface(
+            grid_2d[:, :, 0],
+            grid_2d[:, :, 1],
+            kde_on_grid_2d,
+            alpha=0.7,
+            label="kde",
+        )
+        surf._edgecolors2d = surf._edgecolor3d
+        surf._facecolors2d = surf._facecolor3d
+
+    plt.show()
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
index 0d612ae0..db4e79c8 100644
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -1,12 +1,14 @@
 """Test plotting functions from the plotter module"""
 
 import epi.plotting.plotter as plotter
+from epi.core.kde import calc_kernel_width
 from epi.examples.temperature import TemperatureArtificial
 
 
 def test_plotKDEoverGrid():
     """Test plotting a KDE over a grid."""
     t = TemperatureArtificial()
-    t.generateArtificialData()
-    dataDim, data, dataStdevs = t.dataLoader()
-    plotter.plotKDEoverGrid(data, dataStdevs, resolution=100)
+    params = t.generate_artificial_params()
+    data = t.generate_artificial_data(params)
+    data_stdevs = calc_kernel_width(data)
+    plotter.plotKDEoverGrid(data, data_stdevs, resolution=100)
diff --git a/tests/test_simple_models.py b/tests/test_simple_models.py
index 61157183..3117875d 100644
--- a/tests/test_simple_models.py
+++ b/tests/test_simple_models.py
@@ -7,15 +7,15 @@
 from jax import vmap
 from matplotlib import cm
 
-from epi.core.functions import evalLogTransformedDensity
-from epi.core.kde import calcKernelWidth, evalKDEGauss
-from epi.core.sampling import concatenateEmceeSamplingResults, runEmceeSampling
+from epi.core.inference import InferenceType, inference
+from epi.core.kde import calc_kernel_width, eval_kde_gauss
+from epi.core.transformations import evaluate_density
 from epi.examples.simple_models import Exponential, Linear, LinearODE
-from epi.plotting.plots import plotTest
 
 
 def test_transformationLinear():
-    model = Linear(delete=True, create=True)
+    """ """
+    model = Linear()
 
     # create approx. 1000 data points that are perfectly uniformly distributed over a grid
     # the range of data points is the 2D interval [0,10]x[-2,-4]
@@ -28,7 +28,7 @@ def test_transformationLinear():
     data = np.array([xMesh.flatten(), yMesh.flatten()]).T
 
     # define standard deviations according to silverman
-    dataStdevs = calcKernelWidth(data)
+    data_stdevs = calc_kernel_width(data)
 
     # Now plot the data Gaussian KDE
     KDEresolution = 25
@@ -43,7 +43,7 @@ def test_transformationLinear():
     for i in range(KDEresolution):
         for j in range(KDEresolution):
             evalPoint = np.array([KDExMesh[i, j], KDEyMesh[i, j]])
-            gaussEvals[i, j] = evalKDEGauss(data, evalPoint, dataStdevs)
+            gaussEvals[i, j] = eval_kde_gauss(data, evalPoint, data_stdevs)
 
     fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
     plt.title("Data KDE")
@@ -66,15 +66,19 @@ def test_transformationLinear():
 
     paramEvals = np.zeros((paramResolution, paramResolution))
 
+    # TODO: Use dense grid evaluation function, but pay attention to the changed limits above. Then todo below can also be removed
+    # TODO: Evaluate Density is missing the slice argument here
     for i in range(paramResolution):
         for j in range(paramResolution):
             paramPoint = np.array([paramxMesh[i, j], paramyMesh[i, j]])
-            paramEvals[i, j], _ = evalLogTransformedDensity(
-                paramPoint, model, data, dataStdevs
+            paramEvals[i, j], _ = evaluate_density(
+                paramPoint,
+                model,
+                data,
+                data_stdevs,
+                slice=np.arange(model.param_dim),
             )
 
-    paramEvals = np.exp(paramEvals)
-
     fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
     plt.title("Parameter Density Estimation")
     surf = ax.plot_surface(
@@ -90,17 +94,18 @@ def test_transformationLinear():
 
 
 def test_transformationExponential():
-    model = Exponential(delete=True, create=True)
+    """ """
+    model = Exponential()
 
     # create true parameter points that are drawn uniformly from [0,1]^2
 
-    numDataPoints = 10000
-    trueParam = np.random.rand(numDataPoints, 2) + 1
+    num_data_points = 10000
+    trueParam = np.random.rand(num_data_points, 2) + 1
 
     data = vmap(model.forward, in_axes=0)(trueParam)
 
     # define standard deviations according to silverman
-    dataStdevs = calcKernelWidth(data)
+    data_stdevs = calc_kernel_width(data)
 
     # Now plot the data Gaussian KDE
     KDEresolution = 25
@@ -114,7 +119,7 @@ def test_transformationExponential():
     for i in range(KDEresolution):
         for j in range(KDEresolution):
             evalPoint = np.array([KDExMesh[i, j], KDEyMesh[i, j]])
-            gaussEvals[i, j] = evalKDEGauss(data, evalPoint, dataStdevs)
+            gaussEvals[i, j] = eval_kde_gauss(data, evalPoint, data_stdevs)
 
     fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
     plt.title("Data KDE")
@@ -136,16 +141,15 @@ def test_transformationExponential():
     paramxMesh, paramyMesh = np.meshgrid(paramxGrid, paramyGrid)
 
     paramEvals = np.zeros((paramResolution, paramResolution))
+    fullSlice = np.arange(model.param_dim)
 
     for i in range(paramResolution):
         for j in range(paramResolution):
             paramPoint = np.array([paramxMesh[i, j], paramyMesh[i, j]])
-            paramEvals[i, j], _ = evalLogTransformedDensity(
-                paramPoint, model, data, dataStdevs
+            paramEvals[i, j], _ = evaluate_density(
+                paramPoint, model, data, data_stdevs, slice=fullSlice
             )
 
-    paramEvals = np.exp(paramEvals)
-
     fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
     plt.title("Parameter Density Estimation")
     surf = ax.plot_surface(
@@ -161,25 +165,25 @@ def test_transformationExponential():
 
 
 def test_transformationODELinear():
-    """
-    Exemplary application of Eulerian Parameter Inference a very simplistic ordinary differential equation model.
+    """Exemplary application of Eulerian Parameter Inference a very simplistic ordinary differential equation model.
     We create our toy data by first defining a true parameter distribution.
     The actual data is then obtained by evaluating the model in parameters drawn from this true distribution.
     Ideally, we would be able to reconstruct the true parameter density.
     However, we will see that this parameter inference problem possesses more than one solution and is therefore not well-posed.
+
+    Args:
+
+    Returns:
+
     """
 
     # define the model
-    model = LinearODE(delete=True, create=True)
+    model = LinearODE()
 
     # generate artificial data
-    model.generateArtificialData()
+    num_data_points = 100
+    params = model.generate_artificial_params(num_data_points)
+    data = model.generate_artificial_data(params)
 
     # run MCMC sampling for EPI
-    runEmceeSampling(model, numWalkers=4)
-
-    # combine all intermediate saves to create one large sample chain
-    concatenateEmceeSamplingResults(model)
-
-    # plot the results
-    plotTest(model)
+    inference(model, data, InferenceType.MCMC)
diff --git a/tests/test_slices.py b/tests/test_slices.py
new file mode 100644
index 00000000..83cd1183
--- /dev/null
+++ b/tests/test_slices.py
@@ -0,0 +1,45 @@
+"""
+Test the slices functionality for each of the inference methods.
+"""
+import numpy as np
+import pytest
+
+from epi.core.inference import InferenceType, inference
+from epi.core.model import Model
+from epi.examples.stock import StockArtificial
+
+# Parametrize the test to run for each inference type
+
+
+@pytest.mark.parametrize(
+    "inference_type",
+    InferenceType._member_map_.values(),
+    ids=InferenceType._member_names_,
+)
+def test_slices(inference_type):
+    """ """
+    model: Model = StockArtificial()
+
+    # generate artificial data
+    if model.is_artificial():
+        num_data_points = 100
+        params = model.generate_artificial_params(num_data_points)
+        data = model.generate_artificial_data(params)
+    else:
+        raise Exception("This test is only for artificial data")
+
+    slice1 = np.array([0])
+    slice2 = np.array([1, 2])
+    slice3 = np.array([3, 4, 5])
+    slices = [slice1, slice2, slice3]
+    if inference_type == InferenceType.MCMC:
+        kwargs = {"num_steps": 100}
+    else:
+        kwargs = {}
+    inference(
+        model,
+        data,
+        inference_type,
+        slices=slices,
+        **kwargs,
+    )
diff --git a/tests/test_transformations.py b/tests/test_transformations.py
new file mode 100644
index 00000000..4caeb6a1
--- /dev/null
+++ b/tests/test_transformations.py
@@ -0,0 +1,95 @@
+import jax.numpy as jnp
+import numpy as np
+import pytest
+
+from epi.core.kde import calc_kernel_width, eval_kde_gauss
+from epi.core.model import ArtificialModelInterface, JaxModel
+from epi.core.transformations import calc_gram_determinant
+
+
+def test_calc_gram_determinant():
+    # Test case 1: When the jacobian is a square matrix
+    jac = jnp.array([[1, 2], [3, 4]])
+    expected_result = jnp.abs(jnp.linalg.det(jac))
+    assert expected_result == 2.0
+    assert calc_gram_determinant(jac) == expected_result
+
+    # Test case 2: When the jacobian is not a square matrix, and the columns are linearly independent
+    jac = jnp.array([[1, 0, 0], [0, 1, 0]]).T
+    expected_result = jnp.sqrt(jnp.linalg.det(jnp.matmul(jac.T, jac)))
+    assert expected_result == 1.0
+    assert calc_gram_determinant(jac) == expected_result
+
+    # Test case 3: When the jacobian is a zero matrix
+    jac = jnp.zeros((2, 2))
+    expected_result = 0.0
+    assert calc_gram_determinant(jac) == expected_result
+
+    # Test case 4: When the jacobian has negative determinant
+    jac = jnp.array([[2, 1], [1, 2]])
+    expected_result = jnp.abs(jnp.linalg.det(jac))
+    assert expected_result == 3.0
+    assert calc_gram_determinant(jac) == expected_result
+
+
+class X2Model(JaxModel, ArtificialModelInterface):
+    param_dim = 1
+    data_dim = 1
+    CENTRAL_PARAM = np.array([1.0])
+    PARAM_LIMITS = np.array([[0.0, 2.0]])
+
+    def __init__(self):
+        super(JaxModel, self).__init__(self.CENTRAL_PARAM, self.PARAM_LIMITS)
+
+    @classmethod
+    def forward(cls, param):
+        return param**2
+
+    def generate_artificial_params(self, num_samples: int) -> jnp.ndarray:
+        return np.random.randn(num_samples, self.param_dim)
+
+
+def test_evaluate_density(caplog):
+    from epi.core.transformations import evaluate_density
+
+    param = X2Model.CENTRAL_PARAM
+    x2_model = X2Model()
+    # Model and calc_gram_determinant has its own tests, so we can use it here to test the transformations
+    sim_res, jac = x2_model.forward_and_jacobian(param)
+    correction = calc_gram_determinant(jac)
+
+    # KDE has its own tests, so we can use it here to test the transformations
+    data = np.array([[0.0], [2.0]])
+    data_stdevs = calc_kernel_width(data)
+    pure_density = eval_kde_gauss(data, sim_res, data_stdevs)
+
+    # Test case 1: When the slice is one dimensional
+    slice = np.array([0])
+    density, _ = evaluate_density(param, x2_model, data, data_stdevs, slice)
+    assert density == pure_density * correction
+
+    # Test case 2: When the slice is empty
+    slice = np.array([])
+    with pytest.raises(IndexError):
+        density, _ = evaluate_density(
+            param, x2_model, data, data_stdevs, slice
+        )
+
+    # Test case 3: When the slice is two dimensional, but the model is one dimensional
+    slice = np.array([0, 1])
+    with pytest.raises(IndexError):
+        density, _ = evaluate_density(
+            param, x2_model, data, data_stdevs, slice
+        )
+
+    # Test case 4: When the param is out of bounds
+    slice = np.array([0])
+    param = np.array([2.1])
+    # Other arguments would change too, but shouldn't matter for this test
+    # set logger level to debug to see the warning
+    from epi import logger
+
+    logger.setLevel("INFO")
+    density, _ = evaluate_density(param, x2_model, data, data_stdevs, slice)
+    assert density == 0.0
+    assert "Parameters outside of predefined range" in caplog.text