Repository: TUW-GEO/rt1 (language: python)

Issue:
Test pypi
add action to publish release to PyPi
- avoid having numpy as a dependency for importing methods from `rt1.general_functions`


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/Publish_to_PyPi.yml b/.github/workflows/Publish_to_PyPi.yml
new file mode 100644
index 00000000..b6eefcc3
--- /dev/null
+++ b/.github/workflows/Publish_to_PyPi.yml
@@ -0,0 +1,37 @@
+name: Upload to PyPI
+
+on:
+   # trigger only on release
+   release:
+     types: [published]
+   # to trigger workflow manually from actions-tab
+   workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      # Sets up python3
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+
+      - name: Upgrade pip, install setuptools, wheel and twine
+        run: |
+          # Upgrade pip
+          python -m pip install --upgrade pip
+          # Install build dependencies
+          python -m pip install setuptools wheel twine
+
+      # Upload to TestPyPI
+      - name: Build and Upload to PyPI
+        run: |
+          python setup.py sdist bdist_wheel
+          python -m twine upload dist/*
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
+          # use the following lines for testing:
+          #TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
+          #TWINE_REPOSITORY: testpypi
\ No newline at end of file
diff --git a/.github/workflows/RT1_ubuntu.yml b/.github/workflows/RT1_ubuntu.yml
new file mode 100644
index 00000000..eb4f17df
--- /dev/null
+++ b/.github/workflows/RT1_ubuntu.yml
@@ -0,0 +1,44 @@
+name: RT1_ubuntu
+
+on: [push]
+
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        # set operating systems to test
+        os: [ubuntu-latest]
+        # set python versions to test
+        python-version: ['3.6', '3.7']
+
+    name: RT1_test ${{ matrix.os }}  ${{ matrix.python-version }}
+    steps:
+      # checkout the repository
+      - uses: actions/checkout@v2
+      # install miniconda environment
+      - uses: conda-incubator/setup-miniconda@v2
+        with:
+          environment-file: tests/test_env.yml
+          activate-environment: test_rt1
+
+          show-channel-urls: true
+          use-only-tar-bz2: true
+
+          python-version: ${{ matrix.python-version }}
+
+          auto-activate-base: false
+          auto-update-conda: true
+      # run pytest
+      - name: test RT1
+        # do this to ensure proper activation of conda environment
+        shell: bash -l {0}
+        run: |
+          python -m pytest --cov=rt1 --cov-report=xml
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v1
+        with:
+          env_vars: ${{ matrix.os }}, ${{ matrix.python-version }}
+          name: rt1_coverage
+          fail_ci_if_error: true
+          verbose: true
\ No newline at end of file
diff --git a/.github/workflows/RT1_windows.yml b/.github/workflows/RT1_windows.yml
new file mode 100644
index 00000000..ae297aeb
--- /dev/null
+++ b/.github/workflows/RT1_windows.yml
@@ -0,0 +1,44 @@
+name: RT1_windows
+
+on: [push]
+
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        # set operating systems to test
+        os: [windows-latest]
+        # set python versions to test
+        python-version: ['3.6', '3.7']
+
+    name: RT1_test ${{ matrix.os }}  ${{ matrix.python-version }}
+    steps:
+      # checkout the repository
+      - uses: actions/checkout@v2
+      # install miniconda environment
+      - uses: conda-incubator/setup-miniconda@v2
+        with:
+          environment-file: tests/test_env.yml
+          activate-environment: test_rt1
+
+          show-channel-urls: true
+          use-only-tar-bz2: true
+
+          python-version: ${{ matrix.python-version }}
+
+          auto-activate-base: false
+          auto-update-conda: true
+      # run pytest
+      - name: test RT1
+        # do this to ensure proper activation of conda environment
+        shell: bash -l {0}
+        run: |
+          python -m pytest --cov=rt1 --cov-report=xml
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v1
+        with:
+          env_vars: ${{ matrix.os }}, ${{ matrix.python-version }}
+          name: rt1_coverage
+          fail_ci_if_error: true
+          verbose: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 850a13d6..0aac5d3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,7 @@ rt1.egg-info
 .spyproject
 _additional_and_old_scripts
 rt1/.pylint.d
-.gitmodules
\ No newline at end of file
+.gitmodules
+coverage.xml
+tests/testdump1.dump
+tests/testdump2.dump
\ No newline at end of file
diff --git a/.stickler.yml b/.stickler.yml
index 20615fa0..e8044e32 100644
--- a/.stickler.yml
+++ b/.stickler.yml
@@ -1,13 +1,18 @@
 # stickler-ci configuration
 linters:
-#    flake8:
-#       ignore: 'E501'
-#       max-line-length: 255
-    pep8:
-      ignore: 'E501'
-files:
-    ignore: []
-    
-    
-    
+    flake8:
+       python: 3
+       # E203: 	whitespace before ":"
+       # E501: line too long
+       # W503: line break before binary operator
+       # W504: line break after binary operator
+       ignore: 'E203, E501, W503, W504'
+       max-line-length: 88
+       fixer: false
+
+review:
+    # Don't use 'failure' build status when
+    # a pull request contains style errors.
+    fail_on_comments: false
+
 
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ce6f5b09..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-# the method for using conda within travis has been
-# adapted from: https://conda.io/docs/travis.html
-
-language: python
-python:
-  # We don't actually use the Travis Python, but this keeps it organized.
-  - "3.6"
-  - "3.7"
-
-# blocklist
-branches:
-  except:
-  - rtread
-
-install:
-  - sudo apt-get update
-  # We do this conditionally because it saves us some downloading if the
-  # version is the same.
-  - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
-  - bash miniconda.sh -b -p $HOME/miniconda
-  - export PATH="$HOME/miniconda/bin:$PATH"
-  - export PYTHONPATH="."
-  - hash -r
-  - conda config --set always_yes yes --set changeps1 no
-
-  - conda update -q conda
-  # Useful for debugging any issues with conda
-  - conda info -a
-
-  # nose must be installed explicitly to ensure that the virtual conda-environment is used within the tests
-  # (otherwise import-errors will occur for non-standard packages like scipy sympy and symengine)
-  - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION matplotlib pytest pytest-cov numpy sympy scipy pandas
-  - source activate test-environment
-  - conda install -c conda-forge python-symengine
-  - conda install cloudpickle
-  - conda install pytables
-  - conda install -c conda-forge coveralls
-
-script:
-  - pytest --cov=rt1
-
-after_success:
-  - coveralls
-
-deploy:
-  # automatically deploy a release from the master-branch on pypi
-  provider: pypi
-  user: "__token__"
-  password: $pypi_token
-  skip_existing: true
-  on:
-    branch: master
-    tags: true
-
-after_deploy:
-  # test installation from pip
-  - conda create -q -n test-deploy-environment python=$TRAVIS_PYTHON_VERSION
-  - source activate test-deploy-environment
-  - pip install rt1
\ No newline at end of file
diff --git a/README.md b/README.md
index 2aaf7079..b6efc99b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
-[![Build Status](https://travis-ci.org/TUW-GEO/rt1.svg?branch=dev)](https://travis-ci.org/TUW-GEO/rt1)
-[![Documentation Status](https://readthedocs.org/projects/rt1/badge/?version=latest)](http://rt1.readthedocs.io/)
-[![Coverage Status](https://coveralls.io/repos/github/TUW-GEO/rt1/badge.svg?branch=dev)](https://coveralls.io/github/TUW-GEO/rt1?branch=dev)
+[![Windows_build](https://github.com/TUW-GEO/rt1/workflows/RT1_windows/badge.svg)](https://github.com/TUW-GEO/rt1/actions/workflows/RT1_windows.yml)
+[![Ubuntu_build](https://github.com/TUW-GEO/rt1/workflows/RT1_ubuntu/badge.svg)](https://github.com/TUW-GEO/rt1/actions/workflows/RT1_ubuntu.yml)
+[![Coverage Status](https://codecov.io/gh/TUW-GEO/rt1/branch/dev/graph/badge.svg?token=tVCw5zvIe3)](https://codecov.io/gh/TUW-GEO/rt1)
 [![pypi version](https://img.shields.io/pypi/v/rt1)](https://pypi.org/project/rt1/)
+[![Documentation Status](https://readthedocs.org/projects/rt1/badge/?version=latest)](http://rt1.readthedocs.io/)
 [![DOI](https://zenodo.org/badge/69531751.svg)](https://zenodo.org/badge/latestdoi/69531751)
 
 # RT1 - bistatic scattering model for first order scattering of random media
@@ -58,11 +59,11 @@ For a basic install, the following line should be fine:
 
     pip install rt1
 
-In order to get a significant speedup in the symbolic computations,
-it is recommended to install the module with the optional
-symengine-dependency, i.e.:
+In order to get a significant speedup in the symbolic computations and enable
+NetCDF functionalities, it is recommended to install the module with the
+optional dependencies `symengine` and `xarray` by using:
 
-    pip install rt1[symengine]
+    pip install rt1[full]
 
 ## References
 * Quast & Wagner (2016): [doi:10.1364/AO.55.005379](https://doi.org/10.1364/AO.55.005379)
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..fb02c179
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,8 @@
+coverage:
+  status:
+    project:
+      default:
+        threshold: 100%
+    patch:
+      default:
+        threshold: 100%
\ No newline at end of file
diff --git a/rt1/__init__.py b/rt1/__init__.py
index a5d78e90..9ae983e3 100644
--- a/rt1/__init__.py
+++ b/rt1/__init__.py
@@ -1,4 +1,191 @@
 """Import module for RT1 module"""
 
-__version__ = '1.2.1'
-__author__ = 'Raphael Quast'
+import logging
+import logging.handlers
+import sys
+from textwrap import indent
+import multiprocessing as mp
+from .general_functions import groupby_unsorted
+
+__version__ = "1.3"
+__author__ = "Raphael Quast"
+
+
+# add a logging-level "Prog." that is used to report progess (1 above INFO)
+_PROGRESS_LEVEL_NUM = 21
+logging.addLevelName(_PROGRESS_LEVEL_NUM, "PROG.")
+
+
+def progress(self, message, *args, **kws):
+    if self.isEnabledFor(_PROGRESS_LEVEL_NUM):
+        self._log(_PROGRESS_LEVEL_NUM, message, args, **kws)
+
+
+logging.Logger.progress = progress
+
+
+def set_log_handler_level(level, name="rt1_consolehandler"):
+    """
+    set the level of the logger
+    Parameters
+    ----------
+    level : the desired logging-level
+        50 CRITICAL
+        40 ERROR
+        30 WARNING
+        21 PROGRESS
+        20 INFO
+        10 DEBUG
+
+    name : str, optional
+        The name of the handler to address.
+
+            - 'rt1_consolehandler'
+            - 'rt1_filehandler'
+
+        The default is 'rt1_consolehandler'.
+
+    """
+
+    log = setup_logger()
+
+    h = [val for val in log.handlers if val.name == name][0]
+    h.setLevel(level)
+
+
+def start_log_to_file(path, name="rt1_filehandler", level=logging.INFO):
+    """
+    forward all log-messages to a file
+    to close the file use the `stop_log_to_file()` method!
+
+    Notice: this is not process-save and therefore extensive multiprocessing
+    can lead to an unexpected scrambling of the messges!
+    -> rtprocess.run_processing writes logs to the "cfg" folder in a
+    process-save way!
+
+    Parameters
+    ----------
+    path : str or pathlib.Path
+        the file-path where the file should be stored (including filename!)
+    name : str, optional
+        the name of the filehandler. The default is 'rt1_filehandler'.
+    level : int, optional
+        the log-level attached to the handler. The default is logging.INFO.
+    """
+    try:
+        # check if file-handler already exists, and if yes stop and remove it
+        stop_log_to_file(name=name)
+
+        log = setup_logger()
+        # get formatting from consolehandler (always present)
+        hc = [val for val in log.handlers if val.name == "rt1_consolehandler"][0]
+
+        # setup a new filehandler
+        fh = logging.FileHandler(path, "a")
+        fh.setFormatter(hc.formatter)
+        fh.set_name(name)
+        # initialize the file-handler with level 1 to get all infos
+        fh.setLevel(level)
+
+        log.addHandler(fh)
+
+        log.debug(
+            f"log-file for handler {name} added at location" + f' "{fh.baseFilename}"!'
+        )
+
+    except IndexError as err:
+        log.exception(err)
+
+
+def stop_log_to_file(name="rt1_filehandler"):
+    """
+    stop and remove an existing logging handler
+
+    Parameters
+    ----------
+    name : str, optional
+        the name of the handler. The default is 'rt1_filehandler'.
+    """
+    log = setup_logger()
+    handlers = groupby_unsorted(log.handlers, key=lambda x: x.name)
+
+    # remove existing rt1_filehandler
+    if name in handlers:
+        for i, h in enumerate(handlers[name]):
+            h.close()
+            log.removeHandler(h)
+            log.debug(
+                f'{i + 1} existing logfile-handler "{name}" at'
+                + f' "{h.baseFilename}" closed and removed!'
+            )
+
+
+class WrappedFixedIndentingLog(logging.Formatter):
+    def __init__(self, fmt=None, datefmt=None, style="%", indent=4):
+        super().__init__(fmt=fmt, datefmt=datefmt, style=style)
+
+        self._indent = indent
+
+    def format(self, record):
+        return indent(super().format(record), " " * self._indent).strip()
+
+
+def _get_logger_formatter(simple=True):
+
+    if simple is False:
+        logfmt = (
+            "%(asctime)s - "
+            + mp.current_process().name
+            + ": "
+            + "%(levelname)-8s ("
+            + "%(filename)s:%(lineno)d - "
+            + "%(funcName)s)"
+            + "\n"
+            + "%(message)s"
+        )
+    else:
+        logfmt = (
+            "%(asctime)s - "
+            +
+            # (mp.current_process().name + ':').ljust(21) +
+            "%(processName)-21s"
+            + "%(levelname)-8s"
+            + "%(message)s"
+        )
+
+    formatter = WrappedFixedIndentingLog(logfmt, indent=51, datefmt="%Y-%m-%d %H:%M:%S")
+    return formatter
+
+
+def setup_logger(
+    log_name="rt1",
+    console_out=True,
+    console_level=logging.WARNING,
+    simple=True,
+):
+
+    logger = logging.getLogger(log_name)
+
+    # check if the logger has a Handler, if yes, it's an already existing
+    # one and so we don't want to re-create it!!
+    if logger.hasHandlers():
+        return logger
+
+    logger.setLevel(logging.DEBUG)
+    formatter = _get_logger_formatter(simple)
+
+    if console_out is True:
+        # setup a console-handler that prints to sys.stdout
+        sh = logging.StreamHandler(sys.stdout)
+        sh.setLevel(console_level)
+        sh.setFormatter(formatter)
+        sh.set_name("rt1_consolehandler")
+        logger.addHandler(sh)
+
+    logger.debug(f"Setup logger: {log_name}")
+
+    return logger
+
+
+# initialize a logger for rt1
+log = setup_logger(console_out=True, simple=True, log_name="rt1")
diff --git a/rt1/general_functions.py b/rt1/general_functions.py
index 1ccd2002..eea2dfc6 100644
--- a/rt1/general_functions.py
+++ b/rt1/general_functions.py
@@ -1,13 +1,16 @@
 # -*- coding: utf-8 -*-
 """helper functions that are used both in rtfits and rtplots"""
 import sys
-import numpy as np
 from itertools import tee, islice
 from collections import OrderedDict
 
+try:
+    import numpy as np
+except ModuleNotFoundError:
+    pass
 
-def rectangularize(array, return_mask=False, dim=None,
-                   return_masked=False, dtype=None):
+
+def rectangularize(array, return_mask=False, dim=None, return_masked=False, dtype=None):
     """
     return a rectangularized version of the input-array by repeating the
     last value to obtain the smallest possible rectangular shape.
@@ -92,15 +95,13 @@ def meandatetime(datetimes):
         return datetimes[0]
 
     x = datetimes
-    deltas = (x[0] - x[1:])/len(x)
+    deltas = (x[0] - x[1:]) / len(x)
     meandelta = sum(deltas)
     meandate = x[0] - meandelta
     return meandate
 
 
-def dBsig0convert(val, inc,
-                  dB, sig0,
-                  fitdB, fitsig0):
+def dBsig0convert(val, inc, dB, sig0, fitdB, fitsig0):
     """
     A convenience-function to convert an array of measurements (and it's
     associated incidence-angles).
@@ -134,23 +135,23 @@ def dBsig0convert(val, inc,
         # if results are provided in dB convert them to linear units before
         # applying the sig0-intensity conversion
         if fitdB is True:
-            val = 10**(val/10.)
+            val = 10 ** (val / 10.0)
         # convert sig0 to intensity
         if sig0 is False and fitsig0 is True:
-            val = val/(4.*np.pi*np.cos(inc))
+            val = val / (4.0 * np.pi * np.cos(inc))
         # convert intensity to sig0
         if sig0 is True and fitsig0 is False:
-            val = 4.*np.pi*np.cos(inc)*val
+            val = 4.0 * np.pi * np.cos(inc) * val
         # convert back to dB if required
         if dB is True:
-            val = 10.*np.log10(val)
+            val = 10.0 * np.log10(val)
     elif dB is not fitdB:
         # if dB output is required, convert to dB
         if dB is True and fitdB is False:
-            val = 10.*np.log10(val)
+            val = 10.0 * np.log10(val)
         # if linear output is required, convert to linear units
         if dB is False and fitdB is True:
-            val = 10**(val/10.)
+            val = 10 ** (val / 10.0)
 
     return val
 
@@ -189,13 +190,12 @@ def split_into(iterable, sizes):
             yield list(islice(it, size))
 
 
-def scale(x, out_range=(0, 1),
-          domainfuncs=(np.nanmin, np.nanmax)):
+def scale(x, out_range=(0, 1), domainfuncs=None):
     """
     scale an array between out_range = (min, max) where the range of the
     array is evaluated via the domainfuncs (min-function, max-funcion)
 
-    useful domainfuncs are:
+    the default domainfuncs are:
 
         >>> np.nanmin()
         >>> np.nanmax()
@@ -207,16 +207,18 @@ def scale(x, out_range=(0, 1),
     exceed the specified `out_range`!  (e.g. if the out-range is (0,1),
     a min-function of np.percentile(q=5) might result in negative values!)
     """
-    domain = domainfuncs[0](x), domainfuncs[1](x)
+    if domainfuncs is None:
+        domain = np.nanmin[0](x), np.nanmax[1](x)
+    else:
+        domain = domainfuncs[0](x), domainfuncs[1](x)
 
     y = (x - (domain[1] + domain[0]) / 2) / (domain[1] - domain[0])
-    return y * (out_range[1] - out_range[0]) + (out_range[1] +
-                                                out_range[0]) / 2
+    return y * (out_range[1] - out_range[0]) + (out_range[1] + out_range[0]) / 2
 
 
-def update_progress(progress, max_prog=100,
-                    title="", finalmsg=" DONE\r\n",
-                    progress2=None):
+def update_progress(
+    progress, max_prog=100, title="", finalmsg=" DONE\r\n", progress2=None
+):
     """
     print a progress-bar
 
@@ -224,19 +226,21 @@ def update_progress(progress, max_prog=100,
     """
 
     length = 25  # the length of the progress bar
-    block = int(round(length*progress/max_prog))
+    block = int(round(length * progress / max_prog))
     if progress2 is not None:
-        msg = (f'\r{title} {"#"*block + "-"*(length-block)}' +
-               f' {progress} [{progress2}] / {max_prog}')
+        msg = (
+            f'\r{title} {"#"*block + "-"*(length-block)}'
+            + f" {progress} [{progress2}] / {max_prog}"
+        )
     else:
-        msg = (f'\r{title} {"#"*block + "-"*(length-block)}' +
-               f' {progress} / {max_prog}')
+        msg = (
+            f'\r{title} {"#"*block + "-"*(length-block)}' + f" {progress} / {max_prog}"
+        )
 
     if progress >= max_prog:
-        msg = f'\r{finalmsg:<79}\n'
+        msg = f"\r{finalmsg:<79}\n"
 
-    sys.stdout.write(msg)
-    sys.stdout.flush()
+    return msg
 
 
 def dt_to_hms(td):
@@ -246,7 +250,7 @@ def dt_to_hms(td):
     """
 
     days, hours, minutes = td.days, td.seconds // 3600, td.seconds % 3600 // 60
-    seconds = td.seconds - hours*3600 - minutes*60
+    seconds = td.seconds - hours * 3600 - minutes * 60
     return days, hours, minutes, seconds
 
 
@@ -315,14 +319,14 @@ def interpolate_to_index(data, index, data_index=None, **interp1d_kwargs):
         x = f(index.to_julian_date())
         return Series(x, index)
     elif isinstance(data, DataFrame):
-        f = interp1d(data.index.to_julian_date(),  data.values, axis=0,
-                     **kwargs)
+        f = interp1d(data.index.to_julian_date(), data.values, axis=0, **kwargs)
         x = f(index.to_julian_date())
         return DataFrame(x, index, columns=data.columns)
 
     elif isinstance(data, (list, np.ndarray)):
-        assert data_index is not None, ('you must provide "data_index"' +
-                                        'if data is provided as list or array')
+        assert data_index is not None, (
+            'you must provide "data_index"' + "if data is provided as list or array"
+        )
 
         f = interp1d(data_index.to_julian_date(), data.values, **kwargs)
         x = f(index.to_julian_date())
diff --git a/rt1/processing_config.py b/rt1/processing_config.py
index 07677b3a..52a03508 100644
--- a/rt1/processing_config.py
+++ b/rt1/processing_config.py
@@ -2,9 +2,183 @@
 
 import sys
 import pandas as pd
+import numpy as np
 import traceback
 from pathlib import Path
 from .rtfits import load
+from . import log
+
+try:
+    import xarray as xar
+except ModuleNotFoundError:
+    log.debug("xarray could not be imported, " + "postprocess_xarray will not work!")
+
+
+def defdict_parser(defdict):
+    """
+    get parameter-dynamics specifications from a given defdict
+
+    Parameters
+    ----------
+    defdict : dict
+        a defdict (e.g. fit.defdict)
+
+    Returns
+    -------
+    parameter_specs : dict
+        a dict that contains lists of parameter-names according to their
+        specifications. (keys should be self-explanatory)
+    """
+    parameter_specs = dict(
+        fitted=[],
+        fitted_const=[],
+        fitted_dynamic=[],
+        fitted_dynamic_manual=[],
+        fitted_dynamic_index=[],
+        fitted_dynamic_datetime=[],
+        fitted_dynamic_integer=[],
+        constant=[],
+        auxiliary=[],
+    )
+
+    for key, val in defdict.items():
+        if val[0] is True:
+            parameter_specs["fitted"].append(key)
+            if val[2] is None:
+                parameter_specs["fitted_const"].append(key)
+            else:
+                parameter_specs["fitted_dynamic"].append(key)
+                if val[1] == "manual":
+                    parameter_specs["fitted_dynamic_manual"].append(key)
+                elif val[1] == "index":
+                    parameter_specs["fitted_dynamic_index"].append(key)
+                elif isinstance(val[1], str):
+                    parameter_specs["fitted_dynamic_datetime"].append(key)
+                elif isinstance(val[1], int):
+                    parameter_specs["fitted_dynamic_integer"].append(key)
+        else:
+            if val[1] == "auxiliary":
+                parameter_specs["auxiliary"].append(key)
+            else:
+                parameter_specs["constant"].append(key)
+
+    return parameter_specs
+
+
+def postprocess_xarray(
+    fit,
+    saveparams=None,
+    xindex=("x", -9999),
+    yindex=None,
+    staticlayers=None,
+    auxdata=None,
+):
+    """
+    the identification of parameters is as follows:
+
+        1) 'sig' (conv. to dB) and 'inc' (conv. to degrees) from dataset
+        2) any parameter present in defdict is handled accordingly
+        3) auxdata (a pandas-dataframe) is appended
+
+        4) static layers are generated and added according to the provided dict
+
+
+    Parameters
+    ----------
+    fit : rt1.rtfits.Fits object
+        the fit-object to use
+    saveparams : list, optional
+        a list of strings that correspond to parameter-names that should
+        be included. The default is None.
+    xindex : tuple, optional
+        a tuple (name, value) that will be used as the x-index.
+        The default is ('x', -9999).
+    yindex : tuple, optional
+        a tuple (name, value) that will be used as the y-index.
+        if provided, a multiindex (x, y) will be used!
+        Be warned... when combining xarrays the x- and y- coordinates will
+        be assumed as a rectangular grid!
+        The default is None.
+    staticlayers : dict, optional
+        a dict with parameter-names and values that will be adde das
+        static layers. The default is None.
+    auxdata : pandas.DataFrame, optional
+        a pandas DataFrame that will be concatenated to the DataFrame obtained
+        from combining all 'saveparams'. The default is None.
+
+    Returns
+    -------
+    dfxar : xarray.Dataset
+        a xarray-dataset with all layers defined according to the specs.
+
+    """
+
+    if saveparams is None:
+        saveparams = []
+
+    if staticlayers is None:
+        staticlayers = dict()
+
+    defs = defdict_parser(fit.defdict)
+
+    usedfs = []
+    for key in saveparams:
+
+        if key == "sig":
+            if fit.dB is False:
+                usedfs.append(10.0 * np.log10(fit.dataset.sig))
+            else:
+                usedfs.append(fit.dataset.sig)
+        elif key == "inc":
+            usedfs.append(np.rad2deg(fit.dataset.inc))
+
+        elif key in fit.defdict:
+            if key in defs["fitted_dynamic"]:
+                usedfs.append(fit.res_df[key])
+            elif key in defs["fitted_const"]:
+                staticlayers[key] = fit.res_dict[key][0]
+            elif key in defs["constant"]:
+                staticlayers[key] = fit.defdict[key][1]
+            elif key in defs["auxiliary"]:
+                usedfs.append(fit.dataset[key])
+        elif key in fit.dataset:
+            usedfs.append(fit.dataset[key])
+        else:
+            log.warning(
+                f"the parameter {key} could not be processed"
+                + "during xarray postprocessing"
+            )
+
+    if auxdata is not None:
+        usedfs.append(auxdata)
+
+    # combine all timeseries and set the proper index
+    df = pd.concat(usedfs, axis=1)
+    df.columns.names = ["param"]
+    df.index.names = ["date"]
+
+    if yindex is not None:
+        df = pd.concat([df], keys=[yindex[1]], names=[yindex[0]])
+        df = pd.concat([df], keys=[xindex[1]], names=[xindex[0]])
+
+        # set static layers
+        statics = pd.DataFrame(
+            staticlayers,
+            index=pd.MultiIndex.from_product(
+                iterables=[[xindex[1]], [yindex[1]]], names=["x", "y"]
+            ),
+        )
+
+    else:
+        df = pd.concat([df], keys=[xindex[1]], names=[xindex[0]])
+
+        # set static layers
+        statics = pd.DataFrame(staticlayers, index=[xindex[1]])
+        statics.index.name = xindex[0]
+
+    dfxar = xar.merge([df.to_xarray(), statics.to_xarray()])
+
+    return dfxar
 
 
 class rt1_processing_config(object):
@@ -78,12 +252,12 @@ class rt1_processing_config(object):
 
     def __init__(self, **kwargs):
 
-        if 'save_path' in kwargs and 'dumpfolder' in kwargs:
-            parentpath = Path(kwargs['save_path']) / kwargs['dumpfolder']
+        if "save_path" in kwargs and "dumpfolder" in kwargs:
+            parentpath = Path(kwargs["save_path"]) / kwargs["dumpfolder"]
 
-            self.rt1_procsesing_dumppath = parentpath / 'dumps'
-            self.rt1_procsesing_respath = parentpath / 'results'
-            self.rt1_procsesing_cfgpath = parentpath / 'cfg'
+            self.rt1_procsesing_dumppath = parentpath / "dumps"
+            self.rt1_procsesing_respath = parentpath / "results"
+            self.rt1_procsesing_cfgpath = parentpath / "cfg"
         else:
             self.rt1_procsesing_dumppath = None
             self.rt1_procsesing_respath = None
@@ -118,7 +292,7 @@ def get_names_ids(self, reader_arg):
         """
 
         # the ID used for indexing the processed sites
-        feature_id = reader_arg['gpi']
+        feature_id = reader_arg["gpi"]
 
         # the filename of the dump-file
         filename = f"{feature_id}.dump"
@@ -126,9 +300,11 @@ def get_names_ids(self, reader_arg):
         # the filename of the error-dumpfile
         error_filename = f"{feature_id}_error.txt"
 
-        return dict(feature_id=feature_id,
-                    filename=filename,
-                    error_filename=error_filename)
+        return dict(
+            feature_id=feature_id,
+            filename=filename,
+            error_filename=error_filename,
+        )
 
     def check_dump_exists(self, reader_arg):
         """
@@ -151,8 +327,8 @@ def check_dump_exists(self, reader_arg):
         # check if the file already exists, and if yes, raise a skip-error
         if self.rt1_procsesing_dumppath is not None:
             names_ids = self.get_names_ids(reader_arg)
-            if (self.rt1_procsesing_dumppath / names_ids['filename']).exists():
-                raise Exception('rt1_file_already_exists')
+            if (self.rt1_procsesing_dumppath / names_ids["filename"]).exists():
+                raise Exception("rt1_file_already_exists")
 
     def dump_fit_to_file(self, fit, reader_arg, mini=True):
         """
@@ -172,10 +348,11 @@ def dump_fit_to_file(self, fit, reader_arg, mini=True):
         if self.rt1_procsesing_dumppath is not None:
             names_ids = self.get_names_ids(reader_arg)
 
-            if not (self.rt1_procsesing_dumppath /
-                    names_ids['filename']).exists():
-                fit.dump(self.rt1_procsesing_dumppath / names_ids['filename'],
-                         mini=mini)
+            if not (self.rt1_procsesing_dumppath / names_ids["filename"]).exists():
+                fit.dump(
+                    self.rt1_procsesing_dumppath / names_ids["filename"],
+                    mini=mini,
+                )
 
     def preprocess(self, **kwargs):
         """a function that is called PRIOR to processing"""
@@ -184,11 +361,11 @@ def preprocess(self, **kwargs):
     def reader(self, reader_arg):
         """a function that is called for each site to obtain the dataset"""
         # get the incidence-angles of the data
-        inc = [.1, .2, .3, .4, .5]
+        inc = [0.1, 0.2, 0.3, 0.4, 0.5]
         # get the sig0-values of the data
-        sig = [-10, -11., -11.45, -13, -15]
+        sig = [-10, -11.0, -11.45, -13, -15]
         # get the index-values of the data
-        index = pd.date_range('1.1.2020', '1.5.2020', freq='D')
+        index = pd.date_range("1.1.2020", "1.5.2020", freq="D")
 
         data = pd.DataFrame(dict(inc=inc, sig=sig), index=index)
 
@@ -228,23 +405,27 @@ def postprocess(self, fit, reader_arg):
 
         # get filenames
         names_ids = self.get_names_ids(reader_arg)
+        # parse defdict to find static and dynamic parameter names
+        params = defdict_parser(fit.defdict)
 
         # make a dump of the fit
         self.dump_fit_to_file(fit, reader_arg)
 
-        # get resulting parameter DataFrame
-        df = fit.res_df
-        # add the feature_id as first column-level
-        df.columns = pd.MultiIndex.from_product([[names_ids['feature_id']],
-                                                 df.columns])
-        df.columns.names = ['feature_id', 'param']
+        # add all constant (fitted) parameters as static layers
+        staticlayers = dict()
+        for key in params["fitted_const"]:
+            staticlayers[key] = fit.res_dict[key][0]
 
-        # flush stdout to see output of child-processes
-        sys.stdout.flush()
+        ret = postprocess_xarray(
+            fit=fit,
+            saveparams=["inc", "sig", *params["fitted_dynamic"]],
+            xindex=("ID", names_ids["feature_id"]),
+            staticlayers=staticlayers,
+        )
 
-        return df
+        return ret
 
-    def finaloutput(self, res, format='table'):
+    def finaloutput(self, res, format="table"):
         """
         A function that is called after ALL sites are processed:
 
@@ -287,29 +468,17 @@ def finaloutput(self, res, format='table'):
         """
 
         # concatenate the results
-        res = pd.concat([i for i in res if i is not None], axis=1)
+        resxar = xar.combine_nested([i for i in res if i is not None], concat_dim="ID")
 
         if self.rt1_procsesing_respath is None or self.finalout_name is None:
-            print('both save_path and finalout_name must be specified... ',
-                  'otherwise the final results can NOT be saved!')
-            return res
+            log.info(
+                "both save_path and finalout_name must be specified... "
+                + "otherwise the final results can NOT be saved!"
+            )
+            return resxar
         else:
-
-            hdf_key = 'RT1_result'
-
-            # transpose the dataframe (we want less columns and more rows)
-            res = res.T
-            # ensure that all values are numeric (required for table format)
-            res = res.apply(pd.to_numeric)
-            # ensure that all columns are numeric (required for table format)
-            res.columns = pd.to_numeric(res.columns)
-
-            # create (or append) results to a HDF-store
-            res.to_hdf(self.rt1_procsesing_respath / self.finalout_name,
-                       key=hdf_key, format=format, complevel=7)
-
-        # flush stdout to see output of child-processes
-        sys.stdout.flush()
+            # export netcdf file
+            resxar.to_netcdf(self.rt1_procsesing_respath / self.finalout_name)
 
     def exceptfunc(self, ex, reader_arg):
         """
@@ -337,26 +506,41 @@ def exceptfunc(self, ex, reader_arg):
 
         """
 
+        log.debug(
+            f"catched an {type(ex).__name__} {ex.args} for the "
+            + f"following reader_args: \n{reader_arg}"
+        )
+
         names_ids = self.get_names_ids(reader_arg)
         raise_exception = True
 
-        if 'rt1_skip' in ex.args:
+        if "rt1_skip" in ex.args:
+            log.debug("SKIPPED the following error:")
+            log.debug(traceback.format_exc())
             # ignore skip exceptions
             raise_exception = False
 
-        elif 'rt1_file_already_exists' in ex.args:
+        elif "rt1_file_already_exists" in ex.args:
             # if the fit-dump file already exists, try loading the existing
             # file and apply post-processing (e.g. avoid re-processing results)
             raise_exception = False
 
+            log.debug(
+                f"the file '{names_ids['filename']}' already exists... "
+                + "I'm 'using the existing one!"
+            )
+
             try:
-                fit = load(self.rt1_procsesing_dumppath /
-                           names_ids['filename'])
+                fit = load(self.rt1_procsesing_dumppath / names_ids["filename"])
                 return self.postprocess(fit, reader_arg)
             except Exception:
+                log.debug(
+                    "the has been a problem while loading the "
+                    + f"already processed file '{names_ids['filename']}'"
+                )
                 pass
 
-        elif 'rt1_data_error' in ex.args:
+        elif "rt1_data_error" in ex.args:
             # raised if there was a problem with the data, ignore and continue
             raise_exception = False
 
@@ -365,20 +549,20 @@ def exceptfunc(self, ex, reader_arg):
         # if `save_path`is NOT specified, raise ONLY exceptions that
         # have not been explicitly catched
         if self.rt1_procsesing_dumppath is None and raise_exception is True:
-            print('`save_path` must be specified otherwise exceptions ' +
-                  'that are not explicitly catched by `exceptfunc()` ' +
-                  ' will be raised!')
+            log.info(
+                "`save_path` must be specified otherwise exceptions "
+                + "that are not explicitly catched by `exceptfunc()` "
+                + " will be raised!"
+            )
             raise ex
         else:
-            if 'error_filename' in names_ids:
-                error_filename = names_ids['error_filename']
+            if "error_filename" in names_ids:
+                error_filename = names_ids["error_filename"]
             else:
-                error_filename = names_ids[
-                    'filename'].split('.')[0] + '_error.txt'
+                error_filename = names_ids["filename"].split(".")[0] + "_error.txt"
 
             # dump the encountered exception to a file
-            with open(self.rt1_procsesing_dumppath /
-                      error_filename, 'w') as file:
+            with open(self.rt1_procsesing_dumppath / error_filename, "w") as file:
                 file.write(traceback.format_exc())
 
         # flush stdout to see output of child-processes
diff --git a/rt1/rt1.py b/rt1/rt1.py
index 00b65709..35a50403 100644
--- a/rt1/rt1.py
+++ b/rt1/rt1.py
@@ -6,6 +6,9 @@
 ----------
 Quast & Wagner (2016): doi:10.1364/AO.55.005379
 """
+
+from . import log
+
 import numpy as np
 
 from functools import lru_cache
@@ -18,10 +21,12 @@
 try:
     # if symengine is available, use it to perform series-expansions
     from symengine import expand, Lambdify
-    _init_lambda_backend = 'symengine'
+
+    _init_lambda_backend = "symengine"
 except ImportError:
     from sympy import expand
-    _init_lambda_backend = 'sympy'
+
+    _init_lambda_backend = "sympy"
 
 
 class RT1(object):
@@ -116,15 +121,29 @@ class RT1(object):
                 - >=3 : print all
     """
 
-    def __init__(self, I0, t_0, t_ex, p_0, p_ex, V=None, SRF=None,
-                 fn_input=None, _fnevals_input=None, geometry='mono',
-                 bsf=0., param_dict={}, lambda_backend=_init_lambda_backend,
-                 int_Q=True, verbosity=1):
-
-        assert isinstance(geometry, str), ('ERROR: geometry must be ' +
-                                           'a 4-character string')
-        assert len(geometry) == 4, ('ERROR: geometry must be ' +
-                                    'a 4-character string')
+    def __init__(
+        self,
+        I0,
+        t_0,
+        t_ex,
+        p_0,
+        p_ex,
+        V=None,
+        SRF=None,
+        fn_input=None,
+        _fnevals_input=None,
+        geometry="mono",
+        bsf=0.0,
+        param_dict={},
+        lambda_backend=_init_lambda_backend,
+        int_Q=True,
+        verbosity=1,
+    ):
+
+        assert isinstance(geometry, str), (
+            "ERROR: geometry must be " + "a 4-character string"
+        )
+        assert len(geometry) == 4, "ERROR: geometry must be " + "a 4-character string"
         self.geometry = geometry
 
         self.I0 = I0
@@ -132,10 +151,10 @@ def __init__(self, I0, t_0, t_ex, p_0, p_ex, V=None, SRF=None,
         self.lambda_backend = lambda_backend
         self.int_Q = int_Q
 
-        assert V is not None, 'ERROR: needs to provide volume information'
+        assert V is not None, "ERROR: needs to provide volume information"
         self.V = V
 
-        assert SRF is not None, 'ERROR: needs to provide surface information'
+        assert SRF is not None, "ERROR: needs to provide surface information"
         self.SRF = SRF
 
         self.fn_input = fn_input
@@ -143,7 +162,7 @@ def __init__(self, I0, t_0, t_ex, p_0, p_ex, V=None, SRF=None,
 
         self._set_t_0(t_0)
         self._set_p_0(p_0)
-        if self.geometry != 'mono':
+        if self.geometry != "mono":
             self._set_t_ex(t_ex)
             self._set_p_ex(p_ex)
 
@@ -158,9 +177,10 @@ def __init__(self, I0, t_0, t_ex, p_0, p_ex, V=None, SRF=None,
         # providing omega & tau which is needed to generate linear-combinations
         # of Volume-elements with unambiguous tau- & omega-specifications
 
-        assert self.V.omega is not None, ('Single scattering albedo ' +
-                                          'needs to be provided')
-        assert self.V.tau is not None, 'Optical depth needs to be provided'
+        assert self.V.omega is not None, (
+            "Single scattering albedo " + "needs to be provided"
+        )
+        assert self.V.tau is not None, "Optical depth needs to be provided"
 
         # self.V.omega[0] must be used instead of self.V.omega since the
         # setter functions for omega, tau and NormBRDF add an additional
@@ -168,32 +188,36 @@ def __init__(self, I0, t_0, t_ex, p_0, p_ex, V=None, SRF=None,
         # to distinguish if the input was given as a sympy equation. For
         # details see: http://docs.sympy.org/latest/guide.html#basics
         if not isinstance(self.V.omega, sp.Basic):
-            assert np.any(self.V.omega >= 0.), ('Single scattering albedo ' +
-                                                'must be greater than 0')
+            assert np.any(self.V.omega >= 0.0), (
+                "Single scattering albedo " + "must be greater than 0"
+            )
         if not isinstance(self.V.tau, sp.Basic):
-            assert np.any(self.V.tau >= 0.), ('Optical depth ' +
-                                              'must be greater than 0')
+            assert np.any(self.V.tau >= 0.0), (
+                "Optical depth " + "must be greater than 0"
+            )
         if not isinstance(self.SRF.NormBRDF, sp.Basic):
-            assert np.any(self.SRF.NormBRDF >= 0.), ('NormBRDF ' +
-                                                     'must be greater than 0')
+            assert np.any(self.SRF.NormBRDF >= 0.0), (
+                "NormBRDF " + "must be greater than 0"
+            )
 
     def __getstate__(self):
         # this is required since functions created by
         # symengine are currently not pickleable!
-        print('dropping fn-coefficients to allow pickling...')
-        for delkey in ['_RT1__fn']:
+        log.info("dropping fn-coefficients to allow pickling...")
+        for delkey in ["_RT1__fn"]:
             if delkey in self.__dict__:
-                print('removing', delkey, 'from __dict__')
+                log.info("removing", delkey, "from __dict__")
                 del self.__dict__[delkey]
-        for Nonekey in ['_fn_input']:
+        for Nonekey in ["_fn_input"]:
             if Nonekey in self.__dict__:
-                print('setting', Nonekey, 'to None')
+                log.info("setting", Nonekey, "to None")
                 self.__dict__[Nonekey] = None
 
-        if self.lambda_backend == 'symengine':
-            import warnings
-            warnings.warn('the resulting dump of the _fnevals functions ' +
-                          'generated by symengine will be platform-dependent!')
+        if self.lambda_backend == "symengine":
+            log.info(
+                "the resulting dump of the _fnevals functions "
+                + "generated by symengine will be platform-dependent!"
+            )
 
         return self.__dict__
 
@@ -201,8 +225,12 @@ def __getstate__(self):
     def _cached_props(self):
         """a list of the names of the properties that are cached"""
 
-        names = ['_d_surface_dummy_lambda', '_d_surface_dummy_lambda',
-                 '_mu_0_x', '_mu_ex_x']
+        names = [
+            "_d_surface_dummy_lambda",
+            "_d_surface_dummy_lambda",
+            "_mu_0_x",
+            "_mu_ex_x",
+        ]
         return names
 
     def _clear_cache(self):
@@ -216,11 +244,11 @@ def _cache_info(self):
         for name in self._cached_props:
             try:
                 cinfo = getattr(self, name).cache_info()
-                text += [f'{name:<18}:   ' + f'{cinfo}']
+                text += [f"{name:<18}:   " + f"{cinfo}"]
             except Exception:
-                text += [f'{name:<18}:   ' + '???']
+                text += [f"{name:<18}:   " + "???"]
 
-        return print('\n'.join(text))
+        log.info("\n".join(text))
 
     def prv(self, v, msg):
         """
@@ -240,7 +268,7 @@ def prv(self, v, msg):
             the message to be printed.
         """
         if self.verbosity >= v:
-            print(msg)
+            log.info(msg)
 
     def _get_fn(self):
         try:
@@ -255,25 +283,23 @@ def _set_fn(self, fn):
         # only evaluate fn-coefficients if _fnevals funcions are not
         # already available!
         if fn is None and self.int_Q is True:
-            self.prv(1, 'evaluating fn-coefficients...')
+            self.prv(1, "evaluating fn-coefficients...")
 
             import timeit
+
             tic = timeit.default_timer()
             # precalculate the expansiion coefficients for the interaction term
             expr_int = self._calc_interaction_expansion()
             toc = timeit.default_timer()
-            self.prv(2,
-                     'expansion calculated, it took ' + str(toc-tic) + ' sec')
+            self.prv(2, "expansion calculated, it took " + str(toc - tic) + " sec")
 
             # extract the expansion coefficients
             tic = timeit.default_timer()
             self.__fn = self._extract_coefficients(expr_int)
             toc = timeit.default_timer()
-            self.prv(2,
-                     'coefficients extracted, it took ' +
-                     str(toc - tic) + ' sec')
+            self.prv(2, "coefficients extracted, it took " + str(toc - tic) + " sec")
         else:
-            self.prv(3, 'using provided fn-coefficients')
+            self.prv(3, "using provided fn-coefficients")
             self.__fn = fn
 
     fn = property(_get_fn, _set_fn)
@@ -289,41 +315,49 @@ def _get_fnevals(self):
 
     def _set_fnevals(self, _fnevals):
         if _fnevals is None and self.int_Q is True:
-            self.prv(1, 'generation of _fnevals functions...')
+            self.prv(1, "generation of _fnevals functions...")
             import timeit
+
             tic = timeit.default_timer()
 
             # define new lambda-functions for each fn-coefficient
-            variables = sp.var(('theta_0', 'phi_0', 'theta_ex', 'phi_ex') +
-                               tuple(map(str, self.param_dict.keys())))
+            variables = sp.var(
+                ("theta_0", "phi_0", "theta_ex", "phi_ex")
+                + tuple(map(str, self.param_dict.keys()))
+            )
 
             # use symengine's Lambdify if symengine has been used within
             # the fn-coefficient generation
-            if self.lambda_backend == 'symengine':
-                self.prv(1, 'symengine')
+            if self.lambda_backend == "symengine":
+                self.prv(1, "symengine")
                 # using symengines own "common subexpression elimination"
                 # routine to perform lambdification
 
                 # llvm backend is used to allow pickling of the functions
                 # see https://github.com/symengine/symengine.py/issues/294
-                self.__fnevals = Lambdify(list(variables),
-                                          self.fn, order='F',
-                                          cse=True, backend='llvm'
-                                          )
-            elif self.lambda_backend == 'sympy':
+                self.__fnevals = Lambdify(
+                    list(variables),
+                    self.fn,
+                    order="F",
+                    cse=True,
+                    backend="llvm",
+                )
+            elif self.lambda_backend == "sympy":
                 # using sympy's lambdify without "common subexpression
                 # elimination" to perform lambdification
 
-                self.prv(1, 'sympy')
+                self.prv(1, "sympy")
 
                 sympy_fn = list(map(sp.sympify, self.fn))
 
-                self.__fnevals = sp.lambdify((variables),
-                                             sp.sympify(sympy_fn),
-                                             modules=["numpy", "sympy"],
-                                             dummify=False)
+                self.__fnevals = sp.lambdify(
+                    (variables),
+                    sp.sympify(sympy_fn),
+                    modules=["numpy", "sympy"],
+                    dummify=False,
+                )
 
-                self.__fnevals.__doc__ = ('''
+                self.__fnevals.__doc__ = """
                                     A function to numerically evaluate the
                                     fn-coefficients a for given set of
                                     incidence angles and parameter-values
@@ -332,19 +366,22 @@ def _set_fnevals(self, _fnevals):
                                     The call-signature is:
                                         RT1-object._fnevals(theta_0, phi_0, \
                                         theta_ex, phi_ex, *param_dict.values())
-                                    ''')
+                                    """
 
             else:
-                self.prv(1, 'lambda_backend "' + self.lambda_backend +
-                         '" is not available')
+                self.prv(
+                    1,
+                    'lambda_backend "' + self.lambda_backend + '" is not available',
+                )
 
             toc = timeit.default_timer()
-            self.prv(2,
-                     'lambdification finished, it took ' +
-                     str(toc - tic) + ' sec')
+            self.prv(
+                2,
+                "lambdification finished, it took " + str(toc - tic) + " sec",
+            )
 
         else:
-            self.prv(3, 'using provided _fnevals-functions')
+            self.prv(3, "using provided _fnevals-functions")
             self.__fnevals = _fnevals
 
     _fnevals = property(_get_fnevals, _set_fnevals)
@@ -372,15 +409,15 @@ def _set_p_0(self, p_0):
     p_0 = property(_get_p_0, _set_p_0)
 
     def _get_t_ex(self):
-        if self.geometry == 'mono':
+        if self.geometry == "mono":
             return self._get_t_0()
         else:
             return self.__t_ex
 
     def _set_t_ex(self, t_ex):
         # if geometry is mono, set t_ex to t_0
-        if self.geometry == 'mono':
-            print('t_ex is always equal to t_0 if geometry is "mono"')
+        if self.geometry == "mono":
+            log.info('t_ex is always equal to t_0 if geometry is "mono"')
             pass
         else:
             # if t_ex is given as scalar input, convert it to 1d numpy array
@@ -391,30 +428,33 @@ def _set_t_ex(self, t_ex):
     t_ex = property(_get_t_ex, _set_t_ex)
 
     def _get_p_ex(self):
-        if self.geometry == 'mono':
+        if self.geometry == "mono":
             return self._get_p_0() + np.pi
         else:
             return self.__p_ex
 
     def _set_p_ex(self, p_ex):
         # if geometry is mono, set p_ex to p_0
-        if self.geometry == 'mono':
-            print('p_ex is always equal to (p_0 + PI) if geometry is "mono"')
+        if self.geometry == "mono":
+            log.info('p_ex is equal to (p_0 + PI) if geometry is "mono"!')
             pass
         else:
             # if p_ex is given as scalar input, convert it to 1d numpy array
             if np.isscalar(p_ex):
                 p_ex = np.array([p_ex])
             self.__p_ex = p_ex
+
     p_ex = property(_get_p_ex, _set_p_ex)
 
     # calculate cosines of incident- and exit angle
     def _get_mu_0(self):
         return np.cos(self.t_0)
+
     _mu_0 = property(_get_mu_0)
 
     def _get_mu_ex(self):
         return np.cos(self.t_ex)
+
     _mu_ex = property(_get_mu_ex)
 
     def _extract_coefficients(self, expr):
@@ -439,7 +479,7 @@ def _extract_coefficients(self, expr):
 
         """
 
-        theta_s = sp.Symbol('theta_s')
+        theta_s = sp.Symbol("theta_s")
 
         N_fn = self.SRF.ncoefs + self.V.ncoefs - 1
 
@@ -450,26 +490,28 @@ def _extract_coefficients(self, expr):
         fn = fn + [expr.xreplace(repl0)]
 
         # find f_1 coefficient
-        repl1 = dict([[sp.cos(theta_s)**i, 0] for i in list(range(N_fn, 0, -1))
-                     if i != 1] + [[sp.cos(theta_s), 1]])
+        repl1 = dict(
+            [[sp.cos(theta_s) ** i, 0] for i in list(range(N_fn, 0, -1)) if i != 1]
+            + [[sp.cos(theta_s), 1]]
+        )
         fn = fn + [expr.xreplace(repl1) - fn[0]]
 
         for n in np.arange(2, N_fn, dtype=int):
-            repln = dict([[sp.cos(theta_s)**int(n), 1]])
+            repln = dict([[sp.cos(theta_s) ** int(n), 1]])
             fn = fn + [(expr.xreplace(repln)).xreplace(repl0) - fn[0]]
 
-#        # alternative way of extracting the coefficients:
-#        theta_s = sp.Symbol('theta_s')
-#        # collect terms with equal powers of cos(theta_s)
-#        expr_sort = sp.collect(expr, sp.cos(theta_s), evaluate=False)
-#
-#        # convert generated dictionary to list of coefficients
-#        # the use of  .get() is necessary for getting the dict-values since
-#        # otherwise coefficients that are actually 0. would not appear
-#        #  in the list of fn-coefficients
-#
-#        fn = [expr_sort.get(sp.cos(theta_s) ** n, 0.)
-#              for n in range(self.SRF.ncoefs + self.V.ncoefs - 1)]
+        #        # alternative way of extracting the coefficients:
+        #        theta_s = sp.Symbol('theta_s')
+        #        # collect terms with equal powers of cos(theta_s)
+        #        expr_sort = sp.collect(expr, sp.cos(theta_s), evaluate=False)
+        #
+        #        # convert generated dictionary to list of coefficients
+        #        # the use of  .get() is necessary for getting the dict-values since
+        #        # otherwise coefficients that are actually 0. would not appear
+        #        #  in the list of fn-coefficients
+        #
+        #        fn = [expr_sort.get(sp.cos(theta_s) ** n, 0.)
+        #              for n in range(self.SRF.ncoefs + self.V.ncoefs - 1)]
 
         return fn
 
@@ -500,13 +542,13 @@ def _calc_interaction_expansion(self):
         # preevaluate expansions for volume and surface phase functions
         # this returns symbolic code to be then further used
 
-        volexp = self.V.legexpansion(self.t_0, self.t_ex,
-                                     self.p_0, self.p_ex,
-                                     self.geometry).doit()
+        volexp = self.V.legexpansion(
+            self.t_0, self.t_ex, self.p_0, self.p_ex, self.geometry
+        ).doit()
 
-        brdfexp = self.SRF.legexpansion(self.t_0, self.t_ex,
-                                        self.p_0, self.p_ex,
-                                        self.geometry).doit()
+        brdfexp = self.SRF.legexpansion(
+            self.t_0, self.t_ex, self.p_0, self.p_ex, self.geometry
+        ).doit()
 
         # preparation of the product of p*BRDF for coefficient retrieval
         # this is the eq.23. and would need to be integrated from 0 to 2pi
@@ -517,12 +559,15 @@ def _calc_interaction_expansion(self):
 
         # now we do still simplify the expression to be able to express
         # things as power series of cos(theta_s)
-        theta_s = sp.Symbol('theta_s')
-        replacements = [(sp.sin(theta_s) ** i,
-                         expand((1. - sp.cos(theta_s) ** 2)
-                                ** sp.Rational(i, 2)))
-                        for i in range(1, self.SRF.ncoefs + self.V.ncoefs - 1)
-                        if i % 2 == 0]
+        theta_s = sp.Symbol("theta_s")
+        replacements = [
+            (
+                sp.sin(theta_s) ** i,
+                expand((1.0 - sp.cos(theta_s) ** 2) ** sp.Rational(i, 2)),
+            )
+            for i in range(1, self.SRF.ncoefs + self.V.ncoefs - 1)
+            if i % 2 == 0
+        ]
 
         res = expand(expr.xreplace(dict(replacements)))
 
@@ -549,7 +594,7 @@ def _cosintegral(self, i):
             return (2 ** (-i)) * sp.binomial(i, i * sp.Rational(1, 2))
         else:
             # for odd exponents result is always zero
-            return 0.
+            return 0.0
 
     def _integrate_0_2pi_phis(self, expr):
         """
@@ -579,21 +624,25 @@ def _integrate_0_2pi_phis(self, expr):
               expr over the variable phi_s in the inteVal 0 ... 2*pi
         """
 
-        phi_s = sp.Symbol('phi_s')
+        phi_s = sp.Symbol("phi_s")
 
         # replace first all odd powers of sin(phi_s) as these are
         # all zero for the integral
-        replacements1 = [(sp.sin(phi_s) ** i, 0.)
-                         for i in range(1, self.SRF.ncoefs +
-                                        self.V.ncoefs + 1) if i % 2 == 1]
+        replacements1 = [
+            (sp.sin(phi_s) ** i, 0.0)
+            for i in range(1, self.SRF.ncoefs + self.V.ncoefs + 1)
+            if i % 2 == 1
+        ]
 
         # then substitute the sine**2 by 1-cos**2
-        replacements1 = (replacements1 +
-                         [(sp.sin(phi_s) ** i,
-                           expand((1. -
-                                   sp.cos(phi_s) ** 2) ** sp.Rational(i, 2)))
-                          for i in range(2, self.SRF.ncoefs +
-                                         self.V.ncoefs + 1) if i % 2 == 0])
+        replacements1 = replacements1 + [
+            (
+                sp.sin(phi_s) ** i,
+                expand((1.0 - sp.cos(phi_s) ** 2) ** sp.Rational(i, 2)),
+            )
+            for i in range(2, self.SRF.ncoefs + self.V.ncoefs + 1)
+            if i % 2 == 0
+        ]
 
         res = expand(expr.xreplace(dict(replacements1)))
 
@@ -601,9 +650,10 @@ def _integrate_0_2pi_phis(self, expr):
         # remaining sin(phi_s)**even will be replaced by 0
 
         # integrate the cosine terms
-        replacements3 = [(sp.cos(phi_s) ** i, self._cosintegral(i))
-                         for i in range(1, self.SRF.ncoefs +
-                                        self.V.ncoefs + 1)]
+        replacements3 = [
+            (sp.cos(phi_s) ** i, self._cosintegral(i))
+            for i in range(1, self.SRF.ncoefs + self.V.ncoefs + 1)
+        ]
 
         res = expand(res.xreplace(dict(replacements3)))
         return res
@@ -634,15 +684,15 @@ def calc(self):
         if isinstance(self.V.tau, (int, float)):
             Isurf = self.surface()
             # differentiation for non-existing canopy, as otherwise NAN values
-            if self.V.tau > 0.:
+            if self.V.tau > 0.0:
                 Ivol = self.volume()
                 if self.int_Q is True:
                     Iint = self.interaction()
                 else:
-                    Iint = np.array([0.])
+                    Iint = np.array([0.0])
             else:
-                Ivol = np.array([0.])
-                Iint = np.array([0.])
+                Ivol = np.array([0.0])
+                Iint = np.array([0.0])
         else:
             Isurf = self.surface()
             Ivol = self.volume()
@@ -654,10 +704,14 @@ def calc(self):
                 # (self.V.tau = 0) and replace them with 0
                 wherenan = np.isnan(Iint)
                 if np.any(wherenan) and np.allclose(
-                        *np.broadcast_arrays(wherenan, self.V.tau == 0.)):
-                    self.prv(3, 'Warning replacing nan-values caused by tau=0 \
-                             in the interaction-term with 0!')
-                    Iint[np.where(wherenan)] = 0.
+                    *np.broadcast_arrays(wherenan, self.V.tau == 0.0)
+                ):
+                    self.prv(
+                        3,
+                        "Warning replacing nan-values caused by tau=0 \
+                             in the interaction-term with 0!",
+                    )
+                    Iint[np.where(wherenan)] = 0.0
                 else:
                     pass
 
@@ -678,15 +732,21 @@ def surface(self):
             given set of parameters
         """
         # bare soil contribution
-        I_bs = (self.I0 * self._mu_0
-                * self.SRF.brdf(self.t_0, self.t_ex,
-                                self.p_0, self.p_ex,
-                                param_dict=self.param_dict))
-
-        Isurf = (np.exp(-(self.V.tau / self._mu_0) -
-                        (self.V.tau / self._mu_ex))) * I_bs
-
-        return self.SRF.NormBRDF * ((1. - self.bsf) * Isurf + self.bsf * I_bs)
+        I_bs = (
+            self.I0
+            * self._mu_0
+            * self.SRF.brdf(
+                self.t_0,
+                self.t_ex,
+                self.p_0,
+                self.p_ex,
+                param_dict=self.param_dict,
+            )
+        )
+
+        Isurf = (np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex))) * I_bs
+
+        return self.SRF.NormBRDF * ((1.0 - self.bsf) * Isurf + self.bsf * I_bs)
 
     def surface_slope(self, dB=False, sig0=False):
         """
@@ -711,42 +771,51 @@ def surface_slope(self, dB=False, sig0=False):
         """
         # evaluate the slope of the used brdf
         brdf_slope = self.SRF.brdf_theta_diff(
-                t_0=self.t_0, t_ex=self.t_ex, p_0=self.p_0,
-                p_ex=self.p_ex, geometry='mono',
-                param_dict=self.param_dict, return_symbolic=False,
-                n=1)
+            t_0=self.t_0,
+            t_ex=self.t_ex,
+            p_0=self.p_0,
+            p_ex=self.p_ex,
+            geometry="mono",
+            param_dict=self.param_dict,
+            return_symbolic=False,
+            n=1,
+        )
         # evaluate the used brdf
-        brdf_val = self.SRF.brdf(self.t_0, self.t_ex,
-                                 self.p_0, self.p_ex,
-                                 param_dict=self.param_dict)
+        brdf_val = self.SRF.brdf(
+            self.t_0,
+            self.t_ex,
+            self.p_0,
+            self.p_ex,
+            param_dict=self.param_dict,
+        )
 
         # vegetated soil contribution
-        I_vegs_slope = (self.I0
-                        * np.exp(-(2*self.V.tau / self._mu_0))
-                        * (self._mu_0 * brdf_slope
-                           - (2 * self.V.tau / self._mu_0 + 1)
-                           * np.sin(self.t_0) * brdf_val))
+        I_vegs_slope = (
+            self.I0
+            * np.exp(-(2 * self.V.tau / self._mu_0))
+            * (
+                self._mu_0 * brdf_slope
+                - (2 * self.V.tau / self._mu_0 + 1) * np.sin(self.t_0) * brdf_val
+            )
+        )
 
         # bare soil contribution
-        I_bs_slope = self.I0 * (self._mu_0 * brdf_slope
-                                - np.sin(self.t_0) * brdf_val)
+        I_bs_slope = self.I0 * (self._mu_0 * brdf_slope - np.sin(self.t_0) * brdf_val)
 
         I_slope = self.SRF.NormBRDF * (
-                (1. - self.bsf) * I_vegs_slope
-                + self.bsf * I_bs_slope)
+            (1.0 - self.bsf) * I_vegs_slope + self.bsf * I_bs_slope
+        )
 
         if sig0 is False and dB is False:
             return I_slope
         else:
             I_val = self.surface()
             if sig0 is True and dB is False:
-                return 4. * np.pi * (self._mu_0 * I_slope
-                                     - np.sin(self.t_0) * I_val)
+                return 4.0 * np.pi * (self._mu_0 * I_slope - np.sin(self.t_0) * I_val)
             elif sig0 is False and dB is True:
-                return 10./np.log(10) * I_slope / I_val
+                return 10.0 / np.log(10) * I_slope / I_val
             elif sig0 is True and dB is True:
-                return 10./np.log(10) * (I_slope / I_val
-                                         - np.tan(self.t_0))
+                return 10.0 / np.log(10) * (I_slope / I_val - np.tan(self.t_0))
 
     def surface_curv(self, dB=False, sig0=False):
         """
@@ -772,39 +841,64 @@ def surface_curv(self, dB=False, sig0=False):
 
         # evaluate the slope of the used brdf
         brdf_curv = self.SRF.brdf_theta_diff(
-                t_0=self.t_0, t_ex=self.t_ex, p_0=self.p_0,
-                p_ex=self.p_ex, geometry='mono',
-                param_dict=self.param_dict, return_symbolic=False,
-                n=2)
+            t_0=self.t_0,
+            t_ex=self.t_ex,
+            p_0=self.p_0,
+            p_ex=self.p_ex,
+            geometry="mono",
+            param_dict=self.param_dict,
+            return_symbolic=False,
+            n=2,
+        )
         # evaluate the slope of the used brdf
         brdf_slope = self.SRF.brdf_theta_diff(
-                t_0=self.t_0, t_ex=self.t_ex, p_0=self.p_0,
-                p_ex=self.p_ex, geometry='mono',
-                param_dict=self.param_dict, return_symbolic=False,
-                n=1)
+            t_0=self.t_0,
+            t_ex=self.t_ex,
+            p_0=self.p_0,
+            p_ex=self.p_ex,
+            geometry="mono",
+            param_dict=self.param_dict,
+            return_symbolic=False,
+            n=1,
+        )
         # evaluate the used brdf
-        brdf_val = self.SRF.brdf(self.t_0, self.t_ex,
-                                 self.p_0, self.p_ex,
-                                 param_dict=self.param_dict)
+        brdf_val = self.SRF.brdf(
+            self.t_0,
+            self.t_ex,
+            self.p_0,
+            self.p_ex,
+            param_dict=self.param_dict,
+        )
 
         # vegetated soil contribution
-        I_vegs_curv = (self.I0
-                       * np.exp(-(2. * self.V.tau / self._mu_0)) * (
-                            self._mu_0 * brdf_curv -
-                            2. * np.sin(self.t_0) * brdf_slope * (
-                                    2. * self.V.tau / self._mu_0 + 1.)
-                            + (4. * self.V.tau**2 / self._mu_0**3
-                               * np.sin(self.t_0)**2
-                               - 2. * self.V.tau - self._mu_0) * brdf_val))
+        I_vegs_curv = (
+            self.I0
+            * np.exp(-(2.0 * self.V.tau / self._mu_0))
+            * (
+                self._mu_0 * brdf_curv
+                - 2.0
+                * np.sin(self.t_0)
+                * brdf_slope
+                * (2.0 * self.V.tau / self._mu_0 + 1.0)
+                + (
+                    4.0 * self.V.tau ** 2 / self._mu_0 ** 3 * np.sin(self.t_0) ** 2
+                    - 2.0 * self.V.tau
+                    - self._mu_0
+                )
+                * brdf_val
+            )
+        )
 
         # bare soil contribution
-        I_bs_curv = self.I0 * (self._mu_0 * brdf_curv
-                               - 2. * np.sin(self.t_0) * brdf_slope
-                               - self._mu_0 * brdf_val)
+        I_bs_curv = self.I0 * (
+            self._mu_0 * brdf_curv
+            - 2.0 * np.sin(self.t_0) * brdf_slope
+            - self._mu_0 * brdf_val
+        )
 
         I_curv = self.SRF.NormBRDF * (
-                (1. - self.bsf) * I_vegs_curv
-                + self.bsf * I_bs_curv)
+            (1.0 - self.bsf) * I_vegs_curv + self.bsf * I_bs_curv
+        )
 
         if sig0 is False and dB is False:
             return I_curv
@@ -812,17 +906,23 @@ def surface_curv(self, dB=False, sig0=False):
             I_slope = self.surface_slope(dB=False, sig0=False)
             I_val = self.surface()
             if sig0 is True and dB is False:
-                return 4. * np.pi * (self._mu_0 * I_curv
-                                     - 2. * np.sin(self.t_0)
-                                     * I_slope
-                                     - self._mu_0 * I_val)
+                return (
+                    4.0
+                    * np.pi
+                    * (
+                        self._mu_0 * I_curv
+                        - 2.0 * np.sin(self.t_0) * I_slope
+                        - self._mu_0 * I_val
+                    )
+                )
             elif sig0 is False and dB is True:
-                return 10./np.log(10) * (I_curv / I_val
-                                         - I_slope**2 / I_val**2)
+                return 10.0 / np.log(10) * (I_curv / I_val - I_slope ** 2 / I_val ** 2)
             elif sig0 is True and dB is True:
-                return 10./np.log(10) * (I_curv / I_val
-                                         - I_slope**2 / I_val**2
-                                         - self._mu_0**(-2))
+                return (
+                    10.0
+                    / np.log(10)
+                    * (I_curv / I_val - I_slope ** 2 / I_val ** 2 - self._mu_0 ** (-2))
+                )
 
     def volume(self):
         """
@@ -835,14 +935,19 @@ def volume(self):
             Numerical value of the volume-contribution for the
             given set of parameters
         """
-        vol = ((self.I0 * self.V.omega *
-                self._mu_0 / (self._mu_0 + self._mu_ex))
-               * (1. - np.exp(-(self.V.tau / self._mu_0) -
-                               (self.V.tau / self._mu_ex)))
-               * self.V.p(self.t_0, self.t_ex, self.p_0, self.p_ex,
-                          param_dict=self.param_dict))
-
-        return (1. - self.bsf) * vol
+        vol = (
+            (self.I0 * self.V.omega * self._mu_0 / (self._mu_0 + self._mu_ex))
+            * (1.0 - np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex)))
+            * self.V.p(
+                self.t_0,
+                self.t_ex,
+                self.p_0,
+                self.p_ex,
+                param_dict=self.param_dict,
+            )
+        )
+
+        return (1.0 - self.bsf) * vol
 
     def volume_slope(self, dB=False, sig0=False):
         """
@@ -867,37 +972,55 @@ def volume_slope(self, dB=False, sig0=False):
         """
 
         # evaluate the slope of the used phase-function
-        p_slope = self.V.p_theta_diff(t_0=self.t_0, t_ex=self.t_ex,
-                                      p_0=self.p_0, p_ex=self.p_ex,
-                                      geometry='mono',
-                                      param_dict=self.param_dict,
-                                      return_symbolic=False,
-                                      n=1)
+        p_slope = self.V.p_theta_diff(
+            t_0=self.t_0,
+            t_ex=self.t_ex,
+            p_0=self.p_0,
+            p_ex=self.p_ex,
+            geometry="mono",
+            param_dict=self.param_dict,
+            return_symbolic=False,
+            n=1,
+        )
 
         # evaluate the used phase function
-        p_val = self.V.p(self.t_0, self.t_ex,
-                         self.p_0, self.p_ex,
-                         param_dict=self.param_dict)
+        p_val = self.V.p(
+            self.t_0,
+            self.t_ex,
+            self.p_0,
+            self.p_ex,
+            param_dict=self.param_dict,
+        )
 
         # volume contribution
-        I_slope = (1. - self.bsf) * self.I0 * self.V.omega / 2. * (
-                (np.exp(-(2 * self.V.tau / self._mu_0)) * 2 *
-                 self.V.tau * np.sin(self.t_0) / self._mu_0**2
-                 ) * p_val + (1. - np.exp(-(2*self.V.tau / self._mu_0))
-                              ) * p_slope)
+        I_slope = (
+            (1.0 - self.bsf)
+            * self.I0
+            * self.V.omega
+            / 2.0
+            * (
+                (
+                    np.exp(-(2 * self.V.tau / self._mu_0))
+                    * 2
+                    * self.V.tau
+                    * np.sin(self.t_0)
+                    / self._mu_0 ** 2
+                )
+                * p_val
+                + (1.0 - np.exp(-(2 * self.V.tau / self._mu_0))) * p_slope
+            )
+        )
 
         if sig0 is False and dB is False:
             return I_slope
         else:
             I_val = self.volume()
             if sig0 is True and dB is False:
-                return 4. * np.pi * (self._mu_0 * I_slope
-                                     - np.sin(self.t_0) * I_val)
+                return 4.0 * np.pi * (self._mu_0 * I_slope - np.sin(self.t_0) * I_val)
             elif sig0 is False and dB is True:
-                return 10./np.log(10) * I_slope / I_val
+                return 10.0 / np.log(10) * I_slope / I_val
             elif sig0 is True and dB is True:
-                return 10./np.log(10) * (I_slope / I_val
-                                         - np.tan(self.t_0))
+                return 10.0 / np.log(10) * (I_slope / I_val - np.tan(self.t_0))
 
     def volume_curv(self, dB=False, sig0=False):
         """
@@ -921,36 +1044,61 @@ def volume_curv(self, dB=False, sig0=False):
             volume-contribution
         """
         # evaluate the slope of the used brdf
-        p_curv = self.V.p_theta_diff(t_0=self.t_0, t_ex=self.t_ex,
-                                     p_0=self.p_0, p_ex=self.p_ex,
-                                     geometry='mono',
-                                     param_dict=self.param_dict,
-                                     return_symbolic=False,
-                                     n=2)
+        p_curv = self.V.p_theta_diff(
+            t_0=self.t_0,
+            t_ex=self.t_ex,
+            p_0=self.p_0,
+            p_ex=self.p_ex,
+            geometry="mono",
+            param_dict=self.param_dict,
+            return_symbolic=False,
+            n=2,
+        )
         # evaluate the slope of the used brdf
-        p_slope = self.V.p_theta_diff(t_0=self.t_0, t_ex=self.t_ex,
-                                      p_0=self.p_0, p_ex=self.p_ex,
-                                      geometry='mono',
-                                      param_dict=self.param_dict,
-                                      return_symbolic=False,
-                                      n=1)
+        p_slope = self.V.p_theta_diff(
+            t_0=self.t_0,
+            t_ex=self.t_ex,
+            p_0=self.p_0,
+            p_ex=self.p_ex,
+            geometry="mono",
+            param_dict=self.param_dict,
+            return_symbolic=False,
+            n=1,
+        )
         # evaluate the used brdf
-        p_val = self.V.p(self.t_0, self.t_ex,
-                         self.p_0, self.p_ex,
-                         param_dict=self.param_dict)
-
-        I_curv = (1. - self.bsf) * self.I0 * self.V.omega / 2. * (
-                np.exp(-(2 * self.V.tau / self._mu_0)) * (
-                        2 * self.V.tau / self._mu_0**3) * (
-                                np.sin(self.t_0)**2 + 1.
-                                - 2. * self.V.tau / self._mu_0
-                                * np.sin(self.t_0)**2) * p_val + (
-                                    np.exp(-(2 * self.V.tau / self._mu_0)) *
-                                    4. * self.V.tau / self._mu_0**2
-                                    * np.sin(self.t_0)) * p_slope + (
-                                        1 - np.exp(
-                                            -(2 * self.V.tau / self._mu_0))
-                                        ) * p_curv)
+        p_val = self.V.p(
+            self.t_0,
+            self.t_ex,
+            self.p_0,
+            self.p_ex,
+            param_dict=self.param_dict,
+        )
+
+        I_curv = (
+            (1.0 - self.bsf)
+            * self.I0
+            * self.V.omega
+            / 2.0
+            * (
+                np.exp(-(2 * self.V.tau / self._mu_0))
+                * (2 * self.V.tau / self._mu_0 ** 3)
+                * (
+                    np.sin(self.t_0) ** 2
+                    + 1.0
+                    - 2.0 * self.V.tau / self._mu_0 * np.sin(self.t_0) ** 2
+                )
+                * p_val
+                + (
+                    np.exp(-(2 * self.V.tau / self._mu_0))
+                    * 4.0
+                    * self.V.tau
+                    / self._mu_0 ** 2
+                    * np.sin(self.t_0)
+                )
+                * p_slope
+                + (1 - np.exp(-(2 * self.V.tau / self._mu_0))) * p_curv
+            )
+        )
 
         if sig0 is False and dB is False:
             return I_curv
@@ -958,17 +1106,23 @@ def volume_curv(self, dB=False, sig0=False):
             I_slope = self.volume_slope(dB=False, sig0=False)
             I_val = self.volume()
             if sig0 is True and dB is False:
-                return 4. * np.pi * (self._mu_0 * I_curv
-                                     - 2. * np.sin(self.t_0)
-                                     * I_slope
-                                     - self._mu_0 * I_val)
+                return (
+                    4.0
+                    * np.pi
+                    * (
+                        self._mu_0 * I_curv
+                        - 2.0 * np.sin(self.t_0) * I_slope
+                        - self._mu_0 * I_val
+                    )
+                )
             elif sig0 is False and dB is True:
-                return 10./np.log(10) * (I_curv / I_val
-                                         - I_slope**2 / I_val**2)
+                return 10.0 / np.log(10) * (I_curv / I_val - I_slope ** 2 / I_val ** 2)
             elif sig0 is True and dB is True:
-                return 10./np.log(10) * (I_curv / I_val
-                                         - I_slope**2 / I_val**2
-                                         - self._mu_0**(-2))
+                return (
+                    10.0
+                    / np.log(10)
+                    * (I_curv / I_val - I_slope ** 2 / I_val ** 2 - self._mu_0 ** (-2))
+                )
 
     def tot_slope(self, sig0=False, dB=False):
         """
@@ -992,21 +1146,20 @@ def tot_slope(self, sig0=False, dB=False):
             total-contribution
         """
 
-        I_slope = (self.volume_slope(dB=False, sig0=False) +
-                   self.surface_slope(dB=False, sig0=False))
+        I_slope = self.volume_slope(dB=False, sig0=False) + self.surface_slope(
+            dB=False, sig0=False
+        )
 
         if sig0 is False and dB is False:
             return I_slope
         else:
-            I_val = (self.volume() + self.surface())
+            I_val = self.volume() + self.surface()
             if sig0 is True and dB is False:
-                return 4. * np.pi * (self._mu_0 * I_slope
-                                     - np.sin(self.t_0) * I_val)
+                return 4.0 * np.pi * (self._mu_0 * I_slope - np.sin(self.t_0) * I_val)
             elif sig0 is False and dB is True:
-                return 10./np.log(10) * I_slope / I_val
+                return 10.0 / np.log(10) * I_slope / I_val
             elif sig0 is True and dB is True:
-                return 10./np.log(10) * (I_slope / I_val
-                                         - np.tan(self.t_0))
+                return 10.0 / np.log(10) * (I_slope / I_val - np.tan(self.t_0))
 
     def tot_curv(self, sig0=False, dB=False):
         """
@@ -1030,27 +1183,35 @@ def tot_curv(self, sig0=False, dB=False):
             total-contribution
         """
 
-        I_curv = (self.volume_curv(dB=False, sig0=False) +
-                  self.surface_curv(dB=False, sig0=False))
+        I_curv = self.volume_curv(dB=False, sig0=False) + self.surface_curv(
+            dB=False, sig0=False
+        )
 
         if sig0 is False and dB is False:
             return I_curv
         else:
-            I_slope = (self.volume_slope(dB=False, sig0=False) +
-                       self.surface_slope(dB=False, sig0=False))
-            I_val = (self.volume() + self.surface())
+            I_slope = self.volume_slope(dB=False, sig0=False) + self.surface_slope(
+                dB=False, sig0=False
+            )
+            I_val = self.volume() + self.surface()
             if sig0 is True and dB is False:
-                return 4. * np.pi * (self._mu_0 * I_curv
-                                     - 2. * np.sin(self.t_0)
-                                     * I_slope
-                                     - self._mu_0 * I_val)
+                return (
+                    4.0
+                    * np.pi
+                    * (
+                        self._mu_0 * I_curv
+                        - 2.0 * np.sin(self.t_0) * I_slope
+                        - self._mu_0 * I_val
+                    )
+                )
             elif sig0 is False and dB is True:
-                return 10./np.log(10) * (I_curv / I_val
-                                         - I_slope**2 / I_val**2)
+                return 10.0 / np.log(10) * (I_curv / I_val - I_slope ** 2 / I_val ** 2)
             elif sig0 is True and dB is True:
-                return 10./np.log(10) * (I_curv / I_val
-                                         - I_slope**2 / I_val**2
-                                         - self._mu_0**(-2))
+                return (
+                    10.0
+                    / np.log(10)
+                    * (I_curv / I_val - I_slope ** 2 / I_val ** 2 - self._mu_0 ** (-2))
+                )
 
     def interaction(self):
         """
@@ -1067,11 +1228,17 @@ def interaction(self):
         Fint1 = self._calc_Fint_1()
         Fint2 = self._calc_Fint_2()
 
-        Iint = (self.I0 * self._mu_0 * self.V.omega *
-                (np.exp(-self.V.tau / self._mu_ex) * Fint1 +
-                 np.exp(-self.V.tau / self._mu_0) * Fint2))
+        Iint = (
+            self.I0
+            * self._mu_0
+            * self.V.omega
+            * (
+                np.exp(-self.V.tau / self._mu_ex) * Fint1
+                + np.exp(-self.V.tau / self._mu_0) * Fint2
+            )
+        )
 
-        return self.SRF.NormBRDF * (1. - self.bsf) * Iint
+        return self.SRF.NormBRDF * (1.0 - self.bsf) * Iint
 
     def _calc_Fint_1(self):
         """
@@ -1087,16 +1254,26 @@ def _calc_Fint_1(self):
         mu1, mu2, phi1, phi2 = self._mu_0, self._mu_ex, self.p_0, self.p_ex
 
         # evaluate fn-coefficients
-        if self.lambda_backend == 'symengine':
-            args = np.broadcast_arrays(np.arccos(mu1), phi1, np.arccos(mu2),
-                                       phi2, *self.param_dict.values())
+        if self.lambda_backend == "symengine":
+            args = np.broadcast_arrays(
+                np.arccos(mu1),
+                phi1,
+                np.arccos(mu2),
+                phi2,
+                *self.param_dict.values(),
+            )
 
             # to correct for 0 dimensional arrays if a fn-coefficient
             # is identical to 0 (in a symbolic manner)
             fn = np.broadcast_arrays(*self._fnevals(args))
         else:
-            args = np.broadcast_arrays(np.arccos(mu1), phi1, np.arccos(mu2),
-                                       phi2, *self.param_dict.values())
+            args = np.broadcast_arrays(
+                np.arccos(mu1),
+                phi1,
+                np.arccos(mu2),
+                phi2,
+                *self.param_dict.values(),
+            )
             # to correct for 0 dimensional arrays if a fn-coefficient
             # is identical to 0 (in a symbolic manner)
             fn = np.broadcast_arrays(*self._fnevals(*args))
@@ -1119,16 +1296,26 @@ def _calc_Fint_2(self):
         mu1, mu2, phi1, phi2 = self._mu_ex, self._mu_0, self.p_ex, self.p_0
 
         # evaluate fn-coefficients
-        if self.lambda_backend == 'symengine':
-            args = np.broadcast_arrays(np.arccos(mu1), phi1, np.arccos(mu2),
-                                       phi2, *self.param_dict.values())
+        if self.lambda_backend == "symengine":
+            args = np.broadcast_arrays(
+                np.arccos(mu1),
+                phi1,
+                np.arccos(mu2),
+                phi2,
+                *self.param_dict.values(),
+            )
 
             # to correct for 0 dimensional arrays if a fn-coefficient
             # is identical to 0 (in a symbolic manner)
             fn = np.broadcast_arrays(*self._fnevals(args))
         else:
-            args = np.broadcast_arrays(np.arccos(mu1), phi1, np.arccos(mu2),
-                                       phi2, *self.param_dict.values())
+            args = np.broadcast_arrays(
+                np.arccos(mu1),
+                phi1,
+                np.arccos(mu2),
+                phi2,
+                *self.param_dict.values(),
+            )
             # to correct for 0 dimensional arrays if a fn-coefficient
             # is identical to 0 (in a symbolic manner)
             fn = np.broadcast_arrays(*self._fnevals(*args))
@@ -1154,23 +1341,25 @@ def _mu_ex_x(self):
     def _S2_mu(self, mu, tau):
         nmax = self.V.ncoefs + self.SRF.ncoefs - 1
 
-        hlp1 = (np.exp(-tau / mu) * np.log(mu / (1. - mu))
-                - expi(-tau) + np.exp(-tau / mu)
-                * expi(tau / mu - tau))
+        hlp1 = (
+            np.exp(-tau / mu) * np.log(mu / (1.0 - mu))
+            - expi(-tau)
+            + np.exp(-tau / mu) * expi(tau / mu - tau)
+        )
 
         # cache the results of the expn-evaluations to speed up the S2 loop
         @lru_cache()
         def innerfunc(k):
-            return (mu ** (-k) * (expn(k + 1., tau) -
-                                  np.exp(-tau / mu) / k))
+            return mu ** (-k) * (expn(k + 1.0, tau) - np.exp(-tau / mu) / k)
 
-        S2 = np.array([sum(innerfunc(k) for k in range(1, (n + 1) + 1))
-                       for n in range(nmax)])
+        S2 = np.array(
+            [sum(innerfunc(k) for k in range(1, (n + 1) + 1)) for n in range(nmax)]
+        )
 
         # clear the cache since tau might have changed!
         innerfunc.cache_clear()
 
-        return (S2 + hlp1)
+        return S2 + hlp1
 
     def _dvolume_dtau(self):
         """
@@ -1182,15 +1371,18 @@ def _dvolume_dtau(self):
                Numerical value of dIvol/dtau for the given set of parameters
         """
 
-        dvdt = (self.I0 * self.V.omega
-                * (self._mu_0 / (self._mu_0 + self._mu_ex))
-                * ((1. / self._mu_0 + 1. / self._mu_ex**(-1))
-                   * np.exp(- self.V.tau / self._mu_0 -
-                            self.V.tau / self._mu_ex))
-                * self.V.p(self.t_0, self.t_ex, self.p_0, self.p_ex,
-                           self.param_dict))
+        dvdt = (
+            self.I0
+            * self.V.omega
+            * (self._mu_0 / (self._mu_0 + self._mu_ex))
+            * (
+                (1.0 / self._mu_0 + 1.0 / self._mu_ex ** (-1))
+                * np.exp(-self.V.tau / self._mu_0 - self.V.tau / self._mu_ex)
+            )
+            * self.V.p(self.t_0, self.t_ex, self.p_0, self.p_ex, self.param_dict)
+        )
 
-        return (1. - self.bsf) * dvdt
+        return (1.0 - self.bsf) * dvdt
 
     def _dvolume_domega(self):
         """
@@ -1202,14 +1394,13 @@ def _dvolume_domega(self):
                Numerical value of dIvol/domega for the given set of parameters
         """
 
-        dvdo = ((self.I0 * self._mu_0 / (self._mu_0 + self._mu_ex)) *
-                (
-                1. - np.exp(-(self.V.tau / self._mu_0) -
-                            (self.V.tau / self._mu_ex))
-                ) * self.V.p(self.t_0, self.t_ex, self.p_0, self.p_ex,
-                             self.param_dict))
+        dvdo = (
+            (self.I0 * self._mu_0 / (self._mu_0 + self._mu_ex))
+            * (1.0 - np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex)))
+            * self.V.p(self.t_0, self.t_ex, self.p_0, self.p_ex, self.param_dict)
+        )
 
-        return (1. - self.bsf) * dvdo
+        return (1.0 - self.bsf) * dvdo
 
     def _dvolume_dbsf(self):
         """
@@ -1221,14 +1412,19 @@ def _dvolume_dbsf(self):
                Numerical value of dIvol/domega for the given set of parameters
         """
 
-        vol = ((self.I0 * self.V.omega *
-                self._mu_0 / (self._mu_0 + self._mu_ex)) * (
-                    1. - np.exp(-(self.V.tau / self._mu_0) -
-                                (self.V.tau / self._mu_ex))
-                    ) * self.V.p(self.t_0, self.t_ex, self.p_0, self.p_ex,
-                                 param_dict=self.param_dict))
+        vol = (
+            (self.I0 * self.V.omega * self._mu_0 / (self._mu_0 + self._mu_ex))
+            * (1.0 - np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex)))
+            * self.V.p(
+                self.t_0,
+                self.t_ex,
+                self.p_0,
+                self.p_ex,
+                param_dict=self.param_dict,
+            )
+        )
 
-        return - vol
+        return -vol
 
     def _dvolume_dR(self):
         """
@@ -1241,7 +1437,7 @@ def _dvolume_dR(self):
                Numerical value of dIvol/dR for the given set of parameters
         """
 
-        dvdr = 0.
+        dvdr = 0.0
 
         return dvdr
 
@@ -1255,16 +1451,16 @@ def _dsurface_dtau(self):
                Numerical value of dIsurf/dtau for the given set of parameters
         """
 
-        dsdt = (self.I0
-                * (- 1. / self._mu_0 - 1. / self._mu_ex)
-                * np.exp(- self.V.tau / self._mu_0
-                         - self.V.tau / self._mu_ex)
-                * self._mu_0
-                * self.SRF.brdf(self.t_0, self.t_ex, self.p_0, self.p_ex,
-                                self.param_dict))
+        dsdt = (
+            self.I0
+            * (-1.0 / self._mu_0 - 1.0 / self._mu_ex)
+            * np.exp(-self.V.tau / self._mu_0 - self.V.tau / self._mu_ex)
+            * self._mu_0
+            * self.SRF.brdf(self.t_0, self.t_ex, self.p_0, self.p_ex, self.param_dict)
+        )
 
         # Incorporate BRDF-normalization factor
-        dsdt = self.SRF.NormBRDF * (1. - self.bsf) * dsdt
+        dsdt = self.SRF.NormBRDF * (1.0 - self.bsf) * dsdt
 
         return dsdt
 
@@ -1278,7 +1474,7 @@ def _dsurface_domega(self):
                Numerical value of dIsurf/domega for the given set of parameters
         """
 
-        dsdo = 0.
+        dsdo = 0.0
 
         return dsdo
 
@@ -1293,15 +1489,21 @@ def _dsurface_dR(self):
                Numerical value of dIsurf/dR for the given set of parameters
         """
 
-        I_bs = (self.I0 * self._mu_0
-                * self.SRF.brdf(self.t_0, self.t_ex,
-                                self.p_0, self.p_ex,
-                                param_dict=self.param_dict))
+        I_bs = (
+            self.I0
+            * self._mu_0
+            * self.SRF.brdf(
+                self.t_0,
+                self.t_ex,
+                self.p_0,
+                self.p_ex,
+                param_dict=self.param_dict,
+            )
+        )
 
-        Isurf = (np.exp(-(self.V.tau / self._mu_0) -
-                        (self.V.tau / self._mu_ex))) * I_bs
+        Isurf = (np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex))) * I_bs
 
-        return ((1. - self.bsf) * Isurf + self.bsf * I_bs)
+        return (1.0 - self.bsf) * Isurf + self.bsf * I_bs
 
     def _dsurface_dbsf(self):
         """
@@ -1315,14 +1517,23 @@ def _dsurface_dbsf(self):
             given set of parameters
         """
         # bare soil contribution
-        I_bs = (self.I0 * self._mu_0
-                * self.SRF.brdf(self.t_0, self.t_ex,
-                                self.p_0, self.p_ex,
-                                param_dict=self.param_dict))
-
-        Isurf = (np.exp(-(self.V.tau / self._mu_0) -
-                        (self.V.tau / self._mu_ex))
-                 ) * I_bs * np.ones_like(self.t_0)
+        I_bs = (
+            self.I0
+            * self._mu_0
+            * self.SRF.brdf(
+                self.t_0,
+                self.t_ex,
+                self.p_0,
+                self.p_ex,
+                param_dict=self.param_dict,
+            )
+        )
+
+        Isurf = (
+            (np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex)))
+            * I_bs
+            * np.ones_like(self.t_0)
+        )
 
         return self.SRF.NormBRDF * (I_bs - Isurf)
 
@@ -1344,31 +1555,35 @@ def _d_surface_dummy_lambda(self, key):
             a function that calculates the derivative with respect to key.
 
         """
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
-        args = (theta_0, theta_ex, phi_0, phi_ex) + tuple(
-                self.param_dict.keys())
+        args = (theta_0, theta_ex, phi_0, phi_ex) + tuple(self.param_dict.keys())
 
-        return sp.lambdify(args, sp.diff(self.SRF._func, sp.Symbol(key)),
-                           modules=["numpy", "sympy"])
+        return sp.lambdify(
+            args,
+            sp.diff(self.SRF._func, sp.Symbol(key)),
+            modules=["numpy", "sympy"],
+        )
 
     @lru_cache(20)
     def _d_volume_dummy_lambda(self, key):
         """same as _d_surface_dummy_lambda but for volume"""
 
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
-        args = (theta_0, theta_ex, phi_0, phi_ex) + tuple(
-                self.param_dict.keys())
+        args = (theta_0, theta_ex, phi_0, phi_ex) + tuple(self.param_dict.keys())
 
-        return sp.lambdify(args, sp.diff(self.V._func, sp.Symbol(key)),
-                           modules=["numpy", "sympy"])
+        return sp.lambdify(
+            args,
+            sp.diff(self.V._func, sp.Symbol(key)),
+            modules=["numpy", "sympy"],
+        )
 
     def _d_surface_ddummy(self, key):
         """
@@ -1386,28 +1601,34 @@ def _d_surface_ddummy(self, key):
             Numerical value of dIsurf/dkey for the given set of parameters
         """
 
-        dI_bs = (self.I0 * self._mu_0 * self._d_surface_dummy_lambda(key)(
-            self.t_0, self.t_ex, self.p_0, self.p_ex, **self.param_dict))
+        dI_bs = (
+            self.I0
+            * self._mu_0
+            * self._d_surface_dummy_lambda(key)(
+                self.t_0, self.t_ex, self.p_0, self.p_ex, **self.param_dict
+            )
+        )
 
-        dI_s = (np.exp(-(self.V.tau / self._mu_0) -
-                       (self.V.tau / self._mu_ex))) * dI_bs
+        dI_s = (np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex))) * dI_bs
 
-        return self.SRF.NormBRDF * ((1. - self.bsf) * dI_s + self.bsf * dI_bs)
+        return self.SRF.NormBRDF * ((1.0 - self.bsf) * dI_s + self.bsf * dI_bs)
 
     def _d_volume_ddummy(self, key):
         """same as d_surface_ddummy but for volume"""
 
-        dIvol = (self.I0 * self.V.omega
-                 * self._mu_0 / (self._mu_0 + self._mu_ex)
-                 * (1. - np.exp(-(self.V.tau / self._mu_0) -
-                                (self.V.tau / self._mu_ex)))
-                 * self._d_volume_dummy_lambda(key)(self.t_0, self.t_ex,
-                                                    self.p_0, self.p_ex,
-                                                    **self.param_dict))
-        return (1. - self.bsf) * dIvol
-
-    def jacobian(self, dB=False, sig0=False,
-                 param_list=['omega', 'tau', 'NormBRDF']):
+        dIvol = (
+            self.I0
+            * self.V.omega
+            * self._mu_0
+            / (self._mu_0 + self._mu_ex)
+            * (1.0 - np.exp(-(self.V.tau / self._mu_0) - (self.V.tau / self._mu_ex)))
+            * self._d_volume_dummy_lambda(key)(
+                self.t_0, self.t_ex, self.p_0, self.p_ex, **self.param_dict
+            )
+        )
+        return (1.0 - self.bsf) * dIvol
+
+    def jacobian(self, dB=False, sig0=False, param_list=["omega", "tau", "NormBRDF"]):
         """
         Returns the jacobian of the total backscatter with respect
         to the parameters provided in param_list.
@@ -1453,33 +1674,32 @@ def jacobian(self, dB=False, sig0=False,
         """
 
         if sig0 is True and dB is False:
-            norm = 4. * np.pi * np.cos(self.t_0)
+            norm = 4.0 * np.pi * np.cos(self.t_0)
         elif dB is True:
-            norm = 10. / (np.log(10.) * (self.surface()
-                                         + self.volume()))
+            norm = 10.0 / (np.log(10.0) * (self.surface() + self.volume()))
         else:
-            norm = 1.
+            norm = 1.0
 
         jac = []
         for key in param_list:
 
-            if key == 'omega':
-                jac += [(self._dsurface_domega() +
-                         self._dvolume_domega()) * norm]
-            elif key == 'tau':
-                jac += [(self._dsurface_dtau() +
-                         self._dvolume_dtau()) * norm]
-            elif key == 'NormBRDF':
-                jac += [(self._dsurface_dR() +
-                         self._dvolume_dR()) * norm]
-            elif key == 'bsf':
-                jac += [(self._dsurface_dbsf() +
-                         self._dvolume_dbsf()) * norm]
+            if key == "omega":
+                jac += [(self._dsurface_domega() + self._dvolume_domega()) * norm]
+            elif key == "tau":
+                jac += [(self._dsurface_dtau() + self._dvolume_dtau()) * norm]
+            elif key == "NormBRDF":
+                jac += [(self._dsurface_dR() + self._dvolume_dR()) * norm]
+            elif key == "bsf":
+                jac += [(self._dsurface_dbsf() + self._dvolume_dbsf()) * norm]
             elif key in self.param_dict:
-                jac += [(self._d_surface_ddummy(key) +
-                         self._d_volume_ddummy(key)) * norm]
+                jac += [
+                    (self._d_surface_ddummy(key) + self._d_volume_ddummy(key)) * norm
+                ]
             else:
-                assert False, 'error in jacobian calculation... ' + str(key) \
-                              + ' is not in param_dict'
+                assert False, (
+                    "error in jacobian calculation... "
+                    + str(key)
+                    + " is not in param_dict"
+                )
 
         return jac
diff --git a/rt1/rtfits.py b/rt1/rtfits.py
index 3fee7d0b..c6529fb6 100644
--- a/rt1/rtfits.py
+++ b/rt1/rtfits.py
@@ -1,5 +1,5 @@
 """Class to perform least_squares fitting of RT-1 models to given datasets."""
-import warnings
+import sys
 
 import numpy as np
 import sympy as sp
@@ -13,14 +13,21 @@
 
 from .scatter import Scatter
 from .rt1 import RT1, _init_lambda_backend
-from .general_functions import meandatetime, rectangularize, \
-    split_into, update_progress, groupby_unsorted
+from .general_functions import (
+    meandatetime,
+    rectangularize,
+    split_into,
+    update_progress,
+    groupby_unsorted,
+)
 
 from .rtplots import plot as rt1_plots
 
 from . import surface as rt1_s
 from . import volume as rt1_v
 from . import __version__ as _RT1_version
+from . import log
+from .rtmetrics import _metric_keys
 
 import copy
 from itertools import repeat, count, chain, groupby
@@ -31,7 +38,7 @@
 try:
     import cloudpickle
 except ModuleNotFoundError:
-    warnings.warn('cloudpickle could not be imported, .dump() will not work!')
+    log.warning("cloudpickle could not be imported, .dump() will not work!")
 
 
 def load(path):
@@ -48,7 +55,7 @@ def load(path):
     fit : rtfits.Fits object
     """
 
-    with open(path, 'rb') as file:
+    with open(path, "rb") as file:
         fit = cloudpickle.load(file)
 
     return fit
@@ -239,10 +246,20 @@ class Fits(Scatter):
 
     """
 
-    def __init__(self, sig0=False, dB=False, dataset=None,
-                 defdict=None, set_V_SRF=None, lsq_kwargs=None, int_Q=True,
-                 lambda_backend=None, _fnevals_input=None,
-                 verbose=2, **kwargs):
+    def __init__(
+        self,
+        sig0=False,
+        dB=False,
+        dataset=None,
+        defdict=None,
+        set_V_SRF=None,
+        lsq_kwargs=None,
+        int_Q=True,
+        lambda_backend=None,
+        _fnevals_input=None,
+        verbose=2,
+        **kwargs,
+    ):
 
         self.sig0 = sig0
         self.dB = dB
@@ -270,35 +287,34 @@ def __init__(self, sig0=False, dB=False, dataset=None,
     def __update__(self):
         """needed for downward compatibility"""
 
-        if not hasattr(self, 'plot'):
+        if not hasattr(self, "plot"):
             self.plot = rt1_plots(self)
 
-        if not hasattr(self, 'verbose'):
+        if not hasattr(self, "verbose"):
             self.verbose = 2
 
-        if hasattr(self, 'fitset'):
+        if hasattr(self, "fitset"):
             self.lsq_kwargs = self.fitset
             del self.fitset
-        if not hasattr(self, 'lsq_kwargs'):
+        if not hasattr(self, "lsq_kwargs"):
             self.lsq_kwargs = dict()
-        if not hasattr(self, 'int_Q'):
-            self.int_Q = self.lsq_kwargs.pop('int_Q', True)
-        if not hasattr(self, 'lambda_backend'):
-            self.lambda_backend = self.lsq_kwargs.pop('lambda_backend',
-                                                      _init_lambda_backend)
-        if not hasattr(self, '_fnevals_input'):
-            self._fnevals_input = self.lsq_kwargs.pop('_fnevals_input',
-                                                      None)
-
-        if (hasattr(self, 'res_dict')
-            and np.all([len(val) == 2
-                       for _, val in self.res_dict.items()])
-            and np.all([isinstance(val[0], list)
-                       for _, val in self.res_dict.items()])):
-
-            warnings.warn('updating res-dict to new shape...')
-            self.res_dict = {key: val[0]
-                             for key, val in self.res_dict.items()}
+        if not hasattr(self, "int_Q"):
+            self.int_Q = self.lsq_kwargs.pop("int_Q", True)
+        if not hasattr(self, "lambda_backend"):
+            self.lambda_backend = self.lsq_kwargs.pop(
+                "lambda_backend", _init_lambda_backend
+            )
+        if not hasattr(self, "_fnevals_input"):
+            self._fnevals_input = self.lsq_kwargs.pop("_fnevals_input", None)
+
+        if (
+            hasattr(self, "res_dict")
+            and np.all([len(val) == 2 for _, val in self.res_dict.items()])
+            and np.all([isinstance(val[0], list) for _, val in self.res_dict.items()])
+        ):
+
+            log.debug("updating res-dict to new shape...")
+            self.res_dict = {key: val[0] for key, val in self.res_dict.items()}
 
     def __setstate__(self, d):
         # this is done to support downward-compatibility with pickled results
@@ -306,12 +322,12 @@ def __setstate__(self, d):
         self.__update__()
 
     def __getstate__(self):
-        if '_rt1_dump_mini' in self.__dict__:
+        if "_rt1_dump_mini" in self.__dict__:
             # remove the dummy-attribute that indicates that we do a mini-dump
-            delattr(self, '_rt1_dump_mini')
+            delattr(self, "_rt1_dump_mini")
 
             # remove unnecessary data to save storage
-            removekeys = ['fit_output', '_fnevals_input']
+            removekeys = ["fit_output", "_fnevals_input"]
             returndict = {key: val for key, val in self.__dict__.items()}
             for key in removekeys:
                 returndict[key] = None
@@ -324,9 +340,9 @@ def __setattr__(self, attr, value):
         # TODO write proper setters that do the job
         # clear the cache in case it is not empty and a
         # defining variable is set
-        if attr in ['sig0', 'dB', 'dataset', 'defdict', 'set_V_SRF']:
+        if attr in ["sig0", "dB", "dataset", "defdict", "set_V_SRF"]:
             if not all(i == 0 for i in self._cached_arg_number):
-                warnings.warn(f'{attr} has been set, clearing cache')
+                log.debug(f"{attr} has been set, clearing cache")
                 self._clear_cache()
 
         super().__setattr__(attr, value)
@@ -334,16 +350,34 @@ def __setattr__(self, attr, value):
     @property
     def _cached_props(self):
         """a list of the names of the properties that are cached"""
-        names = ['param_dyn_dict', 'param_dyn_df', '_groupindex', '_N_groups',
-                 '_dataset_used', 'index', '_orig_index',
-                 '_jac_assign_rule', 'meandatetimes', 'inc', 'data', 'mask',
-                 'data_weights', '_idx_assigns', '_param_assigns',
-                 '_param_assigns_dataset', '_val_assigns', '_order',
-                 'interp_vals', '_meandt_interp_assigns',
-                 '_param_dyn_monotonic', '_get_excludesymbs']
-
-        for i in ['tau', 'omega', 'N']:
-            names += [f'_{i}_symb', f'_{i}_func', f'_{i}_diff_func']
+        names = [
+            "param_dyn_dict",
+            "param_dyn_df",
+            "_groupindex",
+            "_N_groups",
+            "_dataset_used",
+            "index",
+            "_orig_index",
+            "_jac_assign_rule",
+            "meandatetimes",
+            "inc",
+            "data",
+            "mask",
+            "data_weights",
+            "_idx_assigns",
+            "_param_assigns",
+            "_param_assigns_dataset",
+            "_val_assigns",
+            "_order",
+            "interp_vals",
+            "_meandt_interp_assigns",
+            "_param_dyn_monotonic",
+            "_get_excludesymbs",
+            "metric",
+        ]
+
+        for i in ["tau", "omega", "N"]:
+            names += [f"_{i}_symb", f"_{i}_func", f"_{i}_diff_func"]
 
         return names
 
@@ -358,7 +392,7 @@ def _clear_cache(self):
                 else:
                     getattr(Fits, name).cache_clear()
 
-            warnings.warn('...cache cleared')
+            log.debug("...cache cleared")
 
     def _cache_info(self):
         """print the state of the lru_cache for all cached properties"""
@@ -371,10 +405,10 @@ def _cache_info(self):
                 else:
                     cinfo = getattr(Fits, name).cache_info()
 
-                text += [f'{name:<18}:   ' + f'{cinfo}']
+                text += [f"{name:<18}:   " + f"{cinfo}"]
             except Exception:
-                text += [f'{name:<18}:   ' + '???']
-        print(*text, sep='\n')
+                text += [f"{name:<18}:   " + "???"]
+        log.info("\n".join(text))
 
     @property
     def _cached_arg_number(self):
@@ -382,10 +416,13 @@ def _cached_arg_number(self):
         nums = []
         for name in self._cached_props:
             try:
-                if '.' in name:
-                    nsplit = name.split('.')
-                    nums.append(getattr(getattr(self, nsplit[0]),
-                                        nsplit[1]).cache_info().currsize)
+                if "." in name:
+                    nsplit = name.split(".")
+                    nums.append(
+                        getattr(getattr(self, nsplit[0]), nsplit[1])
+                        .cache_info()
+                        .currsize
+                    )
                 else:
                     nums.append(getattr(Fits, name).fget.cache_info().currsize)
             except Exception:
@@ -404,8 +441,11 @@ def interp_vals(self):
             a list of the interpolated values.
 
         """
-        interpkeys = [key for key, val in self.defdict.items()
-                      if val[0] is True and len(val) >= 5 and val[4] is True]
+        interpkeys = [
+            key
+            for key, val in self.defdict.items()
+            if val[0] is True and len(val) >= 5 and val[4] is True
+        ]
 
         # TODO make a proper test
         # don't use meandatetimes -> it disrupts parallel processing
@@ -424,13 +464,15 @@ def param_dyn_dict(self):
         else:
 
             # the names of the parameters that will be fitted
-            dyn_keys = [key for key, val in self.defdict.items()
-                        if val[0] is True]
+            dyn_keys = [key for key, val in self.defdict.items() if val[0] is True]
 
             # set frequencies of fitted parameters
             # (group by similar frequencies)
-            grps = groupby_unsorted(self._timescaledict.items(),
-                                    key=itemgetter(1), get=itemgetter(0))
+            grps = groupby_unsorted(
+                self._timescaledict.items(),
+                key=itemgetter(1),
+                get=itemgetter(0),
+            )
             freq = list(grps.keys())
             freqkeys = list(grps.values())
             param_dyn_dict = {}
@@ -442,14 +484,15 @@ def param_dyn_dict(self):
                     if isinstance(f, str):
                         try:
                             grp_idx = self.dataset.index.to_frame().groupby(
-                                                            pd.Grouper(freq=f),
-                                                            sort=False)
+                                pd.Grouper(freq=f), sort=False
+                            )
                         except ValueError:
                             raise ValueError(
-                                f'The provided frequency ({f}) of ' +
-                                f'{freqkeys[i]} is not a valid ' +
-                                'pandas datetime-offset string. ' +
-                                'Check the assignments in defdict!')
+                                f"The provided frequency ({f}) of "
+                                + f"{freqkeys[i]} is not a valid "
+                                + "pandas datetime-offset string. "
+                                + "Check the assignments in defdict!"
+                            )
                         # get unique group indices for each datetime-group
                         for key in freqkeys[i]:
                             grp_data = []
@@ -469,39 +512,52 @@ def param_dyn_dict(self):
                             res[r % len(res)] += 1
 
                         if rest >= ngrps:
-                            warnings.warn(f'grouping {f} of {freqkeys}' +
-                                          ' is actually between ' +
-                                          f'{min([f+i for i in res])} and ' +
-                                          f'{max([f+i for i in res])}')
+                            log.info(
+                                f"grouping {f} of {freqkeys}"
+                                + " is actually between "
+                                + f"{min([f+i for i in res])} and "
+                                + f"{max([f+i for i in res])}"
+                            )
 
                         # (repetitions + rest + number of elements in group)
-                        dyn = chain(*[repeat(ni, f + r) for ni, r in
-                                      zip(range(ngrps), res)])
+                        dyn = chain(
+                            *[repeat(ni, f + r) for ni, r in zip(range(ngrps), res)]
+                        )
                         # get the number of observations for each unique
                         # index in the dataset
                         dat = groupby_unsorted(
-                            zip(dyn,
-                                groupby_unsorted(self.dataset.index).values()),
+                            zip(
+                                dyn,
+                                groupby_unsorted(self.dataset.index).values(),
+                            ),
                             get=lambda x: len(x[1]),
-                            key=itemgetter(0))
+                            key=itemgetter(0),
+                        )
 
                         for key in freqkeys[i]:
-                            param_dyn_dict[key] = list(chain(*[repeat(key,
-                                                                      sum(val))
-                                                               for key, val in
-                                                               dat.items()]))
+                            param_dyn_dict[key] = list(
+                                chain(
+                                    *[repeat(key, sum(val)) for key, val in dat.items()]
+                                )
+                            )
 
             manual_dyn_df = self._manual_dyn_df
             if manual_dyn_df is not None:
                 for key, val in manual_dyn_df.astype(str).items():
                     dd1 = np.char.zfill(
-                        np.array(param_dyn_dict[key], dtype='str'),
-                        len(max(np.array(param_dyn_dict[key], dtype='str'),
-                                key=len)))
+                        np.array(param_dyn_dict[key], dtype="str"),
+                        len(
+                            max(
+                                np.array(param_dyn_dict[key], dtype="str"),
+                                key=len,
+                            )
+                        ),
+                    )
                     dd2 = val.str.zfill(len(max(val, key=len)))
                     # generate a combined (unique) integer
-                    param_dyn_dict[key] = np.array(np.char.add(dd1, dd2),
-                                                   dtype=int)
+                    param_dyn_dict[key] = np.array(
+                        np.char.add(dd1, dd2), dtype=float
+                    ).astype(int)
             return param_dyn_dict
 
     @property
@@ -512,22 +568,24 @@ def param_dyn_df(self):
         (with respect to the unique index values of the provided dataset)
         """
         if self.dataset is not None:
-            groupids = [i[0] for i in groupby_unsorted(
-                        zip(self._groupindex, *self.param_dyn_dict.values()),
-                        key=itemgetter(0),
-                        ).values()]
-            param_dyn_df = pd.DataFrame(groupids,
-                                        columns=['group_id',
-                                                 *self.param_dyn_dict.keys()])
-            param_dyn_df.set_index('group_id', inplace=True)
+            groupids = [
+                i[0]
+                for i in groupby_unsorted(
+                    zip(self._groupindex, *self.param_dyn_dict.values()),
+                    key=itemgetter(0),
+                ).values()
+            ]
+            param_dyn_df = pd.DataFrame(
+                groupids, columns=["group_id", *self.param_dyn_dict.keys()]
+            )
+            param_dyn_df.set_index("group_id", inplace=True)
             return param_dyn_df
 
     @property
     @lru_cache()
     def _param_dyn_monotonic(self):
         """a dict indicating if the param_dyn assignments are monotonic"""
-        return {key: val.is_monotonic
-                for key, val in self.param_dyn_df.items()}
+        return {key: val.is_monotonic for key, val in self.param_dyn_df.items()}
 
     @property
     @lru_cache()
@@ -538,18 +596,17 @@ def _groupindex(self):
         """
 
         # find the max. length of the parameters
-        maxdict = {key: len(str(max(val))) for key, val in
-                   self.param_dyn_dict.items()}
+        maxdict = {key: len(str(max(val))) for key, val in self.param_dyn_dict.items()}
         # find the max. length of the parameters
 
         def doit(x, N):
             return str(x).zfill(N)
+
         for i, [key, val] in enumerate(self.param_dyn_dict.items()):
             if i == 0:
                 conclist = list(map(partial(doit, N=maxdict[key]), val))
             else:
-                conclist = map(add, conclist,
-                               map(partial(doit, N=maxdict[key]), val))
+                conclist = map(add, conclist, map(partial(doit, N=maxdict[key]), val))
         return np.array(list(map(int, conclist)))
 
     @property
@@ -566,17 +623,19 @@ def _dataset_used(self):
             return
         # don't use keys that are not provided in defdict
         # (e.g. "param_dyn" keys and additional datasets irrelevant to the fit)
-        usekeys = ['sig', 'inc'] + [key for key in
-                                    self.defdict if key in self.dataset]
-        if 'data_weights' in self.dataset:
-            usekeys += ['data_weights']
+        usekeys = ["sig", "inc"] + [key for key in self.defdict if key in self.dataset]
+        if "data_weights" in self.dataset:
+            usekeys += ["data_weights"]
 
         # generate new data-frame based on groups
         df = dict()
         for key, val in self.dataset[usekeys].reset_index().items():
-            df[key] = groupby_unsorted(zip(self._groupindex, val),
-                                       key=itemgetter(0), get=itemgetter(1))
-        df = pd.DataFrame(df).rename(columns={'index': 'orig_index'})
+            df[key] = groupby_unsorted(
+                zip(self._groupindex, val),
+                key=itemgetter(0),
+                get=itemgetter(1),
+            )
+        df = pd.DataFrame(df).rename(columns={"index": "orig_index"})
         return df
 
     @property
@@ -587,8 +646,7 @@ def index(self):
         (used to assign the fit-results)
         """
         # get all unique values of each group in _orig_index
-        return np.concatenate([list(dict.fromkeys(i))
-                               for i in self._orig_index])
+        return np.concatenate([list(dict.fromkeys(i)) for i in self._orig_index])
 
     @property
     @lru_cache()
@@ -605,23 +663,40 @@ def _jac_assign_rule(self):
         # reindex param_dyn_df to _groupindex and get a list of all groupitems
         index_grps = dict()
         for key, val in self.param_dyn_dict.items():
-            index_grps[key] = groupby_unsorted(zip(self._groupindex, val),
-                                               key=itemgetter(0),
-                                               get=itemgetter(1))
+            index_grps[key] = groupby_unsorted(
+                zip(self._groupindex, val),
+                key=itemgetter(0),
+                get=itemgetter(1),
+            )
 
         uniquewheredict0 = {
             key: groupby_unsorted(
                 enumerate(chain(*rectangularize(val.values()))),
-                key=itemgetter(1), get=itemgetter(0)) for
-            key, val in index_grps.items()}
+                key=itemgetter(1),
+                get=itemgetter(0),
+            )
+            for key, val in index_grps.items()
+        }
 
         # now get the row- and col-indexes (note that the dyn-keys do not need
         # to start at 0 and must not be sorted!)
-        jac_rules = {key: np.row_stack(
-            (np.fromiter(chain(*(repeat(i, len(val_i)) for i, val_i in
-                                 enumerate(val.values()))), dtype=int),
-             np.fromiter(chain(*val.values()), dtype=int)))
-            for key, val in uniquewheredict0.items()}
+        jac_rules = {
+            key: np.row_stack(
+                (
+                    np.fromiter(
+                        chain(
+                            *(
+                                repeat(i, len(val_i))
+                                for i, val_i in enumerate(val.values())
+                            )
+                        ),
+                        dtype=int,
+                    ),
+                    np.fromiter(chain(*val.values()), dtype=int),
+                )
+            )
+            for key, val in uniquewheredict0.items()
+        }
 
         # shape = self.inc.shape
         # jac_rules = dict()
@@ -652,11 +727,14 @@ def meandatetimes(self):
         idx = self.dataset.index.to_numpy()
         dates = dict()
         for key, val in self.param_dyn_dict.items():
-            dates[key] = list({g: meandatetime(val_g) for g, val_g in
-                               groupby_unsorted(zip(idx, val),
-                                                key=itemgetter(1),
-                                                get=itemgetter(0)).items()
-                               }.values())
+            dates[key] = list(
+                {
+                    g: meandatetime(val_g)
+                    for g, val_g in groupby_unsorted(
+                        zip(idx, val), key=itemgetter(1), get=itemgetter(0)
+                    ).items()
+                }.values()
+            )
         return dates
 
     @lru_cache()
@@ -683,26 +761,40 @@ def _meandt_interp_assigns(self, key):
         """
 
         # get a dict to assign the dyn-numbers to the value-ids
-        assigndict = dict(zip(pd.unique(self.param_dyn_dict[key]),
-                              range(len(self.param_dyn_dict[key]))))
+        assigndict = dict(
+            zip(
+                pd.unique(self.param_dyn_dict[key]),
+                range(len(self.param_dyn_dict[key])),
+            )
+        )
 
         # assign the values with respect to the consecutive appearance of the
         # groups (and their corresponding mean datetime value)
         # --> groupby assigns groups with respect to the order of appearance!
-        return [list(i) for i in zip(*([meandatetime([j[1] for j in i[1]]),
-                                        assigndict[i[0]]]
-                for i in groupby(enumerate(self.dataset.index.to_numpy()),
-                                 key=lambda x: self.param_dyn_dict[key][x[0]]
-                                 )))]
+        return [
+            list(i)
+            for i in zip(
+                *(
+                    [meandatetime([j[1] for j in i[1]]), assigndict[i[0]]]
+                    for i in groupby(
+                        enumerate(self.dataset.index.to_numpy()),
+                        key=lambda x: self.param_dyn_dict[key][x[0]],
+                    )
+                )
+            )
+        ]
 
     @property
     def meandatetimes_group(self):
         """the average datetime-index of each fit-group"""
-        return [meandatetime(val)for key, val in
-                groupby_unsorted(zip(self.dataset.index.to_numpy(),
-                                     self._groupindex),
-                                 key=itemgetter(1),
-                                 get=itemgetter(0)).items()]
+        return [
+            meandatetime(val)
+            for key, val in groupby_unsorted(
+                zip(self.dataset.index.to_numpy(), self._groupindex),
+                key=itemgetter(1),
+                get=itemgetter(0),
+            ).items()
+        ]
 
     @property
     @lru_cache()
@@ -713,7 +805,7 @@ def inc(self):
         values of each row to fit in length.
         """
 
-        return self.__get_data(prop='inc')
+        return self.__get_data(prop="inc")
 
     @property
     @lru_cache()
@@ -723,7 +815,7 @@ def mask(self):
         (see 'inc', and 'data' properties for details)
         """
 
-        return self.__get_data(prop='mask')
+        return self.__get_data(prop="mask")
 
     @property
     @lru_cache()
@@ -734,7 +826,7 @@ def data(self):
         of each row to fit in length
         """
 
-        return self.__get_data(prop='sig')
+        return self.__get_data(prop="sig")
 
     @property
     @lru_cache()
@@ -745,10 +837,10 @@ def data_weights(self):
         values provided as 'data_weights'.
         """
 
-        if 'data_weights' in self.dataset:
-            return self.__get_data(prop='data_weights')
+        if "data_weights" in self.dataset:
+            return self.__get_data(prop="data_weights")
         else:
-            return 1.
+            return 1.0
 
     @property
     @lru_cache()
@@ -766,11 +858,14 @@ def _order(self):
     def _orig_index(self):
         """a list of the (grouped) index-values"""
 
-        orig_index = [np.array(i, dtype=self.dataset.index.dtype) for i in
-                      groupby_unsorted(zip(self._groupindex,
-                                           self.dataset.index.to_numpy()),
-                                       key=itemgetter(0),
-                                       get=itemgetter(1)).values()]
+        orig_index = [
+            np.array(i, dtype=self.dataset.index.dtype)
+            for i in groupby_unsorted(
+                zip(self._groupindex, self.dataset.index.to_numpy()),
+                key=itemgetter(0),
+                get=itemgetter(1),
+            ).values()
+        ]
         return orig_index
 
     @property
@@ -782,8 +877,10 @@ def _idx_assigns(self):
         (e.g. grouped with respect to the temporal dynamics of the parameters)
         """
         return rectangularize(
-            groupby_unsorted(range(len(self._groupindex)),
-                             key=lambda x: self._groupindex[x]).values())
+            groupby_unsorted(
+                range(len(self._groupindex)), key=lambda x: self._groupindex[x]
+            ).values()
+        )
 
     @property
     @lru_cache()
@@ -791,8 +888,9 @@ def _param_assigns(self):
         """indices of each parameter-group (to re-assign to the fit-index)"""
         assigndict = dict()
         for key, val in self.param_dyn_df.items():
-            assigndict[key] = groupby_unsorted(range(len(val)),
-                                               key=lambda x: val.iloc[x])
+            assigndict[key] = groupby_unsorted(
+                range(len(val)), key=lambda x: val.iloc[x]
+            )
 
         return assigndict
 
@@ -802,8 +900,7 @@ def _param_assigns_dataset(self):
         """indices of each parameter-group (to re-assign to the data-index)"""
         assigndict = dict()
         for key, val in self.param_dyn_dict.items():
-            assigndict[key] = groupby_unsorted(range(len(val)),
-                                               key=lambda x: val[x])
+            assigndict[key] = groupby_unsorted(range(len(val)), key=lambda x: val[x])
 
         return assigndict
 
@@ -811,9 +908,9 @@ def _param_assigns_dataset(self):
     @lru_cache()
     def _val_assigns(self):
         """the indices to assign the groups to the dataset-index"""
-        return groupby_unsorted(enumerate(self._groupindex),
-                                key=itemgetter(1),
-                                get=itemgetter(0))
+        return groupby_unsorted(
+            enumerate(self._groupindex), key=itemgetter(1), get=itemgetter(0)
+        )
 
     @property
     def _setdict(self):
@@ -834,8 +931,7 @@ def _setdict(self):
             elif val[0] is False:
                 # treat parameters that are intended to be constants
                 # if value is provided as a scalar, insert it in the definition
-                if isinstance(val[1],
-                              (int, float)) and not isinstance(val[1], bool):
+                if isinstance(val[1], (int, float)) and not isinstance(val[1], bool):
                     setdict[key] = val[1]
                 else:
                     # if value is provided as array, add it to fixed_dict
@@ -851,8 +947,8 @@ def _fixed_dict(self):
             if val[0] is False:
                 # treat parameters that are intended to be constants
                 # if value is provided as a scalar, insert it in the definition
-                if isinstance(val[1], str) and val[1] == 'auxiliary':
-                    _fixed_dict[key] = 'auxiliary'
+                if isinstance(val[1], str) and val[1] == "auxiliary":
+                    _fixed_dict[key] = "auxiliary"
                 else:
                     _fixed_dict[key] = val[1]
 
@@ -868,9 +964,10 @@ def fixed_dict(self):
         fixed_dict = dict()
         if isinstance(self.dataset, pd.DataFrame):
             for key, val in self._fixed_dict.items():
-                if isinstance(val, str) and val == 'auxiliary':
-                    assert key in self._dataset_used, \
-                        f"auxiliary data for '{key}' is missing!"
+                if isinstance(val, str) and val == "auxiliary":
+                    assert (
+                        key in self._dataset_used
+                    ), f"auxiliary data for '{key}' is missing!"
                     fixed_dict[key] = rectangularize(self._dataset_used[key])
 
         return fixed_dict
@@ -883,17 +980,21 @@ def _startvaldict(self):
             if val[0] is True:
                 # in case start-values have been provided via dataset,
                 # take the average-value for each fit-group
-                if val[1] == 'auxiliary':
-                    assert key + '_start' in self.dataset, (
-                        f'you must provide a column {key + "_start"} in ' +
-                        'the dataset if you want to use "auxiliary" start-vals'
-                        )
-                    meanstartvals = list(groupby_unsorted(
-                        zip(self._groupindex, self.dataset[key + '_start']),
-                        key=itemgetter(0), get=itemgetter(1)).values())
+                if val[1] == "auxiliary":
+                    assert key + "_start" in self.dataset, (
+                        f'you must provide a column {key + "_start"} in '
+                        + 'the dataset if you want to use "auxiliary" start-vals'
+                    )
+                    meanstartvals = list(
+                        groupby_unsorted(
+                            zip(self._groupindex, self.dataset[key + "_start"]),
+                            key=itemgetter(0),
+                            get=itemgetter(1),
+                        ).values()
+                    )
 
                     # evaluate the mean start-value for each group
-                    if val[2] == 'index':
+                    if val[2] == "index":
                         # avoid grouping with _param_assigns since anyway
                         # only a single value is given for each group
                         meanstartvals = np.mean(meanstartvals, axis=1)
@@ -902,16 +1003,18 @@ def _startvaldict(self):
                         # parameter (instead of using a loop which can be VERY
                         # slow for a large number of parameters)
                         st, sm = rectangularize(
-                            self._param_assigns[key].values(),
-                            return_mask=True)
+                            self._param_assigns[key].values(), return_mask=True
+                        )
                         # average over each group in the dataset
                         meanstartvals = np.ma.mean(
                             rectangularize(meanstartvals, return_masked=True),
-                            axis=1)
+                            axis=1,
+                        )
                         # assign individual values to each parameter-group
                         meanstartvals = np.ma.mean(
                             np.ma.masked_array(np.take(meanstartvals, st), sm),
-                            axis=1).compressed()
+                            axis=1,
+                        ).compressed()
 
                     startvaldict[key] = meanstartvals
                 else:
@@ -940,8 +1043,10 @@ def _boundsvaldict(self):
             for key, val in boundsvaldict.items():
                 # adjust shape of boundary conditions
                 if uniques[key] >= 1 and len(val[0]) == 1:
-                    boundsvaldict[key] = (val[0]*uniques[key],
-                                          val[1]*uniques[key])
+                    boundsvaldict[key] = (
+                        val[0] * uniques[key],
+                        val[1] * uniques[key],
+                    )
 
         return boundsvaldict
 
@@ -951,19 +1056,27 @@ def _timescaledict(self):
 
         timescaledict = {}
         for key, val in self.defdict.items():
-            if (val[0] is True and val[2] is not None
-                    and val[2] != 'manual' and val[2] != 'index'):
-
-                usevals = list(map(str.strip, str(val[2]).split('+')))
-                assert len(usevals) <= 2, ('there are 2 + symbols in ' +
-                                           'the variability definition ' +
-                                           f'of {key} = {val[2]}')
+            if (
+                val[0] is True
+                and val[2] is not None
+                and val[2] != "manual"
+                and val[2] != "index"
+            ):
+
+                usevals = list(map(str.strip, str(val[2]).split("+")))
+                assert len(usevals) <= 2, (
+                    "there are 2 + symbols in "
+                    + "the variability definition "
+                    + f"of {key} = {val[2]}"
+                )
                 if len(usevals) == 2:
-                    assert 'manual' in usevals, ('you can only combine' +
-                                                 '1 datetime-offset and ' +
-                                                 'the keyword "manual" !')
+                    assert "manual" in usevals, (
+                        "you can only combine"
+                        + "1 datetime-offset and "
+                        + 'the keyword "manual" !'
+                    )
 
-                    usevals.pop(usevals.index('manual'))
+                    usevals.pop(usevals.index("manual"))
 
                 # check if remaining freq can be converted to an integer
                 try:
@@ -984,37 +1097,41 @@ def _manual_dyn_df(self):
         manual_dyn_df = pd.DataFrame()
         for key, val in self.defdict.items():
             if val[0] is True:
-                if val[2] == 'manual':
+                if val[2] == "manual":
                     # set manual parameter dynamics
-                    assert f'{key}_dyn' in self.dataset, (
-                        f'{key}_dyn must be provided in the dataset' +
-                        'if defdict[{key}][2] is set to "manual"')
-
-                    manual_dyn_df[f'{key}'] = self.dataset[f'{key}_dyn']
-                elif val[2] == 'index':
-                    indexdyn = pd.DataFrame({key: 1}, self.dataset.index
-                                            ).groupby(axis=0, level=0,
-                                                      sort=False).ngroup()
+                    assert f"{key}_dyn" in self.dataset, (
+                        f"{key}_dyn must be provided in the dataset"
+                        + 'if defdict[{key}][2] is set to "manual"'
+                    )
+
+                    manual_dyn_df[f"{key}"] = self.dataset[f"{key}_dyn"]
+                elif val[2] == "index":
+                    indexdyn = (
+                        pd.DataFrame({key: 1}, self.dataset.index)
+                        .groupby(axis=0, level=0, sort=False)
+                        .ngroup()
+                    )
                     indexdyn.name = key
-                    manual_dyn_df[f'{key}'] = indexdyn
+                    manual_dyn_df[f"{key}"] = indexdyn
 
                 else:
-                    if (val[2] is not None
-                        and 'manual' in map(str.strip,
-                                            str(val[2]).split('+'))):
+                    if val[2] is not None and "manual" in map(
+                        str.strip, str(val[2]).split("+")
+                    ):
 
-                        assert f'{key}_dyn' in self.dataset, (
-                            f'{key}_dyn must be provided in the dataset' +
-                            'if defdict[{key}][2] is set to "manual"')
+                        assert f"{key}_dyn" in self.dataset, (
+                            f"{key}_dyn must be provided in the dataset"
+                            + 'if defdict[{key}][2] is set to "manual"'
+                        )
 
-                        manual_dyn_df[
-                            f'{key}'] = self.dataset[f'{key}_dyn']
+                        manual_dyn_df[f"{key}"] = self.dataset[f"{key}_dyn"]
 
-                    elif f'{key}_dyn' in self.dataset:
-                        warnings.warn(
-                            'the provided manual-dynanics ' +
-                            f'column "{key}_dyn" is ignored since ' +
-                            f'"defdict[{key}][1]" is set to "{val[2]}".')
+                    elif f"{key}_dyn" in self.dataset:
+                        log.warning(
+                            "the provided manual-dynanics "
+                            + f'column "{key}_dyn" is ignored since '
+                            + f'"defdict[{key}][1]" is set to "{val[2]}".'
+                        )
 
         if manual_dyn_df.empty:
             return None
@@ -1029,27 +1146,29 @@ def _param_R_dict(self):
         """
 
         param_R = dict(**self._startvaldict, **self.fixed_dict)
-        param_R.pop('omega', None)
-        param_R.pop('tau', None)
-        param_R.pop('NormBRDF', None)
-        param_R.pop('bsf', None)
+        param_R.pop("omega", None)
+        param_R.pop("tau", None)
+        param_R.pop("NormBRDF", None)
+        param_R.pop("bsf", None)
 
         toNlist = set(self._tau_symb + self._omega_symb + self._N_symb)
 
         # check of general input-requirements
         # check if all parameters have been provided
-        angset = {'phi_ex', 'phi_0', 'theta_0', 'theta_ex'}
+        angset = {"phi_ex", "phi_0", "theta_0", "theta_ex"}
         vsymb = set(map(str, self.V._func.free_symbols)) - angset
         srfsymb = set(map(str, self.SRF._func.free_symbols)) - angset
 
-        paramset = ((set(map(str, self._startvaldict.keys()))
-                     ^ set(map(str, self.fixed_dict.keys())))
-                    - {'tau', 'omega', 'NormBRDF'})
+        paramset = (
+            set(map(str, self._startvaldict.keys()))
+            ^ set(map(str, self.fixed_dict.keys()))
+        ) - {"tau", "omega", "NormBRDF"}
 
         assert paramset >= (vsymb | srfsymb), (
-            'the parameters ' +
-            str((vsymb | srfsymb) - paramset) +
-            ' must be provided in param_dict')
+            "the parameters "
+            + str((vsymb | srfsymb) - paramset)
+            + " must be provided in param_dict"
+        )
 
         # remove also other symbols that are used in the definitions of
         # tau, omega and NormBRDF
@@ -1067,8 +1186,7 @@ def V(self):
         if callable(self.set_V_SRF):
             V, _ = self.set_V_SRF(**self._setdict)
         elif isinstance(self.set_V_SRF, dict):
-            V = self._init_V_SRF(self.set_V_SRF['V_props'],
-                                 setdict=self._setdict)
+            V = self._init_V_SRF(self.set_V_SRF["V_props"], setdict=self._setdict)
 
         return V
 
@@ -1080,23 +1198,30 @@ def SRF(self):
         if callable(self.set_V_SRF):
             _, SRF = self.set_V_SRF(**self._setdict)
         elif isinstance(self.set_V_SRF, dict):
-            SRF = self._init_V_SRF(self.set_V_SRF['SRF_props'],
-                                   setdict=self._setdict)
+            SRF = self._init_V_SRF(self.set_V_SRF["SRF_props"], setdict=self._setdict)
         return SRF
 
     @property
     def R(self):
         """new initialization of the rt1.RT1 object used"""
 
-        R = RT1(1., self.inc, self.inc,
-                np.zeros_like(self.inc), np.full_like(self.inc, np.pi),
-                V=self.V, SRF=self.SRF,
-                fn_input=None, _fnevals_input=None,
-                geometry='mono', bsf=self._setdict.get('bsf', 0.),
-                int_Q=self.int_Q,
-                lambda_backend=self.lambda_backend,
-                param_dict=self._param_R_dict,
-                verbosity=self.verbose)
+        R = RT1(
+            1.0,
+            self.inc,
+            self.inc,
+            np.zeros_like(self.inc),
+            np.full_like(self.inc, np.pi),
+            V=self.V,
+            SRF=self.SRF,
+            fn_input=None,
+            _fnevals_input=None,
+            geometry="mono",
+            bsf=self._setdict.get("bsf", 0.0),
+            int_Q=self.int_Q,
+            lambda_backend=self.lambda_backend,
+            param_dict=self._param_R_dict,
+            verbosity=self.verbose,
+        )
 
         if self.int_Q is True and self._fnevals_input is None:
             self._fnevals_input = R._fnevals
@@ -1112,8 +1237,7 @@ def R(self):
     def __get_V_SRF_symbs(self, V_SRF, prop):
         """the symbols used to define tau, omega and NormBRDF of V and SRF"""
         try:
-            symbs = list(map(str,
-                             getattr(getattr(self, V_SRF), prop).free_symbols))
+            symbs = list(map(str, getattr(getattr(self, V_SRF), prop).free_symbols))
         except Exception:
             symbs = list()
         return symbs
@@ -1125,9 +1249,10 @@ def __get_V_SRF_funcs(self, V_SRF, prop):
         """
         try:
             func = sp.lambdify(
-                getattr(getattr(self, V_SRF), prop).free_symbols,
+                list(getattr(getattr(self, V_SRF), prop).free_symbols),
                 getattr(getattr(self, V_SRF), prop),
-                modules=['numpy'])
+                modules=["numpy"],
+            )
         except Exception:
             func = None
         return func
@@ -1139,59 +1264,58 @@ def __get_V_SRF_diff_funcs(self, V_SRF, prop):
         """
 
         d_inner = dict()
-        for param in (self.__get_V_SRF_symbs(V_SRF, prop) &
-                      self.param_dyn_dict.keys()):
+        for param in self.__get_V_SRF_symbs(V_SRF, prop) & self.param_dyn_dict.keys():
             d_inner[param] = sp.lambdify(
                 self.__get_V_SRF_symbs(V_SRF, prop),
-                sp.diff(getattr(getattr(self, V_SRF), prop),
-                        sp.Symbol(param)),
-                modules=['numpy'])
+                sp.diff(getattr(getattr(self, V_SRF), prop), sp.Symbol(param)),
+                modules=["numpy"],
+            )
         return d_inner
 
     @property
     @lru_cache()
     def _tau_symb(self):
-        return self.__get_V_SRF_symbs('V', 'tau')
+        return self.__get_V_SRF_symbs("V", "tau")
 
     @property
     @lru_cache()
     def _tau_func(self):
-        return self.__get_V_SRF_funcs('V', 'tau')
+        return self.__get_V_SRF_funcs("V", "tau")
 
     @property
     @lru_cache()
     def _tau_diff_func(self):
-        return self.__get_V_SRF_diff_funcs('V', 'tau')
+        return self.__get_V_SRF_diff_funcs("V", "tau")
 
     @property
     @lru_cache()
     def _omega_symb(self):
-        return self.__get_V_SRF_symbs('V', 'omega')
+        return self.__get_V_SRF_symbs("V", "omega")
 
     @property
     @lru_cache()
     def _omega_func(self):
-        return self.__get_V_SRF_funcs('V', 'omega')
+        return self.__get_V_SRF_funcs("V", "omega")
 
     @property
     @lru_cache()
     def _omega_diff_func(self):
-        return self.__get_V_SRF_diff_funcs('V', 'omega')
+        return self.__get_V_SRF_diff_funcs("V", "omega")
 
     @property
     @lru_cache()
     def _N_symb(self):
-        return self.__get_V_SRF_symbs('SRF', 'NormBRDF')
+        return self.__get_V_SRF_symbs("SRF", "NormBRDF")
 
     @property
     @lru_cache()
     def _N_func(self):
-        return self.__get_V_SRF_funcs('SRF', 'NormBRDF')
+        return self.__get_V_SRF_funcs("SRF", "NormBRDF")
 
     @property
     @lru_cache()
     def _N_diff_func(self):
-        return self.__get_V_SRF_diff_funcs('SRF', 'NormBRDF')
+        return self.__get_V_SRF_diff_funcs("SRF", "NormBRDF")
 
     @property
     def res_df(self):
@@ -1199,16 +1323,19 @@ def res_df(self):
         return a pandas DataFrame with the obtained parameters from the fit
         (performfit must be called prior to accessing this property!)
         """
-        if not hasattr(self, 'res_dict'):
-            print('you must perform the fit first! ...e.g. call performfit()')
+        if not hasattr(self, "res_dict"):
+            log.warning(
+                "you must perform the fit first!" + " ...e.g. call performfit()"
+            )
             return
 
         vals = self._assignvals(self.res_dict)
         for key, val in self._assignvals(self.res_dict).items():
             vals[key] = vals[key][~self.mask]
 
-        resdf = pd.DataFrame(vals, list(chain(*self._orig_index))
-                             ).groupby(level=0).first()
+        resdf = (
+            pd.DataFrame(vals, list(chain(*self._orig_index))).groupby(level=0).first()
+        )
 
         return resdf
 
@@ -1220,18 +1347,20 @@ def res_df_group(self):
         of the corresponding group.
         (performfit must be called prior to accessing this property!)
         """
-        if not hasattr(self, 'res_dict'):
-            print('you must perform the fit first! ...e.g. call performfit()')
+        if not hasattr(self, "res_dict"):
+            log.warning(
+                "you must perform the fit first!" + " ...e.g. call performfit()"
+            )
             return
 
         series = []
         for key, val in self.res_dict.items():
             x = np.empty(len(self.meandatetimes_group), dtype=float)
-            [x.put(ind, val_i) for val_i, ind in
-             zip(val, self._param_assigns[key].values())]
-            series.append(pd.Series(x,
-                                    self.meandatetimes_group,
-                                    name=key))
+            [
+                x.put(ind, val_i)
+                for val_i, ind in zip(val, self._param_assigns[key].values())
+            ]
+            series.append(pd.Series(x, self.meandatetimes_group, name=key))
         resdf = pd.concat(series, axis=1)
         return resdf
 
@@ -1266,11 +1395,10 @@ def _assignvals(self, res_dict, interp_vals=None):
 
             if key in interp_vals:
                 if self._param_dyn_monotonic[key] is False:
-                    warnings.warn(f'interpolation of non-monotonic {key}')
+                    log.info(f"interpolation of non-monotonic {key} !")
                     # use assignments for unsorted param_dyns
                     useindex = self._meandt_interp_assigns(key)[0]
-                    usevals = np.array(
-                        val)[self._meandt_interp_assigns(key)[1]]
+                    usevals = np.array(val)[self._meandt_interp_assigns(key)[1]]
                 else:
                     useindex = self.meandatetimes[key]
                     usevals = val
@@ -1278,42 +1406,53 @@ def _assignvals(self, res_dict, interp_vals=None):
                 # ensure that the indexes are never the same
                 # (add a milisecond if they are...)
                 if firstindex == useindex[0]:
-                    firstindex += np.timedelta64(1, 'ms')
+                    firstindex += np.timedelta64(1, "ms")
                 if lastindex == useindex[-1]:
-                    lastindex -= np.timedelta64(1, 'ms')
+                    lastindex -= np.timedelta64(1, "ms")
 
-                useindex = np.array([firstindex, *useindex, lastindex],
-                                    dtype='datetime64[ns]'
-                                    ).astype(float, copy=False)
+                useindex = np.array(
+                    [firstindex, *useindex, lastindex], dtype="datetime64[ns]"
+                ).astype(float, copy=False)
 
                 if len(usevals) >= 2:
-                    usevals = np.take(usevals, [0,
-                                                *range(len(usevals)), -1])
+                    usevals = np.take(usevals, [0, *range(len(usevals)), -1])
 
                     # interpolate the data to the used timestamps
-                    f = interp1d(useindex.astype(float), usevals,
-                                 fill_value='extrapolate', axis=0,
-                                 kind='quadratic')
+                    f = interp1d(
+                        useindex.astype(float),
+                        usevals,
+                        fill_value="extrapolate",
+                        axis=0,
+                        kind="quadratic",
+                    )
 
-                    x = f(np.array(self.dataset.index,
-                                   dtype='datetime64[ns]'))
+                    x = f(np.array(self.dataset.index, dtype="datetime64[ns]"))
 
                     # assign correct shape
                     use_res_dict[key] = np.take(x, self._idx_assigns)
                 else:
-                    warnings.warn(
-                        'interpolation not possible for ' +
-                        f'({key}) because there are less than 2 values')
+                    log.info(
+                        "interpolation not possible for "
+                        + f"({key}) because there are less than 2 values"
+                    )
 
                     x = np.empty(len(self.dataset), dtype=float)
-                    [x.put(ind, val_i) for val_i, ind in
-                     zip(val, self._param_assigns_dataset[key].values())]
+                    [
+                        x.put(ind, val_i)
+                        for val_i, ind in zip(
+                            val, self._param_assigns_dataset[key].values()
+                        )
+                    ]
                     # assign correct shape
                     use_res_dict[key] = np.take(x, self._idx_assigns)
             else:
                 x = np.empty(len(self.dataset), dtype=float)
-                [x.put(ind, val_i) for val_i, ind in
-                 zip(val, self._param_assigns_dataset[key].values())]
+                [
+                    x.put(ind, val_i)
+                    for val_i, ind in zip(
+                        val, self._param_assigns_dataset[key].values()
+                    )
+                ]
                 # assign correct shape
                 use_res_dict[key] = np.take(x, self._idx_assigns)
 
@@ -1322,7 +1461,7 @@ def _assignvals(self, res_dict, interp_vals=None):
     @lru_cache()
     def _get_excludesymbs(self):
         # symbols used to define the functions
-        angset = {'phi_ex', 'phi_0', 'theta_0', 'theta_ex'}
+        angset = {"phi_ex", "phi_0", "theta_0", "theta_ex"}
         vsymb = set(map(str, self.V._func.free_symbols)) - angset
         srfsymb = set(map(str, self.SRF._func.free_symbols)) - angset
 
@@ -1332,14 +1471,27 @@ def _get_excludesymbs(self):
         # exclude all keys that are not needed to calculate the fn-coefficients
         # vsymb and srfsymb must be subtracted in case the same symbol is used
         # for omega, tau or NormBRDF definition and in the function definiton
-        excludekeys = set(['omega', 'tau', 'NormBRDF', 'bsf',
-                           *[str(i) for i in set(toNlist - vsymb - srfsymb)]])
+        excludekeys = set(
+            [
+                "omega",
+                "tau",
+                "NormBRDF",
+                "bsf",
+                *[str(i) for i in set(toNlist - vsymb - srfsymb)],
+            ]
+        )
 
         return excludekeys
 
-    def _calc_model(self, R=None, res_dict=None, fixed_dict=None,
-                    interp_vals=None, return_components=False,
-                    assign=True):
+    def _calc_model(
+        self,
+        R=None,
+        res_dict=None,
+        fixed_dict=None,
+        interp_vals=None,
+        return_components=False,
+        assign=True,
+    ):
         """
         function to calculate the model-results based on the provided
         parameters
@@ -1393,28 +1545,29 @@ def _calc_model(self, R=None, res_dict=None, fixed_dict=None,
         # update the numeric representations of omega, tau and NormBRDF
         # based on the values for the used symbols provided in res_dict
         if self._omega_func is None:
-            if 'omega' in res_dict:
-                R.V.omega = res_dict['omega']
+            if "omega" in res_dict:
+                R.V.omega = res_dict["omega"]
         else:
             R.V.omega = self._omega_func(
-                **{key: res_dict[key] for key in self._omega_symb})
+                **{key: res_dict[key] for key in self._omega_symb}
+            )
 
         if self._tau_func is None:
-            if 'tau' in res_dict:
-                R.V.tau = res_dict['tau']
+            if "tau" in res_dict:
+                R.V.tau = res_dict["tau"]
         else:
-            R.V.tau = self._tau_func(
-                **{key: res_dict[key] for key in self._tau_symb})
+            R.V.tau = self._tau_func(**{key: res_dict[key] for key in self._tau_symb})
 
         if self._N_func is None:
-            if 'NormBRDF' in res_dict:
-                R.SRF.NormBRDF = res_dict['NormBRDF']
+            if "NormBRDF" in res_dict:
+                R.SRF.NormBRDF = res_dict["NormBRDF"]
         else:
             R.SRF.NormBRDF = self._N_func(
-                **{key: res_dict[key] for key in self._N_symb})
+                **{key: res_dict[key] for key in self._N_symb}
+            )
 
-        if 'bsf' in res_dict:
-            R.bsf = res_dict['bsf']
+        if "bsf" in res_dict:
+            R.bsf = res_dict["bsf"]
 
         # remove all unwanted symbols that are NOT needed for evaluation
         # of the fn-coefficients from res_dict to generate a dict that
@@ -1422,8 +1575,9 @@ def _calc_model(self, R=None, res_dict=None, fixed_dict=None,
         # and the symbols used to define them must be removed)
 
         excludekeys = self._get_excludesymbs()
-        strparam_fn = {str(key): val for key, val in res_dict.items()
-                       if key not in excludekeys}
+        strparam_fn = {
+            str(key): val for key, val in res_dict.items() if key not in excludekeys
+        }
 
         # set the param-dict to the newly generated dict
         R.param_dict = strparam_fn
@@ -1436,17 +1590,23 @@ def _calc_model(self, R=None, res_dict=None, fixed_dict=None,
 
         if self.sig0 is True:
             # convert the calculated results to sigma_0
-            signorm = 4. * np.pi * np.cos(R.t_0)
+            signorm = 4.0 * np.pi * np.cos(R.t_0)
             model_calc = signorm * model_calc
 
         if self.dB is True:
             # convert the calculated results to dB
-            model_calc = 10. * np.log10(model_calc)
+            model_calc = 10.0 * np.log10(model_calc)
 
         return model_calc
 
-    def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
-                  param_dyn_dict=None, order=None):
+    def _calc_jac(
+        self,
+        R=None,
+        res_dict=None,
+        fixed_dict=None,
+        param_dyn_dict=None,
+        order=None,
+    ):
         """
         function to evaluate the jacobian in the shape as required
         by scipy's least_squares function
@@ -1486,28 +1646,29 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
         # update the numeric representations of omega, tau and NormBRDF
         # based on the values for the used symbols provided in res_dict
         if self._omega_func is None:
-            if 'omega' in res_dict:
-                R.V.omega = res_dict['omega']
+            if "omega" in res_dict:
+                R.V.omega = res_dict["omega"]
         else:
             R.V.omega = self._omega_func(
-                **{key: res_dict[key] for key in self._omega_symb})
+                **{key: res_dict[key] for key in self._omega_symb}
+            )
 
         if self._tau_func is None:
-            if 'tau' in res_dict:
-                R.V.tau = res_dict['tau']
+            if "tau" in res_dict:
+                R.V.tau = res_dict["tau"]
         else:
-            R.V.tau = self._tau_func(
-                **{key: res_dict[key] for key in self._tau_symb})
+            R.V.tau = self._tau_func(**{key: res_dict[key] for key in self._tau_symb})
 
         if self._N_func is None:
-            if 'NormBRDF' in res_dict:
-                R.SRF.NormBRDF = res_dict['NormBRDF']
+            if "NormBRDF" in res_dict:
+                R.SRF.NormBRDF = res_dict["NormBRDF"]
         else:
             R.SRF.NormBRDF = self._N_func(
-                **{key: res_dict[key] for key in self._N_symb})
+                **{key: res_dict[key] for key in self._N_symb}
+            )
 
-        if 'bsf' in res_dict:
-            R.bsf = res_dict['bsf']
+        if "bsf" in res_dict:
+            R.bsf = res_dict["bsf"]
 
         # remove all unwanted symbols that are NOT needed for evaluation
         # of the fn-coefficients from res_dict to generate a dict that
@@ -1515,7 +1676,7 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
         # "bsf" and the symbols used to define them must be removed)
 
         # symbols used in the definitions of the functions
-        angset = {'phi_ex', 'phi_0', 'theta_0', 'theta_ex'}
+        angset = {"phi_ex", "phi_0", "theta_0", "theta_ex"}
         vsymb = set(map(str, self.V._func.free_symbols)) - angset
         srfsymb = set(map(str, self.SRF._func.free_symbols)) - angset
 
@@ -1525,11 +1686,17 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
         # exclude all keys that are not needed to calculate the fn-coefficients
         # vsymb and srfsymb must be subtracted in case the same symbol is used
         # for omega, tau or NormBRDF definition and in the function definiton
-        excludekeys = ['omega', 'tau', 'NormBRDF', 'bsf',
-                       *[str(i) for i in set(toNlist - vsymb - srfsymb)]]
-
-        strparam_fn = {str(key): val for key, val in res_dict.items()
-                       if key not in excludekeys}
+        excludekeys = [
+            "omega",
+            "tau",
+            "NormBRDF",
+            "bsf",
+            *[str(i) for i in set(toNlist - vsymb - srfsymb)],
+        ]
+
+        strparam_fn = {
+            str(key): val for key, val in res_dict.items() if key not in excludekeys
+        }
 
         # set the param-dict to the newly generated dict
         R.param_dict = strparam_fn
@@ -1541,13 +1708,13 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
         neworder = [o for o in order]
         if len(self._tau_symb) != 0:
             for i in set(self._tau_symb) & set(param_dyn_dict.keys()):
-                neworder[neworder.index(i)] = 'tau'
+                neworder[neworder.index(i)] = "tau"
         if len(self._omega_symb) != 0:
             for i in set(self._omega_symb) & set(param_dyn_dict.keys()):
-                neworder[neworder.index(i)] = 'omega'
+                neworder[neworder.index(i)] = "omega"
         if len(self._N_symb) != 0:
             for i in set(self._N_symb) & set(param_dyn_dict.keys()):
-                neworder[neworder.index(i)] = 'NormBRDF'
+                neworder[neworder.index(i)] = "NormBRDF"
 
         # calculate the jacobian based on neworder
         # (evaluating only "outer" derivatives with respect to omega,
@@ -1565,8 +1732,8 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
             if len(uniques) == 1:
                 # np.array([np.concatenate(jac[i], axis=0)])
                 newjacdict[key] = np.expand_dims(
-                    np.fromiter(chain(*jac[i]), dtype=float, count=jac_size),
-                    0)
+                    np.fromiter(chain(*jac[i]), dtype=float, count=jac_size), 0
+                )
             else:
                 # if too many unique values occur, use scipy sparse matrix
                 # to avoid memory-overflow due to the large number of zeroes...
@@ -1576,9 +1743,10 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
                 data = np.fromiter(chain(*jac[i]), dtype=float, count=jac_size)
                 row_ind, col_ind = self._jac_assign_rule[key]
                 # generate a sparse matrix
-                m = csr_matrix((data, (row_ind, col_ind)),
-                               shape=(max(row_ind) + 1,
-                                      max(col_ind) + 1))
+                m = csr_matrix(
+                    (data, (row_ind, col_ind)),
+                    shape=(max(row_ind) + 1, max(col_ind) + 1),
+                )
                 newjacdict[key] = m
 
         # evaluate jacobians of the functional representations of tau
@@ -1587,8 +1755,9 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
             # generate a function that evaluates the 'inner' derivative, i.e.:
             # df/dx = df/dtau * dtau/dx = df/dtau * d_inner
             # evaluate the inner derivative
-            df_dx = self._tau_diff_func[i](**{key: res_dict[key] for key in
-                                              self._tau_symb})
+            df_dx = self._tau_diff_func[i](
+                **{key: res_dict[key] for key in self._tau_symb}
+            )
             if not np.isscalar(df_dx):
                 # flatten the array (except if it is a scalar)
                 # happens for example when   f = 5*x   ->   df/dx = 5
@@ -1602,8 +1771,9 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
 
         # same for omega
         for i in set(self._omega_symb) & set(param_dyn_dict.keys()):
-            df_dx = self._omega_diff_func[i](**{key: res_dict[key] for key in
-                                                self._omega_symb})
+            df_dx = self._omega_diff_func[i](
+                **{key: res_dict[key] for key in self._omega_symb}
+            )
             if not np.isscalar(df_dx):
                 df_dx = np.fromiter(chain(*df_dx), dtype=float, count=jac_size)
 
@@ -1614,8 +1784,7 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
 
         # same for NormBRDF
         for i in set(self._N_symb) & set(param_dyn_dict.keys()):
-            df_dx = self._N_diff_func[i](**{key: res_dict[key] for key in
-                                            self._N_symb})
+            df_dx = self._N_diff_func[i](**{key: res_dict[key] for key in self._N_symb})
             if not np.isscalar(df_dx):
                 df_dx = np.fromiter(chain(*df_dx), dtype=float, count=jac_size)
 
@@ -1624,8 +1793,8 @@ def _calc_jac(self, R=None, res_dict=None, fixed_dict=None,
             else:
                 newjacdict[str(i)] = newjacdict[str(i)] * df_dx
 
-        if hasattr(self, 'intermediate_results'):
-            self.intermediate_results['jacobian'] += [newjacdict]
+        if hasattr(self, "intermediate_results"):
+            self.intermediate_results["jacobian"] += [newjacdict]
 
         sparse = False
         for key, val in newjacdict.items():
@@ -1660,26 +1829,29 @@ def __get_data(self, prop):
 
         """
         if isinstance(self._dataset_used, pd.DataFrame):
-            if prop in ['inc', 'sig', 'data_weights']:
+            if prop in ["inc", "sig", "data_weights"]:
                 return rectangularize(self._dataset_used[prop].values)
-            elif prop == 'mask':
-                _, mask = rectangularize(self._dataset_used.inc.values,
-                                         return_mask=True)
-                if prop == 'mask':
+            elif prop == "mask":
+                _, mask = rectangularize(
+                    self._dataset_used.inc.values, return_mask=True
+                )
+                if prop == "mask":
                     return mask
         elif isinstance(self._dataset_used, list):
-            if prop == 'inc':
+            if prop == "inc":
                 return rectangularize([i[0] for i in self._dataset_used])
-            elif prop == 'sig':
+            elif prop == "sig":
                 return rectangularize([i[1] for i in self._dataset_used])
-            elif prop == 'mask':
-                _, mask = rectangularize([i[0] for i in self._dataset_used],
-                                         return_mask=True)
-                if prop == 'mask':
+            elif prop == "mask":
+                _, mask = rectangularize(
+                    [i[0] for i in self._dataset_used], return_mask=True
+                )
+                if prop == "mask":
                     return mask
 
-    def _calc_slope_curv(self, R=None, res_dict=None, fixed_dict=None,
-                         return_components=False):
+    def _calc_slope_curv(
+        self, R=None, res_dict=None, fixed_dict=None, return_components=False
+    ):
         """
         function to calculate the monostatic slope and curvature
         of the model
@@ -1707,17 +1879,17 @@ def _calc_slope_curv(self, R=None, res_dict=None, fixed_dict=None,
             try:
                 R = self.R
             except AttributeError:
-                assert False, 'R is not available and must be provided'
+                assert False, "R is not available and must be provided"
         if res_dict is None:
             try:
                 res_dict = self.res_dict
             except AttributeError:
-                assert False, 'res_dict is not available and must be provided'
+                assert False, "res_dict is not available and must be provided"
         if fixed_dict is None:
             try:
                 fixed_dict = self.fixed_dict
             except AttributeError:
-                assert False, 'fixed_dict is not available > must be provided!'
+                assert False, "fixed_dict is not available > must be provided!"
 
         # ensure correct array-processing
         res_dict = self._assignvals(res_dict)
@@ -1726,28 +1898,29 @@ def _calc_slope_curv(self, R=None, res_dict=None, fixed_dict=None,
         # update the numeric representations of omega, tau and NormBRDF
         # based on the values for the used symbols provided in res_dict
         if self._omega_func is None:
-            if 'omega' in res_dict:
-                R.V.omega = res_dict['omega']
+            if "omega" in res_dict:
+                R.V.omega = res_dict["omega"]
         else:
             R.V.omega = self._omega_func(
-                **{key: res_dict[key] for key in self._omega_symb})
+                **{key: res_dict[key] for key in self._omega_symb}
+            )
 
         if self._tau_func is None:
-            if 'tau' in res_dict:
-                R.V.tau = res_dict['tau']
+            if "tau" in res_dict:
+                R.V.tau = res_dict["tau"]
         else:
-            R.V.tau = self._tau_func(
-                **{key: res_dict[key] for key in self._tau_symb})
+            R.V.tau = self._tau_func(**{key: res_dict[key] for key in self._tau_symb})
 
         if self._N_func is None:
-            if 'NormBRDF' in res_dict:
-                R.SRF.NormBRDF = res_dict['NormBRDF']
+            if "NormBRDF" in res_dict:
+                R.SRF.NormBRDF = res_dict["NormBRDF"]
         else:
             R.SRF.NormBRDF = self._N_func(
-                **{key: res_dict[key] for key in self._N_symb})
+                **{key: res_dict[key] for key in self._N_symb}
+            )
 
-        if 'bsf' in res_dict:
-            R.bsf = res_dict['bsf']
+        if "bsf" in res_dict:
+            R.bsf = res_dict["bsf"]
 
         # remove all unwanted symbols that are NOT needed for evaluation
         # of the fn-coefficients from res_dict to generate a dict that
@@ -1755,7 +1928,7 @@ def _calc_slope_curv(self, R=None, res_dict=None, fixed_dict=None,
         # and the symbols used to define them must be removed)
 
         # symbols used to define the functions
-        angset = {'phi_ex', 'phi_0', 'theta_0', 'theta_ex'}
+        angset = {"phi_ex", "phi_0", "theta_0", "theta_ex"}
         vsymb = set(map(str, R.V._func.free_symbols)) - angset
         srfsymb = set(map(str, R.SRF._func.free_symbols)) - angset
 
@@ -1765,33 +1938,42 @@ def _calc_slope_curv(self, R=None, res_dict=None, fixed_dict=None,
         # exclude all keys that are not needed to calculate the fn-coefficients
         # vsymb and srfsymb must be subtracted in case the same symbol is used
         # for omega, tau or NormBRDF definition and in the function definiton
-        excludekeys = ['omega', 'tau', 'NormBRDF', 'bsf',
-                       *[str(i) for i in set(toNlist - vsymb - srfsymb)]]
-
-        strparam_fn = {str(key): val for key, val in res_dict.items()
-                       if key not in excludekeys}
+        excludekeys = [
+            "omega",
+            "tau",
+            "NormBRDF",
+            "bsf",
+            *[str(i) for i in set(toNlist - vsymb - srfsymb)],
+        ]
+
+        strparam_fn = {
+            str(key): val for key, val in res_dict.items() if key not in excludekeys
+        }
 
         # set the param-dict to the newly generated dict
         R.param_dict = strparam_fn
 
         # calculate slope-values
         if return_components is True:
-            model_slope = [R.tot_slope(sig0=self.sig0, dB=self.dB),
-                           R.surface_slope(sig0=self.sig0, dB=self.dB),
-                           R.volume_slope(sig0=self.sig0, dB=self.dB)]
+            model_slope = [
+                R.tot_slope(sig0=self.sig0, dB=self.dB),
+                R.surface_slope(sig0=self.sig0, dB=self.dB),
+                R.volume_slope(sig0=self.sig0, dB=self.dB),
+            ]
         else:
             model_slope = R.tot_slope(sig0=self.sig0, dB=self.dB)
 
         # calculate curvature-values
         if return_components is True:
-            model_curv = [R.tot_curv(sig0=self.sig0, dB=self.dB),
-                          R.surface_curv(sig0=self.sig0, dB=self.dB),
-                          R.volume_curv(sig0=self.sig0, dB=self.dB)]
+            model_curv = [
+                R.tot_curv(sig0=self.sig0, dB=self.dB),
+                R.surface_curv(sig0=self.sig0, dB=self.dB),
+                R.volume_curv(sig0=self.sig0, dB=self.dB),
+            ]
         else:
             model_curv = R.tot_curv(sig0=self.sig0, dB=self.dB)
 
-        return {'slope': model_slope,
-                'curv': model_curv}
+        return {"slope": model_slope, "curv": model_curv}
 
     def _init_V_SRF(self, props, setdict=None):
         """
@@ -1828,14 +2010,16 @@ def _init_V_SRF(self, props, setdict=None):
         if setdict is None:
             setdict = dict()
 
-        assert ('V_name' in props or 'SRF_name' in props), (
-            'you must provide "V_name" or "SRF_name" in the props-dict!')
-        assert not ('V_name' in props and 'SRF_name' in props), (
-            'provide either "V_name" or "SRF_name" not both!')
+        assert (
+            "V_name" in props or "SRF_name" in props
+        ), 'you must provide "V_name" or "SRF_name" in the props-dict!'
+        assert not (
+            "V_name" in props and "SRF_name" in props
+        ), 'provide either "V_name" or "SRF_name" not both!'
 
         set_dict = dict()
         for key, val in props.items():
-            if key == 'V_name' or key == 'SRF_name':
+            if key == "V_name" or key == "SRF_name":
                 continue
 
             # check if val is directly provided in setdict, if yes use it
@@ -1860,17 +2044,21 @@ def _init_V_SRF(self, props, setdict=None):
 
             set_dict[key] = useval
 
-        if 'V_name' in props:
+        if "V_name" in props:
             # initialize the volume-scattering function
-            V = getattr(rt1_v, props['V_name'])(**set_dict)
+            V = getattr(rt1_v, props["V_name"])(**set_dict)
             return V
-        elif 'SRF_name' in props:
+        elif "SRF_name" in props:
             # initialize the surface-scattering function
-            SRF = getattr(rt1_s, props['SRF_name'])(**set_dict)
+            SRF = getattr(rt1_s, props["SRF_name"])(**set_dict)
             return SRF
 
-    def performfit(self, clear_cache=True, intermediate_results=False,
-                   print_progress=False):
+    def performfit(
+        self,
+        clear_cache=True,
+        intermediate_results=False,
+        print_progress=False,
+    ):
         """
         Perform least-squares fitting of omega, tau, NormBRDF and any
         parameter used to define V and SRF to sets of monostatic measurements.
@@ -1906,9 +2094,11 @@ def performfit(self, clear_cache=True, intermediate_results=False,
 
         # set up the dictionary for storing intermediate results
         if intermediate_results is True:
-            self.intermediate_results = {'parameters': [],
-                                         'residuals': [],
-                                         'jacobian': []}
+            self.intermediate_results = {
+                "parameters": [],
+                "residuals": [],
+                "jacobian": [],
+            }
 
         # will be used to assign the values to the individual parameters
         # (the returned parameters are given as a concatenated array
@@ -1918,8 +2108,8 @@ def performfit(self, clear_cache=True, intermediate_results=False,
 
         if print_progress:
             update_cnt = count(1)
-            if 'max_nfev' in self.lsq_kwargs:
-                max_cnt = self.lsq_kwargs['max_nfev']
+            if "max_nfev" in self.lsq_kwargs:
+                max_cnt = self.lsq_kwargs["max_nfev"]
             else:
                 max_cnt = 1000
 
@@ -1927,23 +2117,32 @@ def performfit(self, clear_cache=True, intermediate_results=False,
         # for scipy's least_squares function
         def fun(params):
             if print_progress:
-                update_progress(next(update_cnt), max_cnt,
-                                title="function evaluations: ",
-                                finalmsg=f"max_nfev ({max_cnt}) reached!")
+                msg = update_progress(
+                    next(update_cnt),
+                    max_cnt,
+                    title="function evaluations: ",
+                    finalmsg=f"max_nfev ({max_cnt}) reached!",
+                )
+
+                sys.stdout.write(msg)
+                sys.stdout.flush()
 
             # generate a dictionary to assign values based on input
             split_vals = split_into(params, splitpos)
             newdict = dict(zip(self._order, split_vals))
             # calculate the residuals and incorporate data-weighting
-            errs = (self.data_weights *
-                    (self._calc_model(R=R, res_dict=newdict) - self.data)
-                    )[~self.mask]
+            errs = (
+                self.data_weights
+                * (self._calc_model(R=R, res_dict=newdict) - self.data)
+            )[~self.mask]
 
             if intermediate_results is True:
-                self.intermediate_results['parameters'] += [newdict]
-                errdict = {'abserr': errs,
-                           'relerr': errs/self.data[~self.mask]}
-                self.intermediate_results['residuals'] += [errdict]
+                self.intermediate_results["parameters"] += [newdict]
+                errdict = {
+                    "abserr": errs,
+                    "relerr": errs / self.data[~self.mask],
+                }
+                self.intermediate_results["residuals"] += [errdict]
 
             return errs
 
@@ -1974,8 +2173,9 @@ def dfun(params):
                     startvals = startvals + list(self._startvaldict[key])
 
         # perform the actual fit
-        res_lsq = least_squares(fun, startvals, bounds=bounds,
-                                jac=dfun, **self.lsq_kwargs)
+        res_lsq = least_squares(
+            fun, startvals, bounds=bounds, jac=dfun, **self.lsq_kwargs
+        )
 
         # generate a dictionary to assign values based on fit-results
         # split the obtained result with respect to the individual parameters
@@ -1991,10 +2191,7 @@ def dfun(params):
             self.res_dict = dict()
             self.fit_output = None
 
-        if print_progress:
-            if next(update_cnt) - 1 < max_cnt:
-                update_progress(max_cnt, max_cnt,
-                                finalmsg=f'Done! ({res_lsq.message})')
+        log.info(f"Done! ({res_lsq.message})")
 
         # set _RT1_version after successful call of performfit
         self._RT1_version = _RT1_version
@@ -2033,15 +2230,14 @@ def dump(self, path, mini=True):
             self._rt1_dump_mini = True
         else:
             try:
-                delattr(self, '_rt1_dump_mini')
+                delattr(self, "_rt1_dump_mini")
             except AttributeError:
                 pass
 
-        with open(path, 'wb') as file:
+        with open(path, "wb") as file:
             cloudpickle.dump(self, file)
 
-    def calc(self, param, inc, return_components=True,
-             fixed_param=None):
+    def calc(self, param, inc, return_components=True, fixed_param=None):
         """
         evaluate the model with respect to a given set of parameters
         and incidence-angles.
@@ -2084,34 +2280,43 @@ def calc(self, param, inc, return_components=True,
 
         R = self.R
         R.t_0 = np.atleast_2d(inc)
-        R.p_0 = np.full_like(R.t_0, 0.)
-        if isinstance(param,
-                      pd.DataFrame) and (fixed_param is None
-                                         or isinstance(fixed_param,
-                                                       pd.DataFrame)):
-            res_dict = {key: np.atleast_1d(val.values)[:, np.newaxis]
-                        for key, val in param.items()}
+        R.p_0 = np.full_like(R.t_0, 0.0)
+        if isinstance(param, pd.DataFrame) and (
+            fixed_param is None or isinstance(fixed_param, pd.DataFrame)
+        ):
+            res_dict = {
+                key: np.atleast_1d(val.values)[:, np.newaxis]
+                for key, val in param.items()
+            }
             if fixed_param is None:
                 fixed_param = dict()
             else:
-                fixed_param = {key: np.atleast_1d(val)[:, np.newaxis] for
-                               key, val in fixed_param.loc[param.index].items()
-                               }
+                fixed_param = {
+                    key: np.atleast_1d(val)[:, np.newaxis]
+                    for key, val in fixed_param.loc[param.index].items()
+                }
 
         else:
-            res_dict = {key: np.atleast_1d(val)[:, np.newaxis]
-                        for key, val in param.items()}
+            res_dict = {
+                key: np.atleast_1d(val)[:, np.newaxis] for key, val in param.items()
+            }
 
             if fixed_param is None:
                 fixed_param = dict()
             else:
-                fixed_param = {key: np.atleast_1d(val)[:, np.newaxis] for
-                               key, val in fixed_param.items()}
-
-        res = self._calc_model(R=R, res_dict=res_dict, fixed_dict=fixed_param,
-                               interp_vals=[],
-                               return_components=return_components,
-                               assign=False)
+                fixed_param = {
+                    key: np.atleast_1d(val)[:, np.newaxis]
+                    for key, val in fixed_param.items()
+                }
+
+        res = self._calc_model(
+            R=R,
+            res_dict=res_dict,
+            fixed_dict=fixed_param,
+            interp_vals=[],
+            return_components=return_components,
+            assign=False,
+        )
 
         return res
 
@@ -2128,13 +2333,18 @@ def calc_model(self, *args, **kwargs):
         res = self._calc_model(*args, **kwargs)
         if len(res.shape) == 3:
             return pd.DataFrame(
-                dict(zip(('tot', 'surf', 'vol', 'inter'),
-                         [i[~self.mask] for i in res])),
-                index=calcindex)  # .groupby(level=0).mean()
+                dict(
+                    zip(
+                        ("tot", "surf", "vol", "inter"),
+                        [i[~self.mask] for i in res],
+                    )
+                ),
+                index=calcindex,
+            )  # .groupby(level=0).mean()
         else:
             return pd.DataFrame(
-                dict(tot=res[~self.mask]),
-                index=calcindex)  # .groupby(level=0).mean()
+                dict(tot=res[~self.mask]), index=calcindex
+            )  # .groupby(level=0).mean()
 
     @property
     def _model_definition(self):
@@ -2150,73 +2360,75 @@ def _model_definition(self):
         outstr : str
         """
 
-        fitted, auxiliary, fixed = '', [], []
+        fitted, auxiliary, fixed = "", [], []
 
-        fitted += ('     NAME'.ljust(14) + '|     START'.ljust(15) +
-                   '|  VARIABILITY'.ljust(16) + '|    BOUNDS'.ljust(15) +
-                   '| INTERPOLATION'.ljust(15)) + ' |'
-        fitted += '\n'
+        fitted += (
+            "     NAME".ljust(14)
+            + "|     START".ljust(15)
+            + "|  VARIABILITY".ljust(16)
+            + "|    BOUNDS".ljust(15)
+            + "| INTERPOLATION".ljust(15)
+        ) + " |"
+        fitted += "\n"
 
         for key, val in self.defdict.items():
             if val[0] is True:
-                name = f'{key:<13}'
-                star = f'{val[1]:.10}'.ljust(13)
+                name = f"{key:<13}"
+                star = f"{val[1]:.10}".ljust(13)
 
                 if val[2] is not None:
                     freqs = map(str.strip, str(val[2]).split("+"))
                     vari = f'{" & ".join(freqs):<14}'
                 else:
-                    vari = '      -       '
+                    vari = "      -       "
 
-                boun = (f'{val[3][0][0]:.5}' + "-" +
-                        f'{val[3][1][0]:.5}').ljust(13)
+                boun = (f"{val[3][0][0]:.5}" + "-" + f"{val[3][1][0]:.5}").ljust(13)
                 try:
-                    inte = f'{str(val[4]):<14}'
+                    inte = f"{str(val[4]):<14}"
                 except IndexError:
-                    inte = 'False'.ljust(14)
+                    inte = "False".ljust(14)
 
-                fitted += (f' {name}| {star}| {vari}| {boun}| {inte}|\n')
+                fitted += f" {name}| {star}| {vari}| {boun}| {inte}|\n"
 
             if val[0] is False:
-                if val[1] == 'auxiliary':
-                    auxiliary += [f'| {key}']
+                if val[1] == "auxiliary":
+                    auxiliary += [f"| {key}"]
                 elif isinstance(val[1], (int, float)):
-                    fixed += [f' {key:<13}= {val[1]}']
+                    fixed += [f" {key:<13}= {val[1]}"]
 
         # print V and SRF definitions
         if isinstance(self.set_V_SRF, dict):
-            vprop = {**self.set_V_SRF['V_props']}
-            vname = vprop.pop('V_name', '?')
-            srfprop = {**self.set_V_SRF['SRF_props']}
-            srfname = srfprop.pop('SRF_name', '?')
+            vprop = {**self.set_V_SRF["V_props"]}
+            vname = vprop.pop("V_name", "?")
+            srfprop = {**self.set_V_SRF["SRF_props"]}
+            srfname = srfprop.pop("SRF_name", "?")
 
             vnames = list(vprop.keys())
-            vnames = [i.ljust(max(map(len, vnames))) + ':' for i in vnames]
+            vnames = [i.ljust(max(map(len, vnames))) + ":" for i in vnames]
             vvals = list(vprop.values())
 
             srfnames = list(srfprop.keys())
-            srfnames = [i.ljust(max(map(len, srfnames))) + ':'
-                        for i in srfnames]
+            srfnames = [i.ljust(max(map(len, srfnames))) + ":" for i in srfnames]
             srfvals = list(srfprop.values())
 
             while len(vnames) < max(len(vnames), len(srfnames)):
-                vnames += ['']
-                vvals += ['']
+                vnames += [""]
+                vvals += [""]
 
             while len(srfnames) < max(len(vnames), len(srfnames)):
-                srfnames += ['']
-                srfvals += ['']
+                srfnames += [""]
+                srfvals += [""]
         else:
             try:
                 vname = self.V.__class__.__name__
             except Exception:
-                vname = '?'
+                vname = "?"
             try:
                 srfname = self.SRF.__class__.__name__
             except Exception:
-                srfname = '?'
+                srfname = "?"
 
-        datefmt = '%d. %B %Y (%H:%M:%S)'
+        datefmt = "%d. %B %Y (%H:%M:%S)"
 
         try:
             outstr = f"used RT1_version:  {self._RT1_version}".ljust(38)
@@ -2224,70 +2436,81 @@ def _model_definition(self):
             outstr = f"RT1_version: {_RT1_version}".ljust(38)
 
         outstr += f"{datetime.now().strftime(datefmt)}\n".rjust(39)
-        outstr += '-'*77 + '\n'
-        outstr += '# SCATTERING FUNCTIONS ' + '\n'
+        outstr += "-" * 77 + "\n"
+        outstr += "# SCATTERING FUNCTIONS " + "\n"
 
         # outstr += f' Volume: {vname}'.ljust(37) + '|'
         # outstr += f' Surface: {srfname}' + '\n\n'
-        outstr += ' VOLUME:'.ljust(37) + '|'
-        outstr += ' SURFACE:' + '\n'
-        outstr += f' {vname}'.ljust(37) + '|'
-        outstr += f' {srfname}' + '\n'
+        outstr += " VOLUME:".ljust(37) + "|"
+        outstr += " SURFACE:" + "\n"
+        outstr += f" {vname}".ljust(37) + "|"
+        outstr += f" {srfname}" + "\n"
         if isinstance(self.set_V_SRF, dict):
-            for vn, vv, sn, sv in zip(vnames, vvals,
-                                      srfnames, srfvals):
-                outstr += f'     {vn} {vv}'.ljust(37) + '|'
-                outstr += f'     {sn} {sv}' + '\n'
-            outstr += '\n'
+            for vn, vv, sn, sv in zip(vnames, vvals, srfnames, srfvals):
+                outstr += f"     {vn} {vv}".ljust(37) + "|"
+                outstr += f"     {sn} {sv}" + "\n"
+            outstr += "\n"
 
-        outstr += f'# Interaction-contribution?        {self.int_Q}'+'\n\n'
+        outstr += f"# Interaction-contribution?        {self.int_Q}" + "\n\n"
 
-        outstr += '-'*29 + ' FITTED PARAMETERS ' + '-'*29 + '\n'
-        outstr += fitted + '\n'
+        outstr += "-" * 29 + " FITTED PARAMETERS " + "-" * 29 + "\n"
+        outstr += fitted + "\n"
 
         try:
-            nparams = [f'{key}: {val}' for key, val in
-                       self.param_dyn_df.nunique().items()]
+            nparams = [
+                f"{key}: {val}" for key, val in self.param_dyn_df.nunique().items()
+            ]
             if len(nparams) > 0:
-                outstr += '# NUMBER OF ESIMATED VALUES ' + '\n'
-                outstr += ',   '.join(nparams) + '\n\n'
+                outstr += "# NUMBER OF ESIMATED VALUES " + "\n"
+                outstr += ",   ".join(nparams) + "\n\n"
         except Exception:
             pass
 
-        outstr += ('-'*9 + ' FIXED PARAMETERS ' + '-'*10 + '|' +
-                   '-'*10 + ' AUXILIARY DATASETS ' + '-'*9) + '\n'
+        outstr += (
+            "-" * 9
+            + " FIXED PARAMETERS "
+            + "-" * 10
+            + "|"
+            + "-" * 10
+            + " AUXILIARY DATASETS "
+            + "-" * 9
+        ) + "\n"
 
         naux = len(max([auxiliary, fixed], key=len))
         for i in range(naux):
             try:
                 outstr += fixed[i].ljust(37)
             except IndexError:
-                outstr += ''.ljust(37)
+                outstr += "".ljust(37)
 
             try:
                 outstr += auxiliary[i].ljust(38)
             except IndexError:
-                outstr += ''.ljust(38)
-            outstr += '\n'
-        outstr += '-'*77
+                outstr += "".ljust(38)
+            outstr += "\n"
+        outstr += "-" * 77
 
         # write least-squares keywords (maintain order)
         lsqkw = self.lsq_kwargs
         keys = [*lsqkw.keys()]
         if len(lsqkw) > 0:
-            outstr += '\n\n# LSQ PARAMETERS ' + '\n'
-            for key1, key2 in zip(keys[:(len(keys) + 1)//2],
-                                  keys[(len(keys) + 1)//2:]):
-                outstr += f' {key1:<15}= {lsqkw[key1]}'.ljust(37) + \
-                          f'  {key2:<15}= {lsqkw[key2]}' + '\n'
-            outstr += '-'*77
+            outstr += "\n\n# LSQ PARAMETERS " + "\n"
+            for key1, key2 in zip(
+                keys[: (len(keys) + 1) // 2], keys[(len(keys) + 1) // 2 :]
+            ):
+                outstr += (
+                    f" {key1:<15}= {lsqkw[key1]}".ljust(37)
+                    + f"  {key2:<15}= {lsqkw[key2]}"
+                    + "\n"
+                )
+            outstr += "-" * 77
         if self.dataset is not None:
             try:
-                outstr += '\n\n# DATASET PROPERTIES ' + '\n'
-                outstr += f'Start-date: {self.dataset.index.min()}'.ljust(37)
-                outstr += f'End-date: {self.dataset.index.max()}\n'
-                outstr += 'Number of observations: '
-                outstr += f'{self.dataset.index.nunique()}'
+                outstr += "\n\n# DATASET PROPERTIES " + "\n"
+                outstr += f"Start-date: {self.dataset.index.min()}".ljust(37)
+                outstr += f"End-date: {self.dataset.index.max()}\n"
+                outstr += "Number of observations: "
+                outstr += f"{self.dataset.index.nunique()}"
             except Exception:
                 pass
 
@@ -2299,17 +2522,23 @@ def model_definition(self):
         print the model-definition
         (use model_definition = fit._model_definition to get the actual string)
         """
-        print(self._model_definition)
+        log.info(self._model_definition)
 
     @classmethod
     def _reinit_object(cls, self, **kwargs):
 
-        args = {'sig0': self.sig0, 'dB': self.dB, 'dataset': self.dataset,
-                'defdict': self.defdict, 'set_V_SRF': self.set_V_SRF,
-                'lsq_kwargs': self.lsq_kwargs, 'int_Q': self.int_Q,
-                'lambda_backend': self.lambda_backend,
-                '_fnevals_input': self._fnevals_input,
-                'interp_vals': self.interp_vals}
+        args = {
+            "sig0": self.sig0,
+            "dB": self.dB,
+            "dataset": self.dataset,
+            "defdict": self.defdict,
+            "set_V_SRF": self.set_V_SRF,
+            "lsq_kwargs": self.lsq_kwargs,
+            "int_Q": self.int_Q,
+            "lambda_backend": self.lambda_backend,
+            "_fnevals_input": self._fnevals_input,
+            "interp_vals": self.interp_vals,
+        }
 
         args.update(**kwargs)
 
@@ -2335,3 +2564,26 @@ def reinit_object(self, **kwargs):
 
         """
         return self._reinit_object(self, **kwargs)
+
+    @property
+    @lru_cache()  # cache this since we need a static reference!
+    def metric(self):
+        """
+        a class to evaluate performance-metrics of variables available in
+            - fit.dataset (e.g. the a-priori available datasets)
+            - fit.calc_model()  (e.g. the estimated total- surface- volume- and
+                                 interaction contribution)
+            - fit.res_df (e.g. the retrieved parameters)
+
+        use it via:
+            >>> fit.metric.KEY1.KEY2.pearsson
+            >>> (0.75, 1.234e-10)
+
+        Returns
+        -------
+        class
+            a rtmetrics.RTmetrics class with the datasets set
+            with respect to KEY1 and KEY2
+        """
+
+        return _metric_keys(self)
diff --git a/rt1/rtmetrics.py b/rt1/rtmetrics.py
new file mode 100644
index 00000000..12ad8b0d
--- /dev/null
+++ b/rt1/rtmetrics.py
@@ -0,0 +1,647 @@
+"""a set of common performance metrics"""
+from scipy import stats
+from functools import lru_cache
+
+from itertools import chain, repeat, permutations
+from operator import itemgetter
+from decimal import Decimal
+
+from .general_functions import groupby_unsorted
+from . import log
+
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+
+
+class _metric_keys(object):
+    """
+    a class to get all available variable-keys that can be used to
+    calculate metrics
+    """
+
+    def __init__(self, fit, d1=None, d2=None, auxdat=None):
+
+        self._datakeys = fit.dataset.select_dtypes(include="number").keys()
+        self._modelkeys = ["tot", "surf", "vol"]
+        self._retrievalkeys = fit.res_dict.keys()
+
+        if auxdat is not None:
+            self._auxkeys = auxdat.keys()
+        else:
+            self._auxkeys = []
+
+        if fit.int_Q is True:
+            self._modelkeys += ["inter"]
+
+        self._check_keys()
+
+        if d1 is not None:
+            assert isinstance(d1, str), "d1 must be a string!"
+            setattr(self, d1, _RTmetrics0())
+
+            if d2 is None:
+                for k2 in self._all_keys:
+                    setattr(
+                        getattr(self, d1),
+                        k2,
+                        _RTmetrics1(
+                            d1=d1,
+                            d2=k2,
+                            fit=fit,
+                            auxdat=auxdat,
+                            all_keys=self._all_keys,
+                        ),
+                    )
+            elif isinstance(d2, str):
+                setattr(getattr(self, d1), d2, _RTmetrics1(d1=d1, d2=d2, fit=fit))
+            else:
+                try:
+                    d2name = d2.name
+                except AttributeError:
+                    d2name = "aux"
+                setattr(
+                    getattr(self, d1),
+                    d2name,
+                    _RTmetrics1(
+                        d1=d1,
+                        d2=d2,
+                        fit=fit,
+                        auxdat=auxdat,
+                        all_keys=self._all_keys,
+                    ),
+                )
+        else:
+            for k1, k2 in permutations(self._all_keys, 2):
+                if not hasattr(self, k1):
+                    setattr(self, k1, _RTmetrics0())
+
+                setattr(
+                    getattr(self, k1),
+                    k2,
+                    _RTmetrics1(
+                        d1=k1,
+                        d2=k2,
+                        fit=fit,
+                        auxdat=auxdat,
+                        all_keys=self._all_keys,
+                    ),
+                )
+
+    def _check_keys(self):
+        # a list of all possible keys that can be used for metric calculations
+        all_keys = chain(
+            self._datakeys, self._modelkeys, self._retrievalkeys, self._auxkeys
+        )
+        # a list of the "sources" that belong to the keys
+        suffix = chain(
+            repeat("dataset", len(self._datakeys)),
+            repeat("calc_model", len(self._modelkeys)),
+            repeat("res_df", len(self._retrievalkeys)),
+            repeat("auxdat", len(self._auxkeys)),
+        )
+        # group by the sources to check if any key is defined more than once
+        grps = groupby_unsorted(
+            zip(suffix, all_keys), key=itemgetter(1), get=itemgetter(0)
+        )
+
+        # make all keys unique (e.g. add a suffix if there are multiple
+        # appearances of the same key -> also warn the user of multiple keys!)
+        newgrps = dict()
+        warnmsg = ""
+        for key, val in grps.items():
+            if len(val) > 1:
+                warnmsg += f'"{key}": '.ljust(15) + "[" + ", ".join(val) + "]"
+                warnmsg += "\n"
+                for i in val:
+                    newgrps[key + "__" + i] = i
+            else:
+                newgrps[key] = val[0]
+
+        if len(warnmsg) > 0:
+            log.warning(
+                "the following keys are present in multiple sources!\n" + warnmsg
+            )
+
+        self._all_keys = newgrps
+
+
+class _RTmetrics0(object):
+    """a dummy class to pass variable names"""
+
+    def __init__(self):
+        pass
+
+
+class _RTmetrics1(object):
+    def __init__(self, d1, d2, fit, auxdat, all_keys):
+
+        assert d1 in all_keys, f'the key "{d1}" could not be found'
+        assert d2 in all_keys, f'the key "{d2}" could not be found'
+
+        self._s1 = all_keys[d1]
+        self._s2 = all_keys[d2]
+
+        if d1.endswith(f"__{self._s1}"):
+            self._d1 = d1[: -len(f"__{self._s1}")]
+        else:
+            self._d1 = d1
+
+        if d2.endswith(f"__{self._s2}"):
+            self._d2 = d2[: -len(f"__{self._s2}")]
+        else:
+            self._d2 = d2
+
+        self.fit = fit
+        self.auxdat = auxdat
+
+    def _get_data(self, source, key):
+        if source == "auxdat":
+            return self.auxdat[key]
+        elif source == "dataset":
+            return self.fit.dataset[key]
+        elif source == "calc_model" and key == "tot":
+            return self.fit.calc_model(return_components=False)[key]
+        elif source == "calc_model":
+            return self.fit.calc_model(return_components=True)[key]
+        elif source == "res_df":
+            return self.fit.res_df[key]
+
+    @property
+    @lru_cache()
+    def d1(self):
+        d1 = self._get_data(self._s1, self._d1)
+        return d1
+
+    @property
+    @lru_cache()
+    def d2(self):
+        d2 = self._get_data(self._s2, self._d2)
+        return d2
+
+    @property
+    @lru_cache()
+    def _unify_idx_data(self):
+        if len(self.d1) != len(self.d2):
+            log.warning(
+                f'index of "{self._d1}" and "{self._d2}" is not '
+                + "the same! -> a concatenation is performed!"
+            )
+
+            # try to unify the index
+            df = pd.concat([self.d1, self.d2], axis=1, copy=False)
+            d1 = df[self._d1].dropna()
+            d2 = df[self._d2].dropna()
+
+            assert len(d1) == len(d2), (
+                "the length of the 2 datasets is "
+                + "not the same!"
+                + f"({len(self.d1)} != {len(d2)})"
+            )
+            return d1, d2
+        else:
+            return self.d1, self.d2
+
+    @property
+    def pearson(self):
+        return RTmetrics.pearson(*self._unify_idx_data)
+
+    @property
+    def spearman(self):
+        return RTmetrics.spearman(*self._unify_idx_data)
+
+    @property
+    def linregress(self):
+        return RTmetrics.linregress(*self._unify_idx_data)
+
+    @property
+    def rmsd(self):
+        return RTmetrics.rmsd(*self._unify_idx_data)
+
+    @property
+    def ub_rmsd(self):
+        return RTmetrics.ub_rmsd(*self._unify_idx_data)
+
+    @property
+    def bias(self):
+        return RTmetrics.bias(*self._unify_idx_data)
+
+    @property
+    def mae(self):
+        return RTmetrics.mae(*self._unify_idx_data)
+
+    @property
+    def mape(self):
+        return RTmetrics.mape(*self._unify_idx_data)
+
+    @property
+    def std_ratio(self):
+        return RTmetrics.std_ratio(*self._unify_idx_data)
+
+    @property
+    def allmetrics(self):
+        return RTmetrics.allmetrics(*self._unify_idx_data)
+
+    @property
+    def metrics_table(self):
+        return RTmetrics.metrics_table(*self._unify_idx_data)
+
+    def scatterplot(self):
+        RTmetrics.scatterplot(*self._unify_idx_data, self._d1, self._d2)
+
+
+class RTmetrics(object):
+
+    # registry of metric methods used for allmetrics and metrics_table
+    # enter the function name of a new metric in here
+    # functions listed in here must have two pandas series d1, d2 as parameters
+    metrics_registry = [
+        "pearson",
+        "spearman",
+        "linregress",
+        "rmsd",
+        "ub_rmsd",
+        "bias",
+        "mae",
+        "mape",
+        "std_ratio",
+    ]
+
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def pearson(d1, d2):
+        """
+        evaluates pearson correlation coefficient of given series d1 and d2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            pearson correlation coefficient
+
+        """
+        return d1.corr(d2, method="pearson")
+
+    @staticmethod
+    def spearman(d1, d2):
+        """
+        evaluates spearman's rank correlation coefficient of given series d1 and d2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            spearman's rank correlation coefficient
+
+        """
+        return d1.corr(d2, method="spearman")
+
+    @staticmethod
+    def linregress(d1, d2):
+        """
+        evaluates pearson correlation coefficient of given series d1 and d2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        dictionary {string: float}
+            float values for slope, intercept, pearson, pvalue, stderr
+
+        """
+        return dict(
+            zip(
+                ["slope", "intercept", "pearson", "pvalue", "stderr"],
+                stats.linregress(d1, d2),
+            )
+        )
+
+    @staticmethod
+    def rmsd(d1, d2):
+        """
+        evaluates root mean square deviation of given series d1 and d2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            root mean square deviation
+
+        """
+        diff_sq = d1.subtract(d2).pow(2)
+        return np.sqrt(diff_sq.mean())
+
+    @staticmethod
+    def ub_rmsd(d1, d2):
+        """
+        evaluates unbiased root mean square deviation of given series d1 and d2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            unbiased root mean square deviation
+
+        """
+        d1_corr = d1 - d1.mean()
+        d2_corr = d2 - d2.mean()
+        diff_sq = d1_corr.subtract(d2_corr).pow(2)
+        return np.sqrt(diff_sq.mean())
+
+    @staticmethod
+    def bias(d1, d2):
+        """
+        evaluates bias of given series d1 and d2: mu_1 - mu_2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            bias
+
+        """
+        return d1.mean() - d2.mean()
+
+    @staticmethod
+    def mae(d1, d2):
+        """
+        evaluates mean absolute error of given Series d1 and d2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            mean absolute error
+
+        """
+        abs_diff = d1.subtract(d2).abs()
+        return abs_diff.mean()
+
+    @staticmethod
+    def mape(d1, d2):
+        """
+        evaluates mean absolute percentage error of given Series d1 and d2 with respect to d1
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            mean absolute percentage error
+
+        """
+        abs_rel_diff = d1.subtract(d2).div(d1).abs()
+        return abs_rel_diff.mean()
+
+    @staticmethod
+    def std_ratio(d1, d2):
+        """
+        evaluates standard deviation ratio of given Series d1 and d2: sigma_1 / sigma_2
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        float
+            standard deviation ratio
+
+        """
+        return d1.std() / d2.std()
+
+    @classmethod
+    def allmetrics(cls, d1, d2):
+        """
+        run all metrics specified in RTmetrics.metrics_registry of given Series d1 and d2
+        metrics have to be specified by function name in metrics_registry
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        dictionary {string: (float or dictionary)}
+            function/metric name and corresponding value
+
+        """
+        return {func: getattr(cls, func)(d1, d2) for func in cls.metrics_registry}
+
+    @classmethod
+    def scatterplot(cls, d1, d2, d1_name, d2_name):
+        """
+        draws scatterplot of two given series d1 and d2
+        list all metrics in a table beside the scatterplot
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+        d1_name : string
+            name of time series 1
+        d2_name : string
+            name of time series 2
+
+        Returns
+        -------
+        scatterplot figure object
+
+        """
+
+        # create plot and axes
+        fig, (ax1, ax2) = plt.subplots(
+            1, 2, figsize=(14, 6), gridspec_kw={"width_ratios": [5, 1]}
+        )
+
+        # create scatterplot
+        ax1.scatter(d1, d2)
+        ax1.set_xlabel(d1_name)
+        ax1.set_ylabel(d2_name)
+
+        # get all metrics and define lists for table data
+        metrics_dict = cls.allmetrics(d1, d2)
+        metric_names = []
+        metric_values = []
+
+        # flatten metrics array and format float values
+        for key, val in cls._flatten_dictionary(metrics_dict).items():
+            metric_names.append(key)
+            if isinstance(val, float):
+                metric_values.append("%1.3f" % val)
+            else:
+                metric_values.append(val)
+
+        # add another dimension for usage in table
+        two_dim_metric_values = [[metric_value] for metric_value in metric_values]
+
+        # remove border to only show table itself
+        ax2.axis("off")
+
+        # plot and create table object
+        metrics_table = ax2.table(
+            cellText=two_dim_metric_values,
+            rowLabels=metric_names,
+            colLabels=["Value"],
+            loc="center",
+        )
+
+        # scale for higher cells
+        metrics_table.scale(1, 1.5)
+
+        plt.show()
+        return fig
+
+    @classmethod
+    def _flatten_dictionary(cls, dictionary, depth=0):
+        """
+        recursively flattens a dictionary, only returns float values from that dictionary
+        keys of sub-dictionaries are prefixed with a '-' according to the depth
+
+        Parameters
+        ----------
+        dictionary : dict
+            dictionary that should be flattened, should contain float or dict values
+        depth : integer
+            recursion depth used for prefixing
+
+        Returns
+        -------
+        dict
+            flat dictionary containing numbers or strings
+
+        """
+        items = []
+        for key, val in dictionary.items():
+            new_key = "-" * depth + " " + key
+            if isinstance(val, dict):
+                items.append((new_key, ""))
+                items.extend(cls._flatten_dictionary(val, depth + 1).items())
+            elif isinstance(val, float):
+                items.append((new_key, val))
+
+        return dict(items)
+
+    @classmethod
+    def metrics_table(cls, d1, d2):
+        """
+        prints a table with all metrics and values returned from the allmetrics method for given series d1 and d2
+        dictionaries returned by allmetrics are handled recursively by _metrics_table_dict_entry
+
+        Parameters
+        ----------
+        d1 : pandas.Series
+            time series 1
+        d2 : pandas.Series
+            time series 2
+
+        Returns
+        -------
+        void
+
+        """
+        metrics_dict = cls.allmetrics(d1, d2)
+
+        header = "-" * 11 + " METRICS " + "-" * 11 + "\n"
+        columns = "     METRIC".ljust(14) + "|     VALUE".ljust(15) + " |" + "\n"
+
+        entries = cls._metrics_table_dict_entry(metrics_dict)
+
+        outstr = header + columns + entries
+        print(outstr)
+
+    @classmethod
+    def _metrics_table_dict_entry(cls, metrics_dict, depth=0):
+        """
+        recursively generates entries string for dictionaries in a metrics table
+        entries of sub-dictionaries are indented
+
+        Parameters
+        ----------
+        metrics_dict : dictionary
+            metrics dictionary to generate string from
+        depth : integer
+            recursion depth
+
+        Returns
+        -------
+        string
+            multiline string containing all floating point entries of a dictionary and its sub-dictionaries
+
+        """
+        entries = ""
+        for key, val in metrics_dict.items():
+            depth_offset = 2 * depth
+            metric = f"{key:<{13 - depth_offset}}"
+
+            if isinstance(val, (float, int)):
+                valstr = (
+                    f"{val:.6f}".ljust(14)
+                    if abs(val) < 1e5
+                    else f"{Decimal(val):.6E}".ljust(14)
+                )
+                entries += " " + "--" * depth + f"{metric}| {valstr}|\n"
+
+            elif isinstance(val, dict):
+                entries += (
+                    " " + "--" * depth + f"{metric}|".ljust(29 - depth_offset) + "|\n"
+                )
+                entries += cls._metrics_table_dict_entry(val, depth + 1)
+
+        return entries
diff --git a/rt1/rtparse.py b/rt1/rtparse.py
index 5bfce65e..d044b234 100644
--- a/rt1/rtparse.py
+++ b/rt1/rtparse.py
@@ -7,6 +7,7 @@
 import importlib.util
 from pathlib import Path
 from .rtfits import Fits
+from . import log
 
 
 class RT1_configparser(object):
@@ -134,8 +135,9 @@ def __init__(self, configpath, interpolation=ExtendedInterpolation()):
         self.configpath = Path(configpath)
         self.interpolation = interpolation
         # setup config (allow empty values -> will result in None)
-        self.config = ConfigParser(allow_no_value=True,
-                                   interpolation=self.interpolation)
+        self.config = ConfigParser(
+            allow_no_value=True, interpolation=self.interpolation
+        )
         # avoid converting uppercase characters
         # (see https://stackoverflow.com/a/19359720/9703451)
         self.config.optionxform = str
@@ -144,47 +146,62 @@ def __init__(self, configpath, interpolation=ExtendedInterpolation()):
         self.cfg = self.config.read(self.configpath)
 
         # keys that will be converted to int, float or bool
-        self.lsq_parse_props = dict(section='least_squares_kwargs',
-                                    int_keys=['verbose', 'max_nfev'],
-                                    float_keys=['ftol', 'gtol', 'xtol'])
-
-        self.fitargs_parse_props = dict(section='fits_kwargs',
-                                        bool_keys=['sig0', 'dB', 'int_Q'],
-                                        int_keys=['verbose'],
-                                        list_keys=[])
+        self.lsq_parse_props = dict(
+            section="least_squares_kwargs",
+            int_keys=["verbose", "max_nfev"],
+            float_keys=["ftol", "gtol", "xtol"],
+        )
+
+        self.fitargs_parse_props = dict(
+            section="fits_kwargs",
+            bool_keys=["sig0", "dB", "int_Q"],
+            int_keys=["verbose"],
+            list_keys=[],
+        )
 
         self._check_integrity()
 
     def _check_integrity(self):
 
         # check if required sections are defined
-        required_sections = ['PROCESS_SPECS', 'CONFIGFILES', 'fits_kwargs',
-                             'least_squares_kwargs', 'defdict', 'RT1_V',
-                             'RT1_SRF']
+        required_sections = [
+            "PROCESS_SPECS",
+            "CONFIGFILES",
+            "fits_kwargs",
+            "least_squares_kwargs",
+            "defdict",
+            "RT1_V",
+            "RT1_SRF",
+        ]
         for key in required_sections:
-            assert key in self.config, (
-                f'a section "{key}" MUST be present in the .ini file!')
+            assert (
+                key in self.config
+            ), f'a section "{key}" MUST be present in the .ini file!'
 
         # check if save_path etc. has been defined
-        required_process_spec_keys = ['path__save_path', 'dumpfolder']
+        required_process_spec_keys = ["path__save_path", "dumpfolder"]
         for key in required_process_spec_keys:
-            assert key in self.config['PROCESS_SPECS'], (
-                f'a key "{key}" MUST be provided in the section PROCESS_SPECS')
+            assert (
+                key in self.config["PROCESS_SPECS"]
+            ), f'a key "{key}" MUST be provided in the section PROCESS_SPECS'
 
         # check if processing-modue is specified
-        if 'processing_cfg_module' in self.config['CONFIGFILES']:
-            proc_module_name = self.config['CONFIGFILES'
-                                           ]['processing_cfg_module']
+        if "processing_cfg_module" in self.config["CONFIGFILES"]:
+            proc_module_name = self.config["CONFIGFILES"]["processing_cfg_module"]
         else:
-            proc_module_name = 'processing_cfg'
-
-        assert ('module__' + proc_module_name) in self.config['CONFIGFILES'], (
-            '"module__' + proc_module_name + '" must be defined in the '
-            + 'CONFIGFILES section... \n(specify "processing_cfg_module" if ' +
-            'you want to use a different name for the processing-module key!)')
-
-    def _parse_dict(self, section, int_keys=[], float_keys=[], bool_keys=[],
-                    list_keys=[]):
+            proc_module_name = "processing_cfg"
+
+        assert ("module__" + proc_module_name) in self.config["CONFIGFILES"], (
+            '"module__'
+            + proc_module_name
+            + '" must be defined in the '
+            + 'CONFIGFILES section... \n(specify "processing_cfg_module" if '
+            + "you want to use a different name for the processing-module key!)"
+        )
+
+    def _parse_dict(
+        self, section, int_keys=[], float_keys=[], bool_keys=[], list_keys=[]
+    ):
         """
         a function to convert the parsed string values to int, float or bool
         (any additional values will be left unchanged)
@@ -217,14 +234,14 @@ def _parse_dict(self, section, int_keys=[], float_keys=[], bool_keys=[],
             elif key in bool_keys:
                 val = inp.getboolean(key)
             elif key in list_keys:
-                if inp[key] == 'None':
+                if inp[key] == "None":
                     val = []
                 else:
-                    val = inp[key].replace(' ', '').split(',')
+                    val = inp[key].replace(" ", "").split(",")
                     val = [i for i in val if len(i) > 0]
             else:
                 # parse None as "real" None
-                if inp[key] == 'None':
+                if inp[key] == "None":
                     val = None
                 else:
                     val = inp[key]
@@ -239,7 +256,7 @@ def _parse_V_SRF(self, section):
         parsed_dict = dict()
         for key in inp:
             val = None
-            if key == 'ncoefs':
+            if key == "ncoefs":
                 val = inp.getint(key)
             else:
                 # try to convert floats, if it fails return the string
@@ -259,38 +276,39 @@ def _parse_defdict(self, section):
         for key in inp:
             val = inp[key]
 
-            if val.startswith('[') and val.endswith(']'):
-                val = val[1:-1].replace(' ', '').split(',')
+            if val.startswith("[") and val.endswith("]"):
+                val = val[1:-1].replace(" ", "").split(",")
             else:
-                assert False, f'the passed defdict for {key} is not a list'
+                assert False, f"the passed defdict for {key} is not a list"
 
             # convert values
             parsed_val = []
-            if val[0] == 'True':
+            if val[0] == "True":
                 parsed_val += [True]
-            elif val[0] == 'False':
+            elif val[0] == "False":
                 parsed_val += [False]
             else:
-                assert False, (f'the passed first defdict-argument of {key} ' +
-                               f'({val[0]}) must be either "True" or "False"')
+                assert False, (
+                    f"the passed first defdict-argument of {key} "
+                    + f'({val[0]}) must be either "True" or "False"'
+                )
 
-            if val[1] == 'auxiliary':
+            if val[1] == "auxiliary":
                 parsed_val += [val[1]]
             else:
                 parsed_val += [float(val[1])]
 
             if len(val) > 2:
-                if val[2] == 'None':
+                if val[2] == "None":
                     parsed_val += [None]
                 else:
                     parsed_val += [val[2]]
 
-                parsed_val += [([float(val[3])],
-                                [float(val[4])])]
+                parsed_val += [([float(val[3])], [float(val[4])])]
 
                 try:
                     interp_Q = val[5]
-                    if interp_Q == 'True':
+                    if interp_Q == "True":
                         parsed_val += [True]
                     else:
                         parsed_val += [False]
@@ -303,7 +321,7 @@ def _parse_defdict(self, section):
 
     def _to_dt(self, s, fmt=None):
         if fmt is None:
-            fmt = '%Y-%m-%d %H:%M:%S.%f'
+            fmt = "%Y-%m-%d %H:%M:%S.%f"
         return datetime.strptime(s, fmt)
 
     def get_config(self):
@@ -319,14 +337,18 @@ def get_config(self):
         lsq_kwargs = self._parse_dict(**self.lsq_parse_props)
 
         fits_kwargs = self._parse_dict(**self.fitargs_parse_props)
-        defdict = self._parse_defdict('defdict')
-        set_V_SRF = dict(V_props=self._parse_V_SRF('RT1_V'),
-                         SRF_props=self._parse_V_SRF('RT1_SRF'))
-
-        return dict(lsq_kwargs=lsq_kwargs,
-                    defdict=defdict,
-                    set_V_SRF=set_V_SRF,
-                    fits_kwargs=fits_kwargs)
+        defdict = self._parse_defdict("defdict")
+        set_V_SRF = dict(
+            V_props=self._parse_V_SRF("RT1_V"),
+            SRF_props=self._parse_V_SRF("RT1_SRF"),
+        )
+
+        return dict(
+            lsq_kwargs=lsq_kwargs,
+            defdict=defdict,
+            set_V_SRF=set_V_SRF,
+            fits_kwargs=fits_kwargs,
+        )
 
     def get_fitobject(self):
         """
@@ -340,9 +362,13 @@ def get_fitobject(self):
         """
         cfg = self.get_config()
 
-        rt1_fits = Fits(dataset=None, defdict=cfg['defdict'],
-                        set_V_SRF=cfg['set_V_SRF'],
-                        lsq_kwargs=cfg['lsq_kwargs'], **cfg['fits_kwargs'])
+        rt1_fits = Fits(
+            dataset=None,
+            defdict=cfg["defdict"],
+            set_V_SRF=cfg["set_V_SRF"],
+            lsq_kwargs=cfg["lsq_kwargs"],
+            **cfg["fits_kwargs"],
+        )
 
         return rt1_fits
 
@@ -365,13 +391,14 @@ def get_all_modules(self, load_copy=False):
         """
 
         processmodules = dict()
-        for key, val in self.config['CONFIGFILES'].items():
-            if not key.startswith('module__'):
+        for key, val in self.config["CONFIGFILES"].items():
+            if not key.startswith("module__"):
                 continue
 
             modulename = key[8:]
-            processmodules[modulename] = self.get_module(modulename,
-                                                         load_copy=load_copy)
+            processmodules[modulename] = self.get_module(
+                modulename, load_copy=load_copy
+            )
 
         return processmodules
 
@@ -396,34 +423,41 @@ def get_module(self, modulename, load_copy=False):
 
         """
 
-        assert f'module__{modulename}' in self.config['CONFIGFILES'], (
-            f'the module "{modulename}" is not defined in the [CONFIGFILES]' +
-            ' section of the .ini file     ...available modules are:  ' +
-            '  ,  '.join(['"' + i.replace('module__', '') + '"'
-                          for i in self.config['CONFIGFILES']
-                          if i.startswith('module__')]))
+        assert f"module__{modulename}" in self.config["CONFIGFILES"], (
+            f'the module "{modulename}" is not defined in the [CONFIGFILES]'
+            + " section of the .ini file     ...available modules are:  "
+            + "  ,  ".join(
+                [
+                    '"' + i.replace("module__", "") + '"'
+                    for i in self.config["CONFIGFILES"]
+                    if i.startswith("module__")
+                ]
+            )
+        )
 
         if load_copy is True:
-            save_path = Path(
-                self.config['PROCESS_SPECS']['path__save_path'].strip())
+            save_path = Path(self.config["PROCESS_SPECS"]["path__save_path"].strip())
 
-            dumpfolder = self.config['PROCESS_SPECS']['dumpfolder'].strip()
+            dumpfolder = self.config["PROCESS_SPECS"]["dumpfolder"].strip()
 
             modulename = Path(
-                self.config['CONFIGFILES'][f'module__{modulename}'].strip()
-                ).name
+                self.config["CONFIGFILES"][f"module__{modulename}"].strip()
+            ).name
 
-            modulepath = save_path / dumpfolder / 'cfg' / modulename
+            modulepath = save_path / dumpfolder / "cfg" / modulename
             assert modulepath.exists(), (
-                f'the module "{modulename}" has not yet been copied to ' +
-                f'"{save_path / dumpfolder / "cfg"}" and can not be ' +
-                'imported using "load_copy=True"')
+                f'the module "{modulename}" has not yet been copied to '
+                + f'"{save_path / dumpfolder / "cfg"}" and can not be '
+                + 'imported using "load_copy=True"'
+            )
         else:
             modulepath = Path(
-                self.config['CONFIGFILES'][f'module__{modulename}'].strip())
+                self.config["CONFIGFILES"][f"module__{modulename}"].strip()
+            )
 
-        spec = importlib.util.spec_from_file_location(name=modulepath.stem,
-                                                      location=modulepath)
+        spec = importlib.util.spec_from_file_location(
+            name=modulepath.stem, location=modulepath
+        )
 
         foo = importlib.util.module_from_spec(spec)
         sys.modules[spec.name] = foo
@@ -441,70 +475,75 @@ def get_process_specs(self):
             the process specs dict.
 
         """
-        inp = self.config['PROCESS_SPECS']
+        inp = self.config["PROCESS_SPECS"]
 
         process_specs = dict()
         for key, val in inp.items():
             # parse None as a 'real' None and not as the string 'None'
-            if val == 'None':
+            if val == "None":
                 val = None
 
-            if key.startswith('datetime__'):
-                date = dict(zip(['s', 'fmt'],
-                                [i.strip() for i in val.split('fmt=')]))
+            if key.startswith("datetime__"):
+                date = dict(zip(["s", "fmt"], [i.strip() for i in val.split("fmt=")]))
                 process_specs[key[10:]] = self._to_dt(**date)
-            elif key.startswith('path__'):
+            elif key.startswith("path__"):
                 # resolve the path in case .. syntax is used to traverse dirs
-                if '..' in val.strip():
+                if ".." in val.strip():
                     try:
                         process_specs[key[6:]] = Path(val.strip()).resolve()
                     except PermissionError:
-                        print('some paths could not be resolved!',
-                              'avoid using ".." syntax in path-specifications',
-                              ' to circuumvent this issue')
+                        log.warning(
+                            "some paths could not be resolved!"
+                            + 'avoid using ".." syntax in path-specifications'
+                            + " to circuumvent this issue"
+                        )
                         process_specs[key[6:]] = Path(val.strip())
                 else:
                     process_specs[key[6:]] = Path(val.strip())
 
-            elif key.startswith('bool__'):
+            elif key.startswith("bool__"):
                 process_specs[key[6:]] = inp.getboolean(key)
-            elif key.startswith('float__'):
+            elif key.startswith("float__"):
                 process_specs[key[7:]] = inp.getfloat(key)
-            elif key.startswith('int__'):
+            elif key.startswith("int__"):
                 process_specs[key[5:]] = inp.getint(key)
-            elif key.startswith('list__'):
+            elif key.startswith("list__"):
                 listkey = key[6:]
-                if val.startswith('[') and (val.endswith(']')
-                                            or 'fmt=' in val):
+                if val.startswith("[") and (val.endswith("]") or "fmt=" in val):
                     # allow direct conversion of values
-                    if listkey.startswith('bool__'):
+                    if listkey.startswith("bool__"):
                         listkey = listkey[6:]
                         conffunc = ConfigParser()._convert_to_boolean
-                    elif listkey.startswith('float__'):
+                    elif listkey.startswith("float__"):
                         listkey = listkey[7:]
                         conffunc = float
-                    elif listkey.startswith('int__'):
+                    elif listkey.startswith("int__"):
                         listkey = listkey[5:]
                         conffunc = int
-                    elif listkey.startswith('datetime__'):
+                    elif listkey.startswith("datetime__"):
                         listkey = listkey[10:]
-                        spl = dict(zip(['val', 'fmt'],
-                                       [i.strip() for i in val.split('fmt=')]))
-                        if 'fmt' in spl:
-                            conffunc = partial(self._to_dt, fmt=spl['fmt'])
+                        spl = dict(
+                            zip(
+                                ["val", "fmt"],
+                                [i.strip() for i in val.split("fmt=")],
+                            )
+                        )
+                        if "fmt" in spl:
+                            conffunc = partial(self._to_dt, fmt=spl["fmt"])
                         else:
                             conffunc = self._to_dt
-                        val = spl['val']
+                        val = spl["val"]
                     else:
+
                         def conffunc(x):
                             return x
 
                     process_specs[listkey] = [
-                        conffunc(i.strip()) for i in
-                        val.strip()[1:-1].split(',')]
+                        conffunc(i.strip()) for i in val.strip()[1:-1].split(",")
+                    ]
 
                 else:
-                    assert False, f'{key} is not a list! (use brackets [] !)'
+                    assert False, f"{key} is not a list! (use brackets [] !)"
             else:
                 process_specs[key] = val
 
diff --git a/rt1/rtplots.py b/rt1/rtplots.py
index abc7d4d5..e6ca4758 100644
--- a/rt1/rtplots.py
+++ b/rt1/rtplots.py
@@ -9,6 +9,7 @@
 import numpy as np
 import sympy as sp
 import pandas as pd
+
 pd.plotting.register_matplotlib_converters()
 
 import matplotlib as mpl
@@ -19,21 +20,33 @@
 from matplotlib.gridspec import GridSpec
 from matplotlib.gridspec import GridSpecFromSubplotSpec
 import matplotlib.ticker as ticker
+
 try:
     from mpl_toolkits.mplot3d import Axes3D
 except:
     pass
 
-from .general_functions import rectangularize, dBsig0convert, meandatetime, \
-    pairwise, split_into
-
-def polarplot(X=None,
-              inc=[15., 35., 55., 75.],
-              multip=2.,
-              label=None, aprox=True,
-              legend=True, legpos=(0.75, 0.5), groundcolor="none",
-              param_dict = [{}],
-              polarax=None):
+from .general_functions import (
+    rectangularize,
+    dBsig0convert,
+    meandatetime,
+    pairwise,
+    split_into,
+)
+
+
+def polarplot(
+    X=None,
+    inc=[15.0, 35.0, 55.0, 75.0],
+    multip=2.0,
+    label=None,
+    aprox=True,
+    legend=True,
+    legpos=(0.75, 0.5),
+    groundcolor="none",
+    param_dict=[{}],
+    polarax=None,
+):
     """
     Generation of polar-plots of the volume- and the surface scattering
     phase function as well as the used approximations in terms of
@@ -86,74 +99,79 @@ def polarplot(X=None,
                specified by V or SRF
     """
 
-    assert isinstance(inc, list), 'Error: incidence-angles for ' + \
-        'polarplot must be a list'
-    assert isinstance(multip, float), 'Error: plotrange-multiplier ' + \
-        'for polarplot must be a floating-point number'
+    assert isinstance(inc, list), (
+        "Error: incidence-angles for " + "polarplot must be a list"
+    )
+    assert isinstance(multip, float), (
+        "Error: plotrange-multiplier " + "for polarplot must be a floating-point number"
+    )
 
     if X is None:
-        assert False, 'Error: You must provide a volume- or surface object!'
+        assert False, "Error: You must provide a volume- or surface object!"
 
     if polarax is None:
         fig = plt.figure(figsize=(7, 7))
-        polarax = fig.add_subplot(111, projection='polar')
+        polarax = fig.add_subplot(111, projection="polar")
     else:
-        assert polarax.name == 'polar', 'you must provide a polar-axes!'
+        assert polarax.name == "polar", "you must provide a polar-axes!"
 
-    if '.surface.' in str(X.__class__):
+    if ".surface." in str(X.__class__):
         if label is None:
-            label = 'Surface-Scattering Phase Function'
-        funcname = 'brdf'
-        angs = ['theta_ex', 'theta_s', 'phi_ex', 'phi_s']
+            label = "Surface-Scattering Phase Function"
+        funcname = "brdf"
+        angs = ["theta_ex", "theta_s", "phi_ex", "phi_s"]
+
         def angsub(ti):
             return ti
-        thetass = np.arange(-np.pi / 2., np.pi / 2., .01)
-
-        polarax.fill(
-            np.arange(np.pi / 2., 3. * np.pi / 2., .01),
-            np.ones_like(np.arange(np.pi / 2.,
-                                   3. * np.pi / 2.,
-                                   .01)
-                         ) * 1 * 1.2, color=groundcolor)
 
+        thetass = np.arange(-np.pi / 2.0, np.pi / 2.0, 0.01)
 
+        polarax.fill(
+            np.arange(np.pi / 2.0, 3.0 * np.pi / 2.0, 0.01),
+            np.ones_like(np.arange(np.pi / 2.0, 3.0 * np.pi / 2.0, 0.01)) * 1 * 1.2,
+            color=groundcolor,
+        )
 
-    if '.volume.' in str(X.__class__):
+    if ".volume." in str(X.__class__):
         if label is None:
-            label = 'Volume-Scattering Phase Function'
+            label = "Volume-Scattering Phase Function"
+
+        funcname = "p"
+        angs = ["theta_0", "theta_s", "phi_0", "phi_s"]
 
-        funcname = 'p'
-        angs = ['theta_0', 'theta_s', 'phi_0', 'phi_s']
         def angsub(ti):
             return np.pi - ti
-        thetass = np.arange(0., 2. * np.pi, .01)
-
 
+        thetass = np.arange(0.0, 2.0 * np.pi, 0.01)
 
     # plot of volume-scattering phase-function's
     pmax = 0
     for n_X, X in enumerate(np.atleast_1d(X)):
         # define a plotfunction of the legendre-approximation of p
         if aprox is True:
-            phasefunktapprox = sp.lambdify((
-                *angs, *param_dict[n_X].keys()),
-                X.legexpansion(*angs, geometry='vvvv').doit(),
-                modules=["numpy", "sympy"])
+            phasefunktapprox = sp.lambdify(
+                (*angs, *param_dict[n_X].keys()),
+                X.legexpansion(*angs, geometry="vvvv").doit(),
+                modules=["numpy", "sympy"],
+            )
 
         # set incidence-angles for which p is calculated
         plottis = np.deg2rad(inc)
-        colors = ['k', 'r',
-                  'g', 'b',
-                  'c', 'm',
-                  'y'] * int(round((len(plottis) / 7. + 1)))
+        colors = ["k", "r", "g", "b", "c", "m", "y"] * int(
+            round((len(plottis) / 7.0 + 1))
+        )
 
         for i in plottis:
-            ts = np.arange(0., 2. * np.pi, .01)
-            pmax_i = multip * np.max(getattr(X, funcname)(np.full_like(ts, i),
-                                        ts,
-                                        0.,
-                                        0.,
-                                        param_dict=param_dict[n_X]))
+            ts = np.arange(0.0, 2.0 * np.pi, 0.01)
+            pmax_i = multip * np.max(
+                getattr(X, funcname)(
+                    np.full_like(ts, i),
+                    ts,
+                    0.0,
+                    0.0,
+                    param_dict=param_dict[n_X],
+                )
+            )
             if pmax_i > pmax:
                 pmax = pmax_i
 
@@ -165,65 +183,93 @@ def angsub(ti):
         for ti in plottis:
             color = colors[i]
             i = i + 1
-            rad = getattr(X, funcname)(ti, thetass, 0., 0.,
-                                       param_dict=param_dict[n_X])
+            rad = getattr(X, funcname)(
+                ti, thetass, 0.0, 0.0, param_dict=param_dict[n_X]
+            )
             if aprox is True:
                 # the use of np.pi-ti stems from the definition
                 # of legexpansion() in volume.py
-                radapprox = phasefunktapprox(angsub(ti),
-                                             thetass,
-                                             0.,
-                                             0.,
-                                             **param_dict[n_X])
+                radapprox = phasefunktapprox(
+                    angsub(ti), thetass, 0.0, 0.0, **param_dict[n_X]
+                )
             # set theta direction to clockwise
             polarax.set_theta_direction(-1)
             # set theta to start at z-axis
-            polarax.set_theta_offset(np.pi / 2.)
+            polarax.set_theta_offset(np.pi / 2.0)
 
             polarax.plot(thetass, rad, color)
             if aprox is True:
-                polarax.plot(thetass, radapprox, color + '--')
-            polarax.arrow(-ti, pmax * 1.2, 0., -pmax * 0.8,
-                          head_width=.0, head_length=.0,
-                          fc=color, ec=color, lw=1, alpha=0.3)
+                polarax.plot(thetass, radapprox, color + "--")
+            polarax.arrow(
+                -ti,
+                pmax * 1.2,
+                0.0,
+                -pmax * 0.8,
+                head_width=0.0,
+                head_length=0.0,
+                fc=color,
+                ec=color,
+                lw=1,
+                alpha=0.3,
+            )
 
             polarax.fill_between(thetass, rad, alpha=0.2, color=color)
-            polarax.set_xticklabels([r'$0^\circ$', r'$45^\circ$',
-                                     r'$90^\circ$', r'$135^\circ$',
-                                     r'$180^\circ$'])
+            polarax.set_xticks(np.deg2rad([0, 45, 90, 125, 180]))
+            polarax.set_xticklabels(
+                [
+                    r"$0^\circ$",
+                    r"$45^\circ$",
+                    r"$90^\circ$",
+                    r"$135^\circ$",
+                    r"$180^\circ$",
+                ]
+            )
             polarax.set_yticklabels([])
             polarax.set_rmax(pmax * 1.2)
-            polarax.set_title(label + '\n')
-            polarax.set_rmin(0.)
+            polarax.set_title(label + "\n")
+            polarax.set_rmin(0.0)
     # add legend for covering layer phase-functions
     if legend is True:
         i = 0
         for ti in plottis:
             color = colors[i]
-            legend_lines += [mlines.Line2D(
-                [], [], color=color,
-                label=r'$\theta_0$ = ' + str(
-                    np.round_(np.rad2deg(ti),
-                              decimals=1)) + r'${}^\circ$')]
+            legend_lines += [
+                mlines.Line2D(
+                    [],
+                    [],
+                    color=color,
+                    label=r"$\theta_0$ = "
+                    + str(np.round_(np.rad2deg(ti), decimals=1))
+                    + r"${}^\circ$",
+                )
+            ]
             i = i + 1
 
         if aprox is True:
-            legend_lines += [mlines.Line2D(
-                [], [], color='k',
-                linestyle='--', label='approx.')]
+            legend_lines += [
+                mlines.Line2D([], [], color="k", linestyle="--", label="approx.")
+            ]
 
-        legend = polarax.legend(bbox_to_anchor=legpos,
-                            loc=2, handles=legend_lines)
-        legend.get_frame().set_facecolor('w')
-        legend.get_frame().set_alpha(.5)
+        legend = polarax.legend(bbox_to_anchor=legpos, loc=2, handles=legend_lines)
+        legend.get_frame().set_facecolor("w")
+        legend.get_frame().set_alpha(0.5)
 
     return fig
 
 
-def hemreflect(R=None, SRF=None, phi_0=0., t_0_step=5., t_0_min=0.,
-               t_0_max=90., simps_N=1000, showpoints=True,
-               returnarray=False, param_dict={}):
-    '''
+def hemreflect(
+    R=None,
+    SRF=None,
+    phi_0=0.0,
+    t_0_step=5.0,
+    t_0_min=0.0,
+    t_0_max=90.0,
+    simps_N=1000,
+    showpoints=True,
+    returnarray=False,
+    param_dict={},
+):
+    """
     Numerical evaluation of the hemispherical reflectance of the given
     BRDF-function using scipy's implementation of the Simpson-rule
     integration scheme.
@@ -268,7 +314,7 @@ def hemreflect(R=None, SRF=None, phi_0=0., t_0_step=5., t_0_min=0.,
     fig : figure
         a matplotlib figure showing the incidence-angle dependent
         hemispherical reflectance
-    '''
+    """
 
     from scipy.integrate import simps
 
@@ -278,8 +324,7 @@ def hemreflect(R=None, SRF=None, phi_0=0., t_0_step=5., t_0_min=0.,
 
         try:
             Nsymb = R.SRF.NormBRDF.free_symbols
-            Nfunc = sp.lambdify(Nsymb, R.SRF.NormBRDF,
-                                modules=['numpy'])
+            Nfunc = sp.lambdify(Nsymb, R.SRF.NormBRDF, modules=["numpy"])
             NormBRDF = Nfunc(*[param_dict[str(i)] for i in Nsymb])
         except Exception:
             NormBRDF = R.SRF.NormBRDF
@@ -287,21 +332,20 @@ def hemreflect(R=None, SRF=None, phi_0=0., t_0_step=5., t_0_min=0.,
         BRDF = SRF.brdf
         try:
             Nsymb = SRF.NormBRDF[0].free_symbols
-            Nfunc = sp.lambdify(Nsymb, SRF.NormBRDF,
-                                modules=['numpy'])
+            Nfunc = sp.lambdify(Nsymb, SRF.NormBRDF, modules=["numpy"])
             NormBRDF = Nfunc(*[param_dict[str(i)] for i in Nsymb])
         except Exception:
             NormBRDF = SRF.NormBRDF
     else:
-        assert False, 'Error: You must provide either R or SRF'
+        assert False, "Error: You must provide either R or SRF"
 
     # set incident (zenith-angle) directions for which the integral
     # should be evaluated!
     incnum = np.arange(t_0_min, t_0_max, t_0_step)
 
     # define grid for integration
-    x = np.linspace(0., np.pi / 2., simps_N)
-    y = np.linspace(0., 2 * np.pi, simps_N)
+    x = np.linspace(0.0, np.pi / 2.0, simps_N)
+    y = np.linspace(0.0, 2 * np.pi, simps_N)
 
     # initialize array for solutions
 
@@ -317,10 +361,12 @@ def hemreflect(R=None, SRF=None, phi_0=0., t_0_step=5., t_0_min=0.,
         # notice the additional  np.sin(thetas)  which oritinates from
         # integrating over theta_s instead of mu_s
         def integfunkt(theta_s, phi_s):
-            return np.sin(theta_s) * np.cos(theta_s) * BRDF(theta_0,
-                                                            theta_s,
-                                                            phi_0, phi_s,
-                                                            param_dict=param_dict)
+            return (
+                np.sin(theta_s)
+                * np.cos(theta_s)
+                * BRDF(theta_0, theta_s, phi_0, phi_s, param_dict=param_dict)
+            )
+
         # evaluate the integral using Simpson's Rule twice
         z = integfunkt(x[:, None], y)
         sol = sol + [simps(simps(z, y), x)]
@@ -328,8 +374,8 @@ def integfunkt(theta_s, phi_s):
     sol = np.array(sol) * NormBRDF
 
     # print warning if the hemispherical reflectance exceeds 1
-    if np.any(sol > 1.):
-        print('ATTENTION, Hemispherical Reflectance > 1 !')
+    if np.any(sol > 1.0):
+        print("ATTENTION, Hemispherical Reflectance > 1 !")
 
     if returnarray is True:
         return sol
@@ -340,30 +386,27 @@ def integfunkt(theta_s, phi_s):
 
         if len(sol.shape) > 1:
             for i, sol in enumerate(sol):
-                axnum.plot(incnum, sol,
-                           label='NormBRDF = ' + str(NormBRDF[i]))
+                axnum.plot(incnum, sol, label="NormBRDF = " + str(NormBRDF[i]))
                 if showpoints is True:
-                    axnum.plot(incnum, sol, 'r.')
+                    axnum.plot(incnum, sol, "r.")
         else:
-            axnum.plot(incnum, sol, 'k',
-                       label='NormBRDF = ' + str(NormBRDF))
+            axnum.plot(incnum, sol, "k", label="NormBRDF = " + str(NormBRDF))
             if showpoints is True:
-                axnum.plot(incnum, sol, 'r.')
+                axnum.plot(incnum, sol, "r.")
 
-        axnum.set_xlabel('$\\theta_0$ [deg]')
-        axnum.set_ylabel('$R(\\theta_0)$')
-        axnum.set_title('Hemispherical reflectance ')
-        axnum.set_ylim(0., np.max(sol) * 1.1)
+        axnum.set_xlabel("$\\theta_0$ [deg]")
+        axnum.set_ylabel("$R(\\theta_0)$")
+        axnum.set_title("Hemispherical reflectance ")
+        axnum.set_ylim(0.0, np.max(sol) * 1.1)
 
         axnum.legend()
 
         axnum.grid()
-        plt.show()
         return fig
 
 
 class plot:
-    '''
+    """
     Generation of plots to visualize rtfits results
 
     - scatter
@@ -396,14 +439,13 @@ class plot:
         a widget to analyze the fit-results of individual timestamps within
         the considered timeseries
 
-    '''
+    """
 
     def __init__(self, fit=None, **kwargs):
         self.fit = fit
 
-    def scatter(self, fit=None, mima=None, pointsize=0.5,
-                regression=True, **kwargs):
-        '''
+    def scatter(self, fit=None, mima=None, pointsize=0.5, regression=True, **kwargs):
+        """
         geerate a scatterplot of modelled vs. original backscatter data
 
         Parameters
@@ -426,8 +468,8 @@ def scatter(self, fit=None, mima=None, pointsize=0.5,
         -------
         fig : matplotlib.figure
             the used matplotlib figure instance
-        '''
-        plot('asdf')
+        """
+        plot("asdf")
         if fit is None:
             fit = self.fit
 
@@ -435,10 +477,12 @@ def scatter(self, fit=None, mima=None, pointsize=0.5,
         ax = fig.add_subplot(111)
 
         # prepare measurements
-        #measures = fit.dataset.sig.values
+        # measures = fit.dataset.sig.values
         # calculate estimates
-        #estimates = fit.calc_model().tot.values
-        measures, estimates = pd.concat([fit.dataset.sig, fit.calc_model().tot], axis=1).values.T
+        # estimates = fit.calc_model().tot.values
+        measures, estimates = pd.concat(
+            [fit.dataset.sig, fit.calc_model().tot], axis=1
+        ).values.T
         if mima is None:
             mi = np.min((measures, estimates))
             ma = np.max((measures, estimates))
@@ -448,44 +492,64 @@ def scatter(self, fit=None, mima=None, pointsize=0.5,
         ax.scatter(estimates, measures, s=pointsize, alpha=0.7, **kwargs)
 
         # plot 45degree-line
-        ax.plot([mi, ma], [mi, ma], 'k--')
+        ax.plot([mi, ma], [mi, ma], "k--")
 
         if fit.sig0 is True:
-            quantity = r'$\sigma_0$'
+            quantity = r"$\sigma_0$"
         else:
-            quantity = 'Intensity'
+            quantity = "Intensity"
 
         if fit.dB is True:
-            scale = '[dB]'
+            scale = "[dB]"
         else:
-            scale = ''
+            scale = ""
 
-        ax.set_xlabel('modelled ' + quantity + scale)
-        ax.set_ylabel('measured ' + quantity + scale)
+        ax.set_xlabel("modelled " + quantity + scale)
+        ax.set_ylabel("measured " + quantity + scale)
 
         if regression is True:
             from scipy.stats import linregress
-            # evaluate linear regression to get r-value etc.
-            slope, intercept, r_value, p_value, std_err = linregress(estimates,
-                                                                     measures)
 
-            ax.plot(np.sort(measures),
-                    intercept + slope * np.sort(measures), 'r--', alpha=0.4)
-
-            ax.text(0.8, .1, '$R^2$ = ' + str(np.round(r_value**2, 2)),
-                    horizontalalignment='center',
-                    verticalalignment='center',
-                    transform=ax.transAxes)
+            # evaluate linear regression to get r-value etc.
+            slope, intercept, r_value, p_value, std_err = linregress(
+                estimates, measures
+            )
+
+            ax.plot(
+                np.sort(measures),
+                intercept + slope * np.sort(measures),
+                "r--",
+                alpha=0.4,
+            )
+
+            ax.text(
+                0.8,
+                0.1,
+                "$R^2$ = " + str(np.round(r_value ** 2, 2)),
+                horizontalalignment="center",
+                verticalalignment="center",
+                transform=ax.transAxes,
+            )
 
         return fig
 
-
-    def fit_timeseries(self, fit=None, dB=True, sig0=True, params=None,
-                       printtot = True, printsurf = True,
-                       printvol = True, printint = True,
-                       printorig = True, months = None, years = None,
-                       ylim=None, printinc = True):
-        '''
+    def fit_timeseries(
+        self,
+        fit=None,
+        dB=True,
+        sig0=True,
+        params=None,
+        printtot=True,
+        printsurf=True,
+        printvol=True,
+        printint=True,
+        printorig=True,
+        months=None,
+        years=None,
+        ylim=None,
+        printinc=True,
+    ):
+        """
         Print individual contributions, resulting parameters and the
         reference dataset of an rt1.rtfits object as timeseries.
 
@@ -518,12 +582,11 @@ def fit_timeseries(self, fit=None, dB=True, sig0=True, params=None,
         --------------
         f : matplotlib.figure
             the used matplotlib figure instance
-        '''
+        """
 
         if fit is None:
             fit = self.fit
 
-
         # get incidence-angles
         inc_array = np.ma.masked_array(fit.inc, fit.mask)
         inc = inc_array.compressed()
@@ -532,25 +595,31 @@ def fit_timeseries(self, fit=None, dB=True, sig0=True, params=None,
 
         # calculate individual contributions
         contrib = fit.calc_model(return_components=True)
-        contrib['$\\sigma_0$ dataset'] = fit.dataset.sig
-        contrib['inc'] = fit.dataset.inc
+        contrib["$\\sigma_0$ dataset"] = fit.dataset.sig
+        contrib["inc"] = fit.dataset.inc
 
         # convert units
-        complist = [i for i in contrib.keys() if i not in ['inc']]
-        contrib[complist] = contrib[complist].apply(dBsig0convert,
-                                                    inc=inc, dB=dB, sig0=sig0,
-                                                    fitdB=fit.dB,
-                                                    fitsig0=fit.sig0)
-
+        complist = [i for i in contrib.keys() if i not in ["inc"]]
+        contrib[complist] = contrib[complist].apply(
+            dBsig0convert,
+            inc=inc,
+            dB=dB,
+            sig0=sig0,
+            fitdB=fit.dB,
+            fitsig0=fit.sig0,
+        )
 
         # drop unneeded columns
-        if printint is False and 'inter' in contrib:
-            contrib = contrib.drop('inter', axis=1)
-        if printtot is False: contrib = contrib.drop('tot', axis=1)
-        if printsurf is False: contrib = contrib.drop('surf', axis=1)
-        if printvol is False: contrib = contrib.drop('vol', axis=1)
+        if printint is False and "inter" in contrib:
+            contrib = contrib.drop("inter", axis=1)
+        if printtot is False:
+            contrib = contrib.drop("tot", axis=1)
+        if printsurf is False:
+            contrib = contrib.drop("surf", axis=1)
+        if printvol is False:
+            contrib = contrib.drop("vol", axis=1)
         if printorig is False:
-            contrib = contrib.drop('$\\sigma_0$ dataset', axis=1)
+            contrib = contrib.drop("$\\sigma_0$ dataset", axis=1)
 
         # select years and months
         if years is not None:
@@ -560,51 +629,71 @@ def fit_timeseries(self, fit=None, dB=True, sig0=True, params=None,
 
         # print incidence-angle dependency
         if printinc is True:
-            f, [ax, ax_inc] = plt.subplots(ncols=2, figsize=(15,5),
-                                           gridspec_kw={'width_ratios':[3,1]},
-                                           sharey=True)
-            f.subplots_adjust(left=0.05, right=0.98, top=0.98,
-                              bottom=0.1, wspace=0.1)
+            f, [ax, ax_inc] = plt.subplots(
+                ncols=2,
+                figsize=(15, 5),
+                gridspec_kw={"width_ratios": [3, 1]},
+                sharey=True,
+            )
+            f.subplots_adjust(left=0.05, right=0.98, top=0.98, bottom=0.1, wspace=0.1)
 
             # -------------------
-            color = {'tot':'r', 'surf':'b', 'vol':'g', 'inter':'y',
-                     '$\\sigma_0$ dataset':'k'}
+            color = {
+                "tot": "r",
+                "surf": "b",
+                "vol": "g",
+                "inter": "y",
+                "$\\sigma_0$ dataset": "k",
+            }
 
             groupedcontrib = contrib.groupby(contrib.index)
 
-            #return contrib, groupedcontrib
+            # return contrib, groupedcontrib
             for label in contrib.keys():
-                if label in ['inc']: continue
-                a=np.rad2deg(rectangularize(
-                    [x.values for _, x in groupedcontrib['inc']],
-                    return_masked=True)).T
-                b=np.array(rectangularize(
-                    [x.values for _, x in groupedcontrib[label]],
-                    return_masked=True)).T
-                x = (np.array([a,b]).T)
-
-                l_col = mpl.collections.LineCollection(x,linewidth =.25,
-                                                       label='x',
-                                                       color=color[label],
-                                                       alpha = 0.5)
+                if label in ["inc"]:
+                    continue
+                a = np.rad2deg(
+                    rectangularize(
+                        [x.values for _, x in groupedcontrib["inc"]],
+                        return_masked=True,
+                    )
+                ).T
+                b = np.array(
+                    rectangularize(
+                        [x.values for _, x in groupedcontrib[label]],
+                        return_masked=True,
+                    )
+                ).T
+                x = np.array([a, b]).T
+
+                l_col = mpl.collections.LineCollection(
+                    x, linewidth=0.25, label="x", color=color[label], alpha=0.5
+                )
                 ax_inc.add_collection(l_col)
                 ax_inc.scatter(a, b, color=color[label], s=1)
                 ax_inc.set_xlim(a.min(), a.max())
-                ax_inc.set_xlabel('$\\theta_0$')
-            ax_inc.set_xlabel('$\\theta_0$')
-
+                ax_inc.set_xlabel("$\\theta_0$")
+            ax_inc.set_xlabel("$\\theta_0$")
 
         else:
-            f, ax = plt.subplots(figsize=(12,5))
-            f.subplots_adjust(left=0.05, right=0.98, top=0.98,
-                              bottom=0.1, wspace=0.05)
+            f, ax = plt.subplots(figsize=(12, 5))
+            f.subplots_adjust(left=0.05, right=0.98, top=0.98, bottom=0.1, wspace=0.05)
 
         for label, val in contrib.items():
-            if label in ['inc']: continue
-            color = {'tot':'r', 'surf':'b', 'vol':'g', 'inter':'y'}
-            if printorig is True: color['$\\sigma_0$ dataset'] = 'k'
-            ax.plot(val.sort_index(), linewidth =.25, marker='.',
-                    ms=2, label=label, color=color[label], alpha = 0.5)
+            if label in ["inc"]:
+                continue
+            color = {"tot": "r", "surf": "b", "vol": "g", "inter": "y"}
+            if printorig is True:
+                color["$\\sigma_0$ dataset"] = "k"
+            ax.plot(
+                val.sort_index(),
+                linewidth=0.25,
+                marker=".",
+                ms=2,
+                label=label,
+                color=color[label],
+                alpha=0.5,
+            )
         # overprint parameters
         if params != None:
             paramdf = fit.res_df
@@ -616,40 +705,42 @@ def fit_timeseries(self, fit=None, dB=True, sig0=True, params=None,
 
             pax = ax.twinx()
             for k in params:
-                pax.plot(paramdf[k], lw=1, marker='.', ms=2, label=k)
-            pax.legend(loc='upper right', ncol=5)
-            pax.set_ylabel('parameter-values')
+                pax.plot(paramdf[k], lw=1, marker=".", ms=2, label=k)
+            pax.legend(loc="upper right", ncol=5)
+            pax.set_ylabel("parameter-values")
 
         # format datetime index
         ax.xaxis.set_minor_locator(mpl.dates.MonthLocator())
-        ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter('%m'))
+        ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter("%m"))
         ax.xaxis.set_major_locator(mpl.dates.YearLocator())
-        ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('\n%Y'))
+        ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("\n%Y"))
 
         # set ylabels
         if sig0 is True:
-            label = '$\\sigma_0$'
+            label = "$\\sigma_0$"
         else:
-            label = 'Intensity'
-        if dB is True: label += ' [dB]'
+            label = "Intensity"
+        if dB is True:
+            label += " [dB]"
         ax.set_ylabel(label)
 
         # generate legend
         hand, lab = ax.get_legend_handles_labels()
         lab, unique_ind = np.unique(lab, return_index=True)
-        ax.legend(handles = list(np.array(hand)[unique_ind]),
-                  labels=list(lab), loc='upper left',
-                  ncol=5)
+        ax.legend(
+            handles=list(np.array(hand)[unique_ind]),
+            labels=list(lab),
+            loc="upper left",
+            ncol=5,
+        )
 
         if ylim is not None:
             ax.set_ylim(ylim)
 
         return f
 
-
-    def fit_errors(self, fit=None, relative=False,
-                   result_selection='all'):
-        '''
+    def fit_errors(self, fit=None, relative=False, result_selection="all"):
+        """
         a function to quickly print residuals for each measurement
         and for each incidence-angle value
 
@@ -665,12 +756,12 @@ def fit_errors(self, fit=None, relative=False,
         --------------
         fig : matplotlib.figure
             the used matplotlib figure instance
-        '''
+        """
 
         if fit is None:
             fit = self.fit
 
-        if result_selection == 'all':
+        if result_selection == "all":
             result_selection = range(len(fit.data))
 
         # Calculate the residuals (based on R, inc and res_dict)
@@ -683,7 +774,7 @@ def fit_errors(self, fit=None, relative=False,
         masked_estimates = np.ma.masked_array(estimates, mask=fit.mask)
         masked_data = np.ma.masked_array(fit.data, mask=fit.mask)
 
-        res = np.ma.sqrt((masked_estimates - masked_data)**2)
+        res = np.ma.sqrt((masked_estimates - masked_data) ** 2)
 
         if relative is True:
             res = res / masked_estimates
@@ -695,57 +786,83 @@ def fit_errors(self, fit=None, relative=False,
         fig = plt.figure(figsize=(14, 10))
         ax = fig.add_subplot(212)
         if relative is True:
-            ax.set_title('Mean relative residual per measurement')
+            ax.set_title("Mean relative residual per measurement")
         else:
-            ax.set_title('Mean absolute residual per measurement')
+            ax.set_title("Mean absolute residual per measurement")
 
         ax2 = fig.add_subplot(211)
         if relative is True:
-            ax2.set_title('Relative residuals per incidence-angle')
+            ax2.set_title("Relative residuals per incidence-angle")
         else:
-            ax2.set_title('Residuals per incidence-angle')
+            ax2.set_title("Residuals per incidence-angle")
 
         # the use of masked arrays might cause python 2 compatibility issues!
-        ax.plot(fit.index[result_selection], res[result_selection], '.',
-                alpha=0.5)
+        ax.plot(fit.index[result_selection], res[result_selection], ".", alpha=0.5)
 
         # plot mean residual for each measurement
-        ax.plot(fit.index[result_selection], np.ma.mean(res[result_selection],
-                                                        axis=1),
-                   'k', linewidth=3, marker='o', fillstyle='none')
+        ax.plot(
+            fit.index[result_selection],
+            np.ma.mean(res[result_selection], axis=1),
+            "k",
+            linewidth=3,
+            marker="o",
+            fillstyle="none",
+        )
 
         # plot total mean of mean residuals per measurement
-        ax.plot(fit.index[result_selection],
-                   [np.ma.mean(np.ma.mean(res[result_selection],
-                                          axis=1))] * len(result_selection),
-                   'k--')
+        ax.plot(
+            fit.index[result_selection],
+            [np.ma.mean(np.ma.mean(res[result_selection], axis=1))]
+            * len(result_selection),
+            "k--",
+        )
 
         # add some legends
         res_h = mlines.Line2D(
-            [], [], color='black', label='Mean res.  per measurement',
-            linestyle='-', linewidth=3, marker='o', fillstyle='none')
+            [],
+            [],
+            color="black",
+            label="Mean res.  per measurement",
+            linestyle="-",
+            linewidth=3,
+            marker="o",
+            fillstyle="none",
+        )
         res_h_dash = mlines.Line2D(
-            [], [], color='black', linestyle='--', label='Average mean res.',
-            linewidth=1, fillstyle='none')
+            [],
+            [],
+            color="black",
+            linestyle="--",
+            label="Average mean res.",
+            linewidth=1,
+            fillstyle="none",
+        )
 
         res_h_dots = mlines.Line2D(
-            [], [], color='black', label='Residuals',
-            linestyle='-', linewidth=0, marker='.', alpha=0.5)
+            [],
+            [],
+            color="black",
+            label="Residuals",
+            linestyle="-",
+            linewidth=0,
+            marker=".",
+            alpha=0.5,
+        )
 
         handles, labels = ax.get_legend_handles_labels()
-        ax.legend(handles=handles + [res_h_dots] + [res_h] + [res_h_dash],
-                     loc=1)
+        ax.legend(handles=handles + [res_h_dots] + [res_h] + [res_h_dash], loc=1)
 
-        ax.set_ylabel('Residual')
+        ax.set_ylabel("Residual")
 
-    #        # evaluate mean residuals per incidence-angle
+        #        # evaluate mean residuals per incidence-angle
         meanincs = np.ma.unique(np.concatenate(inc[result_selection]))
-        mean = np.full_like(meanincs, 0.)
+        mean = np.full_like(meanincs, 0.0)
 
         for a, incval in enumerate(meanincs):
             select = np.where(inc[result_selection] == incval)
-            res_selected = res[result_selection][select[0][:, np.newaxis],
-                               select[1][:, np.newaxis]]
+            res_selected = res[result_selection][
+                select[0][:, np.newaxis], select[1][:, np.newaxis]
+            ]
             mean[a] = np.ma.mean(res_selected)
 
         sortpattern = np.argsort(meanincs)
@@ -755,52 +872,66 @@ def fit_errors(self, fit=None, relative=False,
         # plot residuals per incidence-angle for each measurement
         for i, resval in enumerate(res[result_selection]):
             sortpattern = np.argsort(inc[result_selection[i]])
-            ax2.plot(inc[result_selection[i]][sortpattern], resval[sortpattern],
-                        ':', alpha=0.5, marker='.')
+            ax2.plot(
+                inc[result_selection[i]][sortpattern],
+                resval[sortpattern],
+                ":",
+                alpha=0.5,
+                marker=".",
+            )
 
         # plot mean residual per incidence-angle
-        ax2.plot(meanincs, mean,
-                    'k', linewidth=3, marker='o', fillstyle='none')
+        ax2.plot(meanincs, mean, "k", linewidth=3, marker="o", fillstyle="none")
 
         # add some legends
         res_h2 = mlines.Line2D(
-            [], [], color='black', label='Mean res.  per inc-angle',
-            linestyle='-', linewidth=3, marker='o', fillstyle='none')
+            [],
+            [],
+            color="black",
+            label="Mean res.  per inc-angle",
+            linestyle="-",
+            linewidth=3,
+            marker="o",
+            fillstyle="none",
+        )
         res_h_lines = mlines.Line2D(
-            [], [], color='black', label='Residuals',
-            linestyle=':', alpha=0.5)
+            [], [], color="black", label="Residuals", linestyle=":", alpha=0.5
+        )
 
         handles2, labels2 = ax2.get_legend_handles_labels()
         ax2.legend(handles=handles2 + [res_h_lines] + [res_h2], loc=1)
 
-        ax2.set_xlabel('$\\theta_0$ [deg]')
-        ax2.set_ylabel('Residual')
+        ax2.set_xlabel("$\\theta_0$ [deg]")
+        ax2.set_ylabel("Residual")
 
         # find minimum and maximum incidence angle
         maxinc = np.max(inc)
         mininc = np.min(inc)
 
-        ax2.set_xlim(np.floor(mininc) - 1,
-                        np.ceil(maxinc) + 1)
+        ax2.set_xlim(np.floor(mininc) - 1, np.ceil(maxinc) + 1)
 
         # set major and minor ticks
         ax2.xaxis.set_major_locator(plt.MultipleLocator(1))
-        ax2.xaxis.set_major_formatter(plt.FormatStrFormatter('%d'))
-        ax2.xaxis.set_minor_locator(plt.MultipleLocator(.25))
+        ax2.xaxis.set_major_formatter(plt.FormatStrFormatter("%d"))
+        ax2.xaxis.set_minor_locator(plt.MultipleLocator(0.25))
 
         # set ticks
         if isinstance(fit.index[0], datetime.datetime):
-            plt.setp(ax.get_xticklabels(), rotation=30, ha='right')
+            plt.setp(ax.get_xticklabels(), rotation=30, ha="right")
 
         fig.tight_layout()
 
         return fig
 
-
-    def results(self, fit=None, startvals=False,
-                 legend=False, result_selection='all',
-                 legend_fmt='%d.%m.%Y %H:%M:%S'):
-        '''
+    def results(
+        self,
+        fit=None,
+        startvals=False,
+        legend=False,
+        result_selection="all",
+        legend_fmt="%d.%m.%Y %H:%M:%S",
+    ):
+        """
         a function to quickly print the fit-results and the gained parameters
 
         Parameters
@@ -827,19 +958,18 @@ def results(self, fit=None, startvals=False,
         --------------
         fig : matplotlib.figure
               the used matplotlib figure instance
-        '''
+        """
 
         if fit is None:
             fit = self.fit
 
-        if result_selection == 'all':
+        if result_selection == "all":
             result_selection = range(len(fit.data))
 
-
         # generate figure
         fig = plt.figure(figsize=(14, 10))
         ax = fig.add_subplot(211)
-        ax.set_title('Fit-results')
+        ax.set_title("Fit-results")
 
         # calculate results
         fitplot = fit._calc_model()
@@ -847,18 +977,19 @@ def results(self, fit=None, startvals=False,
         masked_data = np.ma.masked_array(fit.data, fit.mask)
         # get labels
         try:
-            labels = pd.to_datetime(fit.fit_index
-                                    ).strftime(legend_fmt)[result_selection]
+            labels = pd.to_datetime(fit.fit_index).strftime(legend_fmt)[
+                result_selection
+            ]
         except:
             labels = result_selection
 
         for i in result_selection:
-            l, = ax.plot(fit.inc[i], fitplot[i], alpha=0.4, label=labels[i])
-            ax.plot(fit.inc[i], masked_data[i], '.', c=l.get_color())
+            (l,) = ax.plot(fit.inc[i], fitplot[i], alpha=0.4, label=labels[i])
+            ax.plot(fit.inc[i], masked_data[i], ".", c=l.get_color())
 
         if legend is True:
             if len(result_selection) > 20:
-                print('more than 20 lines are plotted... legend is disabled')
+                print("more than 20 lines are plotted... legend is disabled")
             else:
                 ax.legend(loc=1)
 
@@ -867,54 +998,64 @@ def results(self, fit=None, startvals=False,
             startplot = fit._calc_model(res_dict=fit.start_dict)
             for i, val in enumerate(startplot[result_selection]):
                 if i == 0:
-                    label = 'fitstart'
+                    label = "fitstart"
                 else:
-                    label = ''
-                ax.plot(fit.inc[result_selection[i]], val, 'k--', linewidth=1,
-                        alpha=0.5, label=label)
+                    label = ""
+                ax.plot(
+                    fit.inc[result_selection[i]],
+                    val,
+                    "k--",
+                    linewidth=1,
+                    alpha=0.5,
+                    label=label,
+                )
 
         if fit.sig0 is False:
-            label = '$I^{tot}$'
+            label = "$I^{tot}$"
         elif fit.sig0 is True:
-            label= r'$\sigma_0^{tot}$'
+            label = r"$\sigma_0^{tot}$"
 
         if fit.dB is True:
-            label += ' [dB]'
+            label += " [dB]"
 
         ax.set_ylabel(label)
-        ax.set_xlabel('$\\theta_0$')
-
+        ax.set_xlabel("$\\theta_0$")
 
         ax2 = fig.add_subplot(212)
-        ax2.set_title('Estimated parameters')
-        ax2.set_ylabel('Parameters')
-
+        ax2.set_title("Estimated parameters")
+        ax2.set_ylabel("Parameters")
 
         # plot fitted values
         # assign colors
-        colordict = {key:f'C{i%10}' for
-                     i, key in enumerate(fit.res_dict.keys())}
-
+        colordict = {key: f"C{i%10}" for i, key in enumerate(fit.res_dict.keys())}
 
         for key, val in fit.res_df.items():
-            ax2.plot(val, alpha=1., label=key, color=colordict[key])
+            ax2.plot(val, alpha=1.0, label=key, color=colordict[key])
 
         if len(result_selection) < len(fit.data):
             for i, resid in enumerate(result_selection):
-                ax2.text(fit.index[resid],
-                         ax2.get_ylim()[1]*.9,
-                         resid,
-                         bbox=dict(facecolor=f'C{i}', alpha=0.5))
+                ax2.text(
+                    fit.index[resid],
+                    ax2.get_ylim()[1] * 0.9,
+                    resid,
+                    bbox=dict(facecolor=f"C{i}", alpha=0.5),
+                )
         ax2.legend(loc=1)
 
         fig.tight_layout()
         return fig
 
-
-    def single_results(self, fit=None, fit_numbers=None, fit_indexes=None,
-                    hexbinQ=True, hexbinargs={},
-                    convertTodB=False, datetime_unit='h'):
-        '''
+    def single_results(
+        self,
+        fit=None,
+        fit_numbers=None,
+        fit_indexes=None,
+        hexbinQ=True,
+        hexbinargs={},
+        convertTodB=False,
+        datetime_unit="h",
+    ):
+        """
         a function to investigate the quality of the individual fits
 
 
@@ -943,19 +1084,24 @@ def single_results(self, fit=None, fit_numbers=None, fit_indexes=None,
         --------------
         fig : matplotlib.figure
               the used matplotlib figure instance
-        '''
+        """
         if fit is None:
             fit = self.fit
 
         if fit_numbers is not None and fit_indexes is not None:
-            assert False, 'please provide EITHER fit_numbers OR fit_indexes!'
+            assert False, "please provide EITHER fit_numbers OR fit_indexes!"
         elif fit_indexes is not None:
-            #fit_numbers = np.where(fit.index.isin(fit_indexes))[0]
+            # fit_numbers = np.where(fit.index.isin(fit_indexes))[0]
             fit_numbers = np.argmin(
-                np.abs(fit.fit_index -
-                       np.expand_dims(np.atleast_1d(
-                           np.array(fit_indexes, dtype=fit.index.dtype)),-1)),
-                axis=1)
+                np.abs(
+                    fit.fit_index
+                    - np.expand_dims(
+                        np.atleast_1d(np.array(fit_indexes, dtype=fit.index.dtype)),
+                        -1,
+                    )
+                ),
+                axis=1,
+            )
 
         elif fit_numbers is None and fit_indexes is None:
             fit_numbers = [1]
@@ -969,14 +1115,13 @@ def CustomCmap(from_rgb, to_rgb):
             # to color r,g,b
             r2, g2, b2 = to_rgb
 
-            cdict = {'red': ((0, r1, r1),
-                             (1, r2, r2)),
-                     'green': ((0, g1, g1),
-                               (1, g2, g2)),
-                     'blue': ((0, b1, b1),
-                              (1, b2, b2))}
+            cdict = {
+                "red": ((0, r1, r1), (1, r2, r2)),
+                "green": ((0, g1, g1), (1, g2, g2)),
+                "blue": ((0, b1, b1), (1, b2, b2)),
+            }
 
-            cmap = LinearSegmentedColormap('custom_cmap', cdict)
+            cmap = LinearSegmentedColormap("custom_cmap", cdict)
             return cmap
 
         estimates = fit._calc_model()
@@ -988,80 +1133,90 @@ def CustomCmap(from_rgb, to_rgb):
         for m_i, m in enumerate(fit_numbers):
 
             if convertTodB is True:
-                y = 10.*np.log10(estimates[m][~fit.mask[m]])
+                y = 10.0 * np.log10(estimates[m][~fit.mask[m]])
             else:
                 y = estimates[m][~fit.mask[m]]
 
             # plot data
             nindexes = len(indexsplits[m])
             if nindexes > 1:
-                mindate = np.datetime_as_string(indexsplits[m][0],
-                                                unit=datetime_unit)
-                maxdate = np.datetime_as_string(indexsplits[m][-1],
-                                                unit=datetime_unit)
+                mindate = np.datetime_as_string(indexsplits[m][0], unit=datetime_unit)
+                maxdate = np.datetime_as_string(indexsplits[m][-1], unit=datetime_unit)
                 if mindate == maxdate:
-                    label = f'{mindate} [{nindexes}]'
+                    label = f"{mindate} [{nindexes}]"
                 else:
-                    label = f'({mindate} - {maxdate}) [{nindexes}]'
+                    label = f"({mindate} - {maxdate}) [{nindexes}]"
             else:
-                label = np.datetime_as_string(indexsplits[m][0],
-                                              unit=datetime_unit)
+                label = np.datetime_as_string(indexsplits[m][0], unit=datetime_unit)
 
             xdata = np.rad2deg(fit.inc[m][~fit.mask[m]])
 
             if convertTodB is True:
-                ydata = 10.*np.log10(fit.data[m][~fit.mask[m]])
+                ydata = 10.0 * np.log10(fit.data[m][~fit.mask[m]])
             else:
                 ydata = fit.data[m][~fit.mask[m]]
 
             # get color that will be applied to the next line drawn
-            dummy, = ax.plot(xdata[0], ydata[0], '.', alpha=0.)
+            (dummy,) = ax.plot(xdata[0], ydata[0], ".", alpha=0.0)
             color = dummy.get_color()
 
             if hexbinQ is True:
-                args = dict(gridsize=15, mincnt=1,
-                            linewidths=0., vmin=0.5, alpha=0.7)
+                args = dict(gridsize=15, mincnt=1, linewidths=0.0, alpha=0.7)
                 args.update(hexbinargs)
 
                 # evaluate the hexbinplot once to get the maximum number of
                 # datapoints within a single hexagonal (used for normalization)
                 dummyargs = args.copy()
-                dummyargs.update({'alpha': 0.})
+                dummyargs.update({"alpha": 0.0})
                 hb = ax.hexbin(xdata, ydata, **dummyargs)
 
                 # generate colormap that fades from white to the color
                 # of the plotted data  (asdf.get_color())
-                cmap = CustomCmap([1.00, 1.00, 1.00],
-                                  plt.cm.colors.hex2color(color))
+                cmap = CustomCmap([1.00, 1.00, 1.00], plt.cm.colors.hex2color(color))
                 # setup correct normalizing instance
                 norm = Normalize(vmin=0, vmax=hb.get_array().max())
 
                 ax.hexbin(xdata, ydata, cmap=cmap, norm=norm, **args)
 
             # plot datapoints
-            asdf, = ax.plot(xdata, ydata, '.',
-                            color=color, alpha=1.,
-                            label=label, markersize=10)
+            (asdf,) = ax.plot(
+                xdata,
+                ydata,
+                ".",
+                color=color,
+                alpha=1.0,
+                label=label,
+                markersize=10,
+            )
 
             # plot results
             iii = fit.inc[m][~fit.mask[m]]
-            ax.plot(np.rad2deg(iii[np.argsort(iii)]), y[np.argsort(iii)],
-                    '-', color='w', linewidth=3)
-
-            ax.plot(np.rad2deg(iii[np.argsort(iii)]), y[np.argsort(iii)],
-                    '-', color=asdf.get_color(), linewidth=2, marker='x')
-
-        ax.set_xlabel('$\\theta_0$ [deg]')
-        ax.set_ylabel('$\\sigma_0$ [dB]')
-
-        ax.legend(title='# Measurement')
+            ax.plot(
+                np.rad2deg(iii[np.argsort(iii)]),
+                y[np.argsort(iii)],
+                "-",
+                color="w",
+                linewidth=3,
+            )
+
+            ax.plot(
+                np.rad2deg(iii[np.argsort(iii)]),
+                y[np.argsort(iii)],
+                "-",
+                color=asdf.get_color(),
+                linewidth=2,
+                marker="x",
+            )
+
+        ax.set_xlabel("$\\theta_0$ [deg]")
+        ax.set_ylabel("$\\sigma_0$ [dB]")
+
+        ax.legend(title="# Measurement")
 
         return fig
 
-
-    def intermediate_results(self, fit=None, params = None,
-                             cmaps=None):
-        '''
+    def intermediate_results(self, fit=None, params=None, cmaps=None):
+        """
         a function to plot the intermediate-results
         (the data is only available if rtfits.performfit has been called with
         the argument intermediate_results=True!)
@@ -1080,7 +1235,7 @@ def intermediate_results(self, fit=None, params = None,
         -------
         f : matplotlib.figure
             the used matplotlib figure instance
-        '''
+        """
 
         if fit is None:
             fit = self.fit
@@ -1088,8 +1243,10 @@ def intermediate_results(self, fit=None, params = None,
         try:
             fit.intermediate_results
         except AttributeError:
-            assert False, ('No intermediate results are found, you must run' +
-                           ' performfit() with intermediate_results=True!')
+            assert False, (
+                "No intermediate results are found, you must run"
+                + " performfit() with intermediate_results=True!"
+            )
 
         if params is None:
             params = fit.res_dict.keys()
@@ -1099,28 +1256,44 @@ def intermediate_results(self, fit=None, params = None,
         # timeseries-parameters
         tsparams = [i for i in params if i not in constparams]
 
-
         if cmaps is None:
-            cmaps = ['Reds', 'Greens', 'Blues', 'Purples', 'Oranges', 'Greys',
-                     'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu',
-                     'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']
+            cmaps = [
+                "Reds",
+                "Greens",
+                "Blues",
+                "Purples",
+                "Oranges",
+                "Greys",
+                "YlOrBr",
+                "YlOrRd",
+                "OrRd",
+                "PuRd",
+                "RdPu",
+                "BuPu",
+                "GnBu",
+                "PuBu",
+                "YlGnBu",
+                "PuBuGn",
+                "BuGn",
+                "YlGn",
+            ]
 
         interparams = {}
-        for i, valdict in enumerate(fit.intermediate_results['parameters']):
+        for i, valdict in enumerate(fit.intermediate_results["parameters"]):
             for key, val in valdict.items():
                 if key in interparams:
                     interparams[key] += [[i, np.mean(val[0])]]
                 else:
                     interparams[key] = [[i, np.mean(val[0])]]
         intererrs = {}
-        for i, valdict in enumerate(fit.intermediate_results['residuals']):
+        for i, valdict in enumerate(fit.intermediate_results["residuals"]):
             for key, val in valdict.items():
                 if key in intererrs:
                     intererrs[key] += [[i, np.mean(val)]]
                 else:
                     intererrs[key] = [[i, np.mean(val)]]
         interjacs = {}
-        for i, valdict in enumerate(fit.intermediate_results['jacobian']):
+        for i, valdict in enumerate(fit.intermediate_results["jacobian"]):
             for key, val in valdict.items():
                 if key in interjacs:
                     interjacs[key] += [[i, np.mean(val)]]
@@ -1133,124 +1306,174 @@ def intermediate_results(self, fit=None, params = None,
 
         interres_params = {}
         for key in params:
-            interres_p = pd.concat([pd.DataFrame(valdict[key],
-                                                 fit.meandatetimes[key],
-                                    columns=[i])
-                                    for i, valdict in enumerate(
-                                            fit.intermediate_results[
-                                                'parameters'])
-                                    ], axis=1)
+            interres_p = pd.concat(
+                [
+                    pd.DataFrame(valdict[key], fit.meandatetimes[key], columns=[i])
+                    for i, valdict in enumerate(fit.intermediate_results["parameters"])
+                ],
+                axis=1,
+            )
             interres_params[key] = interres_p
 
-
-        f = plt.figure(figsize=(15,10))
+        f = plt.figure(figsize=(15, 10))
         f.subplots_adjust(top=0.98, left=0.05, right=0.95)
-        gs = GridSpec(4, len(interjacs),
-                               #width_ratios=[1, 2],
-                               #height_ratios=[1, 2, 1]
-                               )
-        axsm = plt.subplot(gs[0,:])
-        axerr = plt.subplot(gs[1,:])
+        gs = GridSpec(
+            4,
+            len(interjacs),
+            # width_ratios=[1, 2],
+            # height_ratios=[1, 2, 1]
+        )
+        axsm = plt.subplot(gs[0, :])
+        axerr = plt.subplot(gs[1, :])
         paramaxes, jacaxes = [], []
         for i in range(len(interjacs)):
-            paramaxes += [plt.subplot(gs[2,i])]
-            jacaxes += [plt.subplot(gs[3,i])]
+            paramaxes += [plt.subplot(gs[2, i])]
+            jacaxes += [plt.subplot(gs[3, i])]
 
-        smhandles, smlabels = [],[]
+        smhandles, smlabels = [], []
         nparam = 0
         for [parameter, paramdf] in interres_params.items():
             # plot only temporally varying parameters as timeseries
-            if parameter not in tsparams: continue
+            if parameter not in tsparams:
+                continue
             cmap = plt.get_cmap(cmaps[nparam])
 
             for key, val in paramdf.items():
-                axsm.plot(val,
-                          c=cmap((float(key)/len(paramdf.keys()))),
-                          lw=0.25, marker='.', ms=2)
+                axsm.plot(
+                    val,
+                    c=cmap((float(key) / len(paramdf.keys()))),
+                    lw=0.25,
+                    marker=".",
+                    ms=2,
+                )
 
             # add colorbar
-            axcb = f.add_axes([axsm.get_position().x1-.01*(nparam+1),
-                               axsm.get_position().y0,
-                               .01,
-                               axsm.get_position().y1-axsm.get_position().y0])
+            axcb = f.add_axes(
+                [
+                    axsm.get_position().x1 - 0.01 * (nparam + 1),
+                    axsm.get_position().y0,
+                    0.01,
+                    axsm.get_position().y1 - axsm.get_position().y0,
+                ]
+            )
 
             cbbounds = [1] + list(np.arange(2, len(paramdf.keys()) + 1, 1))
 
-
             cb = mpl.colorbar.ColorbarBase(
-                axcb, cmap=cmap, orientation = 'vertical',
-                boundaries = [0] + cbbounds + [cbbounds[-1]+1],
-                spacing='proportional',
-                norm=mpl.colors.BoundaryNorm(cbbounds, cmap.N))
-            axcb.text(0.51, 0.5, parameter, rotation=90, fontsize=8,
-                      horizontalalignment='center',
-                      verticalalignment='center')
-            if nparam > 0: cb.set_ticks([])
-
-            smhandles += [mpl.lines.Line2D([],[], color=cmap(.9))]
+                axcb,
+                cmap=cmap,
+                orientation="vertical",
+                boundaries=[0] + cbbounds + [cbbounds[-1] + 1],
+                spacing="proportional",
+                norm=mpl.colors.BoundaryNorm(cbbounds, cmap.N),
+            )
+            axcb.text(
+                0.51,
+                0.5,
+                parameter,
+                rotation=90,
+                fontsize=8,
+                horizontalalignment="center",
+                verticalalignment="center",
+            )
+            if nparam > 0:
+                cb.set_ticks([])
+
+            smhandles += [mpl.lines.Line2D([], [], color=cmap(0.9))]
             smlabels += [parameter]
             nparam += 1
 
-        axsm.legend(handles=smhandles, labels=smlabels, loc='upper left')
+        axsm.legend(handles=smhandles, labels=smlabels, loc="upper left")
 
         axsmbounds = list(axsm.get_position().bounds)
-        axsmbounds[2] = axsmbounds[2] - 0.015*nparam
+        axsmbounds[2] = axsmbounds[2] - 0.015 * nparam
         axsm.set_position(axsmbounds)
 
-        for [pax, jax, [key, val]] in zip(paramaxes, jacaxes,
-                                          interparams.items()):
-            if key not in interjacs: continue
+        for [pax, jax, [key, val]] in zip(paramaxes, jacaxes, interparams.items()):
+            if key not in interjacs:
+                continue
 
             if key in tsparams:
-                label = f'{key} (mean)'
+                label = f"{key} (mean)"
             else:
                 label = key
 
-            pax.plot(*val, label=label, marker='.', ms=3, lw=0.5)
-            pax.legend(loc='upper center')
-            jax.plot(interjacs[key][0], interjacs[key][1], label=label,
-                     marker='.', ms=3, lw=0.5)
-            jax.legend(loc='upper center')
-
-        paramaxes[-1].text(1.03, .5, 'Parameter estimates', rotation=90,
-                           fontweight='bold',
-                           horizontalalignment='left',
-                           verticalalignment='center',
-                           transform=paramaxes[-1].transAxes)
-        jacaxes[-1].text(1.03, .5, 'Jacobi determinant', rotation=90,
-                           fontweight='bold',
-                           horizontalalignment='left',
-                           verticalalignment='center',
-                           transform=jacaxes[-1].transAxes)
+            pax.plot(*val, label=label, marker=".", ms=3, lw=0.5)
+            pax.legend(loc="upper center")
+            jax.plot(
+                interjacs[key][0],
+                interjacs[key][1],
+                label=label,
+                marker=".",
+                ms=3,
+                lw=0.5,
+            )
+            jax.legend(loc="upper center")
+
+        paramaxes[-1].text(
+            1.03,
+            0.5,
+            "Parameter estimates",
+            rotation=90,
+            fontweight="bold",
+            horizontalalignment="left",
+            verticalalignment="center",
+            transform=paramaxes[-1].transAxes,
+        )
+        jacaxes[-1].text(
+            1.03,
+            0.5,
+            "Jacobi determinant",
+            rotation=90,
+            fontweight="bold",
+            horizontalalignment="left",
+            verticalalignment="center",
+            transform=jacaxes[-1].transAxes,
+        )
 
         for key, val in intererrs.items():
-            if key == 'abserr':
-                axerr.semilogy(val[0],np.abs(val[1]), label='absolute error', marker='.',
-                               ms=3, lw=0.5, c='r')
-                axerr.legend(ncol=5, loc='upper left')
-            if key == 'relerr':
+            if key == "abserr":
+                axerr.semilogy(
+                    val[0],
+                    np.abs(val[1]),
+                    label="absolute error",
+                    marker=".",
+                    ms=3,
+                    lw=0.5,
+                    c="r",
+                )
+                axerr.legend(ncol=5, loc="upper left")
+            if key == "relerr":
                 axrelerr = axerr.twinx()
-                axrelerr.semilogy(val[0],np.abs(val[1]), label='relative error', marker='.',
-                                  ms=3, lw=0.5, c='g')
-                axrelerr.legend(ncol=5, loc='upper right')
-
+                axrelerr.semilogy(
+                    val[0],
+                    np.abs(val[1]),
+                    label="relative error",
+                    marker=".",
+                    ms=3,
+                    lw=0.5,
+                    c="g",
+                )
+                axrelerr.legend(ncol=5, loc="upper right")
 
         return f
 
-
-    def printsig0analysis(self, fit=None,
-                          range1=1,
-                          range2=None,
-                          use_index='groups',
-                          printfullt_0=True,
-                          printfulldata=True,
-                          dB = True,
-                          sig0=True,
-                          printcomponents1=True,
-                          printcomponents2=True,
-                          printparamnames=None,
-                          printparamstyles=dict()):
-        '''
+    def printsig0analysis(
+        self,
+        fit=None,
+        range1=1,
+        range2=None,
+        use_index="groups",
+        printfullt_0=True,
+        printfulldata=True,
+        dB=True,
+        sig0=True,
+        printcomponents1=True,
+        printcomponents2=True,
+        printparamnames=None,
+        printparamstyles=dict(),
+    ):
+        """
         A widget to analyze the results of a rt1.rtfits.Fits object.
         (In order to keep the widget responsive, a reference to the returns
         must be kept!)
@@ -1300,7 +1523,7 @@ def printsig0analysis(self, fit=None,
             a reference to the matplotlib-objects corresponding to:
             [figure, first_slider, (second_slider)].
 
-        '''
+        """
 
         if fit is None:
             fit = self.fit
@@ -1309,17 +1532,16 @@ def printsig0analysis(self, fit=None,
             printparamnames = fit.res_dict.keys()
 
         # gridspec for incidence-angle and parameter plots
-        gs = GridSpec(3, 2, height_ratios=[.65, .25, .1])
+        gs = GridSpec(3, 2, height_ratios=[0.65, 0.25, 0.1])
         gs.update(top=0.98, left=0.1, right=0.9, bottom=0.025)
         # sub-gridspec for sliders
         gs2 = GridSpecFromSubplotSpec(2, 1, subplot_spec=gs[2, :])
 
-
         f = plt.figure(figsize=(10, 6))
         # add plot-axes
-        ax = f.add_subplot(gs[0,0])
-        ax1 = f.add_subplot(gs[0,1])
-        ax2 = f.add_subplot(gs[1,:])
+        ax = f.add_subplot(gs[0, 0])
+        ax1 = f.add_subplot(gs[0, 1])
+        ax2 = f.add_subplot(gs[1, :])
 
         # add slider axes
         slider_ax = f.add_subplot(gs2[0])
@@ -1329,17 +1551,17 @@ def printsig0analysis(self, fit=None,
         ax.grid()
         ax1.grid()
 
-
-        if use_index == 'dataset':
+        if use_index == "dataset":
             sig0_vals = fit.calc_model(return_components=True)
-            sig0_vals['data'] = fit.dataset.sig
-            sig0_vals['incs'] = fit.dataset.inc
-            sig0_vals = sig0_vals.groupby(
-                level=0).agg(list).to_dict(orient='list')
-            sig0_vals = {key:rectangularize(val, return_masked=True) for
-                         key, val in sig0_vals.items()}
-            sig0_vals['indexes'] = pd.to_datetime(fit.index)
-        elif use_index == 'groups':
+            sig0_vals["data"] = fit.dataset.sig
+            sig0_vals["incs"] = fit.dataset.inc
+            sig0_vals = sig0_vals.groupby(level=0).agg(list).to_dict(orient="list")
+            sig0_vals = {
+                key: rectangularize(val, return_masked=True)
+                for key, val in sig0_vals.items()
+            }
+            sig0_vals["indexes"] = pd.to_datetime(fit.index)
+        elif use_index == "groups":
             # calculate backscatter values and ensure correct index-order
             # ( in case a unordered dyn-dict is used)
             mask = fit.mask
@@ -1347,81 +1569,135 @@ def printsig0analysis(self, fit=None,
             sig0_vals = fit._calc_model(return_components=True)
             # apply mask and convert to pandas dataframe
             sig0_vals = [np.ma.masked_array(con, mask) for con in sig0_vals]
-            sig0_vals = dict(zip(['tot', 'surf', 'vol', 'inter'], sig0_vals))
+            sig0_vals = dict(zip(["tot", "surf", "vol", "inter"], sig0_vals))
 
-            sig0_vals['data'] = np.ma.masked_array(fit.data, mask)
-            sig0_vals['incs'] = np.ma.masked_array(fit.inc, mask)
-            sig0_vals['indexes'] = pd.to_datetime(fit.meandatetimes_group)
+            sig0_vals["data"] = np.ma.masked_array(fit.data, mask)
+            sig0_vals["incs"] = np.ma.masked_array(fit.inc, mask)
+            sig0_vals["indexes"] = pd.to_datetime(fit.meandatetimes_group)
         # convert to sig0 and dB if necessary
         sig0_vals_I_linear = dict()
-        for key in ['tot', 'surf', 'vol', 'inter', 'data']:
-            if key not in sig0_vals: continue
-            sig0_vals[key] = dBsig0convert(sig0_vals[key], sig0_vals['incs'],
-                                           dB = dB, sig0=sig0,
-                                           fitdB=fit.dB,
-                                           fitsig0=fit.sig0)
-            sig0_vals_I_linear[key] = dBsig0convert(sig0_vals[key],
-                                                    sig0_vals['incs'],
-                                                    dB = False, sig0=False,
-                                                    fitdB=dB, fitsig0=sig0)
+        for key in ["tot", "surf", "vol", "inter", "data"]:
+            if key not in sig0_vals:
+                continue
+            sig0_vals[key] = dBsig0convert(
+                sig0_vals[key],
+                sig0_vals["incs"],
+                dB=dB,
+                sig0=sig0,
+                fitdB=fit.dB,
+                fitsig0=fit.sig0,
+            )
+            sig0_vals_I_linear[key] = dBsig0convert(
+                sig0_vals[key],
+                sig0_vals["incs"],
+                dB=False,
+                sig0=False,
+                fitdB=dB,
+                fitsig0=sig0,
+            )
         if printfullt_0 is True:
-            inc=np.deg2rad(np.arange(1, 89, 1))
-            if use_index == 'dataset':
-                newsig0_vals = fit.calc(param=fit.res_df,
-                        inc=inc,
-                        fixed_param=fit.dataset[fit.fixed_dict.keys()],
-                        return_components=True)
-            elif use_index == 'groups':
+            inc = np.deg2rad(np.arange(1, 89, 1))
+            if use_index == "dataset":
+                newsig0_vals = fit.calc(
+                    param=fit.res_df,
+                    inc=inc,
+                    fixed_param=fit.dataset[fit.fixed_dict.keys()],
+                    return_components=True,
+                )
+            elif use_index == "groups":
                 if len(fit.fixed_dict) > 0:
                     # get the average value of the fixed-parameters
                     # for each group
-                    usefixedparams = fit.dataset[fit.fixed_dict.keys()].groupby(
-                        fit._groupindex).mean().set_index(
-                            pd.to_datetime(fit.meandatetimes_group))
+                    usefixedparams = (
+                        fit.dataset[fit.fixed_dict.keys()]
+                        .groupby(fit._groupindex, sort=False)
+                        .mean()
+                        .set_index(pd.to_datetime(fit.meandatetimes_group))
+                    )
                 else:
                     usefixedparams = dict()
-                newsig0_vals = fit.calc(param=fit.res_df_group,
-                                        inc=inc,
-                                        fixed_param=usefixedparams,
-                                        return_components=True)
+                newsig0_vals = fit.calc(
+                    param=fit.res_df_group,
+                    inc=inc,
+                    fixed_param=usefixedparams,
+                    return_components=True,
+                )
 
-            newsig0_vals = dict(zip(['tot', 'surf', 'vol', 'inter'],
-                                    newsig0_vals))
+            newsig0_vals = dict(zip(["tot", "surf", "vol", "inter"], newsig0_vals))
 
-            newsig0_vals['incs'] = np.broadcast_to(inc,
-                                                   newsig0_vals['tot'].shape)
+            newsig0_vals["incs"] = np.broadcast_to(inc, newsig0_vals["tot"].shape)
 
             newsig0_vals_I_linear = dict()
-            for key in ['tot', 'surf', 'vol', 'inter']:
-                if key not in newsig0_vals: continue
-                newsig0_vals[key] = dBsig0convert(newsig0_vals[key],
-                                                  newsig0_vals['incs'],
-                                                  fitdB=fit.dB,
-                                                  fitsig0=fit.sig0,
-                                                  dB = dB, sig0=sig0)
+            for key in ["tot", "surf", "vol", "inter"]:
+                if key not in newsig0_vals:
+                    continue
+                newsig0_vals[key] = dBsig0convert(
+                    newsig0_vals[key],
+                    newsig0_vals["incs"],
+                    fitdB=fit.dB,
+                    fitsig0=fit.sig0,
+                    dB=dB,
+                    sig0=sig0,
+                )
                 newsig0_vals_I_linear[key] = dBsig0convert(
-                    newsig0_vals[key], newsig0_vals['incs'], dB = False,
-                    sig0=False, fitdB=dB, fitsig0=sig0)
-
+                    newsig0_vals[key],
+                    newsig0_vals["incs"],
+                    dB=False,
+                    sig0=False,
+                    fitdB=dB,
+                    fitsig0=sig0,
+                )
 
-            ax.set_xlim([-2 + np.rad2deg(np.ma.min(newsig0_vals['incs'])),
-                          2 + np.rad2deg(np.ma.max(newsig0_vals['incs']))])
-            ax.set_ylim([np.min([np.ma.min(newsig0_vals['tot']),
-                                 np.ma.min(sig0_vals['data'])]),
-                         np.max([np.ma.max(newsig0_vals['tot']),
-                                 np.ma.max(sig0_vals['data'])])])
+            ax.set_xlim(
+                [
+                    -2 + np.rad2deg(np.ma.min(newsig0_vals["incs"])),
+                    2 + np.rad2deg(np.ma.max(newsig0_vals["incs"])),
+                ]
+            )
+            ax.set_ylim(
+                [
+                    np.min(
+                        [
+                            np.ma.min(newsig0_vals["tot"]),
+                            np.ma.min(sig0_vals["data"]),
+                        ]
+                    ),
+                    np.max(
+                        [
+                            np.ma.max(newsig0_vals["tot"]),
+                            np.ma.max(sig0_vals["data"]),
+                        ]
+                    ),
+                ]
+            )
         else:
-            ax.set_xlim([-2 + np.rad2deg(np.ma.min(sig0_vals['incs'][0])),
-                          2 + np.rad2deg(np.ma.max(sig0_vals['incs'][0]))])
-            ax.set_ylim([np.min([np.ma.min(sig0_vals['tot']),
-                                 np.ma.min(sig0_vals['data'])]),
-                         np.max([np.ma.max(sig0_vals['tot']),
-                                 np.ma.max(sig0_vals['data'])])])
+            ax.set_xlim(
+                [
+                    -2 + np.rad2deg(np.ma.min(sig0_vals["incs"][0])),
+                    2 + np.rad2deg(np.ma.max(sig0_vals["incs"][0])),
+                ]
+            )
+            ax.set_ylim(
+                [
+                    np.min(
+                        [
+                            np.ma.min(sig0_vals["tot"]),
+                            np.ma.min(sig0_vals["data"]),
+                        ]
+                    ),
+                    np.max(
+                        [
+                            np.ma.max(sig0_vals["tot"]),
+                            np.ma.max(sig0_vals["data"]),
+                        ]
+                    ),
+                ]
+            )
 
         # ensure sort-order
-        if use_index == 'dataset':
+        if use_index == "dataset":
             inc_sortp = np.argsort(fit.index)
-        elif use_index == 'groups':
+        elif use_index == "groups":
             inc_sortp = np.argsort(fit.meandatetimes_group)
 
         for key, val in sig0_vals.items():
@@ -1431,42 +1707,51 @@ def printsig0analysis(self, fit=None,
         for key, val in newsig0_vals_I_linear.items():
             newsig0_vals_I_linear[key] = val[inc_sortp]
 
-
         # print full data points in the background
         if printfulldata is True:
-            ax.plot(np.rad2deg(sig0_vals['incs']),
-                    sig0_vals['data'], lw=0., marker='.', ms=.5,  color = 'k',
-                    alpha = 0.5)
-
-
+            ax.plot(
+                np.rad2deg(sig0_vals["incs"]),
+                sig0_vals["data"],
+                lw=0.0,
+                marker=".",
+                ms=0.5,
+                color="k",
+                alpha=0.5,
+            )
 
         # plot parameters as specified in printparamnames
         axparamplot = ax2
         handles, labels = [], []
         i = 0
-        pos = 1 - len(printparamnames)//2 * 0.035
+        pos = 1 - len(printparamnames) // 2 * 0.035
         for key in printparamnames:
             try:
-                style = dict(color='C' + str(i), marker='.', lw=0.75)
+                style = dict(color="C" + str(i), marker=".", lw=0.75)
                 style.update(printparamstyles.get(key, dict()))
 
                 if i > 0:
                     axparamplot = ax2.twinx()
-                    axparamplot.tick_params(axis='y', which='both',
-                                            labelsize=5, length=2)
+                    axparamplot.tick_params(
+                        axis="y", which="both", labelsize=5, length=2
+                    )
                     pos += 0.035
-                    axparamplot.spines["right"].set_position(('axes', pos))
-                    axparamplot.tick_params(axis='y', which='both',
-                                            labelsize=5, length=2)
+                    axparamplot.spines["right"].set_position(("axes", pos))
+                    axparamplot.tick_params(
+                        axis="y", which="both", labelsize=5, length=2
+                    )
                 if key in fit.res_df:
-                    l, = axparamplot.plot(fit.res_df.sort_index()[key],
-                                          label = key, **style)
+                    (l,) = axparamplot.plot(
+                        fit.res_df.sort_index()[key], label=key, **style
+                    )
                 elif key in fit.dataset:
-                    l, = axparamplot.plot(fit.dataset.sort_index()[key],
-                                          label = key, **style)
+                    (l,) = axparamplot.plot(
+                        fit.dataset.sort_index()[key], label=key, **style
+                    )
                 else:
-                    print(f'parameter "{key}" not found in "fit.res_df"',
-                          'and "fit.dataset"')
+                    print(
+                        f'parameter "{key}" not found in "fit.res_df"',
+                        'and "fit.dataset"',
+                    )
                     continue
 
                 # add handles and labels to legend
@@ -1475,398 +1760,570 @@ def printsig0analysis(self, fit=None,
 
                 # change color of axis to fit color of lines
                 axparamplot.yaxis.label.set_color(l.get_color())
-                axparamplot.tick_params(axis='y', colors=l.get_color())
+                axparamplot.tick_params(axis="y", colors=l.get_color())
                 # shift twin-axes if necessary
                 i += 1
             except:
                 pass
-        axparamplot.legend(handles=handles, labels=labels, loc='upper center',
-                           ncol=len(printparamnames))
+        axparamplot.legend(
+            handles=handles,
+            labels=labels,
+            loc="upper center",
+            ncol=len(printparamnames),
+        )
 
         ax2.xaxis.set_minor_locator(mpl.dates.MonthLocator())
-        ax2.xaxis.set_minor_formatter(mpl.dates.DateFormatter('%m'))
+        ax2.xaxis.set_minor_formatter(mpl.dates.DateFormatter("%m"))
         ax2.xaxis.set_major_locator(mpl.dates.YearLocator())
-        ax2.xaxis.set_major_formatter(mpl.dates.DateFormatter('\n%Y'))
+        ax2.xaxis.set_major_formatter(mpl.dates.DateFormatter("\n%Y"))
 
-        indicator_bounds = [ax2.get_ylim()[0]*1.05, ax2.get_ylim()[1]*0.95]
+        indicator_bounds = [ax2.get_ylim()[0] * 1.05, ax2.get_ylim()[1] * 0.95]
 
         # -----------------------------------------------------------
         # plot lines
-        def plotlines(dayrange, printcomponents, printfullt_0,
-                      styledict_dict, styledict_fullt0_dict):
+        def plotlines(
+            dayrange,
+            printcomponents,
+            printfullt_0,
+            styledict_dict,
+            styledict_fullt0_dict,
+        ):
 
-            incs = np.rad2deg(sig0_vals['incs'])
+            incs = np.rad2deg(sig0_vals["incs"])
 
             lines = []
             for day in np.arange(0, dayrange, 1):
-                lines += ax.plot(incs[day], sig0_vals['tot'][day],
-                                 **styledict_dict['tot'])
+                lines += ax.plot(
+                    incs[day], sig0_vals["tot"][day], **styledict_dict["tot"]
+                )
                 if printcomponents:
-                    lines += ax.plot(incs[day], sig0_vals['surf'][day],
-                                     **styledict_dict['surf'])
-                    lines += ax.plot(incs[day], sig0_vals['vol'][day],
-                                     **styledict_dict['vol'])
+                    lines += ax.plot(
+                        incs[day],
+                        sig0_vals["surf"][day],
+                        **styledict_dict["surf"],
+                    )
+                    lines += ax.plot(
+                        incs[day],
+                        sig0_vals["vol"][day],
+                        **styledict_dict["vol"],
+                    )
                     if fit.int_Q is True:
-                        lines += ax.plot(incs[day], sig0_vals['inter'][day],
-                                         **styledict_dict['inter'])
-
-                lines += ax.plot(incs[day], sig0_vals['data'][day],
-                                 **styledict_dict['data'])
-                lines += ax2.plot([sig0_vals['indexes'][day]]*2,
-                                  indicator_bounds,
-                                  **styledict_dict['indicator'])
+                        lines += ax.plot(
+                            incs[day],
+                            sig0_vals["inter"][day],
+                            **styledict_dict["inter"],
+                        )
+
+                lines += ax.plot(
+                    incs[day], sig0_vals["data"][day], **styledict_dict["data"]
+                )
+                lines += ax2.plot(
+                    [sig0_vals["indexes"][day]] * 2,
+                    indicator_bounds,
+                    **styledict_dict["indicator"],
+                )
 
             lines_frac = []
             for day in np.arange(0, dayrange, 1):
-                lintot = sig0_vals_I_linear['tot'][day]
-                linsurf = sig0_vals_I_linear['surf'][day]
-                linvol = sig0_vals_I_linear['vol'][day]
+                lintot = sig0_vals_I_linear["tot"][day]
+                linsurf = sig0_vals_I_linear["surf"][day]
+                linvol = sig0_vals_I_linear["vol"][day]
 
                 if fit.int_Q is True:
-                    lininter = sig0_vals_I_linear['inter'][day]
+                    lininter = sig0_vals_I_linear["inter"][day]
 
-                lines_frac += ax1.plot(incs[day], linsurf/lintot,
-                                       **styledict_dict['surf'])
-                lines_frac += ax1.plot(incs[day], linvol/lintot,
-                                       **styledict_dict['vol'])
+                lines_frac += ax1.plot(
+                    incs[day], linsurf / lintot, **styledict_dict["surf"]
+                )
+                lines_frac += ax1.plot(
+                    incs[day], linvol / lintot, **styledict_dict["vol"]
+                )
                 if fit.int_Q is True:
-                    lines_frac += ax1.plot(incs[day], lininter/lintot,
-                                           **styledict_dict['inter'])
+                    lines_frac += ax1.plot(
+                        incs[day], lininter / lintot, **styledict_dict["inter"]
+                    )
 
             # plot full-incidence-angle lines
             linesfull = []
             lines_frac_full = []
             if printfullt_0 is True:
-                newincs = np.rad2deg(newsig0_vals['incs'])
+                newincs = np.rad2deg(newsig0_vals["incs"])
 
                 for day in np.arange(0, dayrange, 1):
 
                     sortp = np.argsort(newincs[day])
 
-                    linesfull += ax.plot(newincs[day][sortp],
-                                         newsig0_vals['tot'][day][sortp],
-                                         **styledict_fullt0_dict['tot'])
+                    linesfull += ax.plot(
+                        newincs[day][sortp],
+                        newsig0_vals["tot"][day][sortp],
+                        **styledict_fullt0_dict["tot"],
+                    )
                     if printcomponents:
-                        linesfull += ax.plot(newincs[day][sortp],
-                                             newsig0_vals['surf'][day][sortp],
-                                             **styledict_fullt0_dict['surf'])
-                        linesfull += ax.plot(newincs[day][sortp],
-                                             newsig0_vals['vol'][day][sortp],
-                                             **styledict_fullt0_dict['vol'])
+                        linesfull += ax.plot(
+                            newincs[day][sortp],
+                            newsig0_vals["surf"][day][sortp],
+                            **styledict_fullt0_dict["surf"],
+                        )
+                        linesfull += ax.plot(
+                            newincs[day][sortp],
+                            newsig0_vals["vol"][day][sortp],
+                            **styledict_fullt0_dict["vol"],
+                        )
                         if fit.int_Q is True:
                             linesfull += ax.plot(
                                 newincs[day][sortp],
-                                newsig0_vals['inter'][day][sortp],
-                                **styledict_fullt0_dict['inter'])
+                                newsig0_vals["inter"][day][sortp],
+                                **styledict_fullt0_dict["inter"],
+                            )
 
-                    lintot = newsig0_vals_I_linear['tot'][day]
-                    linsurf = newsig0_vals_I_linear['surf'][day]
-                    linvol = newsig0_vals_I_linear['vol'][day]
+                    lintot = newsig0_vals_I_linear["tot"][day]
+                    linsurf = newsig0_vals_I_linear["surf"][day]
+                    linvol = newsig0_vals_I_linear["vol"][day]
                     if fit.int_Q is True:
-                        lininter = newsig0_vals_I_linear['inter'][day]
+                        lininter = newsig0_vals_I_linear["inter"][day]
 
                     lines_frac_full += ax1.plot(
-                        newincs[day][sortp], (linsurf/lintot)[sortp],
-                        **styledict_fullt0_dict['surf'])
-                    lines_frac_full += ax1.plot(newincs[day][sortp],
-                                                (linvol/lintot)[sortp],
-                                                **styledict_fullt0_dict['vol'])
+                        newincs[day][sortp],
+                        (linsurf / lintot)[sortp],
+                        **styledict_fullt0_dict["surf"],
+                    )
+                    lines_frac_full += ax1.plot(
+                        newincs[day][sortp],
+                        (linvol / lintot)[sortp],
+                        **styledict_fullt0_dict["vol"],
+                    )
                     if fit.int_Q is True:
                         lines_frac_full += ax1.plot(
-                            newincs[day][sortp], (lininter/lintot)[sortp],
-                            **styledict_fullt0_dict['inter'])
+                            newincs[day][sortp],
+                            (lininter / lintot)[sortp],
+                            **styledict_fullt0_dict["inter"],
+                        )
 
             # add unique legend entries
             ha, la = ax.get_legend_handles_labels()
             unila, wherela = np.unique(la, return_index=True)
             # sort legend entries
-            sort_order = dict(data_1=0, data_2=1, total=2,
-                              surface=3, volume=4, interaction=5)
+            sort_order = dict(
+                data_1=0, data_2=1, total=2, surface=3, volume=4, interaction=5
+            )
 
-            halas = [[ha, la] for ha, la in zip(np.array(la)[wherela],
-                                                np.array(ha)[wherela])]
+            halas = [
+                [ha, la] for ha, la in zip(np.array(la)[wherela], np.array(ha)[wherela])
+            ]
             halas.sort(key=lambda val: sort_order[val[0]])
-            ax.legend(handles=[i[1] for i in halas],
-                      labels=[i[0] for i in halas])
+            ax.legend(handles=[i[1] for i in halas], labels=[i[0] for i in halas])
 
             return lines, lines_frac, linesfull, lines_frac_full
 
         # plot first set of lines
-        styletot      = {'lw':1, 'marker':'o', 'ms':3, 'color':'k',
-                         'label':'total'}
-        stylevol   = {'lw':1, 'marker':'o', 'ms':3, 'color':'g',
-                      'markerfacecolor':'gray', 'label':'volume'}
-        stylesurf  = {'lw':1, 'marker':'o', 'ms':3, 'color':'y',
-                      'markerfacecolor':'gray', 'label':'surface'}
-        styleinter = {'lw':1, 'marker':'o', 'ms':3, 'color':'c',
-                      'markerfacecolor':'gray', 'label':'interaction'}
-        styledata  = {'lw':0, 'marker':'s', 'ms':5, 'color':'k',
-                      'markerfacecolor':'gray', 'label':'data_1'}
-        styleindicator = {'c':'k'}
-
-        stylefullt0tot = {'lw':.25, 'color':'k'}
-        stylefullt0vol = {'lw':.25, 'color':'g'}
-        stylefullt0surf = {'lw':.25, 'color':'y'}
-        stylefullt0inter = {'lw':.25, 'color':'c'}
-
-        styledict_dict = dict(zip(['tot', 'surf', 'vol', 'inter',
-                                   'data', 'indicator'],
-                                  [styletot, stylesurf, stylevol, styleinter,
-                                   styledata, styleindicator]))
-        styledict_fullt0_dict = dict(zip(['tot', 'surf', 'vol', 'inter'],
-                                  [stylefullt0tot, stylefullt0surf,
-                                   stylefullt0vol, stylefullt0inter]))
+        styletot = {
+            "lw": 1,
+            "marker": "o",
+            "ms": 3,
+            "color": "k",
+            "label": "total",
+        }
+        stylevol = {
+            "lw": 1,
+            "marker": "o",
+            "ms": 3,
+            "color": "g",
+            "markerfacecolor": "gray",
+            "label": "volume",
+        }
+        stylesurf = {
+            "lw": 1,
+            "marker": "o",
+            "ms": 3,
+            "color": "y",
+            "markerfacecolor": "gray",
+            "label": "surface",
+        }
+        styleinter = {
+            "lw": 1,
+            "marker": "o",
+            "ms": 3,
+            "color": "c",
+            "markerfacecolor": "gray",
+            "label": "interaction",
+        }
+        styledata = {
+            "lw": 0,
+            "marker": "s",
+            "ms": 5,
+            "color": "k",
+            "markerfacecolor": "gray",
+            "label": "data_1",
+        }
+        styleindicator = {"c": "k"}
+
+        stylefullt0tot = {"lw": 0.25, "color": "k"}
+        stylefullt0vol = {"lw": 0.25, "color": "g"}
+        stylefullt0surf = {"lw": 0.25, "color": "y"}
+        stylefullt0inter = {"lw": 0.25, "color": "c"}
+
+        styledict_dict = dict(
+            zip(
+                ["tot", "surf", "vol", "inter", "data", "indicator"],
+                [
+                    styletot,
+                    stylesurf,
+                    stylevol,
+                    styleinter,
+                    styledata,
+                    styleindicator,
+                ],
+            )
+        )
+        styledict_fullt0_dict = dict(
+            zip(
+                ["tot", "surf", "vol", "inter"],
+                [
+                    stylefullt0tot,
+                    stylefullt0surf,
+                    stylefullt0vol,
+                    stylefullt0inter,
+                ],
+            )
+        )
 
         lines, lines_frac, linesfull, lines_frac_full = plotlines(
-            range1, printcomponents1, printfullt_0, styledict_dict,
-            styledict_fullt0_dict)
-
-
+            range1,
+            printcomponents1,
+            printfullt_0,
+            styledict_dict,
+            styledict_fullt0_dict,
+        )
 
         if range2 is not None:
             # plot second set of lines
-            styletot       = {'lw':1, 'marker':'o', 'ms':3, 'color':'r',
-                              'dashes':[5, 5],  'markerfacecolor':'none'}
-            stylevol    = {'lw':1, 'marker':'o', 'ms':3, 'color':'g',
-                           'dashes':[5, 5],  'markerfacecolor':'r'}
-            stylesurf   = {'lw':1, 'marker':'o', 'ms':3, 'color':'y',
-                           'dashes':[5, 5],  'markerfacecolor':'r'}
-            styleinter  = {'lw':1, 'marker':'o', 'ms':3, 'color':'c',
-                           'dashes':[5, 5],  'markerfacecolor':'r'}
-            styledata   = {'lw':0, 'marker':'s', 'ms':5, 'color':'r',
-                           'markerfacecolor':'none', 'label':'data_2'}
-            styleindicator = {'c':'gray', 'ls':'--'}
-
-            stylefullt0tot = {'lw':.25, 'color':'r', 'dashes':[5, 5]}
-            stylefullt0vol = {'lw':.25, 'color':'g', 'dashes':[5, 5]}
-            stylefullt0surf = {'lw':.25, 'color':'y', 'dashes':[5, 5]}
-            stylefullt0inter = {'lw':.25, 'color':'c', 'dashes':[5, 5]}
-
-            styledict_dict = dict(zip(['tot', 'surf', 'vol', 'inter',
-                                       'data', 'indicator'],
-                                  [styletot, stylesurf, stylevol, styleinter,
-                                   styledata, styleindicator]))
-            styledict_fullt0_dict = dict(zip(['tot', 'surf', 'vol', 'inter'],
-                                      [stylefullt0tot, stylefullt0surf,
-                                       stylefullt0vol, stylefullt0inter]))
+            styletot = {
+                "lw": 1,
+                "marker": "o",
+                "ms": 3,
+                "color": "r",
+                "dashes": [5, 5],
+                "markerfacecolor": "none",
+            }
+            stylevol = {
+                "lw": 1,
+                "marker": "o",
+                "ms": 3,
+                "color": "g",
+                "dashes": [5, 5],
+                "markerfacecolor": "r",
+            }
+            stylesurf = {
+                "lw": 1,
+                "marker": "o",
+                "ms": 3,
+                "color": "y",
+                "dashes": [5, 5],
+                "markerfacecolor": "r",
+            }
+            styleinter = {
+                "lw": 1,
+                "marker": "o",
+                "ms": 3,
+                "color": "c",
+                "dashes": [5, 5],
+                "markerfacecolor": "r",
+            }
+            styledata = {
+                "lw": 0,
+                "marker": "s",
+                "ms": 5,
+                "color": "r",
+                "markerfacecolor": "none",
+                "label": "data_2",
+            }
+            styleindicator = {"c": "gray", "ls": "--"}
+
+            stylefullt0tot = {"lw": 0.25, "color": "r", "dashes": [5, 5]}
+            stylefullt0vol = {"lw": 0.25, "color": "g", "dashes": [5, 5]}
+            stylefullt0surf = {"lw": 0.25, "color": "y", "dashes": [5, 5]}
+            stylefullt0inter = {"lw": 0.25, "color": "c", "dashes": [5, 5]}
+
+            styledict_dict = dict(
+                zip(
+                    ["tot", "surf", "vol", "inter", "data", "indicator"],
+                    [
+                        styletot,
+                        stylesurf,
+                        stylevol,
+                        styleinter,
+                        styledata,
+                        styleindicator,
+                    ],
+                )
+            )
+            styledict_fullt0_dict = dict(
+                zip(
+                    ["tot", "surf", "vol", "inter"],
+                    [
+                        stylefullt0tot,
+                        stylefullt0surf,
+                        stylefullt0vol,
+                        stylefullt0inter,
+                    ],
+                )
+            )
 
             lines2, lines_frac2, linesfull2, lines_frac_full2 = plotlines(
-                range2, printcomponents2, printfullt_0, styledict_dict,
-                styledict_fullt0_dict)
-
-
+                range2,
+                printcomponents2,
+                printfullt_0,
+                styledict_dict,
+                styledict_fullt0_dict,
+            )
 
         # define function to update lines based on slider-input
-        def animate(day0, lines, linesfull,
-                    lines_frac, lines_frac_full,
-                    dayrange, printcomponents,
-                    label):
+        def animate(
+            day0,
+            lines,
+            linesfull,
+            lines_frac,
+            lines_frac_full,
+            dayrange,
+            printcomponents,
+            label,
+        ):
 
             day0 = int(day0)
 
-
             label.set_position([day0, label.get_position()[1]])
             if dayrange == 1:
-                label.set_text(
-                    sig0_vals['indexes'][day0].strftime('%d. %b %Y %H:%M'))
+                label.set_text(sig0_vals["indexes"][day0].strftime("%d. %b %Y %H:%M"))
             elif dayrange > 1:
 
-                lday_0 = sig0_vals['indexes'][day0].strftime('%d. %b %Y %H:%M')
-                lday_1 = sig0_vals['indexes'][day0 + dayrange -1
-                                              ].strftime('%d. %b %Y %H:%M')
+                lday_0 = sig0_vals["indexes"][day0].strftime("%d. %b %Y %H:%M")
+                lday_1 = sig0_vals["indexes"][day0 + dayrange - 1].strftime(
+                    "%d. %b %Y %H:%M"
+                )
                 label.set_text(f"{lday_0} - {lday_1}")
 
-
-            maxdays = len(sig0_vals['incs'])
+            maxdays = len(sig0_vals["incs"])
             i = 0
             for day in np.arange(day0, day0 + dayrange, 1):
-                if day >= maxdays: continue
-                lines[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                lines[i].set_ydata(sig0_vals['tot'][day])
+                if day >= maxdays:
+                    continue
+                lines[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                lines[i].set_ydata(sig0_vals["tot"][day])
                 i += 1
                 if printcomponents:
-                    lines[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                    lines[i].set_ydata(sig0_vals['surf'][day])
+                    lines[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                    lines[i].set_ydata(sig0_vals["surf"][day])
                     i += 1
-                    lines[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                    lines[i].set_ydata(sig0_vals['vol'][day])
+                    lines[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                    lines[i].set_ydata(sig0_vals["vol"][day])
                     if fit.int_Q is True:
-                       i += 1
-                       lines[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                       lines[i].set_ydata(sig0_vals['inter'][day])
+                        i += 1
+                        lines[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                        lines[i].set_ydata(sig0_vals["inter"][day])
                     i += 1
 
                 # update data measurements
-                lines[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                lines[i].set_ydata(sig0_vals['data'][day])
+                lines[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                lines[i].set_ydata(sig0_vals["data"][day])
                 i += 1
                 # update day-indicator line
-                lines[i].set_xdata([sig0_vals['indexes'][day]]*2)
+                lines[i].set_xdata([sig0_vals["indexes"][day]] * 2)
                 i += 1
 
             i = 0
             for day in np.arange(day0, day0 + dayrange, 1):
-                if day >= maxdays: continue
-                lintot = sig0_vals_I_linear['tot'][day]
-                linsurf = sig0_vals_I_linear['surf'][day]
-                linvol = sig0_vals_I_linear['vol'][day]
+                if day >= maxdays:
+                    continue
+                lintot = sig0_vals_I_linear["tot"][day]
+                linsurf = sig0_vals_I_linear["surf"][day]
+                linvol = sig0_vals_I_linear["vol"][day]
                 if fit.int_Q is True:
-                    lininter = sig0_vals_I_linear['inter'][day]
+                    lininter = sig0_vals_I_linear["inter"][day]
 
-                lines_frac[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                lines_frac[i].set_ydata(linsurf/lintot)
+                lines_frac[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                lines_frac[i].set_ydata(linsurf / lintot)
                 i += 1
-                lines_frac[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                lines_frac[i].set_ydata(linvol/lintot)
+                lines_frac[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                lines_frac[i].set_ydata(linvol / lintot)
                 if fit.int_Q is True:
                     i += 1
-                    lines_frac[i].set_xdata(np.rad2deg(sig0_vals['incs'][day]))
-                    lines_frac[i].set_ydata(lininter/lintot)
+                    lines_frac[i].set_xdata(np.rad2deg(sig0_vals["incs"][day]))
+                    lines_frac[i].set_ydata(lininter / lintot)
                 i += 1
 
             if printfullt_0 is True:
                 i = 0
                 for day in np.arange(day0, day0 + dayrange, 1):
-                    if day >= maxdays: continue
-                    day_inc_new = np.rad2deg(newsig0_vals['incs'][day])
+                    if day >= maxdays:
+                        continue
+                    day_inc_new = np.rad2deg(newsig0_vals["incs"][day])
                     sortp = np.argsort(day_inc_new)
 
                     linesfull[i].set_xdata(day_inc_new[sortp])
-                    linesfull[i].set_ydata(newsig0_vals['tot'][day][sortp])
+                    linesfull[i].set_ydata(newsig0_vals["tot"][day][sortp])
                     i += 1
                     if printcomponents:
                         linesfull[i].set_xdata(day_inc_new[sortp])
-                        linesfull[i].set_ydata(newsig0_vals['surf'][day][sortp])
+                        linesfull[i].set_ydata(newsig0_vals["surf"][day][sortp])
                         i += 1
                         linesfull[i].set_xdata(day_inc_new[sortp])
-                        linesfull[i].set_ydata(newsig0_vals['vol'][day][sortp])
+                        linesfull[i].set_ydata(newsig0_vals["vol"][day][sortp])
                         if fit.int_Q is True:
                             i += 1
                             linesfull[i].set_xdata(day_inc_new[sortp])
-                            linesfull[i].set_ydata(newsig0_vals['inter'][day][sortp])
+                            linesfull[i].set_ydata(newsig0_vals["inter"][day][sortp])
                         i += 1
                 i = 0
                 for day in np.arange(day0, day0 + dayrange, 1):
-                    if day >= maxdays: continue
-                    day_inc_new = np.rad2deg(newsig0_vals['incs'][day])
+                    if day >= maxdays:
+                        continue
+                    day_inc_new = np.rad2deg(newsig0_vals["incs"][day])
                     sortp = np.argsort(day_inc_new)
 
-                    lintot = newsig0_vals_I_linear['tot'][day]
-                    linsurf = newsig0_vals_I_linear['surf'][day]
-                    linvol = newsig0_vals_I_linear['vol'][day]
+                    lintot = newsig0_vals_I_linear["tot"][day]
+                    linsurf = newsig0_vals_I_linear["surf"][day]
+                    linvol = newsig0_vals_I_linear["vol"][day]
                     if fit.int_Q is True:
-                        lininter = newsig0_vals_I_linear['inter'][day]
+                        lininter = newsig0_vals_I_linear["inter"][day]
 
                     lines_frac_full[i].set_xdata(day_inc_new[sortp])
-                    lines_frac_full[i].set_ydata((linsurf/lintot)[sortp])
+                    lines_frac_full[i].set_ydata((linsurf / lintot)[sortp])
                     i += 1
                     lines_frac_full[i].set_xdata(day_inc_new[sortp])
-                    lines_frac_full[i].set_ydata((linvol/lintot)[sortp])
+                    lines_frac_full[i].set_ydata((linvol / lintot)[sortp])
                     if fit.int_Q is True:
                         i += 1
                         lines_frac_full[i].set_xdata(day_inc_new[sortp])
-                        lines_frac_full[i].set_ydata((lininter/lintot)[sortp])
+                        lines_frac_full[i].set_ydata((lininter / lintot)[sortp])
                     i += 1
 
             return lines
 
         # define function to update slider-range based on zoom
         def updatesliderboundary(evt, slider):
-            indexes = sig0_vals['indexes']
+            indexes = sig0_vals["indexes"]
             # Get the range for the new area
             xstart, ystart, xdelta, ydelta = ax2.viewLim.bounds
             xend = xstart + xdelta
 
             # convert to datetime-objects and ensure that they are in the
             # same time-zone as the sig0_vals indexes
-            xend = mpl.dates.num2date(xend).replace(
-                tzinfo=sig0_vals['indexes'].tzinfo)
+            xend = mpl.dates.num2date(xend).replace(tzinfo=sig0_vals["indexes"].tzinfo)
             xstart = mpl.dates.num2date(xstart).replace(
-                tzinfo=sig0_vals['indexes'].tzinfo)
+                tzinfo=sig0_vals["indexes"].tzinfo
+            )
 
-            zoomindex = np.where(np.logical_and(indexes > xstart,
-                                                indexes < xend))[0]
+            zoomindex = np.where(np.logical_and(indexes > xstart, indexes < xend))[0]
             slider.valmin = zoomindex[0] - 1
             slider.valmax = zoomindex[-1] + 1
 
-            slider.ax.set_xlim(slider.valmin,
-                               slider.valmax)
+            slider.ax.set_xlim(slider.valmin, slider.valmax)
 
         # create the slider
-        a_slider = Slider(slider_ax,            # axes object for the slider
-                          'solid lines',        # name of the slider parameter
-                          0,                    # minimal value of parameter
-                          len(sig0_vals['tot']) - 1,   # maximal value of parameter
-                          valinit=0,            # initial value of parameter
-                          valfmt="%i",          # print slider-value as integer
-                          valstep=1,
-                          closedmax=True)
+        a_slider = Slider(
+            slider_ax,  # axes object for the slider
+            "solid lines",  # name of the slider parameter
+            0,  # minimal value of parameter
+            len(sig0_vals["tot"]) - 1,  # maximal value of parameter
+            valinit=0,  # initial value of parameter
+            valfmt="%i",  # print slider-value as integer
+            valstep=1,
+            closedmax=True,
+        )
         a_slider.valtext.set_visible(False)
 
-        slider_ax.set_xticks(np.arange(slider_ax.get_xlim()[0] - 1,
-                                       slider_ax.get_xlim()[1] + 1, 1,
-                                       dtype=int))
+        slider_ax.set_xticks(
+            np.arange(
+                slider_ax.get_xlim()[0] - 1,
+                slider_ax.get_xlim()[1] + 1,
+                1,
+                dtype=int,
+            )
+        )
         slider_ax.tick_params(bottom=False, labelbottom=False)
         slider_ax.grid()
 
         label = slider_ax.text(
-            0, 0.5, f"{sig0_vals['indexes'][0].strftime('%d. %b %Y %H:%M')}",
-            horizontalalignment='center', verticalalignment='center',
-            fontsize=8, bbox=dict(facecolor='w', alpha=0.75,
-                                  boxstyle='round,pad=.2'))
+            0,
+            0.5,
+            f"{sig0_vals['indexes'][0].strftime('%d. %b %Y %H:%M')}",
+            horizontalalignment="center",
+            verticalalignment="center",
+            fontsize=8,
+            bbox=dict(facecolor="w", alpha=0.75, boxstyle="round,pad=.2"),
+        )
 
         # set slider to call animate function when changed
-        a_slider.on_changed(partial(animate,
-                                    lines=lines,
-                                    linesfull=linesfull,
-                                    lines_frac = lines_frac,
-                                    lines_frac_full = lines_frac_full,
-                                    dayrange=range1,
-                                    printcomponents=printcomponents1,
-                                    label=label))
+        a_slider.on_changed(
+            partial(
+                animate,
+                lines=lines,
+                linesfull=linesfull,
+                lines_frac=lines_frac,
+                lines_frac_full=lines_frac_full,
+                dayrange=range1,
+                printcomponents=printcomponents1,
+                label=label,
+            )
+        )
 
         # update slider boundary with respect to zoom of second plot
-        ax2.callbacks.connect('xlim_changed', partial(updatesliderboundary,
-                                                      slider=a_slider))
+        ax2.callbacks.connect(
+            "xlim_changed", partial(updatesliderboundary, slider=a_slider)
+        )
         if range2 is not None:
 
             # here we create the slider
-            b_slider = Slider(slider_bx,         # axes object for the slider
-                              'dashed lines',    # name of the slider parameter
-                              0,                 # minimal value of parameter
-                              len(sig0_vals['tot']) - 1,# maximal value of parameter
-                              valinit=0,         # initial value of parameter
-                              valfmt="%i",
-                              valstep=1,
-                              closedmax=True)
+            b_slider = Slider(
+                slider_bx,  # axes object for the slider
+                "dashed lines",  # name of the slider parameter
+                0,  # minimal value of parameter
+                len(sig0_vals["tot"]) - 1,  # maximal value of parameter
+                valinit=0,  # initial value of parameter
+                valfmt="%i",
+                valstep=1,
+                closedmax=True,
+            )
 
             b_slider.valtext.set_visible(False)
 
-            slider_bx.set_xticks(np.arange(slider_bx.get_xlim()[0] - 1,
-                                           slider_bx.get_xlim()[1] + 1, 1,
-                                           dtype=int))
+            slider_bx.set_xticks(
+                np.arange(
+                    slider_bx.get_xlim()[0] - 1,
+                    slider_bx.get_xlim()[1] + 1,
+                    1,
+                    dtype=int,
+                )
+            )
             slider_bx.tick_params(bottom=False, labelbottom=False)
             slider_bx.grid()
 
             label2 = slider_bx.text(
-                0, 0.5,
+                0,
+                0.5,
                 f"{sig0_vals['indexes'][0].strftime('%d. %b %Y %H:%M')}",
-                horizontalalignment='center', verticalalignment='center',
-                fontsize=8, bbox=dict(facecolor='w', alpha=0.75,
-                                      boxstyle='round,pad=.2'))
-
-            b_slider.on_changed(partial(animate,
-                                        lines=lines2,
-                                        linesfull=linesfull2,
-                                        lines_frac = lines_frac2,
-                                        lines_frac_full = lines_frac_full2,
-                                        dayrange=range2,
-                                        printcomponents=printcomponents2,
-                                        label=label2))
-
-            ax2.callbacks.connect('xlim_changed', partial(updatesliderboundary,
-                                                          slider=b_slider))
+                horizontalalignment="center",
+                verticalalignment="center",
+                fontsize=8,
+                bbox=dict(facecolor="w", alpha=0.75, boxstyle="round,pad=.2"),
+            )
+
+            b_slider.on_changed(
+                partial(
+                    animate,
+                    lines=lines2,
+                    linesfull=linesfull2,
+                    lines_frac=lines_frac2,
+                    lines_frac_full=lines_frac_full2,
+                    dayrange=range2,
+                    printcomponents=printcomponents2,
+                    label=label2,
+                )
+            )
 
+            ax2.callbacks.connect(
+                "xlim_changed", partial(updatesliderboundary, slider=b_slider)
+            )
 
         # a reference to the sliders must be returned in order to
         # remain interactive
@@ -1875,11 +2332,18 @@ def updatesliderboundary(evt, slider):
         else:
             return f, a_slider
 
-
-    def analyzemodel(self, fit=None, defdict=None, inc=None,
-                     labels = None, dB=True, sig0=True, int_Q=None,
-                     fillcomponents=True):
-        '''
+    def analyzemodel(
+        self,
+        fit=None,
+        defdict=None,
+        inc=None,
+        labels=None,
+        dB=True,
+        sig0=True,
+        int_Q=None,
+        fillcomponents=True,
+    ):
+        """
         Analyze the range of backscatter for a given model-configuration
         based on the defined parameter-ranges
 
@@ -1917,12 +2381,12 @@ def analyzemodel(self, fit=None, defdict=None, inc=None,
         f, slider, buttons
             the matplotlib figure, slider and button instances
 
-        '''
+        """
 
         if fit is None:
             fit = self.fit
 
-        res_dict = getattr(fit, 'res_dict', None)
+        res_dict = getattr(fit, "res_dict", None)
 
         if defdict is None:
             defdict = fit.defdict
@@ -1930,36 +2394,35 @@ def analyzemodel(self, fit=None, defdict=None, inc=None,
         if inc is None:
             inc = np.deg2rad(np.linspace(1, 89, 100))
 
-
         if labels is None:
             labels = dict()
 
         if int_Q is None:
             int_Q = fit.int_Q
 
-
         # get parameter ranges from defdict and fit
         minparams, maxparams, startparams, fixparams = {}, {}, {}, {}
         for key, val in defdict.items():
             if val[0] is True:
-                 minparams[key] = val[3][0][0]
-                 maxparams[key] = val[3][1][0]
-                 # try to use fitted-values as start values for the parameters
-                 if res_dict is not None and key in res_dict:
-                     startparams[key] = np.mean(res_dict[key][0])
-                 else:
-                     startparams[key] = val[1]
+                minparams[key] = val[3][0][0]
+                maxparams[key] = val[3][1][0]
+                # try to use fitted-values as start values for the parameters
+                if res_dict is not None and key in res_dict:
+                    startparams[key] = np.mean(res_dict[key][0])
+                else:
+                    startparams[key] = val[1]
             if val[0] is False:
                 if isinstance(val[1], (int, float)):
-                         # don't add constants to startparams (they are
-                         # directly inserted into the functions)
-                         #startparams[key] = val[1]
-                         fixparams[key] = val[1]
-                elif val[1] == 'auxiliary':
-                    assert (key in fit.dataset or
-                            key in fit.fixed_dict), (f'auxiliary dataset for {key} ' +
-                                                     'not found in fit.dataset or ' +
-                                                     'fit.fixed_dict')
+                    # don't add constants to startparams (they are
+                    # directly inserted into the functions)
+                    # startparams[key] = val[1]
+                    fixparams[key] = val[1]
+                elif val[1] == "auxiliary":
+                    assert key in fit.dataset or key in fit.fixed_dict, (
+                        f"auxiliary dataset for {key} "
+                        + "not found in fit.dataset or "
+                        + "fit.fixed_dict"
+                    )
                     if key in fit.dataset:
                         minparams[key] = fit.dataset[key].min()
                         maxparams[key] = fit.dataset[key].max()
@@ -1970,95 +2433,126 @@ def analyzemodel(self, fit=None, defdict=None, inc=None,
                         maxparams[key] = fit.fixed_dict[key].max()
                         startparams[key] = fit.fixed_dict[key].mean()
 
-        if 'bsf' not in defdict:
-            startparams['bsf']  = fit.R.bsf
-            fixparams['bsf']  = fit.R.bsf
+        if "bsf" not in defdict:
+            startparams["bsf"] = fit.R.bsf
+            fixparams["bsf"] = fit.R.bsf
 
-        modelresult = dict(zip(['tot','surf','vol','inter'],
-                                fit.calc(startparams, inc=inc)))
+        modelresult = dict(
+            zip(["tot", "surf", "vol", "inter"], fit.calc(startparams, inc=inc))
+        )
         # convert to sig0 and dB if required
         for key, val in modelresult.items():
-            modelresult[key] = dBsig0convert(val[0], inc, dB, sig0,
-                                             fit.dB, fit.sig0)
-
+            modelresult[key] = dBsig0convert(val[0], inc, dB, sig0, fit.dB, fit.sig0)
 
-        f = plt.figure(figsize=(12,9))
+        f = plt.figure(figsize=(12, 9))
         f.subplots_adjust(top=0.93, right=0.98, left=0.07)
-                      # generate figure grid and populate with axes
-        gs = GridSpec(1 + len(minparams)//2, 1 + 3 ,
-                      height_ratios=[8] + [1]*(len(minparams) // 2),
-                      width_ratios=[.75, 1, 1, 1])
-        gs.update(wspace=.3)
-
-        gsslider = GridSpec(1 + len(minparams)//2, 1 + 3 ,
-                            height_ratios=[8] + [1]*(len(minparams) // 2),
-                            width_ratios=[.75, 1, 1, 1])
-        gsslider.update(wspace=.3, bottom=0.05)
-
-        gsbutton = GridSpec(1 + len(minparams)//2, 1 + 3 ,
-                            height_ratios=[8] + [1]*(len(minparams)//2),
-                            width_ratios=[.75, 1, 1, 1])
-        gsbutton.update(hspace=0.75, wspace=.1, bottom=0.05)
-
-
-        ax = f.add_subplot(gs[0,0:])
+        # generate figure grid and populate with axes
+        gs = GridSpec(
+            1 + len(minparams) // 2,
+            1 + 3,
+            height_ratios=[8] + [1] * (len(minparams) // 2),
+            width_ratios=[0.75, 1, 1, 1],
+        )
+        gs.update(wspace=0.3)
+
+        gsslider = GridSpec(
+            1 + len(minparams) // 2,
+            1 + 3,
+            height_ratios=[8] + [1] * (len(minparams) // 2),
+            width_ratios=[0.75, 1, 1, 1],
+        )
+        gsslider.update(wspace=0.3, bottom=0.05)
+
+        gsbutton = GridSpec(
+            1 + len(minparams) // 2,
+            1 + 3,
+            height_ratios=[8] + [1] * (len(minparams) // 2),
+            width_ratios=[0.75, 1, 1, 1],
+        )
+        gsbutton.update(hspace=0.75, wspace=0.1, bottom=0.05)
+
+        ax = f.add_subplot(gs[0, 0:])
 
         paramaxes = {}
         col = 0
         for i, key in enumerate(minparams):
-            if i%3 == 0: col += 1
-            paramaxes[key] = f.add_subplot(gsslider[col, 1 + i%3])
+            if i % 3 == 0:
+                col += 1
+            paramaxes[key] = f.add_subplot(gsslider[col, 1 + i % 3])
 
         buttonax = f.add_subplot(gsbutton[1:, 0])
         # hide frame of button-axes
-        buttonax.axis('off')
+        buttonax.axis("off")
         # add values of fixed parameters
         if len(fixparams) > 0:
-            ax.text(.01, .98, 'fixed parameters:\n' +
-                    ''.join([f'{key}={round(val, 5)}   ' for
-                             key, val in fixparams.items()]))
+            ax.text(
+                0.01,
+                0.98,
+                "fixed parameters:\n"
+                + "".join(
+                    [f"{key}={round(val, 5)}   " for key, val in fixparams.items()]
+                ),
+            )
 
         # overplot data used in fit
         try:
-            ax.plot(fit.dataset.inc,
-                    dBsig0convert(fit.dataset.sig, fit.dataset.inc,
-                                  dB, sig0, fit.dB, fit.sig0),
-                    zorder=0, marker='.', alpha=0.5, lw=0,
-                    markerfacecolor='none', markeredgecolor='k')
+            ax.plot(
+                fit.dataset.inc,
+                dBsig0convert(
+                    fit.dataset.sig,
+                    fit.dataset.inc,
+                    dB,
+                    sig0,
+                    fit.dB,
+                    fit.sig0,
+                ),
+                zorder=0,
+                marker=".",
+                alpha=0.5,
+                lw=0,
+                markerfacecolor="none",
+                markeredgecolor="k",
+            )
 
         except:
             pass
 
         # plot initial curves
-        ltot, = ax.plot(inc, modelresult['tot'], 'k',
-                        label = 'total contribution')
+        (ltot,) = ax.plot(inc, modelresult["tot"], "k", label="total contribution")
 
-        lsurf, = ax.plot(inc, modelresult['surf'], 'b',
-                         label = 'surface contribution')
+        (lsurf,) = ax.plot(inc, modelresult["surf"], "b", label="surface contribution")
 
-        lvol, = ax.plot(inc, modelresult['vol'], 'g',
-                        label = 'volume contribution')
+        (lvol,) = ax.plot(inc, modelresult["vol"], "g", label="volume contribution")
 
         if int_Q is True:
-            lint, = ax.plot(inc, modelresult['inter'], 'y',
-                            label = 'interaction contribution')
-
-
-        if dB is True: ax.set_ylim(-35, 5)
+            (lint,) = ax.plot(
+                inc,
+                modelresult["inter"],
+                "y",
+                label="interaction contribution",
+            )
+
+        if dB is True:
+            ax.set_ylim(-35, 5)
         ax.xaxis.set_major_locator(plt.MaxNLocator(10))
-        ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x,y: f"{np.rad2deg(x):.1f}"))
+        ax.xaxis.set_major_formatter(
+            plt.FuncFormatter(lambda x, y: f"{np.rad2deg(x):.1f}")
+        )
         # a legend for the lines
-        leg0 = ax.legend(ncol=4, bbox_to_anchor=(.5, 1.1), loc='upper center')
+        leg0 = ax.legend(ncol=4, bbox_to_anchor=(0.5, 1.1), loc="upper center")
         # add the line-legend as individual artist
         ax.add_artist(leg0)
 
-        if dB is True and sig0 is True: ax.set_ylabel(r'$\sigma_0$ [dB]')
-        if dB is True and sig0 is False: ax.set_ylabel(r'$I/I_0$ [dB]')
-        if dB is False and sig0 is True: ax.set_ylabel(r'$\sigma_0$')
-        if dB is False and sig0 is False: ax.set_ylabel(r'$I/I_0$')
-
-        ax.set_xlabel(r'$\theta_0$ [deg]')
+        if dB is True and sig0 is True:
+            ax.set_ylabel(r"$\sigma_0$ [dB]")
+        if dB is True and sig0 is False:
+            ax.set_ylabel(r"$I/I_0$ [dB]")
+        if dB is False and sig0 is True:
+            ax.set_ylabel(r"$\sigma_0$")
+        if dB is False and sig0 is False:
+            ax.set_ylabel(r"$I/I_0$")
 
+        ax.set_xlabel(r"$\theta_0$ [deg]")
 
         # create the slider for the parameter
         paramslider = {}
@@ -2073,45 +2567,49 @@ def analyzemodel(self, fit=None, defdict=None, inc=None,
 
             startval = startparams[key]
 
-            paramslider[key] = Slider(paramaxes[key], # axes object for the slider
-                              keylabel,               # name of the slider
-                              minparams[key],         # minimal value
-                              maxparams[key],         # maximal value
-                              startval,               # initial value
-                              #valfmt="%i"            # slider-value as integer
-                              color='gray')
-            paramslider[key].label.set_position([.05, 0.5])
-            paramslider[key].label.set_bbox(dict(boxstyle="round,pad=0.5",
-                                                 facecolor='w'))
-            paramslider[key].label.set_horizontalalignment('left')
-            paramslider[key].valtext.set_position([.8, 0.5])
-
-
-        buttons = CheckButtons(buttonax, buttonlabels,
-                               [False for i in buttonlabels])
+            paramslider[key] = Slider(
+                paramaxes[key],  # axes object for the slider
+                keylabel,  # name of the slider
+                minparams[key],  # minimal value
+                maxparams[key],  # maximal value
+                startval,  # initial value
+                # valfmt="%i"            # slider-value as integer
+                color="gray",
+            )
+            paramslider[key].label.set_position([0.05, 0.5])
+            paramslider[key].label.set_bbox(
+                dict(boxstyle="round,pad=0.5", facecolor="w")
+            )
+            paramslider[key].label.set_horizontalalignment("left")
+            paramslider[key].valtext.set_position([0.8, 0.5])
+
+        buttons = CheckButtons(buttonax, buttonlabels, [False for i in buttonlabels])
 
         params = startparams.copy()
         # define function to update lines based on slider-input
         def animate(value, key):
-            #params = copy.deepcopy(startparams)
+            # params = copy.deepcopy(startparams)
 
             params[key] = value
-            modelresult = dict(zip(['tot','surf','vol','inter'],
-                                fit.calc(params, inc=inc)))
+            modelresult = dict(
+                zip(["tot", "surf", "vol", "inter"], fit.calc(params, inc=inc))
+            )
             # convert to sig0 and dB if required
             for key, val in modelresult.items():
-                modelresult[key] = dBsig0convert(val[0], inc, dB, sig0,
-                                                 fit.dB, fit.sig0)
+                modelresult[key] = dBsig0convert(
+                    val[0], inc, dB, sig0, fit.dB, fit.sig0
+                )
 
             # update the data
-            ltot.set_ydata(modelresult['tot'].T)
-            lsurf.set_ydata(modelresult['surf'].T)
-            lvol.set_ydata(modelresult['vol'].T)
-            if int_Q is True: lint.set_ydata(modelresult['inter'].T)
+            ltot.set_ydata(modelresult["tot"].T)
+            lsurf.set_ydata(modelresult["surf"].T)
+            lvol.set_ydata(modelresult["vol"].T)
+            if int_Q is True:
+                lint.set_ydata(modelresult["inter"].T)
 
             # poverprint boundaries
-            hatches = [r'//', r'\\ ', '+', 'oo', '--', '..']
-            colors = ['C' + str(i) for i in range(10)]
+            hatches = [r"//", r"\\ ", "+", "oo", "--", ".."]
+            colors = ["C" + str(i) for i in range(10)]
             ax.collections.clear()
             legendhandles = []
             for i, [key_i, key_Q] in enumerate(printvariationQ.items()):
@@ -2122,76 +2620,110 @@ def animate(value, key):
                     keylabel = key_i
 
                 # reset color of text-backtround
-                paramslider[key_i].label.get_bbox_patch().set_facecolor('w')
+                paramslider[key_i].label.get_bbox_patch().set_facecolor("w")
                 if key_Q is True:
                     # set color of text-background to hatch-color
-                    #paramslider[key_i].label.set_color(colors[i%len(colors)])
-                    paramslider[key_i].label.get_bbox_patch(
-                        ).set_facecolor(colors[i%len(colors)])
+                    # paramslider[key_i].label.set_color(colors[i%len(colors)])
+                    paramslider[key_i].label.get_bbox_patch().set_facecolor(
+                        colors[i % len(colors)]
+                    )
 
                     fillparams = params.copy()
                     fillparams[key_i] = minparams[key_i]
 
                     # don't use bsf=1 since no vegetation-term would be present
-                    if fillparams.get('bsf', 0.) == 1.:
-                        fillparams['bsf'] = 0.999
-
-                    modelresultmin = dict(zip(['tot','surf','vol','inter'],
-                                        fit.calc(fillparams, inc=inc)))
+                    if fillparams.get("bsf", 0.0) == 1.0:
+                        fillparams["bsf"] = 0.999
+
+                    modelresultmin = dict(
+                        zip(
+                            ["tot", "surf", "vol", "inter"],
+                            fit.calc(fillparams, inc=inc),
+                        )
+                    )
                     # convert to sig0 and dB if required
                     for key, val in modelresultmin.items():
-                        modelresultmin[key] = dBsig0convert(val[0], inc,
-                                                            dB, sig0,
-                                                            fit.dB, fit.sig0)
-
+                        modelresultmin[key] = dBsig0convert(
+                            val[0], inc, dB, sig0, fit.dB, fit.sig0
+                        )
 
                     fillparams[key_i] = maxparams[key_i]
 
                     # don't use bsf=1 since no vegetation-term would be present
-                    if fillparams.get('bsf', 0.) == 1.:
-                        fillparams['bsf'] = 0.999
-
-                    modelresultmax = dict(zip(['tot','surf','vol','inter'],
-                                        fit.calc(fillparams, inc=inc)))
+                    if fillparams.get("bsf", 0.0) == 1.0:
+                        fillparams["bsf"] = 0.999
+
+                    modelresultmax = dict(
+                        zip(
+                            ["tot", "surf", "vol", "inter"],
+                            fit.calc(fillparams, inc=inc),
+                        )
+                    )
                     # convert to sig0 and dB if required
                     for key, val in modelresultmax.items():
-                        modelresultmax[key] = dBsig0convert(val[0], inc,
-                                                            dB, sig0,
-                                                            fit.dB, fit.sig0)
-
-
-
-                    legendhandles += [ax.fill_between(
-                        inc, modelresultmax['tot'], modelresultmin['tot'],
-                        facecolor='none', hatch=hatches[i%len(hatches)],
-                        edgecolor=colors[i%len(colors)],
-                        label = 'total variability (' + keylabel + ')')]
+                        modelresultmax[key] = dBsig0convert(
+                            val[0], inc, dB, sig0, fit.dB, fit.sig0
+                        )
+
+                    legendhandles += [
+                        ax.fill_between(
+                            inc,
+                            modelresultmax["tot"],
+                            modelresultmin["tot"],
+                            facecolor="none",
+                            hatch=hatches[i % len(hatches)],
+                            edgecolor=colors[i % len(colors)],
+                            label="total variability (" + keylabel + ")",
+                        )
+                    ]
 
                     if fillcomponents is True:
 
-                        legendhandles += [ax.fill_between(
-                            inc, modelresultmax['surf'], modelresultmin['surf'],
-                            color='b', alpha = 0.1,
-                            label = 'surf variability (' + keylabel + ')')]
-
-                        legendhandles += [ax.fill_between(
-                            inc, modelresultmax['vol'], modelresultmin['vol'],
-                            color='g', alpha = 0.1,
-                            label = 'vol variability (' + keylabel + ')')]
+                        legendhandles += [
+                            ax.fill_between(
+                                inc,
+                                modelresultmax["surf"],
+                                modelresultmin["surf"],
+                                color="b",
+                                alpha=0.1,
+                                label="surf variability (" + keylabel + ")",
+                            )
+                        ]
+
+                        legendhandles += [
+                            ax.fill_between(
+                                inc,
+                                modelresultmax["vol"],
+                                modelresultmin["vol"],
+                                color="g",
+                                alpha=0.1,
+                                label="vol variability (" + keylabel + ")",
+                            )
+                        ]
 
                         if int_Q is True:
-                            legendhandles += [ax.fill_between(
-                                inc, modelresultmax['inter'],
-                                modelresultmin['inter'], color='y', alpha = 0.1,
-                                label = 'int variability (' + keylabel + ')')]
+                            legendhandles += [
+                                ax.fill_between(
+                                    inc,
+                                    modelresultmax["inter"],
+                                    modelresultmin["inter"],
+                                    color="y",
+                                    alpha=0.1,
+                                    label="int variability (" + keylabel + ")",
+                                )
+                            ]
 
                 # a legend for the hatches
-                leg1 = ax.legend(handles=legendhandles,
-                                 labels=[i.get_label() for i in legendhandles])
+                leg1 = ax.legend(
+                    handles=legendhandles,
+                    labels=[i.get_label() for i in legendhandles],
+                )
 
-                if len(legendhandles) == 0: leg1.remove()
+                if len(legendhandles) == 0:
+                    leg1.remove()
 
         printvariationQ = {key: False for key in minparams}
+
         def buttonfunc(label):
             # if labels of the buttons have been changed by the labels-argument
             # set the name to the corresponding key (= the actual parameter name)
@@ -2199,7 +2731,7 @@ def buttonfunc(label):
                 if label == val:
                     label = key
 
-            #ax.collections.clear()
+            # ax.collections.clear()
             if printvariationQ[label] is True:
                 ax.collections.clear()
                 printvariationQ[label] = False
@@ -2215,11 +2747,13 @@ def buttonfunc(label):
         for key, slider in paramslider.items():
             slider.on_changed(partial(animate, key=key))
 
-
         # define textboxes that allow changing the slider-boundaries
-        bounds = dict(zip(minparams.keys(),
-                          np.array([list(minparams.values()),
-                                    list(maxparams.values())]).T))
+        bounds = dict(
+            zip(
+                minparams.keys(),
+                np.array([list(minparams.values()), list(maxparams.values())]).T,
+            )
+        )
 
         def submit(val, key, minmax):
             slider = paramslider[key]
@@ -2245,36 +2779,51 @@ def submit(val, key, minmax):
         textboxes_buttons = {}
         for i, [key, val] in enumerate(paramslider.items()):
 
-            axbox0 = plt.axes([val.ax.get_position().x0,
-                               val.ax.get_position().y1,
-                               0.05, 0.025])
-            text_box0 = TextBox(axbox0, '', initial=str(round(bounds[key][0], 4)))
+            axbox0 = plt.axes(
+                [
+                    val.ax.get_position().x0,
+                    val.ax.get_position().y1,
+                    0.05,
+                    0.025,
+                ]
+            )
+            text_box0 = TextBox(axbox0, "", initial=str(round(bounds[key][0], 4)))
             text_box0.on_submit(partial(submit, key=key, minmax=0))
 
-
-            axbox1 = plt.axes([val.ax.get_position().x1 - 0.05,
-                               val.ax.get_position().y1,
-                               0.05, 0.025])
-            text_box1 = TextBox(axbox1, '', initial=str(round(bounds[key][1], 4)))
+            axbox1 = plt.axes(
+                [
+                    val.ax.get_position().x1 - 0.05,
+                    val.ax.get_position().y1,
+                    0.05,
+                    0.025,
+                ]
+            )
+            text_box1 = TextBox(axbox1, "", initial=str(round(bounds[key][1], 4)))
             text_box1.on_submit(partial(submit, key=key, minmax=1))
 
+            textboxes_buttons[key + "_min"] = text_box0
+            textboxes_buttons[key + "_max"] = text_box1
 
-            textboxes_buttons[key + '_min'] = text_box0
-            textboxes_buttons[key + '_max'] = text_box1
-
-        textboxes_buttons['buttons'] = buttons
+        textboxes_buttons["buttons"] = buttons
 
         return f, paramslider, textboxes_buttons
 
-
-    def intermediate_residuals(self, fit=None, grp='M', err='relerr',
-                              label_formatter=None, plottype='3D',
-                              iter_slice=slice(2,None), f_gs=None,
-                              colorbar=True, project_contour=False,
-                              fmt='%d.%m.%y %H:%M:%S',
-                              axtitle=None,
-                              cmap='coolwarm'):
-        '''
+    def intermediate_residuals(
+        self,
+        fit=None,
+        grp="M",
+        err="relerr",
+        label_formatter=None,
+        plottype="3D",
+        iter_slice=slice(2, None),
+        f_gs=None,
+        colorbar=True,
+        project_contour=False,
+        fmt="%d.%m.%y %H:%M:%S",
+        axtitle=None,
+        cmap="coolwarm",
+    ):
+        """
         generate a 2D or 3D  plot of the intermediate residuals during the fit
 
         Parameters
@@ -2334,15 +2883,16 @@ def intermediate_residuals(self, fit=None, grp='M', err='relerr',
         ax : matplotlib.axes
             the matplotlib axes used
 
-        '''
+        """
         if fit is None:
             fit = self.fit
         try:
             fit.intermediate_results
         except AttributeError:
-            assert False, ('No intermediate results are found, you must run' +
-                           ' performfit() with intermediate_results=True!')
-
+            assert False, (
+                "No intermediate results are found, you must run"
+                + " performfit() with intermediate_results=True!"
+            )
 
         # get the step size of the iterator to correctly assign the labels
         if iter_slice and iter_slice.step:
@@ -2350,11 +2900,13 @@ def intermediate_residuals(self, fit=None, grp='M', err='relerr',
         else:
             iterstep = 1
 
-
         if isinstance(grp, tuple):
             if grp[0] in fit.res_df:
-                grps = pd.cut(fit.res_df[grp[0]].reindex(fit.dataset.index),
-                              grp[1], include_lowest=True)
+                grps = pd.cut(
+                    fit.res_df[grp[0]].reindex(fit.dataset.index),
+                    grp[1],
+                    include_lowest=True,
+                )
             elif grp[0] in fit.dataset:
                 grps = pd.cut(fit.dataset[grp[0]], grp[1], include_lowest=True)
             grpvals = grps.cat.categories.mid.values
@@ -2362,201 +2914,232 @@ def intermediate_residuals(self, fit=None, grp='M', err='relerr',
 
             # group the residuals with respect to the defined group
             resarr = np.abs(
-                [pd.DataFrame(i[err], fit.dataset.index).groupby(
-                    grps).mean().values.flatten()
-                 for i in fit.intermediate_results['residuals'][iter_slice]]
-                )
+                [
+                    pd.DataFrame(i[err], fit.dataset.index)
+                    .groupby(grps)
+                    .mean()
+                    .values.flatten()
+                    for i in fit.intermediate_results["residuals"][iter_slice]
+                ]
+            )
             if axtitle is None:
                 use_label = grp[0]
             else:
                 use_label = axtitle
 
         elif isinstance(grp, str):
-            if grp == 'groups':
+            if grp == "groups":
                 # group the residuals with respect to the defined group
                 grplabels = pd.to_datetime(fit.meandatetimes_group)
                 resarr = np.abs(
-                    [pd.DataFrame(i[err], fit.dataset.index).groupby(
-                        fit._groupindex).mean().values.flatten()
-                     for i in fit.intermediate_results['residuals'][iter_slice]]
-                    )
+                    [
+                        pd.DataFrame(i[err], fit.dataset.index)
+                        .groupby(fit._groupindex)
+                        .mean()
+                        .values.flatten()
+                        for i in fit.intermediate_results["residuals"][iter_slice]
+                    ]
+                )
                 grpvals = np.arange(resarr.shape[1])
-                ymin, ymax = grpvals.min() - .5, grpvals.max() + .5
+                ymin, ymax = grpvals.min() - 0.5, grpvals.max() + 0.5
+
                 def label_formatter(x, pos=None):
                     if x in grpvals:
                         return grplabels[int(x)].strftime(fmt)
                     else:
-                        return ''
+                        return ""
 
-                use_label = ''
-            elif grp == 'dataset':
+                use_label = ""
+            elif grp == "dataset":
                 # group the residuals with respect to the dataset-index
                 grplabels = pd.to_datetime(fit.index)
                 resarr = np.abs(
-                    [pd.DataFrame(i[err], fit.dataset.index).groupby(
-                        level=0).mean().values.flatten()
-                     for i in fit.intermediate_results['residuals'][iter_slice]]
-                    )
+                    [
+                        pd.DataFrame(i[err], fit.dataset.index)
+                        .groupby(level=0)
+                        .mean()
+                        .values.flatten()
+                        for i in fit.intermediate_results["residuals"][iter_slice]
+                    ]
+                )
                 grpvals = np.arange(resarr.shape[1])
-                ymin, ymax = grpvals.min() - .5, grpvals.max() + .5
+                ymin, ymax = grpvals.min() - 0.5, grpvals.max() + 0.5
+
                 def label_formatter(x, pos=None):
                     if x in grpvals:
                         return grplabels[int(x)].strftime(fmt)
                     else:
-                        return ''
+                        return ""
 
-                use_label = ''
+                use_label = ""
             else:
                 # group the residuals with respect to the defined group
-                grplabels = pd.DataFrame(
-                    fit.intermediate_results['residuals'][0][err],
-                    fit.dataset.index).groupby(pd.Grouper(freq=grp)
-                                               ).mean().index
+                grplabels = (
+                    pd.DataFrame(
+                        fit.intermediate_results["residuals"][0][err],
+                        fit.dataset.index,
+                    )
+                    .groupby(pd.Grouper(freq=grp))
+                    .mean()
+                    .index
+                )
 
                 resarr = np.abs(
-                    [pd.DataFrame(i[err], fit.dataset.index).groupby(
-                        pd.Grouper(freq=grp)).mean().values.flatten()
-                     for i in fit.intermediate_results['residuals'][iter_slice]]
-                    )
+                    [
+                        pd.DataFrame(i[err], fit.dataset.index)
+                        .groupby(pd.Grouper(freq=grp))
+                        .mean()
+                        .values.flatten()
+                        for i in fit.intermediate_results["residuals"][iter_slice]
+                    ]
+                )
                 grpvals = np.arange(resarr.shape[1])
-                ymin, ymax = grpvals.min() - .5, grpvals.max() + .5
-
+                ymin, ymax = grpvals.min() - 0.5, grpvals.max() + 0.5
 
                 def label_formatter(x, pos=None):
                     if x in grpvals:
                         return grplabels[int(x)].strftime(fmt)
 
                     else:
-                        return ''
+                        return ""
 
-                use_label = ''
+                use_label = ""
 
         # mask for nan-values
         resarr = np.ma.masked_array(resarr, np.isnan(resarr))
 
-        xvals = np.arange(1,
-                          len(fit.intermediate_results['residuals']
-                              ) + 1)[iter_slice]
+        xvals = np.arange(1, len(fit.intermediate_results["residuals"]) + 1)[iter_slice]
         yvals = np.sort(pd.unique(grpvals))
 
-        if plottype == '3D':
+        if plottype == "3D":
             # generate a 3D surface plot
 
             Y, X = np.meshgrid(yvals, xvals)
             if f_gs is None:
-                fig = plt.figure(figsize=(10,8))
-                ax = fig.add_subplot(111, projection='3d')
+                fig = plt.figure(figsize=(10, 8))
+                ax = fig.add_subplot(111, projection="3d")
             else:
                 fig = f_gs[0]
-                ax = fig.add_subplot(f_gs[1], projection='3d')
+                ax = fig.add_subplot(f_gs[1], projection="3d")
             ax.grid()
             for a in (ax.xaxis, ax.yaxis, ax.zaxis):
-                a.pane.set_color('none')
+                a.pane.set_color("none")
 
             if project_contour:
                 # project a contour-plot to the bottom
                 ax.contourf(
-                    X, Y, resarr,
-                    zdir='z',
-                    offset=-(np.nanmax(resarr) - np.nanmin(resarr))*.1,
-                    cmap=plt.cm.coolwarm, alpha=0.5,
-                    extent=[xvals.min() - iterstep, xvals.max() ,
-                            ymin, ymax],
-                    origin='lower')
-
-                ax.set_zlim(np.nanmin(resarr) -
-                            (np.nanmax(resarr) - np.nanmin(resarr))*.1,
-                            np.nanmax(resarr))
+                    X,
+                    Y,
+                    resarr,
+                    zdir="z",
+                    offset=-(np.nanmax(resarr) - np.nanmin(resarr)) * 0.1,
+                    cmap=plt.cm.coolwarm,
+                    alpha=0.5,
+                    extent=[xvals.min() - iterstep, xvals.max(), ymin, ymax],
+                    origin="lower",
+                )
+
+                ax.set_zlim(
+                    np.nanmin(resarr) - (np.nanmax(resarr) - np.nanmin(resarr)) * 0.1,
+                    np.nanmax(resarr),
+                )
 
             # plot a 3D surface
-            surf = ax.plot_surface(X, Y, resarr,
-                                    cmap=cmap,
-                                    linewidth=0, antialiased=True,
-                                    vmin=np.nanmin(resarr),
-                                    vmax=np.nanmax(resarr),
-                                    rcount=500,
-                                    ccount=500,
-                                    alpha=1)
+            surf = ax.plot_surface(
+                X,
+                Y,
+                resarr,
+                cmap=cmap,
+                linewidth=0,
+                antialiased=True,
+                vmin=np.nanmin(resarr),
+                vmax=np.nanmax(resarr),
+                rcount=500,
+                ccount=500,
+                alpha=1,
+            )
 
             # find points that are surrounded by a mask and
             # plot a connection-line and scatterpoints
-            maskp = [i == (True, False, True)
-                     for i in pairwise(
-                             [True, *np.all(resarr.mask, axis=0), True], 3)]
+            maskp = [
+                i == (True, False, True)
+                for i in pairwise([True, *np.all(resarr.mask, axis=0), True], 3)
+            ]
 
             for i in np.arange(len(maskp))[maskp]:
-                ax.plot_wireframe(X.T[[i]], Y.T[[i]],
-                                  resarr.T[[i]],
-                                  linewidth=.25,
-                                  antialiased=False,
-                                  linestyle=(0, (10,10)),
-                                  alpha=.25,
-                                  color='k')
-
-                surf = ax.scatter(X.T[i],
-                                  Y.T[i],
-                                  resarr.T[i],
-                                  c=resarr.T[i],
-                                  cmap=cmap,
-                                  vmin=np.nanmin(resarr),
-                                  vmax=np.nanmax(resarr))
+                ax.plot_wireframe(
+                    X.T[[i]],
+                    Y.T[[i]],
+                    resarr.T[[i]],
+                    linewidth=0.25,
+                    antialiased=False,
+                    linestyle=(0, (10, 10)),
+                    alpha=0.25,
+                    color="k",
+                )
 
+                surf = ax.scatter(
+                    X.T[i],
+                    Y.T[i],
+                    resarr.T[i],
+                    c=resarr.T[i],
+                    cmap=cmap,
+                    vmin=np.nanmin(resarr),
+                    vmax=np.nanmax(resarr),
+                )
 
             # incorporate the ticker if provided
             if label_formatter is not None:
-                ax.yaxis.set_major_formatter(
-                    ticker.FuncFormatter(label_formatter))
-
+                ax.yaxis.set_major_formatter(ticker.FuncFormatter(label_formatter))
 
-        elif plottype == '2D':
+        elif plottype == "2D":
             # generate a 2D imshow plot
             if f_gs is None:
-                fig = plt.figure(figsize=(10,8))
+                fig = plt.figure(figsize=(10, 8))
                 ax = fig.add_subplot(111)
             else:
                 fig = f_gs[0]
                 ax = f_gs[0].add_subplot(f_gs[1])
 
-            surf = ax.imshow(resarr.T,
-                             cmap=cmap,
-                             vmin=np.nanmin(resarr),
-                             vmax=np.nanmax(resarr),
-                             extent=[xvals.min() - .5*iterstep,
-                                     xvals.max() + .5*iterstep,
-                                     ymin, ymax],
-                             origin='lower',
-                             aspect='auto'
-                             )
+            surf = ax.imshow(
+                resarr.T,
+                cmap=cmap,
+                vmin=np.nanmin(resarr),
+                vmax=np.nanmax(resarr),
+                extent=[
+                    xvals.min() - 0.5 * iterstep,
+                    xvals.max() + 0.5 * iterstep,
+                    ymin,
+                    ymax,
+                ],
+                origin="lower",
+                aspect="auto",
+            )
 
             def xformatter(x, pos=None):
                 if x in xvals:
                     return int(x)
                 else:
-                    return ''
+                    return ""
 
             # show only relevant tick-labels on x-axis
             ax.xaxis.set_major_formatter(ticker.FuncFormatter(xformatter))
 
-
             if label_formatter is not None:
-                ax.yaxis.set_major_formatter(
-                    ticker.FuncFormatter(label_formatter))
+                ax.yaxis.set_major_formatter(ticker.FuncFormatter(label_formatter))
 
             ax.set_ylim(ymin, ymax)
 
-
         if colorbar is True:
             cb = plt.colorbar(surf)
-            if err == 'abserr':
+            if err == "abserr":
+                cb.set_label(r"Absolute error   $(x_{fit} - x_{data})$")
+            if err == "relerr":
                 cb.set_label(
-                    r'Absolute error   $(x_{fit} - x_{data})$')
-            if err == 'relerr':
-                cb.set_label(
-                    r'Relative error   $\frac{(x_{fit} - x_{data})}{x_{data}}$')
+                    r"Relative error   $\frac{(x_{fit} - x_{data})}{x_{data}}$"
+                )
 
         ax.set_ylabel(use_label)
-        ax.set_xlabel('# fit-iteration')
+        ax.set_xlabel("# fit-iteration")
         fig.tight_layout()
         return fig
-
-
diff --git a/rt1/rtprocess.py b/rt1/rtprocess.py
index 7dd00da4..a297bb1d 100644
--- a/rt1/rtprocess.py
+++ b/rt1/rtprocess.py
@@ -1,11 +1,12 @@
 import multiprocessing as mp
 from timeit import default_timer
-from datetime import timedelta
-from itertools import repeat
+from datetime import datetime, timedelta
+from itertools import repeat, islice
+from functools import partial
 import ctypes
 import sys
+import traceback
 from textwrap import dedent
-import warnings
 
 from pathlib import Path
 import shutil
@@ -13,26 +14,24 @@
 import pandas as pd
 from numpy.random import randint as np_randint
 
-from .general_functions import dt_to_hms, update_progress
+from .general_functions import dt_to_hms, update_progress, groupby_unsorted
 from .rtparse import RT1_configparser
 from .rtfits import load
+from . import log, _get_logger_formatter
+import logging
 
 
 try:
     import xarray as xar
 except ModuleNotFoundError:
-    print('xarray could not be imported,',
-          'NetCDF-features of RT1_results will not work!')
+    log.info(
+        "xarray could not be imported, "
+        + "NetCDF-features of RT1_results will not work!"
+    )
 
-try:
-    from netCDF4 import Dataset
-except ModuleNotFoundError:
-    print('netCDF4.Dataset could be not imported,',
-          'some NetCDF-features of RT1_results will not work!')
 
-
-def _confirm_input(msg='are you sure?', stopmsg='STOP', callbackdict=None):
-    '''
+def _confirm_input(msg="are you sure?", stopmsg="STOP", callbackdict=None):
+    """
     Parameters
     ----------
     msg : str, optional
@@ -41,7 +40,7 @@ def _confirm_input(msg='are you sure?', stopmsg='STOP', callbackdict=None):
         The message displayed if the answer is False. The default is 'STOP'.
     callbackdict : dict, optional
         A dict of the form {key: [msg, callback]} . The default is None.
-    '''
+    """
     default_answers = {"YES": True, "Y": True, "NO": False, "N": False}
 
     # prompt input
@@ -72,24 +71,30 @@ def _make_folderstructure(save_path, subfolders):
     if save_path is not None:
         # generate "save_path" directory if it does not exist
         if not save_path.exists():
-            print(f'"{save_path}"\ndoes not exist... creating directory')
+            log.info(f'"{save_path}"\ndoes not exist... creating directory')
             save_path.mkdir(parents=True, exist_ok=True)
 
         for folder in subfolders:
             # generate subfolder if it does not exist
             mkpath = save_path / folder
             if not mkpath.exists():
-                print(f'"{mkpath}"\ndoes not exist... creating directory')
+                log.info(f'"{mkpath}"\ndoes not exist... creating directory')
                 mkpath.mkdir(parents=True, exist_ok=True)
 
 
 def _setup_cnt(N_items, ncpu):
-
-    manager = mp.Manager()
-    lock = manager.Lock()
-    p_totcnt = manager.Value(ctypes.c_ulonglong, 0)
-    p_meancnt = manager.Value(ctypes.c_ulonglong, 0)
-    p_time = manager.Value(ctypes.c_float, 0)
+    if ncpu > 1:
+        manager = mp.Manager()
+        lock = manager.Lock()
+        p_totcnt = manager.Value(ctypes.c_ulonglong, 0)
+        p_meancnt = manager.Value(ctypes.c_ulonglong, 0)
+        p_time = manager.Value(ctypes.c_float, 0)
+    else:
+        # don't use a manager if single-core processing is used
+        p_totcnt = mp.Value(ctypes.c_ulonglong, 0)
+        p_meancnt = mp.Value(ctypes.c_ulonglong, 0)
+        p_time = mp.Value(ctypes.c_float, 0)
+        lock = None
 
     process_cnt = [p_totcnt, p_meancnt, N_items, p_time, ncpu, lock]
     return process_cnt
@@ -104,8 +109,9 @@ def _increase_cnt(process_cnt, start, err=False):
         return
 
     p_totcnt, p_meancnt, p_max, p_time, p_ncpu, lock = process_cnt
-    # ensure that only one process is allowed to write simultaneously
-    lock.acquire()
+    if lock is not None:
+        # ensure that only one process is allowed to write simultaneously
+        lock.acquire()
     try:
         if err is False:
             end = default_timer()
@@ -113,22 +119,26 @@ def _increase_cnt(process_cnt, start, err=False):
             p_totcnt.value += 1
 
             # update the estimate of the mean time needed to process a site
-            p_time.value = (p_meancnt.value * p_time.value
-                            + (end - start)) / (p_meancnt.value + 1)
+            p_time.value = (p_meancnt.value * p_time.value + (end - start)) / (
+                p_meancnt.value + 1
+            )
             # increase the mean counter
             p_meancnt.value += 1
             # get the remaining time and update the progressbar
-            remain = timedelta(
-                seconds=(p_max - p_totcnt.value) / p_ncpu * p_time.value)
+            remain = timedelta(seconds=(p_max - p_totcnt.value) / p_ncpu * p_time.value)
             d, h, m, s = dt_to_hms(remain)
 
-            update_progress(
-                p_totcnt.value, p_max,
+            msg = update_progress(
+                p_totcnt.value,
+                p_max,
                 title=f"approx. {d} {h:02}:{m:02}:{s:02} remaining",
                 finalmsg=(
-                    "finished! " +
-                    f"({p_max} [{p_totcnt.value - p_meancnt.value}] fits)"),
-                progress2=p_totcnt.value - p_meancnt.value)
+                    "finished! "
+                    + f"({p_max} [{p_totcnt.value - p_meancnt.value}] fits)"
+                ),
+                progress2=p_totcnt.value - p_meancnt.value,
+            )
+            log.progress(msg.strip())
         else:
             # only increase the total counter
             p_totcnt.value += 1
@@ -137,30 +147,49 @@ def _increase_cnt(process_cnt, start, err=False):
             else:
                 # get the remaining time and update the progressbar
                 remain = timedelta(
-                    seconds=(p_max - p_totcnt.value
-                             ) / p_ncpu * p_time.value)
+                    seconds=(p_max - p_totcnt.value) / p_ncpu * p_time.value
+                )
                 d, h, m, s = dt_to_hms(remain)
                 title = f"approx. {d} {h:02}:{m:02}:{s:02} remaining"
 
-            update_progress(
-                p_totcnt.value, p_max,
+            msg = update_progress(
+                p_totcnt.value,
+                p_max,
                 title=title,
                 finalmsg=(
-                    "finished! " +
-                    f"({p_max} [{p_totcnt.value - p_meancnt.value}] fits)"),
-                progress2=p_totcnt.value - p_meancnt.value)
-        # release the lock
-        lock.release()
+                    "finished! "
+                    + f"({p_max} [{p_totcnt.value - p_meancnt.value}] fits)"
+                ),
+                progress2=p_totcnt.value - p_meancnt.value,
+            )
+            log.progress(msg.strip())
+
+        if lock is not None:
+            # release the lock
+            lock.release()
     except Exception:
-        # release the lock in case an error occured
-        lock.release()
+        msg = "???"
+        if lock is not None:
+            # release the lock in case an error occured
+            lock.release()
         pass
 
+    sys.stdout.write(msg)
+    sys.stdout.flush()
+
 
 class RTprocess(object):
-    def __init__(self, config_path=None, autocontinue=False,
-                 proc_cls=None, parent_fit=None, init_kwargs=None):
-        '''
+    def __init__(
+        self,
+        config_path=None,
+        autocontinue=False,
+        copy=True,
+        proc_cls=None,
+        parent_fit=None,
+        init_kwargs=None,
+        setup=True,
+    ):
+        """
         A class to perform parallelized processing.
 
         Parameters
@@ -170,6 +199,10 @@ def __init__(self, config_path=None, autocontinue=False,
         autocontinue : bool, optional
             indicator if user-input should be raised (True) in case the
             dump-folder already exists. The default is False.
+        copy : bool
+            indicator if '.ini' files and modules should be copied to
+            (and imported from) the dumppath/cfg folder or not.
+            The default is True.
         proc_cls : class, optional
             the processing-class. (if None it will be imported use the
             'module__processing_cfg' from the 'CONFIGFILES' section of
@@ -195,105 +228,195 @@ def __init__(self, config_path=None, autocontinue=False,
             Additional keyword-arguments passed to the initialization of the
             'proc_cls' class. (used to append-to or partially overwrite
             definitions passed via the config-file). The default is None.
+        setup : bool, optional
+            indicator if `RTprocess.setup()` should be called during
+            initialization or not. This is required for multiprocessing
+            on Windows since `setup()` needs to be called outside of the
+            `if __name__ == '__main__'` protector to ensure correct import
+            of the used processing_config class.
+            Notice that if setup is set to True (the default) the
+            folder-structure etc. will be initialized as soon as the
+            class object is created!
+            The default is True.
+        """
 
-
-        '''
         self._config_path = config_path
         self.autocontinue = autocontinue
 
+        self._postprocess = True
+
+        self.copy = copy
+
         self._proc_cls = proc_cls
         self._parent_fit = parent_fit
         if init_kwargs is None:
             self.init_kwargs = dict()
         else:
 
-            assert all([isinstance(i, str) for i in init_kwargs.values()]), (
-                'the values of "init_kwargs" MUST be strings !')
+            assert all(
+                [isinstance(i, str) for i in init_kwargs.values()]
+            ), 'the values of "init_kwargs" MUST be strings !'
             self.init_kwargs = init_kwargs
 
-    def setup(self, copy=True):
-        '''
+        if setup:
+            self.setup()
+
+    def _listener_process(self, queue):
+
+        # adapted from https://docs.python.org/3.7/howto/logging-cookbook.html
+        # logging-to-a-single-file-from-multiple-processes
+        if not hasattr(self, "dumppath"):
+            log.error("listener process called without dumppath specified!")
+            return
+
+        datestring = datetime.now().strftime("%d_%m_%Y_%H_%M_%S")
+        path = self.dumppath / "cfg" / f"RT1_process({datestring}).log"
+
+        log.debug("listener process started for path: " + str(path))
+
+        log2 = logging.getLogger()
+        log2.setLevel(1)
+        h = logging.FileHandler(path)
+        h.setFormatter(_get_logger_formatter())
+        h.setLevel(1)
+        log2.addHandler(h)
+
+        while True:
+            try:
+                record = queue.get()
+                if record is None:  # quit listener if the message is None
+                    break
+                log2.handle(record)  # ...No level or filter logic applied
+            except Exception:
+                print("Whoops! Problem:", file=sys.stderr)
+                traceback.print_exc(file=sys.stderr)
+
+    @staticmethod
+    def _worker_configurer(queue, loglevel=10):
+
+        log.debug("configuring worker... ")
+        log.setLevel(loglevel)
+        h = logging.handlers.QueueHandler(queue)  # Just the one handler needed
+        h.setLevel(loglevel)
+        h.name = "rtprocessing_queuehandler"
+        log.addHandler(h)
+
+        logging.captureWarnings(True)
+        warnings_logger = logging.getLogger("py.warnings")
+        warnings_logger
+        warnings_logger.addHandler(h)
+
+    def setup(self):
+        """
         perform necessary tasks to run a processing-routine
           - initialize the folderstructure (only from MainProcess!)
           - copy modules and .ini files (if copy=True) (only from MainProcess!)
           - load modules and set parent-fit-object
+        """
 
-        Parameters
-        ----------
-        copy : bool
-            indicator if '.ini' files and modules should be copied to
-            the dumppath/cfg folder or not. The default is True.
-        '''
         if self._config_path is not None and self._proc_cls is None:
 
             self.config_path = Path(self._config_path)
-            assert self.config_path.exists(), (f'the file {self.config_path} '
-                                               + 'does not exist!')
+            assert self.config_path.exists(), (
+                f"the file {self.config_path} " + "does not exist!"
+            )
 
             self.cfg = RT1_configparser(self.config_path)
 
             # update specs with init_kwargs
+            # remember warnings and log them later in case dumppath is changed
             for key, val in self.init_kwargs.items():
-                if key in self.cfg.config['PROCESS_SPECS']:
-                    warnings.warn(
-                        f'"{key} = {self.cfg.config["PROCESS_SPECS"][key]}"' +
-                        'will be overwritten by the definition provided via' +
-                        f'"init_kwargs": "{key} = {val}" ')
+                if key in self.cfg.config["PROCESS_SPECS"]:
+                    log.warning(
+                        f'"{key} = {self.cfg.config["PROCESS_SPECS"][key]}" '
+                        + "will be overwritten by the definition provided via "
+                        + f'"init_kwargs": "{key} = {val}" '
+                    )
                     # update the parsed config (for import of modules etc.)
-                    self.cfg.config['PROCESS_SPECS'][key] = val
+                    self.cfg.config["PROCESS_SPECS"][key] = val
 
             specs = self.cfg.get_process_specs()
 
-            self.dumppath = specs['save_path'] / specs['dumpfolder']
+            self.dumppath = specs["save_path"] / specs["dumpfolder"]
 
-            if mp.current_process().name == 'MainProcess':
+            if mp.current_process().name == "MainProcess":
                 # init folderstructure and copy files only from main process
                 if self.autocontinue is False:
 
                     if self.dumppath.exists():
+
                         def remove_folder():
-                            shutil.rmtree(
-                                specs['save_path'] / specs['dumpfolder'])
-                            warnings.warn(
-                               f'"{specs["save_path"]/specs["dumpfolder"]}"' +
-                               '\nhas successfully been removed.\n')
+
+                            # try to close existing filehandlers to avoid
+                            # errors if the files are located in the folder
+                            # if they are actually in the cfg folder, delete
+                            # the corresponding handler as well!
+                            for h in log.handlers:
+                                try:
+                                    h.close()
+                                    if Path(h.baseFilename).parent.samefile(
+                                        self.dumppath / "cfg"
+                                    ):
+                                        log.removeHandler(h)
+                                except Exception:
+                                    pass
+
+                            # remove folderstructure
+                            shutil.rmtree(self.dumppath)
+
+                            log.info(
+                                f'"{self.dumppath}"'
+                                + "\nhas successfully been removed.\n"
+                            )
 
                         _confirm_input(
-                            msg=(f'the path \n "{self.dumppath}"\n' +
-                                 ' already exists...' +
-                                 '\n- to continue type YES or Y' +
-                                 '\n- to abort type NO or N' +
-                                 '\n- to remove the existing directory and ' +
-                                 'all subdirectories type REMOVE \n \n'),
-                            callbackdict={'REMOVE': [
-                                (f'\n"{self.dumppath}"\n will be removed!' +
-                                 ' are you sure? (y, n): '),
-                                remove_folder]})
+                            msg=(
+                                f'the path \n "{self.dumppath}"\n'
+                                + " already exists..."
+                                + "\n- to continue type YES or Y"
+                                + "\n- to abort type NO or N"
+                                + "\n- to remove the existing directory and "
+                                + "all subdirectories type REMOVE \n \n"
+                            ),
+                            stopmsg='aborting due to manual input "NO"',
+                            callbackdict={
+                                "REMOVE": [
+                                    (
+                                        f'\n"{self.dumppath}"\n will be removed!'
+                                        + " are you sure? (y, n): "
+                                    ),
+                                    remove_folder,
+                                ]
+                            },
+                        )
 
                 # initialize the folderstructure
-                _make_folderstructure(specs['save_path'] / specs['dumpfolder'],
-                                      ['results', 'cfg', 'dumps'])
+                _make_folderstructure(
+                    specs["save_path"] / specs["dumpfolder"],
+                    ["results", "cfg", "dumps"],
+                )
 
-                if copy is True:
+                if self.copy is True:
                     self._copy_cfg_and_modules()
 
             # load the processing-class
-            if 'processing_cfg_module' in specs:
-                proc_module_name = specs['processing_cfg_module']
+            if "processing_cfg_module" in specs:
+                proc_module_name = specs["processing_cfg_module"]
             else:
-                proc_module_name = 'processing_cfg'
+                proc_module_name = "processing_cfg"
 
-            if 'processing_cfg_class' in specs:
-                proc_class_name = specs['processing_cfg_class']
+            if "processing_cfg_class" in specs:
+                proc_class_name = specs["processing_cfg_class"]
             else:
-                proc_class_name = 'processing_cfg'
+                proc_class_name = "processing_cfg"
 
             # load ALL modules to ensure that the importer finds them
-            procmodule = self.cfg.get_all_modules(
-                load_copy=copy)[proc_module_name]
+            procmodule = self.cfg.get_all_modules(load_copy=self.copy)[proc_module_name]
 
-            warnings.warn(f'processing config class "{proc_class_name}"' +
-                          f'  will be imported from \n"{procmodule}"')
+            log.debug(
+                f'processing config class "{proc_class_name}"'
+                + f'  will be imported from \n"{procmodule}"'
+            )
 
             self.proc_cls = getattr(procmodule, proc_class_name)(**specs)
 
@@ -306,66 +429,75 @@ def remove_folder():
             self.dumppath = None
 
         # check if all necessary functions are defined in the  processing-class
-        for key in ['preprocess', 'reader', 'postprocess', 'finaloutput',
-                    'exceptfunc']:
-            assert hasattr(self.proc_cls, key), (
-                f'a function {key}() MUST be provided in the config-class!')
-
-        assert self.parent_fit is not None, (
-            'you MUST provide a valid config-file or a parent_fit-object!')
+        for key in [
+            "preprocess",
+            "reader",
+            "postprocess",
+            "finaloutput",
+            "exceptfunc",
+        ]:
+            assert hasattr(
+                self.proc_cls, key
+            ), f"a function {key}() MUST be provided in the config-class!"
+
+        assert (
+            self.parent_fit is not None
+        ), "you MUST provide a valid config-file or a parent_fit-object!"
 
     def _copy_cfg_and_modules(self):
         # if copy is True, copy the config-file and re-import the cfg
         # from the copied file
-        copypath = self.dumppath / 'cfg' / self.cfg.configpath.name
+        copypath = self.dumppath / "cfg" / self.cfg.configpath.name
         if (copypath).exists():
-            warnings.warn(
-                f'the file \n"{copypath / self.cfg.configpath.name}"\n' +
-                'already exists... NO copying is performed and the ' +
-                'existing one is used!\n')
+            log.warning(
+                f'the file \n"{copypath / self.cfg.configpath.name}"\n'
+                + "already exists... NO copying is performed and the "
+                + "existing one is used!\n"
+            )
         else:
             if len(self.init_kwargs) == 0:
                 # if no init_kwargs have been provided, copy the
                 # original file
                 shutil.copy(self.cfg.configpath, copypath.parent)
-                warnings.warn(f'"{self.cfg.configpath.name}" copied to\n' +
-                              f'"{copypath.parent}"')
+                log.info(
+                    f'"{self.cfg.configpath.name}" copied to\n' + f'"{copypath.parent}"'
+                )
             else:
                 # if init_kwargs have been provided, write the updated
                 # config to the folder
-                with open(copypath.parent /
-                          self.cfg.configpath.name, 'w') as file:
+                with open(copypath.parent / self.cfg.configpath.name, "w") as file:
                     self.cfg.config.write(file)
 
-                warnings.warn(
-                    f'the config-file "{self.cfg.configpath}" has been' +
-                    ' updated with the init_kwargs and saved to' +
-                    f'"{copypath.parent / self.cfg.configpath.name}"')
+                log.info(
+                    f'the config-file "{self.cfg.configpath}" has been'
+                    + " updated with the init_kwargs and saved to"
+                    + f'"{copypath.parent / self.cfg.configpath.name}"'
+                )
 
             # remove the config and re-read the config from the copied path
             del self.cfg
             self.cfg = RT1_configparser(copypath)
 
         # copy modules
-        for key, val in self.cfg.config['CONFIGFILES'].items():
-            if key.startswith('module__'):
+        for key, val in self.cfg.config["CONFIGFILES"].items():
+            if key.startswith("module__"):
                 modulename = key[8:]
 
-                module_path = self.cfg.config[
-                    'CONFIGFILES'][f'module__{modulename}']
+                module_path = self.cfg.config["CONFIGFILES"][f"module__{modulename}"]
 
                 location = Path(module_path.strip())
 
-                copypath = self.dumppath / 'cfg' / location.name
+                copypath = self.dumppath / "cfg" / location.name
 
                 if copypath.exists():
-                    warnings.warn(f'the file \n"{copypath}" \nalready ' +
-                                  'exists ... NO copying is performed ' +
-                                  'and the existing one is used!\n')
+                    log.warning(
+                        f'the file \n"{copypath}" \nalready '
+                        + "exists ... NO copying is performed "
+                        + "and the existing one is used!\n"
+                    )
                 else:
                     shutil.copy(location, copypath)
-                    warnings.warn(
-                        f'"{location.name}" copied to \n"{copypath}"')
+                    log.info(f'"{location.name}" copied to \n"{copypath}"')
 
     def _evalfunc(self, reader_arg=None, process_cnt=None):
         """
@@ -383,6 +515,7 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
         -------
         The used 'rt1.rtfit.Fits' object or the output of 'postprocess()'
         """
+
         if process_cnt is not None:
             start = _start_cnt()
         try:
@@ -393,16 +526,19 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
             if isinstance(read_data, pd.DataFrame):
                 dataset = read_data
                 aux_data = None
-            elif (isinstance(read_data, (list, tuple))
-                  and isinstance(read_data[0], pd.DataFrame)):
+            elif isinstance(read_data, (list, tuple)) and isinstance(
+                read_data[0], pd.DataFrame
+            ):
                 if len(read_data) == 2:
                     dataset, aux_data = read_data
                 elif len(read_data) > 2:
                     dataset = read_data[0]
                     aux_data = read_data[1:]
             else:
-                raise TypeError('the first return-value of reader function ' +
-                                'must be a pandas DataFrame')
+                raise TypeError(
+                    "the first return-value of reader function "
+                    + "must be a pandas DataFrame"
+                )
             # initialize a new fits-object and perform the fit
             fit = self.parent_fit.reinit_object(dataset=dataset)
             fit.performfit()
@@ -413,14 +549,20 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
 
             # append reader_arg
             fit.reader_arg = reader_arg
-            _increase_cnt(process_cnt, start, err=False)
+
+            if process_cnt is not None:
+                _increase_cnt(process_cnt, start, err=False)
+
+            # dump a fit-file
+            if self._dump_fit:
+                self.proc_cls.dump_fit_to_file(fit, reader_arg, mini=True)
 
             # if a post-processing function is provided, return its output,
-            # else return the fit-object directly
-            if callable(self.proc_cls.postprocess):
+            # else return None
+            if self._postprocess and callable(self.proc_cls.postprocess):
                 ret = self.proc_cls.postprocess(fit, reader_arg)
             else:
-                ret = fit
+                ret = None
 
             return ret
 
@@ -437,9 +579,36 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
             else:
                 raise ex
 
-    def processfunc(self, ncpu=1, print_progress=True,
-                    reader_args=None, pool_kwargs=None,
-                    preprocess_kwargs=None):
+    def _initializer(self, initializer, queue, *args):
+        """
+        decorate a provided initializer with the "_worker_configurer()" to
+        enable logging from subprocesses while keeping the initializer a
+        pickleable function (e.g. avoid using an actual decorator for this)
+        (>> returns from provided initializer are returned)
+        """
+
+        # preload ALL modules to ensure that the importer finds them at the
+        # desired locations (e.g. the cfg-folders)
+        _ = self.cfg.get_all_modules(load_copy=self.copy)
+
+        if queue is not None:
+            self._worker_configurer(queue)
+
+        if initializer is not None:
+            res = initializer(*args)
+            return res
+
+    def processfunc(
+        self,
+        ncpu=1,
+        print_progress=True,
+        reader_args=None,
+        pool_kwargs=None,
+        preprocess_kwargs=None,
+        queue=None,
+        dump_fit=True,
+        postprocess=True,
+    ):
         """
         Evaluate a RT-1 model on a single core or in parallel using
 
@@ -500,6 +669,16 @@ def processfunc(self, ncpu=1, print_progress=True,
             >>> preprocess(**preprocess_kwargs)
 
             The default is None.
+        queue : multiprocessing.Manager().queue
+            the queue used for logging
+        dump_fit : bool, optional
+            indicator if a fit-object should be dumped or not.
+            (e.g. by invoking processing_config.dump_fit_to_file() )
+            The default is True
+        postprocess bool, optional
+            indicator if postprocess() and finalout() workflows should be
+            executed or not
+
         Returns
         -------
         res : list
@@ -507,36 +686,51 @@ def processfunc(self, ncpu=1, print_progress=True,
             postprocess-function.
 
         """
+        self._postprocess = postprocess
+        self._dump_fit = dump_fit
 
         if callable(self.proc_cls.preprocess):
             setupdict = self.proc_cls.preprocess(**preprocess_kwargs)
             if setupdict is None:
                 setupdict = dict()
-            assert isinstance(setupdict, dict), (
-                'the preprocess() function must return a dict!')
+            assert isinstance(
+                setupdict, dict
+            ), "the preprocess() function must return a dict!"
         else:
             setupdict = dict()
 
         # check if reader args is provided in setupdict
         if reader_args is None:
-            assert 'reader_args' in setupdict, (
-                'if "reader_args" is not passed directly to processfunc() ' +
-                ', the preprocess() function must return a key "reader_args"!')
+            assert "reader_args" in setupdict, (
+                'if "reader_args" is not passed directly to processfunc() '
+                + ', the preprocess() function must return a key "reader_args"!'
+            )
 
-            reader_args = setupdict['reader_args']
+            reader_args = setupdict["reader_args"]
         else:
-            assert 'reader_args' not in setupdict, (
-                '"reader_args" is provided as argument to processfunc() ' +
-                'AND via the return-dict of the preprocess() function!')
+            assert "reader_args" not in setupdict, (
+                '"reader_args" is provided as argument to processfunc() '
+                + "AND via the return-dict of the preprocess() function!"
+            )
 
-        print(f'processing {len(reader_args)} features')
+        log.info(f"processing {len(reader_args)} features")
 
-        if 'pool_kwargs' in setupdict:
-            pool_kwargs = setupdict['pool_kwargs']
+        if "pool_kwargs" in setupdict:
+            pool_kwargs = setupdict["pool_kwargs"]
 
         if pool_kwargs is None:
             pool_kwargs = dict()
 
+        # add provided initializer and queue (used for subprocess-logging)
+        # to the initargs and use "self._initializer" as initializer-function
+        # Note: this is a pickleable way for decorating the initializer!
+        pool_kwargs["initargs"] = [
+            pool_kwargs.pop("initializer", None),
+            queue,
+            *pool_kwargs.pop("initargs", []),
+        ]
+        pool_kwargs["initializer"] = self._initializer
+
         if self.parent_fit.int_Q is True:
             # pre-evaluate the fn-coefficients if interaction terms are used
             self.parent_fit._fnevals_input = self.parent_fit.R._fnevals
@@ -549,49 +743,54 @@ def processfunc(self, ncpu=1, print_progress=True,
             process_cnt = None
 
         if ncpu > 1:
-            print('start of parallel evaluation')
+            log.info(f"start of parallel evaluation on {ncpu} cores")
             with mp.Pool(ncpu, **pool_kwargs) as pool:
                 # loop over the reader_args
-                res_async = pool.starmap_async(self._evalfunc,
-                                               zip(reader_args,
-                                                   repeat(process_cnt)))
+                res_async = pool.starmap_async(
+                    self._evalfunc, zip(reader_args, repeat(process_cnt))
+                )
 
                 pool.close()  # Marks the pool as closed.
-                pool.join()   # Waits for workers to exit.
+                pool.join()  # Waits for workers to exit.
                 res = res_async.get()
         else:
-            print('start of single-core evaluation')
+            log.info("start of single-core evaluation")
 
             # call the initializer if it has been provided
-            if 'initializer' in pool_kwargs:
-                if 'initargs' in pool_kwargs:
-                    pool_kwargs['initializer'](*pool_kwargs['initargs'])
+            if "initializer" in pool_kwargs:
+                if "initargs" in pool_kwargs:
+                    pool_kwargs["initializer"](*pool_kwargs["initargs"])
                 else:
-                    pool_kwargs['initializer']()
+                    pool_kwargs["initializer"]()
             res = []
             for reader_arg in reader_args:
-                res.append(self._evalfunc(reader_arg=reader_arg,
-                                          process_cnt=process_cnt))
+                res.append(
+                    self._evalfunc(reader_arg=reader_arg, process_cnt=process_cnt)
+                )
 
-        if callable(self.proc_cls.finaloutput):
+        if self._postprocess and callable(self.proc_cls.finaloutput):
             return self.proc_cls.finaloutput(res)
         else:
             return res
 
-    def run_processing(self, ncpu=1, copy=True, print_progress=True,
-                       reader_args=None, pool_kwargs=None,
-                       preprocess_kwargs=None):
-        '''
+    def run_processing(
+        self,
+        ncpu=1,
+        print_progress=True,
+        reader_args=None,
+        pool_kwargs=None,
+        preprocess_kwargs=None,
+        logfile_level=1,
+        dump_fit=True,
+        postprocess=True,
+    ):
+        """
         Start the processing
 
         Parameters
         ----------
         ncpu : int
             The number of cpu's to use. The default is 1.
-        copy : bool, optional
-            Indicator if the used config-file and all modules specified in the
-            "CONFIGFILES" section of the config-file should be copied to
-            "/dumpfolder/cfg" or not. The default is True.
         print_progress : bool, optional
             Indicator if a progress-bar should be printed or not.
             If True, it might be wise to suppress warnings during runtime
@@ -623,39 +822,333 @@ def run_processing(self, ncpu=1, copy=True, print_progress=True,
             >>> preprocess(**preprocess_kwargs)
 
             The default is None.
+        logfile_level : int
+            the log-level of the log-file that will be generated as
+            "processing folder / cfg / RT1_processing.log"
+
+            If None no log-file will be generated.
+
+            for information on the level values see:
+                https://docs.python.org/3/library/logging.html#logging-levels
+        dump_fit: bool, optional
+            indicator if `processing_config.dump_fit_to_file()` should be
+            called after finishing the fit.
+            The default is True
+        postprocess : bool, optional
+            indicator if postprocess() and finaloutput() workflows should be
+            executed or not.
+            The default is True.
+        """
+
+        try:
+            if logfile_level is not None and ncpu > 1:
+                # start a listener-process that takes care of the logs from
+                # multiprocessing workers
+                queue = mp.Manager().Queue(-1)
+                listener = mp.Process(target=self._listener_process, args=[queue])
+
+                # make the queue listen also to the MainProcess start the
+                # listener-process that writes the file to disc AFTER
+                # initialization of the  folder-structure!
+                # (otherwise the log-file can not be generated!)
+                self._worker_configurer(queue)
+            else:
+                queue = None
+
+            # initialize all necessary properties if setup was not yet called
+            self.setup()
+
+            if logfile_level is not None and ncpu > 1:
+                # start the listener after the setup-function completed, since
+                # otherwise the folder-structure does not yet exist and the
+                # file to which the process is writing can not be generated!
+                listener.start()
+
+            if preprocess_kwargs is None:
+                preprocess_kwargs = dict()
+
+            # save the used model-definition string to a file
+            if self.dumppath is not None:
+                with open(self.dumppath / "cfg" / "model_definition.txt", "w") as file:
+
+                    outtxt = ""
+                    if hasattr(self.proc_cls, "description"):
+                        outtxt += dedent(self.proc_cls.description)
+                        outtxt += "\n\n"
+                        outtxt += "_" * 77
+                        outtxt += "\n\n"
+
+                    outtxt += self.parent_fit._model_definition
+                    print(outtxt, file=file)
+
+                    log.info(outtxt)
+
+            _ = self.processfunc(
+                ncpu=ncpu,
+                print_progress=print_progress,
+                reader_args=reader_args,
+                pool_kwargs=pool_kwargs,
+                preprocess_kwargs=preprocess_kwargs,
+                queue=queue,
+                dump_fit=dump_fit,
+                postprocess=postprocess,
+            )
+
+        except Exception as err:
+            log.exception("there has been something wrong during processing!")
+            raise err
+
+        finally:
+            # turn off capturing warnings
+            logging.captureWarnings(False)
+
+            if logfile_level is not None:
+                if ncpu > 1:
+
+                    # tell the queue to stop
+                    queue.put_nowait(None)
+
+                    # stop the listener process
+                    listener.join()
+
+                # remove any remaining file-handler and queue-handlers after
+                # the processing is done
+                handlers = groupby_unsorted(log.handlers, key=lambda x: x.name)
+                for h in handlers.get("rtprocessing_queuehandler", []):
+                    # close the handler (removing it does not close it!)
+                    h.close()
+                    # remove the file-handler from the log
+                    log.handlers.remove(h)
+
+    def run_finaloutput(
+        self,
+        ncpu=1,
+        use_N_files=None,
+        finalout_name=None,
+        finaloutput=None,
+        postprocess=None,
+        print_progress=True,
+        logfile_level=1,
+    ):
+        """
+        run postprocess and finaloutput for available .dump files
+
+        Parameters
+        ----------
+        ncpu : int
+            The number of cpu's to use. The default is 1.
+        use_N_files : int, optional
+            The number of files to process (e.g. a subset of all files)
+        finalout_name : str, optional
+            override the finalout_name provided in the ".ini" file
+        finaloutput : callable, optional
+            override the finaloutput function provided via the
+            "processing_config.py" script
+            NOTICE: the call-signature is
+            `finaloutput(proc_cls, ...)` where proc_cls is the
+            used instance of the "processing_cfg" class!
+            -> this way you have access to all arguments specified in
+            proc_cls such as ".save_path", ".dumpfolder" etc.
+        postprocess : callable, optional
+            override the postprocess function provided via the
+            "processing_config.py" script
+            NOTICE: the call signature is similar to finaloutput above!
+        print_progress : bool, optional
+            Indicator if a progress-bar should be printed or not.
+            If True, it might be wise to suppress warnings during runtime
+            to avoid unwanted outputs. This can be achieved by using:
+
+                >>> import warnings
+                ... warnings.simplefilter('ignore')
+
+            The default is True.
+        logfile_level : int
+            the log-level of the log-file that will be generated as
+            "processing folder / cfg / RT1_processing.log"
 
-        '''
-        print('############################################################\n')
+            If None no log-file will be generated.
 
-        # initialize all necessary properties
-        self.setup(copy=copy)
+            for information on the level values see:
+                https://docs.python.org/3/library/logging.html#logging-levels
 
-        if preprocess_kwargs is None:
-            preprocess_kwargs = dict()
+        """
+        try:
+            if logfile_level is not None and ncpu > 1:
+                # start a listener-process that takes care of the logs from
+                # multiprocessing workers
+                queue = mp.Manager().Queue(-1)
+                listener = mp.Process(target=self._listener_process, args=[queue])
+
+                # make the queue listen also to the MainProcess start the
+                # listener-process that writes the file to disc AFTER
+                # initialization of the  folder-structure!
+                # (otherwise the log-file can not be generated!)
+                self._worker_configurer(queue)
+            else:
+                queue = None
+
+            # initialize all necessary properties with autocontinue=True
+            initial_autocontinue = self.autocontinue
+            self.autocontinue = True
+            self.setup()
+            self.autocontinue = initial_autocontinue
+
+            if logfile_level is not None and ncpu > 1:
+                # start the listener after the setup-function completed, since
+                # otherwise the folder-structure does not yet exist and the
+                # file to which the process is writing can not be generated!
+                listener.start()
+
+            return self._run_finalout(
+                ncpu,
+                use_N_files=use_N_files,
+                finalout_name=finalout_name,
+                finaloutput=finaloutput,
+                postprocess=postprocess,
+                queue=queue,
+                print_progress=print_progress,
+            )
+
+        except Exception as err:
+            log.exception("there was an error during finalout generation!")
+            raise err
+
+        finally:
+
+            # turn off capturing warnings
+            logging.captureWarnings(False)
+
+            if logfile_level is not None:
+                if ncpu > 1:
+                    # tell the queue to stop
+                    queue.put_nowait(None)
+
+                    # stop the listener process
+                    listener.join()
+
+                # remove any remaining file-handler and queue-handlers after
+                # the processing is done
+                handlers = groupby_unsorted(log.handlers, key=lambda x: x.name)
+                for h in handlers.get("rtprocessing_queuehandler", []):
+                    # close the handler (removing it does not close it!)
+                    h.close()
+                    # remove the file-handler from the log
+                    log.handlers.remove(h)
+
+    def _run_postprocess(self, fitpath, process_cnt=None):
+        if process_cnt is not None:
+            start = _start_cnt()
+
+        # load fitobjects based on fit-paths
+        try:
+            fit = load(fitpath)
+        except Exception:
+            log.error(f"there was something wrong with {fitpath.name}")
+            _increase_cnt(process_cnt, start, err=True)
 
-        # save the used model-definition string to a file
-        if self.dumppath is not None:
-            with open(self.dumppath / 'cfg' / 'model_definition.txt',
-                      'w') as file:
+        try:
+            # run postprocessing
+            ret = self.proc_cls.postprocess(fit, fit.reader_arg)
 
-                outtxt = ''
-                if hasattr(self.proc_cls, 'description'):
-                    outtxt += dedent(self.proc_cls.description)
-                    outtxt += '\n\n'
-                    outtxt += '_'*77
-                    outtxt += '\n\n'
+            if process_cnt is not None:
+                _increase_cnt(process_cnt, start, err=False)
 
-                outtxt += self.parent_fit._model_definition
+            return ret
 
-                print(outtxt, file=file)
+        except Exception as ex:
+            if callable(self.proc_cls.exceptfunc):
+                ex_ret = self.proc_cls.exceptfunc(ex, fit.reader_arg)
+                if ex_ret is None or ex_ret is False:
+                    _increase_cnt(process_cnt, start, err=True)
+                else:
+                    _increase_cnt(process_cnt, start, err=False)
+                return ex_ret
+            else:
+                raise ex
 
-        _ = self.processfunc(ncpu=ncpu, print_progress=print_progress,
-                             reader_args=reader_args, pool_kwargs=pool_kwargs,
-                             preprocess_kwargs=preprocess_kwargs)
+    def _run_finalout(
+        self,
+        ncpu,
+        use_N_files=None,
+        finalout_name=None,
+        finaloutput=None,
+        postprocess=None,
+        queue=None,
+        print_progress=True,
+    ):
+
+        # override finalout_name
+        if finalout_name is not None:
+            assert isinstance(finalout_name, str), "finalout_name must be a string"
+            self.proc_cls.finalout_name = finalout_name
+
+        # override finaloutput function
+        if finaloutput is not None:
+            assert callable(finaloutput), "finaloutput must be callable!"
+            self.proc_cls.finaloutput = partial(finaloutput, self.proc_cls)
+        # override postprocess function
+        if postprocess is not None:
+            assert callable(postprocess), "postprocess must be callable!"
+            self.proc_cls.postprocess = partial(postprocess, self.proc_cls)
+
+        # initialize RTresults class
+        useres = getattr(RTresults(self.dumppath), self.proc_cls.dumpfolder)
+
+        # get dump-files
+        dumpiter = useres.dump_files
+
+        if use_N_files is not None:
+            fitlist = list(islice(dumpiter, use_N_files))
+        else:
+            fitlist = list(dumpiter)
+
+        # add provided initializer and queue (used for subprocess-logging)
+        # to the initargs and use "self._initializer" as initializer-function
+        # Note: this is a pickleable way for decorating the initializer!
+        pool_kwargs = dict(initializer=self._initializer, initargs=[None, queue])
+
+        if print_progress is True:
+            # initialize shared values that will be used to track the number
+            # of completed processes and the mean time to complete a process
+            process_cnt = _setup_cnt(N_items=len(fitlist), ncpu=ncpu)
+        else:
+            process_cnt = None
+
+        if ncpu > 1:
+            log.info(f"start of finalout generation on {ncpu} cores")
+            with mp.Pool(ncpu, **pool_kwargs) as pool:
+                # loop over the reader_args
+                res_async = pool.starmap_async(
+                    self._run_postprocess, zip(fitlist, repeat(process_cnt))
+                )
+
+                pool.close()  # Marks the pool as closed.
+                pool.join()  # Waits for workers to exit.
+                res = res_async.get()
+        else:
+            log.info("start of single-core finalout generation")
+
+            # call the initializer if it has been provided
+            if "initializer" in pool_kwargs:
+                if "initargs" in pool_kwargs:
+                    pool_kwargs["initializer"](*pool_kwargs["initargs"])
+                else:
+                    pool_kwargs["initializer"]()
+            res = []
+            for fitpath in fitlist:
+                res.append(
+                    self._run_postprocess(fitpath=fitpath, process_cnt=process_cnt)
+                )
+
+        if callable(self.proc_cls.finaloutput):
+            return self.proc_cls.finaloutput(res)
+            log.info("finished generation of finaloutput!")
+        else:
+            return res
 
 
 class RTresults(object):
-    '''
+    """
     A class to provide easy access to processed results.
     On initialization the class will traverse the provided "parent_path"
     and recognize any sub-folder that matches the expected folder-structure
@@ -704,52 +1197,60 @@ class RTresults(object):
     ----------
     parent_path : str
         the parent-path where the results are stored.
-    '''
+    """
 
     def __init__(self, parent_path):
         self._parent_path = Path(parent_path)
 
         self._paths = dict()
 
-        if all(i in [i.stem for i in self._parent_path.iterdir()]
-               for i in ['cfg', 'results', 'dumps']):
+        if all(
+            i in [i.stem for i in self._parent_path.iterdir()]
+            for i in ["cfg", "results", "dumps"]
+        ):
             self._paths[self._parent_path.stem] = self._parent_path
-            print('... adding result', self._parent_path.stem)
-            setattr(self, self._parent_path.stem,
-                    self._RT1_fitresult(self._parent_path.stem,
-                                        self._parent_path))
+            log.info(f"... adding result {self._parent_path.stem}")
+            setattr(
+                self,
+                self._parent_path.stem,
+                self._RT1_fitresult(self._parent_path.stem, self._parent_path),
+            )
 
         for p in self._parent_path.iterdir():
             if p.is_dir():
-                if all(i in [i.stem for i in p.iterdir()]
-                       for i in ['cfg', 'results', 'dumps']):
+                if all(
+                    i in [i.stem for i in p.iterdir()]
+                    for i in ["cfg", "results", "dumps"]
+                ):
                     self._paths[p.stem] = p
-                    print('... adding result', p.stem)
-                    setattr(self, p.stem,
-                            self._RT1_fitresult(p.stem, p))
+                    log.info(f"... adding result {p.stem}")
+                    setattr(self, p.stem, self._RT1_fitresult(p.stem, p))
 
     class _RT1_fitresult(object):
         def __init__(self, name, path):
             self.name = name
             self.path = Path(path)
-            self._result_path = self.path / 'results'
-            self._dump_path = self.path / 'dumps'
-            self._cfg_path = self.path / 'cfg'
+            self._result_path = self.path / "results"
+            self._dump_path = self.path / "dumps"
+            self._cfg_path = self.path / "cfg"
+
+            self._nc_paths = self._get_results(".nc")
 
         def _get_results(self, ending):
-            assert self._result_path.exists(), f'{self._result_path}' + \
-                ' does not exist'
+            assert self._result_path.exists(), (
+                f"{self._result_path}" + " does not exist"
+            )
 
-            results = {i.stem: i for i in self._result_path.iterdir()
-                       if i.suffix == ending}
+            results = {
+                i.stem: i for i in self._result_path.iterdir() if i.suffix == ending
+            }
 
-            assert len(results) > 0, f'there is no "{ending}" file' + \
-                f' in "{self._result_path}"'
+            log.info(f'there is no "{ending}" file in "{self._result_path}"')
 
             return results
 
         def load_nc(self, result_name=None, use_xarray=True):
-            '''
+            """
             open a NetCDF file stored in the "results"-folder
 
             can be used as context-manager, e.g.:
@@ -772,26 +1273,25 @@ def load_nc(self, result_name=None, use_xarray=True):
             -------
             file : a file-handler for the NetCDF file
                 the return of either xarray.Dataset or NetCDF4.Dataset
-            '''
-            results = self._get_results('.nc')
+            """
+            results = self._get_results(".nc")
+            assert len(results) > 0, "no NetCDF file in the results folder!"
 
             assert len(results) == 1 or result_name in results, (
-                 ('there is more than 1 result... ' +
-                  'provide "result_name":\n    - ' +
-                  '\n    - '.join(results.keys())))
+                "there is more than 1 result... "
+                + 'provide "result_name":\n    - '
+                + "\n    - ".join(results.keys())
+            )
 
             if result_name is None:
                 result_name = list(results.keys())[0]
 
-            # print(f'loading nc-file for {result_name}')
-            if use_xarray is True:
-                file = xar.open_dataset(results[result_name])
-            else:
-                file = Dataset(results[result_name])
+            log.info(f"loading nc-file for {result_name}")
+            file = xar.open_dataset(results[result_name])
             return file
 
-        def load_fit(self, ID=None, return_ID=False):
-            '''
+        def load_fit(self, ID=0, return_ID=False):
+            """
             load one of the available .dump-files located in the "dumps"
             folder.  (using rt1.rtfits.load() )
 
@@ -800,10 +1300,14 @@ def load_fit(self, ID=None, return_ID=False):
 
             Parameters
             ----------
-            ID : str, optional
-                The name of the dump-file to be loaded (without the .dump
-                extension). If None, a random file will be selected.
-                The default is None.
+            ID : str or int, or pathli.Path
+                If str:  The name of the dump-file to be loaded
+                         (without the .dump extension).
+                If int:  load the nth file found in the dumpfolder
+                if Path: the full path to the .dump file
+                If None: load a random file from the folder
+                         (might take some time for very large amounts of files)
+                The default is 0.
             return_ID : bool, optional
                 If True, a tuple (fit, ID) is returned, otherwise only
                 the fit is returned
@@ -812,17 +1316,28 @@ def load_fit(self, ID=None, return_ID=False):
             -------
             fit : rt1.rtfits.Fits
                 the loaded rt1.rtfits.Fits result.
-            '''
+            """
 
-            if ID is None:
-                allfiles = list(self.dump_files)
-                Nid = np_randint(0, len(allfiles) - 1)
+            if isinstance(ID, Path):
+                filepath = ID
+            else:
+                if ID is None:
+                    if not hasattr(self, "_n_dump_files"):
+                        self._n_dump_files = len(list(self.dump_files))
+                    Nid = np_randint(0, self._n_dump_files)
+
+                    filepath = next(islice(self.dump_files, Nid, None))
 
-                ID = allfiles[Nid].stem
-                print(f'loading random ID ({ID}) from {len(allfiles)} ' +
-                      'available files')
+                    log.info(
+                        f"loading random ID ({filepath.stem}) from "
+                        + f" {self._n_dump_files} available files"
+                    )
+                elif isinstance(ID, int):
+                    filepath = next(islice(self.dump_files, ID, None))
+                elif isinstance(ID, str):
+                    filepath = self._dump_path / (ID + ".dump")
 
-            fit = load(self._dump_path / (ID + '.dump'))
+            fit = load(filepath)
 
             if return_ID is True:
                 return (fit, ID)
@@ -830,7 +1345,7 @@ def load_fit(self, ID=None, return_ID=False):
                 return fit
 
         def load_cfg(self, cfg_name=None):
-            '''
+            """
             load the configfile stored in the "cfg" folder
 
             Parameters
@@ -844,15 +1359,17 @@ def load_cfg(self, cfg_name=None):
             cfg : rt1.rtparse.RT1_configparser
                 a configparser instance of the selected configuration.
 
-            '''
-            cfgfiles = list(self._cfg_path.glob('*.ini'))
+            """
+            cfgfiles = list(self._cfg_path.glob("*.ini"))
             assert len(cfgfiles) > 0, 'NO ".ini"-file found!'
 
             if cfg_name is None:
                 assert len(cfgfiles) == 1, (
-                    'there is more than 1 .ini file in the cfg folder...' +
-                    'provide a "cfg_name":\n' +
-                    '    - ' + '\n    - '.join([i.name for i in cfgfiles]))
+                    "there is more than 1 .ini file in the cfg folder..."
+                    + 'provide a "cfg_name":\n'
+                    + "    - "
+                    + "\n    - ".join([i.name for i in cfgfiles])
+                )
 
                 cfg_name = cfgfiles[0].name
 
@@ -861,26 +1378,23 @@ def load_cfg(self, cfg_name=None):
 
         @property
         def dump_files(self):
-            '''
+            """
             a generator of the available dump-files
-            '''
-            return (i for i in self._dump_path.iterdir()
-                    if i.suffix == '.dump' and 'error' not in i.stem)
+            """
+            return (
+                i
+                for i in self._dump_path.iterdir()
+                if i.suffix == ".dump" and "error" not in i.stem
+            )
 
         @property
         def NetCDF_variables(self):
-            '''
+            """
             print all available NetCDF-files and their variables
-            '''
-            results = self._get_results('.nc')
-
+            """
+            results = self._get_results(".nc")
+            assert len(results) > 0, "no NetCDF file in the results folder!"
             for r in results:
-                print('\nresult: ', r)
-                with self.load_nc(r, use_xarray=False) as ncfile:
-                    space = len(max(ncfile.variables.keys(), key=len))
-                    for key, val in ncfile.variables.items():
-                        if key in ncfile.dimensions.keys():
-                            print('dimension: ', *zip(val.dimensions,
-                                                      val.shape))
-                        else:
-                            print(f'{key:<{space + 7}}', val.dimensions)
+                print(f"\n################ result:  {r}.nc")
+                with self.load_nc(r) as ncfile:
+                    print(ncfile)
diff --git a/rt1/scatter.py b/rt1/scatter.py
index 4a82b59f..5b2cddeb 100644
--- a/rt1/scatter.py
+++ b/rt1/scatter.py
@@ -48,9 +48,11 @@ def scat_angle(self, t_0, t_ex, p_0, p_ex, a):
               the generalized scattering angle
 
         """
-        return a[0] * sp.cos(t_0) * sp.cos(t_ex) + a[1] * sp.sin(t_0) * \
-            sp.sin(t_ex) * sp.cos(p_0) * sp.cos(p_ex) + a[2] * sp.sin(t_0) * \
-            sp.sin(t_ex) * sp.sin(p_0) * sp.sin(p_ex)
+        return (
+            a[0] * sp.cos(t_0) * sp.cos(t_ex)
+            + a[1] * sp.sin(t_0) * sp.sin(t_ex) * sp.cos(p_0) * sp.cos(p_ex)
+            + a[2] * sp.sin(t_0) * sp.sin(t_ex) * sp.sin(p_0) * sp.sin(p_ex)
+        )
 
     def _get_legcoef(self, n0):
         """
@@ -58,7 +60,7 @@ def _get_legcoef(self, n0):
         used mainly for testing purposes
         the actual coefficient are used in the symbolic expansion
         """
-        n = sp.Symbol('n')
+        n = sp.Symbol("n")
         return self.legcoefs.xreplace({n: int(n0)}).evalf()
 
     def _eval_legpoly(self, t_0, t_s, p_0, p_s, geometry=None):
@@ -68,23 +70,23 @@ def _eval_legpoly(self, t_0, t_s, p_0, p_s, geometry=None):
         the actual coefficient are used in the symbolic expansion
         """
 
-        assert geometry is not None, 'Geometry needs to be specified!'
-
-        theta_0 = sp.Symbol('theta_0')
-        theta_s = sp.Symbol('theta_s')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_s = sp.Symbol('phi_s')
-        phi_ex = sp.Symbol('phi_ex')
-
-        res = self.legexpansion(t_0,
-                                t_s,
-                                p_0,
-                                p_s,
-                                geometry).xreplace({theta_0: t_0,
-                                                    theta_s: t_s,
-                                                    phi_0: p_0,
-                                                    phi_s: p_s,
-                                                    theta_ex: t_s,
-                                                    phi_ex: p_s})
+        assert geometry is not None, "Geometry needs to be specified!"
+
+        theta_0 = sp.Symbol("theta_0")
+        theta_s = sp.Symbol("theta_s")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_s = sp.Symbol("phi_s")
+        phi_ex = sp.Symbol("phi_ex")
+
+        res = self.legexpansion(t_0, t_s, p_0, p_s, geometry).xreplace(
+            {
+                theta_0: t_0,
+                theta_s: t_s,
+                phi_0: p_0,
+                phi_s: p_s,
+                theta_ex: t_s,
+                phi_ex: p_s,
+            }
+        )
         return res.evalf()
diff --git a/rt1/surface.py b/rt1/surface.py
index 7cddcbc2..e2a076ed 100644
--- a/rt1/surface.py
+++ b/rt1/surface.py
@@ -16,9 +16,9 @@ def __init__(self, **kwargs):
         # explicitly provided by the chosen class.
         # this results in a peak in specular-direction which is suitable
         # for describing surface BRDF's
-        self.a = getattr(self, 'a', [1., 1., 1.])
+        self.a = getattr(self, "a", [1.0, 1.0, 1.0])
 
-        self.NormBRDF = kwargs.pop('NormBRDF', 1.)
+        self.NormBRDF = kwargs.pop("NormBRDF", 1.0)
         # quick way for visualizing the functions as polarplot
         self.polarplot = partial(polarplot, X=self)
         update_wrapper(self.polarplot, polarplot)
@@ -52,10 +52,10 @@ def brdf(self, t_0, t_ex, p_0, p_ex, param_dict={}):
         """
 
         # define sympy objects
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
         # replace arguments and evaluate expression
         # sp.lambdify is used to allow array-inputs
@@ -71,9 +71,16 @@ def brdf(self, t_0, t_ex, p_0, p_ex, param_dict={}):
         # (this happens e.g. for the Isotropic brdf).
         # The following query is implemented to ensure correct array-output:
         # TODO this is not a proper test !
-        if not isinstance(brdffunc(np.array([.1, .2, .3]), .1, .1, .1,
-                                   **{key: .12 for key in param_dict.keys()}
-                                   ), np.ndarray):
+        if not isinstance(
+            brdffunc(
+                np.array([0.1, 0.2, 0.3]),
+                0.1,
+                0.1,
+                0.1,
+                **{key: 0.12 for key in param_dict.keys()}
+            ),
+            np.ndarray,
+        ):
             brdffunc = np.vectorize(brdffunc)
 
         return brdffunc(t_0, t_ex, p_0, p_ex, **param_dict)
@@ -138,73 +145,89 @@ def legexpansion(self, t_0, t_ex, p_0, p_ex, geometry):
         """
         assert self.ncoefs > 0
 
-        theta_s = sp.Symbol('theta_s')
-        phi_s = sp.Symbol('phi_s')
+        theta_s = sp.Symbol("theta_s")
+        phi_s = sp.Symbol("phi_s")
 
         NBRDF = self.ncoefs
-        n = sp.Symbol('n')
+        n = sp.Symbol("n")
 
         # define sympy variables based on chosen geometry
-        if geometry == 'mono':
-            assert len(np.unique(p_0)) == 1, 'p_0 must contain only a ' + \
-                'single unique value for monostatic geometry'
+        if geometry == "mono":
+            assert len(np.unique(p_0)) == 1, (
+                "p_0 must contain only a "
+                + "single unique value for monostatic geometry"
+            )
 
-            theta_0 = sp.Symbol('theta_0')
+            theta_0 = sp.Symbol("theta_0")
             theta_ex = theta_0
             phi_0 = np.unique(p_0)[0]
             phi_ex = np.unique(p_0)[0] + sp.pi
         else:
-            if geometry[0] == 'v':
-                theta_0 = sp.Symbol('theta_0')
-            elif geometry[0] == 'f':
-                assert len(np.unique(t_0)) == 1, 't_0 must contain only a ' + \
-                    'single unique value for geometry[0] == f'
+            if geometry[0] == "v":
+                theta_0 = sp.Symbol("theta_0")
+            elif geometry[0] == "f":
+                assert len(np.unique(t_0)) == 1, (
+                    "t_0 must contain only a "
+                    + "single unique value for geometry[0] == f"
+                )
 
                 theta_0 = np.unique(t_0)[0]
             else:
-                raise AssertionError('wrong choice of theta_0 geometry')
+                raise AssertionError("wrong choice of theta_0 geometry")
 
-            if geometry[1] == 'v':
-                theta_ex = sp.Symbol('theta_ex')
-            elif geometry[1] == 'f':
-                assert len(np.unique(t_ex)) == 1, 't_ex must contain only' + \
-                    ' a single unique value for geometry[1] == f'
+            if geometry[1] == "v":
+                theta_ex = sp.Symbol("theta_ex")
+            elif geometry[1] == "f":
+                assert len(np.unique(t_ex)) == 1, (
+                    "t_ex must contain only"
+                    + " a single unique value for geometry[1] == f"
+                )
 
                 theta_ex = np.unique(t_ex)[0]
             else:
-                raise AssertionError('wrong choice of theta_ex geometry')
+                raise AssertionError("wrong choice of theta_ex geometry")
 
-            if geometry[2] == 'v':
-                phi_0 = sp.Symbol('phi_0')
-            elif geometry[2] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_0 must contain only' + \
-                    ' a single unique value for geometry[2] == f'
+            if geometry[2] == "v":
+                phi_0 = sp.Symbol("phi_0")
+            elif geometry[2] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_0 must contain only"
+                    + " a single unique value for geometry[2] == f"
+                )
 
                 phi_0 = np.unique(p_0)[0]
             else:
-                raise AssertionError('wrong choice of phi_0 geometry')
+                raise AssertionError("wrong choice of phi_0 geometry")
 
-            if geometry[3] == 'v':
-                phi_ex = sp.Symbol('phi_ex')
-            elif geometry[3] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_ex must contain only' + \
-                    ' a single unique value for geometry[3] == f'
+            if geometry[3] == "v":
+                phi_ex = sp.Symbol("phi_ex")
+            elif geometry[3] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_ex must contain only"
+                    + " a single unique value for geometry[3] == f"
+                )
 
                 phi_ex = np.unique(p_ex)[0]
             else:
-                raise AssertionError('wrong choice of phi_ex geometry')
-
-        return sp.Sum(self.legcoefs *
-                      sp.legendre(n, self.scat_angle(theta_s,
-                                                     theta_ex,
-                                                     phi_s,
-                                                     phi_ex, self.a)),
-                      (n, 0, NBRDF - 1))
-
-    def brdf_theta_diff(self, t_0, t_ex, p_0, p_ex,
-                        geometry, param_dict={},
-                        return_symbolic=False,
-                        n=1):
+                raise AssertionError("wrong choice of phi_ex geometry")
+
+        return sp.Sum(
+            self.legcoefs
+            * sp.legendre(n, self.scat_angle(theta_s, theta_ex, phi_s, phi_ex, self.a)),
+            (n, 0, NBRDF - 1),
+        )
+
+    def brdf_theta_diff(
+        self,
+        t_0,
+        t_ex,
+        p_0,
+        p_ex,
+        geometry,
+        param_dict={},
+        return_symbolic=False,
+        n=1,
+    ):
         """
         Calculation of the derivative of the BRDF with respect to
         the scattering-angles t_ex
@@ -260,11 +283,13 @@ def brdf_theta_diff(self, t_0, t_ex, p_0, p_ex,
         """
 
         # define sympy variables based on chosen geometry
-        if geometry == 'mono':
-            assert len(np.unique(p_0)) == 1, 'p_0 must contain only a ' + \
-                'single unique value for monostatic geometry'
+        if geometry == "mono":
+            assert len(np.unique(p_0)) == 1, (
+                "p_0 must contain only a "
+                + "single unique value for monostatic geometry"
+            )
 
-            theta_0 = sp.Symbol('theta_0')
+            theta_0 = sp.Symbol("theta_0")
             theta_ex = theta_0
             phi_0 = np.unique(p_0)[0]
             phi_ex = np.unique(p_0)[0] + sp.pi
@@ -272,76 +297,95 @@ def brdf_theta_diff(self, t_0, t_ex, p_0, p_ex,
             t_ex = t_0
             p_ex = p_0 + np.pi
         else:
-            if geometry[0] == 'v':
-                theta_0 = sp.Symbol('theta_0')
-            elif geometry[0] == 'f':
-                assert len(np.unique(t_0)) == 1, 't_0 must contain only a ' + \
-                    'single unique value for geometry[0] == f'
+            if geometry[0] == "v":
+                theta_0 = sp.Symbol("theta_0")
+            elif geometry[0] == "f":
+                assert len(np.unique(t_0)) == 1, (
+                    "t_0 must contain only a "
+                    + "single unique value for geometry[0] == f"
+                )
 
                 theta_0 = np.unique(t_0)[0]
             else:
-                raise AssertionError('wrong choice of theta_0 geometry')
+                raise AssertionError("wrong choice of theta_0 geometry")
 
-            if geometry[1] == 'v':
-                theta_ex = sp.Symbol('theta_ex')
-            elif geometry[1] == 'f':
-                assert len(np.unique(t_ex)) == 1, 't_ex must contain only' + \
-                    ' a single unique value for geometry[1] == f'
+            if geometry[1] == "v":
+                theta_ex = sp.Symbol("theta_ex")
+            elif geometry[1] == "f":
+                assert len(np.unique(t_ex)) == 1, (
+                    "t_ex must contain only"
+                    + " a single unique value for geometry[1] == f"
+                )
 
                 theta_ex = np.unique(t_ex)[0]
             else:
-                raise AssertionError('wrong choice of theta_ex geometry')
+                raise AssertionError("wrong choice of theta_ex geometry")
 
-            if geometry[2] == 'v':
-                phi_0 = sp.Symbol('phi_0')
-            elif geometry[2] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_0 must contain only' + \
-                    ' a single unique value for geometry[2] == f'
+            if geometry[2] == "v":
+                phi_0 = sp.Symbol("phi_0")
+            elif geometry[2] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_0 must contain only"
+                    + " a single unique value for geometry[2] == f"
+                )
 
                 phi_0 = np.unique(p_0)[0]
             else:
-                raise AssertionError('wrong choice of phi_0 geometry')
+                raise AssertionError("wrong choice of phi_0 geometry")
 
-            if geometry[3] == 'v':
-                phi_ex = sp.Symbol('phi_ex')
-            elif geometry[3] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_ex must contain only' + \
-                    ' a single unique value for geometry[3] == f'
+            if geometry[3] == "v":
+                phi_ex = sp.Symbol("phi_ex")
+            elif geometry[3] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_ex must contain only"
+                    + " a single unique value for geometry[3] == f"
+                )
 
                 phi_ex = np.unique(p_ex)[0]
             else:
-                raise AssertionError('wrong choice of phi_ex geometry')
+                raise AssertionError("wrong choice of phi_ex geometry")
 
-        if geometry[1] == 'f':
-            dfunc_dtheta_0 = 0.
+        if geometry[1] == "f":
+            dfunc_dtheta_0 = 0.0
         else:
-            func = self._func.xreplace({sp.Symbol('theta_0'): theta_0,
-                                        sp.Symbol('theta_ex'): theta_ex,
-                                        sp.Symbol('phi_0'): phi_0,
-                                        sp.Symbol('phi_ex'): phi_ex})
+            func = self._func.xreplace(
+                {
+                    sp.Symbol("theta_0"): theta_0,
+                    sp.Symbol("theta_ex"): theta_ex,
+                    sp.Symbol("phi_0"): phi_0,
+                    sp.Symbol("phi_ex"): phi_ex,
+                }
+            )
 
             dfunc_dtheta_0 = sp.diff(func, theta_ex, n)
 
         if return_symbolic is True:
             return dfunc_dtheta_0
         else:
-            args = (sp.Symbol('theta_0'),
-                    sp.Symbol('theta_ex'),
-                    sp.Symbol('phi_0'),
-                    sp.Symbol('phi_ex')) + tuple(param_dict.keys())
+            args = (
+                sp.Symbol("theta_0"),
+                sp.Symbol("theta_ex"),
+                sp.Symbol("phi_0"),
+                sp.Symbol("phi_ex"),
+            ) + tuple(param_dict.keys())
 
-            brdffunc = sp.lambdify(args, dfunc_dtheta_0,
-                                   modules=["numpy", "sympy"])
+            brdffunc = sp.lambdify(args, dfunc_dtheta_0, modules=["numpy", "sympy"])
 
             # in case _func is a constant, lambdify will produce a function
             # with scalar output which is not suitable for further processing
             # (this happens e.g. for the Isotropic brdf).
             # The following query is implemented to ensure correct array-output
             # TODO this is not a proper test !
-            if not isinstance(brdffunc(
-                    np.array([.1, .2, .3]), .1, .1, .1,
-                    **{key: .12 for key in param_dict.keys()}
-                                       ), np.ndarray):
+            if not isinstance(
+                brdffunc(
+                    np.array([0.1, 0.2, 0.3]),
+                    0.1,
+                    0.1,
+                    0.1,
+                    **{key: 0.12 for key in param_dict.keys()}
+                ),
+                np.ndarray,
+            ):
                 brdffunc = np.vectorize(brdffunc)
 
             return brdffunc(t_0, t_ex, p_0, p_ex, **param_dict)
@@ -418,10 +462,10 @@ def __init__(self, **kwargs):
 
             def _set_function(self):
                 """def phase function as sympy object for later evaluation"""
-                self._func = 0.
+                self._func = 0.0
 
             def _set_legcoefficients(self):
-                self.legcoefs = 0.
+                self.legcoefs = 0.0
 
         # initialize a combined phase-function class element
         SRFcomb = BRDFfunction(NormBRDf=self.NormBRDF)
@@ -431,9 +475,14 @@ def _set_legcoefficients(self):
         #   (this is necessary for correct evaluation of fn-coefficients)
 
         # find BRDF functions with equal a parameters
-        equals = [np.where((np.array([VV[1].a for VV in self.SRFchoices]) ==
-                            tuple(V[1].a)).all(axis=1))[0]
-                  for V in self.SRFchoices]
+        equals = [
+            np.where(
+                (np.array([VV[1].a for VV in self.SRFchoices]) == tuple(V[1].a)).all(
+                    axis=1
+                )
+            )[0]
+            for V in self.SRFchoices
+        ]
 
         # evaluate index of BRDF-functions that have equal a parameter
 
@@ -465,8 +514,8 @@ def _set_legcoefficients(self):
         # combine legendre-expansions for each a-parameter based on given
         # combined legendre-coefficients
         SRFcomb.legexpansion = lambda t_0, t_ex, p_0, p_ex, geometry: np.sum(
-            [lexp(t_0, t_ex, p_0, p_ex, geometry)
-             for lexp in dummylegexpansion])
+            [lexp(t_0, t_ex, p_0, p_ex, geometry) for lexp in dummylegexpansion]
+        )
 
         for SRF in self.SRFchoices:
             # set parameters based on chosen classes to define analytic
@@ -493,12 +542,12 @@ def __init__(self, **kwargs):
 
     def _set_legcoefficients(self):
         self.ncoefs = 1
-        n = sp.Symbol('n')
-        self.legcoefs = (1. / sp.pi) * sp.KroneckerDelta(0, n)
+        n = sp.Symbol("n")
+        self.legcoefs = (1.0 / sp.pi) * sp.KroneckerDelta(0, n)
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        self._func = 1. / sp.pi
+        self._func = 1.0 / sp.pi
 
 
 class CosineLobe(Surface):
@@ -523,46 +572,62 @@ class CosineLobe(Surface):
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
 
-    def __init__(self, ncoefs=None, i=None, a=[1., 1., 1.], **kwargs):
-        assert ncoefs is not None, ('Error: number of coefficients ' +
-                                    'needs to be provided!')
-        assert i is not None, 'Error: Cosine lobe power needs to be specified!'
+    def __init__(self, ncoefs=None, i=None, a=[1.0, 1.0, 1.0], **kwargs):
+        assert ncoefs is not None, (
+            "Error: number of coefficients " + "needs to be provided!"
+        )
+        assert i is not None, "Error: Cosine lobe power needs to be specified!"
         super(CosineLobe, self).__init__(**kwargs)
         assert ncoefs > 0
         self.i = i
-        assert isinstance(self.i, int), ('Error: Cosine lobe power needs ' +
-                                         'to be an integer!')
-        assert i >= 0, 'ERROR: Power of Cosine-Lobe needs to be greater than 0'
+        assert isinstance(self.i, int), (
+            "Error: Cosine lobe power needs " + "to be an integer!"
+        )
+        assert i >= 0, "ERROR: Power of Cosine-Lobe needs to be greater than 0"
         self.a = a
-        assert isinstance(self.a, list), ('Error: Generalization-parameter ' +
-                                          'needs to be a list')
-        assert len(a) == 3, ('Error: Generalization-parameter list must ' +
-                             'contain 3 values')
-        assert all(type(x) == float for x in a), ('Error: Generalization-' +
-                                                  'parameter array must ' +
-                                                  'contain only floating-' +
-                                                  'point values!')
+        assert isinstance(self.a, list), (
+            "Error: Generalization-parameter " + "needs to be a list"
+        )
+        assert len(a) == 3, (
+            "Error: Generalization-parameter list must " + "contain 3 values"
+        )
+        assert all(type(x) == float for x in a), (
+            "Error: Generalization-"
+            + "parameter array must "
+            + "contain only floating-"
+            + "point values!"
+        )
         self.ncoefs = int(ncoefs)
         self._set_function()
         self._set_legcoefficients()
 
     def _set_legcoefficients(self):
-        n = sp.Symbol('n')
+        n = sp.Symbol("n")
         # A13   The Rational(is needed as otherwise a Gamma function
         # Pole error is issued)
-        self.legcoefs = 1. / sp.pi * ((2 ** (-2 - self.i) * (1 + 2 * n) *
-                                       sp.sqrt(sp.pi) * sp.gamma(1 + self.i)) /
-                                      (sp.gamma((2 - n + self.i) *
-                                                sp.Rational(1, 2))
-                                       * sp.gamma((3 + n + self.i) *
-                                                  sp.Rational(1, 2))))
+        self.legcoefs = (
+            1.0
+            / sp.pi
+            * (
+                (
+                    2 ** (-2 - self.i)
+                    * (1 + 2 * n)
+                    * sp.sqrt(sp.pi)
+                    * sp.gamma(1 + self.i)
+                )
+                / (
+                    sp.gamma((2 - n + self.i) * sp.Rational(1, 2))
+                    * sp.gamma((3 + n + self.i) * sp.Rational(1, 2))
+                )
+            )
+        )
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
         # self._func = sp.Max(self.scat_angle(theta_i,
         #                                    theta_s,
@@ -574,7 +639,7 @@ def _set_function(self):
         #     (this is done because   sp.lambdify('x',sp.Max(x), "numpy")
         #      generates a function that can not interpret array inputs.)
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, a=self.a)
-        self._func = 1. / sp.pi * (x * (1. + sp.sign(x)) / 2.) ** self.i
+        self._func = 1.0 / sp.pi * (x * (1.0 + sp.sign(x)) / 2.0) ** self.i
 
 
 class HenyeyGreenstein(Surface):
@@ -600,38 +665,42 @@ class HenyeyGreenstein(Surface):
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
 
-    def __init__(self, t=None, ncoefs=None, a=[1., 1., 1.], **kwargs):
-        assert t is not None, 't parameter needs to be provided!'
-        assert ncoefs is not None, 'Number of coeff. needs to be specified'
+    def __init__(self, t=None, ncoefs=None, a=[1.0, 1.0, 1.0], **kwargs):
+        assert t is not None, "t parameter needs to be provided!"
+        assert ncoefs is not None, "Number of coeff. needs to be specified"
         super(HenyeyGreenstein, self).__init__(**kwargs)
         self.t = t
         self.ncoefs = ncoefs
         assert self.ncoefs > 0
 
         self.a = a
-        assert isinstance(self.a, list), 'Error: Generalization-parameter ' + \
-            'needs to be a list'
-        assert len(a) == 3, 'Error: Generalization-parameter list must ' + \
-            'contain 3 values'
+        assert isinstance(self.a, list), (
+            "Error: Generalization-parameter " + "needs to be a list"
+        )
+        assert len(a) == 3, (
+            "Error: Generalization-parameter list must " + "contain 3 values"
+        )
         self._set_function()
         self._set_legcoefficients()
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, a=self.a)
 
-        self._func = 1. * (1. - self.t ** 2.) / ((sp.pi) *
-                                                 (1. + self.t ** 2. -
-                                                  2. * self.t * x) ** 1.5)
+        self._func = (
+            1.0
+            * (1.0 - self.t ** 2.0)
+            / ((sp.pi) * (1.0 + self.t ** 2.0 - 2.0 * self.t * x) ** 1.5)
+        )
 
     def _set_legcoefficients(self):
-        n = sp.Symbol('n')
-        self.legcoefs = 1. * (1. / (sp.pi)) * (2. * n + 1) * self.t ** n
+        n = sp.Symbol("n")
+        self.legcoefs = 1.0 * (1.0 / (sp.pi)) * (2.0 * n + 1) * self.t ** n
 
 
 class HG_nadirnorm(Surface):
@@ -657,56 +726,74 @@ class HG_nadirnorm(Surface):
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
 
-    def __init__(self, t=None, ncoefs=None, a=[1., 1., 1.], **kwargs):
-        assert t is not None, 't parameter needs to be provided!'
-        assert ncoefs is not None, 'Number of coeffs needs to be specified'
+    def __init__(self, t=None, ncoefs=None, a=[1.0, 1.0, 1.0], **kwargs):
+        assert t is not None, "t parameter needs to be provided!"
+        assert ncoefs is not None, "Number of coeffs needs to be specified"
         super(HG_nadirnorm, self).__init__(**kwargs)
         self.t = t
         self.ncoefs = ncoefs
         assert self.ncoefs > 0
 
         self.a = a
-        assert isinstance(self.a, list), 'Error: Generalization-parameter ' + \
-            'needs to be a list'
-        assert len(a) == 3, 'Error: Generalization-parameter list must ' + \
-            'contain 3 values'
+        assert isinstance(self.a, list), (
+            "Error: Generalization-parameter " + "needs to be a list"
+        )
+        assert len(a) == 3, (
+            "Error: Generalization-parameter list must " + "contain 3 values"
+        )
         self._set_function()
         self._set_legcoefficients()
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, a=self.a)
 
-        nadir_hemreflect = 4 * ((1. - self.t**2.) *
-                                (1. - self.t * (-self.t + self.a[0]) -
-                                 sp.sqrt((1 + self.t**2 -
-                                          2 * self.a[0] * self.t) *
-                                         (1 + self.t**2))) /
-                                (2. * self.a[0]**2. * self.t**2. *
-                                 sp.sqrt(1. + self.t**2. -
-                                         2. * self.a[0] * self.t)))
-
-        self._func = (1. / nadir_hemreflect) * ((1. - self.t ** 2.) /
-                                                ((sp.pi) *
-                                                 (1. + self.t ** 2. -
-                                                  2. * self.t * x) ** 1.5))
+        nadir_hemreflect = 4 * (
+            (1.0 - self.t ** 2.0)
+            * (
+                1.0
+                - self.t * (-self.t + self.a[0])
+                - sp.sqrt(
+                    (1 + self.t ** 2 - 2 * self.a[0] * self.t) * (1 + self.t ** 2)
+                )
+            )
+            / (
+                2.0
+                * self.a[0] ** 2.0
+                * self.t ** 2.0
+                * sp.sqrt(1.0 + self.t ** 2.0 - 2.0 * self.a[0] * self.t)
+            )
+        )
+
+        self._func = (1.0 / nadir_hemreflect) * (
+            (1.0 - self.t ** 2.0)
+            / ((sp.pi) * (1.0 + self.t ** 2.0 - 2.0 * self.t * x) ** 1.5)
+        )
 
     def _set_legcoefficients(self):
-        nadir_hemreflect = 4 * ((1. - self.t**2.) *
-                                (1. - self.t *
-                                 (-self.t + self.a[0]) -
-                                 sp.sqrt((1 + self.t**2 -
-                                          2 * self.a[0] * self.t) *
-                                         (1 + self.t**2))) /
-                                (2. * self.a[0]**2. * self.t**2. *
-                                 sp.sqrt(1. + self.t**2. -
-                                         2. * self.a[0] * self.t)))
-
-        n = sp.Symbol('n')
-        self.legcoefs = (1. / nadir_hemreflect) * ((1. / (sp.pi)) *
-                                                   (2. * n + 1) * self.t ** n)
+        nadir_hemreflect = 4 * (
+            (1.0 - self.t ** 2.0)
+            * (
+                1.0
+                - self.t * (-self.t + self.a[0])
+                - sp.sqrt(
+                    (1 + self.t ** 2 - 2 * self.a[0] * self.t) * (1 + self.t ** 2)
+                )
+            )
+            / (
+                2.0
+                * self.a[0] ** 2.0
+                * self.t ** 2.0
+                * sp.sqrt(1.0 + self.t ** 2.0 - 2.0 * self.a[0] * self.t)
+            )
+        )
+
+        n = sp.Symbol("n")
+        self.legcoefs = (1.0 / nadir_hemreflect) * (
+            (1.0 / (sp.pi)) * (2.0 * n + 1) * self.t ** n
+        )
diff --git a/rt1/volume.py b/rt1/volume.py
index 3297103c..e5224cd6 100644
--- a/rt1/volume.py
+++ b/rt1/volume.py
@@ -11,14 +11,14 @@ class Volume(Scatter):
     """basic volume class"""
 
     def __init__(self, **kwargs):
-        self.omega = kwargs.pop('omega', None)
-        self.tau = kwargs.pop('tau', None)
+        self.omega = kwargs.pop("omega", None)
+        self.tau = kwargs.pop("tau", None)
 
         # set scattering angle generalization-matrix to [-1,1,1] if it is not
         # explicitly provided by the chosen class this results in a peak in
         # forward-direction which is suitable for describing volume-scattering
         # phase-functions
-        self.a = getattr(self, 'a', [-1., 1., 1.])
+        self.a = getattr(self, "a", [-1.0, 1.0, 1.0])
 
         # add a quick way for visualizing the functions as polarplot
         self.polarplot = partial(polarplot, X=self)
@@ -49,10 +49,10 @@ def p(self, t_0, t_ex, p_0, p_ex, param_dict={}):
             Numerical value of the volume-scattering phase-function
         """
         # define sympy objects
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
 
         # replace arguments and evaluate expression
         # sp.lambdify is used to allow array-inputs
@@ -68,15 +68,31 @@ def p(self, t_0, t_ex, p_0, p_ex, param_dict={}):
         # (this happens e.g. for the Isotropic brdf).
         # The following query is implemented to ensure correct array-output:
         # TODO this is not a proper test !
-        if not isinstance(pfunc(np.array([.1, .2, .3]), .1, .1, .1,
-                                **{key: .12 for key in param_dict.keys()}
-                                ), np.ndarray):
+        if not isinstance(
+            pfunc(
+                np.array([0.1, 0.2, 0.3]),
+                0.1,
+                0.1,
+                0.1,
+                **{key: 0.12 for key in param_dict.keys()}
+            ),
+            np.ndarray,
+        ):
             pfunc = np.vectorize(pfunc)
 
         return pfunc(t_0, t_ex, p_0, p_ex, **param_dict)
 
-    def p_theta_diff(self, t_0, t_ex, p_0, p_ex, geometry,
-                     param_dict={}, return_symbolic=False, n=1):
+    def p_theta_diff(
+        self,
+        t_0,
+        t_ex,
+        p_0,
+        p_ex,
+        geometry,
+        param_dict={},
+        return_symbolic=False,
+        n=1,
+    ):
         """
         Calculation of the derivative of p with respect to
         the scattering-angles t_ex
@@ -132,11 +148,13 @@ def p_theta_diff(self, t_0, t_ex, p_0, p_ex, geometry,
         """
 
         # define sympy variables based on chosen geometry
-        if geometry == 'mono':
-            assert len(np.unique(p_0)) == 1, 'p_0 must contain only a ' + \
-                'single unique value for monostatic geometry'
+        if geometry == "mono":
+            assert len(np.unique(p_0)) == 1, (
+                "p_0 must contain only a "
+                + "single unique value for monostatic geometry"
+            )
 
-            theta_0 = sp.Symbol('theta_0')
+            theta_0 = sp.Symbol("theta_0")
             theta_ex = theta_0
             phi_0 = np.unique(p_0)[0]
             phi_ex = np.unique(p_0)[0] + sp.pi
@@ -144,75 +162,95 @@ def p_theta_diff(self, t_0, t_ex, p_0, p_ex, geometry,
             t_ex = t_0
             p_ex = p_0 + np.pi
         else:
-            if geometry[0] == 'v':
-                theta_0 = sp.Symbol('theta_0')
-            elif geometry[0] == 'f':
-                assert len(np.unique(t_0)) == 1, 't_0 must contain only a ' + \
-                    'single unique value for geometry[0] == f'
+            if geometry[0] == "v":
+                theta_0 = sp.Symbol("theta_0")
+            elif geometry[0] == "f":
+                assert len(np.unique(t_0)) == 1, (
+                    "t_0 must contain only a "
+                    + "single unique value for geometry[0] == f"
+                )
 
                 theta_0 = np.unique(t_0)[0]
             else:
-                raise AssertionError('wrong choice of theta_0 geometry')
+                raise AssertionError("wrong choice of theta_0 geometry")
 
-            if geometry[1] == 'v':
-                theta_ex = sp.Symbol('theta_ex')
-            elif geometry[1] == 'f':
-                assert len(np.unique(t_ex)) == 1, 't_ex must contain only' + \
-                    ' a single unique value for geometry[1] == f'
+            if geometry[1] == "v":
+                theta_ex = sp.Symbol("theta_ex")
+            elif geometry[1] == "f":
+                assert len(np.unique(t_ex)) == 1, (
+                    "t_ex must contain only"
+                    + " a single unique value for geometry[1] == f"
+                )
 
                 theta_ex = np.unique(t_ex)[0]
             else:
-                raise AssertionError('wrong choice of theta_ex geometry')
+                raise AssertionError("wrong choice of theta_ex geometry")
 
-            if geometry[2] == 'v':
-                phi_0 = sp.Symbol('phi_0')
-            elif geometry[2] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_0 must contain only' + \
-                    ' a single unique value for geometry[2] == f'
+            if geometry[2] == "v":
+                phi_0 = sp.Symbol("phi_0")
+            elif geometry[2] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_0 must contain only"
+                    + " a single unique value for geometry[2] == f"
+                )
 
                 phi_0 = np.unique(p_0)[0]
             else:
-                raise AssertionError('wrong choice of phi_0 geometry')
+                raise AssertionError("wrong choice of phi_0 geometry")
 
-            if geometry[3] == 'v':
-                phi_ex = sp.Symbol('phi_ex')
-            elif geometry[3] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_ex must contain only' + \
-                    ' a single unique value for geometry[3] == f'
+            if geometry[3] == "v":
+                phi_ex = sp.Symbol("phi_ex")
+            elif geometry[3] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_ex must contain only"
+                    + " a single unique value for geometry[3] == f"
+                )
 
                 phi_ex = np.unique(p_ex)[0]
             else:
-                raise AssertionError('wrong choice of phi_ex geometry')
+                raise AssertionError("wrong choice of phi_ex geometry")
 
-        if geometry[1] == 'f':
-            dfunc_dtheta_0 = 0.
+        if geometry[1] == "f":
+            dfunc_dtheta_0 = 0.0
         else:
-            func = self._func.xreplace({sp.Symbol('theta_0'): theta_0,
-                                        sp.Symbol('theta_ex'): theta_ex,
-                                        sp.Symbol('phi_0'): phi_0,
-                                        sp.Symbol('phi_ex'): phi_ex})
+            func = self._func.xreplace(
+                {
+                    sp.Symbol("theta_0"): theta_0,
+                    sp.Symbol("theta_ex"): theta_ex,
+                    sp.Symbol("phi_0"): phi_0,
+                    sp.Symbol("phi_ex"): phi_ex,
+                }
+            )
 
             dfunc_dtheta_0 = sp.diff(func, theta_ex, n)
 
         if return_symbolic is True:
             return dfunc_dtheta_0
         else:
-            args = (sp.Symbol('theta_0'),
-                    sp.Symbol('theta_ex'),
-                    sp.Symbol('phi_0'),
-                    sp.Symbol('phi_ex')) + tuple(param_dict.keys())
+            args = (
+                sp.Symbol("theta_0"),
+                sp.Symbol("theta_ex"),
+                sp.Symbol("phi_0"),
+                sp.Symbol("phi_ex"),
+            ) + tuple(param_dict.keys())
 
-            pfunc = sp.lambdify(args, dfunc_dtheta_0,
-                                modules=["numpy", "sympy"])
+            pfunc = sp.lambdify(args, dfunc_dtheta_0, modules=["numpy", "sympy"])
 
             # in case _func is a constant, lambdify will produce a function
             # with scalar output which is not suitable for further processing
             # (this happens e.g. for the Isotropic brdf).
             # The following query is implemented to ensure correct array-output
             # TODO this is not a proper test !
-            if not isinstance(pfunc(
-                    np.array([.1, .2, .3]), .1, .1, .1,
-                    **{key: .12 for key in param_dict.keys()}), np.ndarray):
+            if not isinstance(
+                pfunc(
+                    np.array([0.1, 0.2, 0.3]),
+                    0.1,
+                    0.1,
+                    0.1,
+                    **{key: 0.12 for key in param_dict.keys()}
+                ),
+                np.ndarray,
+            ):
                 pfunc = np.vectorize(pfunc)
 
             return pfunc(t_0, t_ex, p_0, p_ex, **param_dict)
@@ -279,71 +317,82 @@ def legexpansion(self, t_0, t_ex, p_0, p_ex, geometry):
         """
         assert self.ncoefs > 0
 
-        theta_s = sp.Symbol('theta_s')
-        phi_s = sp.Symbol('phi_s')
+        theta_s = sp.Symbol("theta_s")
+        phi_s = sp.Symbol("phi_s")
 
         NP = self.ncoefs
-        n = sp.Symbol('n')
+        n = sp.Symbol("n")
 
         # define sympy variables based on chosen geometry
-        if geometry == 'mono':
+        if geometry == "mono":
 
-            assert len(np.unique(p_0)) == 1, 'p_0 must contain only a ' + \
-                'single unique value for monostatic geometry'
+            assert len(np.unique(p_0)) == 1, (
+                "p_0 must contain only a "
+                + "single unique value for monostatic geometry"
+            )
 
-            theta_0 = sp.Symbol('theta_0')
+            theta_0 = sp.Symbol("theta_0")
             theta_ex = theta_0
             phi_0 = np.unique(p_0)[0]
             phi_ex = np.unique(p_0)[0] + sp.pi
         else:
-            if geometry[0] == 'v':
-                theta_0 = sp.Symbol('theta_0')
-            elif geometry[0] == 'f':
-                assert len(np.unique(t_0)) == 1, 't_0 must contain only a ' + \
-                    'single unique value for geometry[0] == f'
+            if geometry[0] == "v":
+                theta_0 = sp.Symbol("theta_0")
+            elif geometry[0] == "f":
+                assert len(np.unique(t_0)) == 1, (
+                    "t_0 must contain only a "
+                    + "single unique value for geometry[0] == f"
+                )
 
                 theta_0 = np.unique(t_0)[0]
             else:
-                raise AssertionError('wrong choice of theta_i geometry')
+                raise AssertionError("wrong choice of theta_i geometry")
 
-            if geometry[1] == 'v':
-                theta_ex = sp.Symbol('theta_ex')
-            elif geometry[1] == 'f':
-                assert len(np.unique(t_ex)) == 1, 't_ex must contain only' + \
-                    ' a single unique value for geometry[1] == f'
+            if geometry[1] == "v":
+                theta_ex = sp.Symbol("theta_ex")
+            elif geometry[1] == "f":
+                assert len(np.unique(t_ex)) == 1, (
+                    "t_ex must contain only"
+                    + " a single unique value for geometry[1] == f"
+                )
 
                 theta_ex = np.unique(t_ex)[0]
             else:
-                raise AssertionError('wrong choice of theta_ex geometry')
+                raise AssertionError("wrong choice of theta_ex geometry")
 
-            if geometry[2] == 'v':
-                phi_0 = sp.Symbol('phi_0')
-            elif geometry[2] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_0 must contain only' + \
-                    ' a single unique value for geometry[2] == f'
+            if geometry[2] == "v":
+                phi_0 = sp.Symbol("phi_0")
+            elif geometry[2] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_0 must contain only"
+                    + " a single unique value for geometry[2] == f"
+                )
 
                 phi_0 = np.unique(p_0)[0]
             else:
-                raise AssertionError('wrong choice of phi_i geometry')
+                raise AssertionError("wrong choice of phi_i geometry")
 
-            if geometry[3] == 'v':
-                phi_ex = sp.Symbol('phi_ex')
-            elif geometry[3] == 'f':
-                assert len(np.unique(p_0)) == 1, 'p_ex must contain only' + \
-                    ' a single unique value for geometry[3] == f'
+            if geometry[3] == "v":
+                phi_ex = sp.Symbol("phi_ex")
+            elif geometry[3] == "f":
+                assert len(np.unique(p_0)) == 1, (
+                    "p_ex must contain only"
+                    + " a single unique value for geometry[3] == f"
+                )
 
                 phi_ex = np.unique(p_ex)[0]
             else:
-                raise AssertionError('wrong choice of phi_ex geometry')
+                raise AssertionError("wrong choice of phi_ex geometry")
 
         # correct for backscattering
-        return sp.Sum(self.legcoefs *
-                      sp.legendre(n,
-                                  self.scat_angle(sp.pi - theta_0,
-                                                  theta_s,
-                                                  phi_0,
-                                                  phi_s,
-                                                  self.a)), (n, 0, NP - 1))
+        return sp.Sum(
+            self.legcoefs
+            * sp.legendre(
+                n,
+                self.scat_angle(sp.pi - theta_0, theta_s, phi_0, phi_s, self.a),
+            ),
+            (n, 0, NP - 1),
+        )
 
 
 class LinCombV(Volume):
@@ -431,7 +480,7 @@ def __init__(self, **kwargs):
             def _set_function(self):
                 """def phase function as sympy object for later evaluation"""
 
-                self._func = 0.
+                self._func = 0.0
 
             def _set_legcoefficients(self):
                 """
@@ -440,12 +489,17 @@ def _set_legcoefficients(self):
                 subsituting 'n'
                 """
 
-                self.legcoefs = 0.
+                self.legcoefs = 0.0
 
         # find phase functions with equal a parameters
-        equals = [np.where((np.array([VV[1].a for VV in self.Vchoices]) ==
-                            tuple(V[1].a)).all(axis=1))[0]
-                  for V in self.Vchoices]
+        equals = [
+            np.where(
+                (np.array([VV[1].a for VV in self.Vchoices]) == tuple(V[1].a)).all(
+                    axis=1
+                )
+            )[0]
+            for V in self.Vchoices
+        ]
 
         # evaluate index of phase-functions that have equal a parameter
         equal_a = list({tuple(row) for row in equals})
@@ -483,8 +537,8 @@ def _set_legcoefficients(self):
         # combine legendre-expansions for each a-parameter based on given
         # combined legendre-coefficients
         Vcomb.legexpansion = lambda t_0, t_ex, p_0, p_ex, geometry: np.sum(
-            [lexp(t_0, t_ex, p_0, p_ex, geometry)
-             for lexp in dummylegexpansion])
+            [lexp(t_0, t_ex, p_0, p_ex, geometry) for lexp in dummylegexpansion]
+        )
 
         for V in self.Vchoices:
             # set parameters based on chosen classes to define analytic
@@ -522,12 +576,12 @@ def __init__(self, **kwargs):
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, self.a)
-        self._func = 3. / (16. * sp.pi) * (1. + x ** 2.)
+        self._func = 3.0 / (16.0 * sp.pi) * (1.0 + x ** 2.0)
 
     def _set_legcoefficients(self):
         """
@@ -537,12 +591,14 @@ def _set_legcoefficients(self):
         # only 3 coefficients are needed to correctly represent
         # the Rayleigh scattering function
         self.ncoefs = 3
-        n = sp.Symbol('n')
-        self.legcoefs = ((3. / (16. * sp.pi)) * ((4. / 3.) *
-                                                 sp.KroneckerDelta(0, n) +
-                                                 (2. / 3.) *
-                                                 sp.KroneckerDelta(2, n))
-                         ).expand()
+        n = sp.Symbol("n")
+        self.legcoefs = (
+            (3.0 / (16.0 * sp.pi))
+            * (
+                (4.0 / 3.0) * sp.KroneckerDelta(0, n)
+                + (2.0 / 3.0) * sp.KroneckerDelta(2, n)
+            )
+        ).expand()
 
 
 class HenyeyGreenstein(Volume):
@@ -569,16 +625,18 @@ class HenyeyGreenstein(Volume):
         (http://rt1.readthedocs.io/en/latest/theory.html#equation-general_scat_angle)
     """
 
-    def __init__(self, t=None, ncoefs=None, a=[-1., 1., 1.], **kwargs):
-        assert t is not None, 't parameter needs to be provided!'
-        assert ncoefs is not None, 'Number of coeffs needs to be specified'
+    def __init__(self, t=None, ncoefs=None, a=[-1.0, 1.0, 1.0], **kwargs):
+        assert t is not None, "t parameter needs to be provided!"
+        assert ncoefs is not None, "Number of coeffs needs to be specified"
         super(HenyeyGreenstein, self).__init__(**kwargs)
         self.t = t
         self.a = a
-        assert isinstance(self.a, list), 'Error: Generalization-parameter ' + \
-            'needs to be a list'
-        assert len(a) == 3, 'Error: Generalization-parameter list must ' + \
-            'contain 3 values'
+        assert isinstance(self.a, list), (
+            "Error: Generalization-parameter " + "needs to be a list"
+        )
+        assert len(a) == 3, (
+            "Error: Generalization-parameter list must " + "contain 3 values"
+        )
         self.ncoefs = ncoefs
         assert self.ncoefs > 0
         self._set_function()
@@ -586,22 +644,22 @@ def __init__(self, t=None, ncoefs=None, a=[-1., 1., 1.], **kwargs):
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, self.a)
-        self._func = (1. - self.t ** 2.) / ((4. * sp.pi) *
-                                            (1. + self.t ** 2. -
-                                             2. * self.t * x) ** 1.5)
+        self._func = (1.0 - self.t ** 2.0) / (
+            (4.0 * sp.pi) * (1.0 + self.t ** 2.0 - 2.0 * self.t * x) ** 1.5
+        )
 
     def _set_legcoefficients(self):
         """
         set Legrende coefficients
         needs to be a function that can be later evaluated by subsituting 'n'
         """
-        n = sp.Symbol('n')
-        self.legcoefs = (1. / (4. * sp.pi)) * (2. * n + 1) * self.t ** n
+        n = sp.Symbol("n")
+        self.legcoefs = (1.0 / (4.0 * sp.pi)) * (2.0 * n + 1) * self.t ** n
 
 
 class HGRayleigh(Volume):
@@ -632,18 +690,22 @@ class HGRayleigh(Volume):
         (http://rt1.readthedocs.io/en/latest/theory.html#equation-general_scat_angle)
     """
 
-    def __init__(self, t=None, ncoefs=None, a=[-1., 1., 1.], **kwargs):
-        assert t is not None, 't parameter needs to be provided!'
-        assert ncoefs is not None, 'Number of coeffs needs to be specified'
+    def __init__(self, t=None, ncoefs=None, a=[-1.0, 1.0, 1.0], **kwargs):
+        assert t is not None, "t parameter needs to be provided!"
+        assert ncoefs is not None, "Number of coeffs needs to be specified"
         super(HGRayleigh, self).__init__(**kwargs)
         self.t = t
         self.a = a
-        assert isinstance(self.a, list), 'Error: Generalization-parameter ' + \
-            'needs to be a list'
-        assert len(a) == 3, 'Error: Generalization-parameter list must ' + \
-            'contain 3 values'
-        assert all(type(x) == float for x in a), 'Error: Generalization-' + \
-            'parameter array must contain only floating-point values!'
+        assert isinstance(self.a, list), (
+            "Error: Generalization-parameter " + "needs to be a list"
+        )
+        assert len(a) == 3, (
+            "Error: Generalization-parameter list must " + "contain 3 values"
+        )
+        assert all(type(x) == float for x in a), (
+            "Error: Generalization-"
+            + "parameter array must contain only floating-point values!"
+        )
         self.ncoefs = ncoefs
         assert self.ncoefs > 0
         self._set_function()
@@ -651,33 +713,53 @@ def __init__(self, t=None, ncoefs=None, a=[-1., 1., 1.], **kwargs):
 
     def _set_function(self):
         """define phase function as sympy object for later evaluation"""
-        theta_0 = sp.Symbol('theta_0')
-        theta_ex = sp.Symbol('theta_ex')
-        phi_0 = sp.Symbol('phi_0')
-        phi_ex = sp.Symbol('phi_ex')
+        theta_0 = sp.Symbol("theta_0")
+        theta_ex = sp.Symbol("theta_ex")
+        phi_0 = sp.Symbol("phi_0")
+        phi_ex = sp.Symbol("phi_ex")
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, self.a)
-        self._func = 3. / (8. * sp.pi) * (1. /
-                                          (2. + self.t ** 2) *
-                                          (1 + x ** 2) *
-                                          (1. - self.t ** 2.) /
-                                          ((1. + self.t ** 2. -
-                                            2. * self.t * x) ** 1.5))
+        self._func = (
+            3.0
+            / (8.0 * sp.pi)
+            * (
+                1.0
+                / (2.0 + self.t ** 2)
+                * (1 + x ** 2)
+                * (1.0 - self.t ** 2.0)
+                / ((1.0 + self.t ** 2.0 - 2.0 * self.t * x) ** 1.5)
+            )
+        )
 
     def _set_legcoefficients(self):
         """
         set Legrende coefficients
         needs to be a function that can be later evaluated by subsituting 'n'
         """
-        n = sp.Symbol('n')
+        n = sp.Symbol("n")
         self.legcoefs = sp.Piecewise(
-            (3. / (8. * sp.pi) * 1. / (2. + self.t ** 2) *
-             ((n + 2.) * (n + 1.) /
-              (2. * n + 3) * self.t ** (n + 2.) +
-              (n + 1.) ** 2. / (2. * n + 3.) * self.t ** n +
-              (5. * n ** 2. - 1.) / (2. * n - 1.) * self.t ** n), n < 2),
-            (3. / (8. * sp.pi) * 1. / (2. + self.t ** 2) *
-             (n * (n - 1.) / (2. * n - 1.) * self.t ** (n - 2.) +
-              (n + 2.) * (n + 1.) / (2. * n + 3) * self.t ** (n + 2.) +
-              (n + 1.) ** 2. / (2. * n + 3.) * self.t ** n +
-              (5. * n ** 2. - 1.) / (2. * n - 1.) * self.t ** n), True)
+            (
+                3.0
+                / (8.0 * sp.pi)
+                * 1.0
+                / (2.0 + self.t ** 2)
+                * (
+                    (n + 2.0) * (n + 1.0) / (2.0 * n + 3) * self.t ** (n + 2.0)
+                    + (n + 1.0) ** 2.0 / (2.0 * n + 3.0) * self.t ** n
+                    + (5.0 * n ** 2.0 - 1.0) / (2.0 * n - 1.0) * self.t ** n
+                ),
+                n < 2,
+            ),
+            (
+                3.0
+                / (8.0 * sp.pi)
+                * 1.0
+                / (2.0 + self.t ** 2)
+                * (
+                    n * (n - 1.0) / (2.0 * n - 1.0) * self.t ** (n - 2.0)
+                    + (n + 2.0) * (n + 1.0) / (2.0 * n + 3) * self.t ** (n + 2.0)
+                    + (n + 1.0) ** 2.0 / (2.0 * n + 3.0) * self.t ** n
+                    + (5.0 * n ** 2.0 - 1.0) / (2.0 * n - 1.0) * self.t ** n
+                ),
+                True,
+            ),
         )
diff --git a/setup.py b/setup.py
index 0144085f..f7380460 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@
 For COPYING and LICENSE details, please refer to the LICENSE file
 """
 
-from setuptools import setup
+from setuptools import setup, find_packages
 #from setuptools import find_packages
 from rt1 import __version__
 
@@ -16,7 +16,7 @@
 
       description='RT1 - bistatic single scattering radiative transfer model',
 
-      packages=['rt1'],
+      packages=find_packages(),  # ['rt1'],
       package_dir={'rt1': 'rt1'},
       include_package_data=False,
 
@@ -38,7 +38,7 @@
 
       install_requires=["numpy>=1.16", "sympy>=1.4", "scipy>=1.2",
                         "pandas>=0.24", "matplotlib>=3.0"],
-      extras_require={'symengine' : ["symengine>=0.4"]},
+      extras_require={'full' : ["symengine>=0.4", "xarray>=0.16"]},
 
       keywords=["physics", "radiative transfer"],
 


Test patch:
diff --git a/tests/sig0_dB.dump b/tests/sig0_dB.dump
index e6371214..2458ae47 100644
Binary files a/tests/sig0_dB.dump and b/tests/sig0_dB.dump differ
diff --git a/tests/sig0_linear.dump b/tests/sig0_linear.dump
index 164df559..22541a35 100644
Binary files a/tests/sig0_linear.dump and b/tests/sig0_linear.dump differ
diff --git a/tests/test_config.ini b/tests/test_config.ini
index ffdcf02c..d3cb9693 100644
--- a/tests/test_config.ini
+++ b/tests/test_config.ini
@@ -81,7 +81,7 @@ module__processing_cfg = tests/parallel_processing_config.py
 
 [PROCESS_SPECS]
 
-finalout_name = results.h5
+finalout_name = results.nc
 dumpfolder = dump01
 error_dumpfolder = ${PROCESS_SPECS:dumpfolder}
 
diff --git a/tests/test_configparser.py b/tests/test_configparser.py
index 1940763f..566ee780 100644
--- a/tests/test_configparser.py
+++ b/tests/test_configparser.py
@@ -81,7 +81,7 @@ def test_load_cfg(self):
 
         #----------------------------------------- check parsed process_specs
 
-        process_specs = {'finalout_name' : 'results.h5',
+        process_specs = {'finalout_name' : 'results.nc',
                          'dumpfolder' : 'dump01',
                          'save_path' : Path('tests/proc_test'),
                          'f0' : 1245.,
diff --git a/tests/test_env.yml b/tests/test_env.yml
new file mode 100644
index 00000000..1bd0485e
--- /dev/null
+++ b/tests/test_env.yml
@@ -0,0 +1,17 @@
+name: test_rt1
+channels:
+  - conda-forge
+
+dependencies:
+  - numpy
+  - sympy
+  - scipy
+  - pandas
+  - xarray
+  - matplotlib
+  - cloudpickle
+  - python-symengine
+
+  - coveralls
+  - pytest
+  - pytest-cov
diff --git a/tests/test_parallel_processing.py b/tests/test_parallel_processing.py
index 15f66f72..54d4d472 100644
--- a/tests/test_parallel_processing.py
+++ b/tests/test_parallel_processing.py
@@ -1,17 +1,15 @@
-import sys
 from pathlib import Path
-sys.path.append(str(Path('H:/python_modules/rt_model_python/rt1')))
 import unittest
 import unittest.mock as mock
-from pathlib import Path
 from rt1.rtprocess import RTprocess, RTresults
-import shutil
 
 import warnings
 warnings.simplefilter('ignore')
 
+
 # use "test_0_---"   "test_1_---"   to ensure test-order
 
+
 class TestRTfits(unittest.TestCase):
 
     def test_0_parallel_processing(self):
@@ -20,10 +18,10 @@ def test_0_parallel_processing(self):
 
         proc = RTprocess(config_path, autocontinue=True)
 
-        proc.run_processing(ncpu=4, reader_args = reader_args)
+        proc.run_processing(ncpu=4, reader_args=reader_args)
 
         # run again to check what happens if files already exist
-        proc.run_processing(ncpu=4, reader_args = reader_args)
+        proc.run_processing(ncpu=4, reader_args=reader_args)
 
         #----------------------------------------- check if files have been copied
         assert Path('tests/proc_test/dump01/cfg').exists(), 'folder-generation did not work'
@@ -38,16 +36,20 @@ def test_1_rtresults(self):
         results = RTresults('tests/proc_test')
         assert hasattr(results, 'dump01'), 'dumpfolder not found by RTresults'
 
+        dumpfiles = [i for i in results.dump01.dump_files]
+        print(dumpfiles)
+
         fit = results.dump01.load_fit()
         cfg = results.dump01.load_cfg()
-        dumpfiles = [i for i in results.dump01.dump_files]
 
-        with self.assertRaises(AssertionError):
-            # TODO implement netcdf export and properly test netcdf's !
-            ncfile = results.dump01.load_nc()
-        with self.assertRaises(AssertionError):
-            # TODO implement netcdf export and properly test netcdf's !
-            ncfile = results.dump01.NetCDF_variables
+        with results.dump01.load_nc() as ncfile:
+            processed_ids = list(ncfile.ID.values)
+
+        processed_ids.sort()
+        assert processed_ids == [1, 2, 3], 'NetCDF export does not include all IDs'
+
+        # check if NetCDF_variables works as expected
+        results.dump01.NetCDF_variables
 
         # remove the save_path directory
         #print('deleting save_path directory...')
@@ -62,20 +64,24 @@ def test_2_single_core_processing(self):
         with self.assertRaises(SystemExit):
             with mock.patch('builtins.input', side_effect=['N']):
                 proc = RTprocess(config_path, autocontinue=False)
-                proc.setup()
 
+        with self.assertRaises(SystemExit):
+            with mock.patch('builtins.input', side_effect=['N']):
+                proc = RTprocess(config_path, autocontinue=False,
+                                 setup=False)
+                proc.setup()
         with self.assertRaises(SystemExit):
             with mock.patch('builtins.input', side_effect=['REMOVE', 'N']):
                 proc = RTprocess(config_path, autocontinue=False)
-                proc.setup()
 
         with mock.patch('builtins.input', side_effect=['REMOVE', 'Y']):
             proc = RTprocess(config_path, autocontinue=False)
-            proc.setup()
         assert len(list(Path('tests/proc_test/dump01/dumps').iterdir())) == 0, 'user-input REMOVE did not work'
 
         with mock.patch('builtins.input', side_effect=['REMOVE', 'Y']):
-            proc.run_processing(ncpu=1, reader_args = reader_args, copy=False)
+            proc = RTprocess(config_path, autocontinue=False,
+                             copy=False, setup=False)
+            proc.run_processing(ncpu=1, reader_args=reader_args)
 
         #----------------------------------------- check if files have been copied
         assert Path('tests/proc_test/dump01/cfg').exists(), 'folder-generation did not work'
@@ -100,7 +106,7 @@ def test_3_parallel_processing_init_kwargs(self):
                              dumpfolder='dump02')
                          )
 
-        proc.run_processing(ncpu=4, reader_args = reader_args)
+        proc.run_processing(ncpu=4, reader_args=reader_args)
 
         #----------------------------------------- check if files have been copied
         assert Path('tests/proc_test2/dump02/cfg').exists(), 'folder-generation did not work'
@@ -110,8 +116,32 @@ def test_3_parallel_processing_init_kwargs(self):
         assert Path('tests/proc_test2/dump02/cfg/parallel_processing_config.py').exists(), 'copying did not work'
 
 
-if __name__ == "__main__":
-    unittest.main()
+    def test_4_postprocess_and_finalout(self):
+        config_path = Path(__file__).parent.absolute() / 'test_config.ini'
+        reader_args = [dict(gpi=i) for i in [1, 2, 3, 4]]
+
+        with mock.patch('builtins.input', side_effect=['REMOVE', 'Y']):
+            proc = RTprocess(config_path,
+                             init_kwargs=dict(
+                                 path__save_path='tests/proc_test3',
+                                 dumpfolder='dump03'))
+
+            proc.run_processing(ncpu=4, reader_args=reader_args,
+                                postprocess=False)
 
+        results = RTresults('tests/proc_test3')
+        assert hasattr(results, 'dump03'), 'dumpfolder dump02 not found by RTresults'
 
+        finalout_name = results.dump03.load_cfg().get_process_specs()['finalout_name']
 
+        assert not Path(f'tests/proc_test3/dump03/results/{finalout_name}.nc').exists(), 'disabling postprocess did not work'
+
+        proc.run_finaloutput(ncpu=1, finalout_name='ncpu_1.nc')
+        assert Path('tests/proc_test3/dump03/results/ncpu_1.nc').exists(), 'run_finalout with ncpu=1 not work'
+
+        proc.run_finaloutput(ncpu=4, finalout_name='ncpu_2.nc')
+        assert Path('tests/proc_test3/dump03/results/ncpu_2.nc').exists(), 'run_finalout with ncpu=2 not work'
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_rtfit_dumps.py b/tests/test_rtfit_dumps.py
index 637fe0e9..26943f73 100644
--- a/tests/test_rtfit_dumps.py
+++ b/tests/test_rtfit_dumps.py
@@ -143,7 +143,7 @@ def test_performfit(self):
 
             for key, val in old_results.items():
                 self.assertTrue(np.allclose(fit.res_dict[key],
-                                            old_results[key], atol=1e-4, rtol=1e-4),
+                                            old_results[key], atol=1e-3, rtol=1e-3),
                                 msg=f'fitted values for {msg} fit of {key} ' +
                                      f'differ by {np.subtract(fit.res_dict[key], old_results[key]).mean()}')
 
diff --git a/tests/test_rtmetrics.py b/tests/test_rtmetrics.py
new file mode 100644
index 00000000..831f34b0
--- /dev/null
+++ b/tests/test_rtmetrics.py
@@ -0,0 +1,127 @@
+import unittest
+from itertools import combinations
+
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+from rt1.rtmetrics import RTmetrics
+from rt1 import rtfits
+
+
+class TestRTMetrics(unittest.TestCase):
+
+    @staticmethod
+    def mock_series():
+        d1 = pd.Series([43.04, 20.55, 8.98, -15.27, 29.18, -12.48, 78.35,
+                        92.73, -23.31, 14.12])
+        d2 = pd.Series([11.52, 116.34, 60.88, 9.73, 35.51, 26.69, 119.53,
+                        -16.41, 25.05, -68.33])
+        results = {
+            'pearson': 0.13745925602259895,
+            'spearman': 0.10303030303030303,
+            'linregress': {
+                'slope': 0.2014594067161711,
+                'intercept': 27.29877405497224,
+                'pearson': 0.13745925602259895,
+                'pvalue': 0.7049253466715144,
+                'stderr': 0.513246861349088},
+            'rmsd': 61.48676800743392,
+            'ub_rmsd': 60.901701092826634,
+            'bias': -8.462000000000003,
+            'mae': 53.084,
+            'mape': 2.578235110085792,
+            'std_ratio': 0.682317387225608}
+        return (d1, d2, results)
+
+    @staticmethod
+    def mock_fit():
+        return rtfits.load('./tests/sig0_dB.dump')
+
+    def test_metrics(self):
+        d1, d2, expected_values = self.mock_series()
+        # loop through all possible metrics
+        for metric in RTmetrics.metrics_registry:
+            if metric == 'linregress':
+                continue
+
+            res = getattr(RTmetrics, metric)(d1, d2)
+            self.assertAlmostEqual(res, expected_values[metric])
+
+    def test_linregress(self):
+        d1, d2, expected_values = self.mock_series()
+        linregress = RTmetrics.linregress(d1, d2)
+        linregress_expected = expected_values['linregress']
+
+        self.assertAlmostEqual(linregress['slope'],
+                               linregress_expected['slope'])
+        self.assertAlmostEqual(linregress['intercept'],
+                               linregress_expected['intercept'])
+        self.assertAlmostEqual(linregress['pearson'],
+                               linregress_expected['pearson'])
+        self.assertAlmostEqual(linregress['pvalue'],
+                               linregress_expected['pvalue'])
+        self.assertAlmostEqual(linregress['stderr'],
+                               linregress_expected['stderr'])
+
+    def test_fit_metric(self):
+
+        fit = self.mock_fit()
+
+        # loop through all possible parameter combinations
+        for [p1, s1], [p2, s2] in combinations(
+                fit.metric._all_keys.items(), 2):
+
+            metric_fit_params = getattr(getattr(fit.metric, p1), p2)
+
+            # loop through all possible metrics
+            for metric in RTmetrics.metrics_registry:
+                fit_metric = getattr(metric_fit_params, metric)
+
+                # remove suffix if present
+                if p1.endswith(f'__{s1}'):
+                    p1 = p1[:-len(f'__{s1}')]
+                if p2.endswith(f'__{s2}'):
+                    p2 = p2[:-len(f'__{s2}')]
+
+                d1 = getattr(fit, s1)
+                if s1 == 'calc_model':
+                    d1 = getattr(d1(return_components=True), p1)
+                else:
+                    d1 = getattr(d1, p1)
+
+                d2 = getattr(fit, s2)
+                if s2 == 'calc_model':
+                    d2 = getattr(d2(return_components=True), p2)
+                else:
+                    d2 = getattr(d2, p2)
+
+                df = pd.concat([d1, d2], axis=1)
+                fit_func = getattr(RTmetrics, metric)(df[p1], df[p2])
+
+                assertmsg = ("there's been something wrong during metric-evaluation!\n"
+                             f'    p1="{p1}", p2="{p2}", metric="{metric}"\n' +
+                             f'    fit_metric={fit_metric}\n' +
+                             f'    fit_func={fit_func}\n')
+
+                if metric == 'linregress':
+                    for key, val in fit_metric.items():
+                        if np.isnan(val):
+                            assert np.isnan(fit_func[key]), assertmsg
+                        else:
+                            assert val == fit_func[key], assertmsg
+                else:
+                    if np.isnan(fit_metric):
+                        assert np.isnan(fit_func), assertmsg
+                    else:
+                        assert fit_metric == fit_func, assertmsg
+
+    def test_scatterplot(self):
+        plt.ion()
+        fit = self.mock_fit()
+        _ = RTmetrics.scatterplot(fit.res_df.SM, fit.res_df.omega,
+                                  "SM", "omega")
+
+
+if __name__ == "__main__":
+    unittest.main()