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

Issue:
parallel HDF5 export
### new
- HDF5 containers are now used instead of pickle-dumps to store `Fits` and `MultiFits` objects
- `RTprocess` is now directly appending results to an HDF5-container during parallel processing
- colored log-messages can be used (requires `colorama` package)
- there's now a proer container for `defdict`

### changes
- `processfunc` is now private (e.g. `_processfunc`) -> use `run_processing` instead!



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/Publish_to_PyPi.yml b/.github/workflows/Publish_to_PyPi.yml
index b6eefcc3..368ab47b 100644
--- a/.github/workflows/Publish_to_PyPi.yml
+++ b/.github/workflows/Publish_to_PyPi.yml
@@ -34,4 +34,4 @@ jobs:
           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
+          #TWINE_REPOSITORY: testpypi
diff --git a/.github/workflows/RT1_ubuntu.yml b/.github/workflows/RT1_ubuntu.yml
index b039a608..0f960c32 100644
--- a/.github/workflows/RT1_ubuntu.yml
+++ b/.github/workflows/RT1_ubuntu.yml
@@ -10,7 +10,7 @@ jobs:
         # set operating systems to test
         os: [ubuntu-latest]
         # set python versions to test
-        python-version: ['3.7']
+        python-version: ['3.7', '3.8', '3.9']
 
     name: RT1_test ${{ matrix.os }}  ${{ matrix.python-version }}
     steps:
@@ -36,8 +36,9 @@ jobs:
         run: |
           python -m pytest --cov=rt1 --cov-report=xml
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v3
         with:
+          token: ${{ secrets.CODECOV_TOKEN }}
           env_vars: ${{ matrix.os }}, ${{ matrix.python-version }}
           name: rt1_coverage
           fail_ci_if_error: true
diff --git a/.github/workflows/RT1_windows.yml b/.github/workflows/RT1_windows.yml
index 338ca18b..82acccf0 100644
--- a/.github/workflows/RT1_windows.yml
+++ b/.github/workflows/RT1_windows.yml
@@ -10,7 +10,7 @@ jobs:
         # set operating systems to test
         os: [windows-latest]
         # set python versions to test
-        python-version: ['3.7']
+        python-version: ['3.7', '3.8', '3.9']
 
     name: RT1_test ${{ matrix.os }}  ${{ matrix.python-version }}
     steps:
@@ -36,8 +36,9 @@ jobs:
         run: |
           python -m pytest --cov=rt1 --cov-report=xml
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v3
         with:
+          token: ${{ secrets.CODECOV_TOKEN }}
           env_vars: ${{ matrix.os }}, ${{ matrix.python-version }}
           name: rt1_coverage
           fail_ci_if_error: true
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 00000000..910273f0
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,15 @@
+name: pre-commit
+
+#on:
+#  pull_request:
+#  push:
+#    branches: [master, dev]
+on: [pull_request]
+
+jobs:
+  pre-commit:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - uses: actions/setup-python@v2
+    - uses: pre-commit/action@v2.0.3
diff --git a/.gitignore b/.gitignore
index ee0ace7a..eac124da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,4 @@ tests/testdump2.dump
 tests/proc_test
 tests/proc_test2
 tests/proc_test3
-tests/proc_multi
\ No newline at end of file
+tests/proc_multi
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..06153a4b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,14 @@
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+    -   id: check-ast
+    -   id: check-merge-conflict
+    -   id: end-of-file-fixer
+    -   id: trailing-whitespace
+        args: ["--markdown-linebreak-ext=md"]
+    -   id: mixed-line-ending
+-   repo: https://github.com/psf/black
+    rev: 23.3.0
+    hooks:
+    -   id: black
diff --git a/.stickler.yml b/.stickler.yml
index e8044e32..9f788cb9 100644
--- a/.stickler.yml
+++ b/.stickler.yml
@@ -14,5 +14,3 @@ review:
     # Don't use 'failure' build status when
     # a pull request contains style errors.
     fail_on_comments: false
-
-
diff --git a/.zenodo.json b/.zenodo.json
index b58c476d..67b02e8e 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -14,4 +14,4 @@
             "affiliation": "TU Wien Department of Geodesy and Geoinformation"
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/COPYRIGHT b/COPYRIGHT
index 51c0eaab..e0c04641 100644
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -1,2 +1,1 @@
 copyright by authors. For details regarding the license, see the file LICENSE
-
diff --git a/MANIFEST.in b/MANIFEST.in
index 8a8257a8..8704b9ee 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -9,4 +9,4 @@ prune doc
 prune tests
 
 prune _additional_and_old_scripts
-prune rt1/rtread
\ No newline at end of file
+prune rt1/rtread
diff --git a/NOTICE b/NOTICE
index a2273688..4a4881de 100644
--- a/NOTICE
+++ b/NOTICE
@@ -5,4 +5,4 @@ https://github.com/TUW-GEO/rt1
 Within publications, giving reference to the following original publications is obligatory:
 
 * Quast & Wagner (2016): doi:10.1364/AO.55.005379
-* Quast, Albergel, Calvet, Wagner (2019) : doi:10.3390/rs11030285
\ No newline at end of file
+* Quast, Albergel, Calvet, Wagner (2019) : doi:10.3390/rs11030285
diff --git a/README.md b/README.md
index ac3bee47..c29e63dd 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,7 @@ The documentation of the package is found [here](http://rt1.readthedocs.io/).
 
 ## Overview of the RT1 python-module structure
 
-![RT1_overview](https://user-images.githubusercontent.com/22773387/118656393-c4fbd200-b7ea-11eb-9ef9-913abb275e98.png)
-
+![RT1_overview](https://user-images.githubusercontent.com/22773387/126987504-eddbb998-02c3-4eb1-bf9c-20ade2c5fe07.png)
 
 - 🏞️ **rt1.volume** and **rt1.surface**
 
@@ -36,7 +35,7 @@ The documentation of the package is found [here](http://rt1.readthedocs.io/).
   - a flexible interface to perform parameter estimation from incidence-angle  
     dependent backscatter-timeseries via non-linear least-squares fitting routines.
 
-  - integrates with a set of pre-defined plot-functions and interactive   
+  - integrates with a set of pre-defined plot-functions and interactive  
     widgets that can be used to quickly analyze the obtained results
 
 - :rocket: **rt1.rtprocess**
@@ -72,4 +71,3 @@ optional dependencies `symengine` and `xarray` by using:
 ## References
 * Quast & Wagner (2016): [doi:10.1364/AO.55.005379](https://doi.org/10.1364/AO.55.005379)
 * Quast, Albergel, Calvet, Wagner (2019) : [doi:10.3390/rs11030285](https://doi.org/10.3390/rs11030285)
-
diff --git a/codecov.yml b/codecov.yml
index fb02c179..ca497b96 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -5,4 +5,4 @@ coverage:
         threshold: 100%
     patch:
       default:
-        threshold: 100%
\ No newline at end of file
+        threshold: 100%
diff --git a/doc/_static/my_theme.css b/doc/_static/my_theme.css
index 9689f17f..7d711bdd 100644
--- a/doc/_static/my_theme.css
+++ b/doc/_static/my_theme.css
@@ -2,4 +2,4 @@
 
 .wy-nav-content {
     max-width: none !important;
-}
\ No newline at end of file
+}
diff --git a/doc/conf.py b/doc/conf.py
index 1168dde2..7400dbfc 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -23,20 +23,34 @@
 # Enable equation-numbering
 math_number_all = True
 numpydoc_show_class_members = False
+
+
 # define a function that adds custom css stylesheet (used to override window-width restrictions)
 # basic idea taken from:  https://samnicholls.net/2016/06/15/how-to-sphinx-readthedocs/
 def setup(app):
-    app.add_stylesheet('my_theme.css')
+    app.add_stylesheet("my_theme.css")
 
 
 MOCK_MODULES = [
-'numpy', 'pandas',
-'scipy', 'scipy.special', 'scipy.optimize', 'scipy.linalg', 'scipy.sparse', 'scipy.interpolate',
-'sympy', 'sympy.abc',
-'symengine',
-'matplotlib', 'matplotlib.pyplot', 'matplotlib.colors', 'matplotlib.lines', 'matplotlib.widgets',
-'matplotlib.gridspec',
-'mpl_toolkits', 'mpl_toolkits.mplot3d',
+    "numpy",
+    "pandas",
+    "scipy",
+    "scipy.special",
+    "scipy.optimize",
+    "scipy.linalg",
+    "scipy.sparse",
+    "scipy.interpolate",
+    "sympy",
+    "sympy.abc",
+    "symengine",
+    "matplotlib",
+    "matplotlib.pyplot",
+    "matplotlib.colors",
+    "matplotlib.lines",
+    "matplotlib.widgets",
+    "matplotlib.gridspec",
+    "mpl_toolkits",
+    "mpl_toolkits.mplot3d",
 ]
 for mod_name in MOCK_MODULES:
     sys.modules[mod_name] = mock.Mock()
@@ -45,210 +59,216 @@ def setup(app):
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('..') + os.sep)
+sys.path.insert(0, os.path.abspath("..") + os.sep)
 
 # -- General configuration ------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.mathjax', 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'numpydoc',
-	'sphinx.ext.autosummary'
+    "sphinx.ext.mathjax",
+    "sphinx.ext.autodoc",
+    "sphinx.ext.napoleon",
+    "numpydoc",
+    "sphinx.ext.autosummary",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'Bistatic first order scattering model'
-copyright = u'2016, Raphael Quast, Alexander Loew'
+project = "Bistatic first order scattering model"
+copyright = "2016, Raphael Quast, Alexander Loew"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.1'
+version = "0.1"
 # The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = "0.1"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = ["_build"]
 
 # The reST default role (used for this markup: `text`) to use for all
 # documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
+# keep_warnings = False
 
 
 # -- Options for HTML output ----------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
+html_theme = "default"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
 # directly to the root of the documentation.
-#html_extra_path = []
+# html_extra_path = []
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'Bistaticfirstorderscatteringmodeldoc'
+htmlhelp_basename = "Bistaticfirstorderscatteringmodeldoc"
 
 
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+    # The paper size ('letterpaper' or 'a4paper').
+    # 'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    # 'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    # 'preamble': '',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-  ('index', 'Bistaticfirstorderscatteringmodel.tex', u'Bistatic first order scattering model Documentation',
-   u'Raphael Quast, Alexander Loew', 'manual'),
+    (
+        "index",
+        "Bistaticfirstorderscatteringmodel.tex",
+        "Bistatic first order scattering model Documentation",
+        "Raphael Quast, Alexander Loew",
+        "manual",
+    ),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output ---------------------------------------
@@ -256,12 +276,17 @@ def setup(app):
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'bistaticfirstorderscatteringmodel', u'Bistatic first order scattering model Documentation',
-     [u'Raphael Quast, Alexander Loew'], 1)
+    (
+        "index",
+        "bistaticfirstorderscatteringmodel",
+        "Bistatic first order scattering model Documentation",
+        ["Raphael Quast, Alexander Loew"],
+        1,
+    )
 ]
 
 # If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -270,19 +295,25 @@ def setup(app):
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'Bistaticfirstorderscatteringmodel', u'Bistatic first order scattering model Documentation',
-   u'Raphael Quast, Alexander Loew', 'Bistaticfirstorderscatteringmodel', 'One line description of project.',
-   'Miscellaneous'),
+    (
+        "index",
+        "Bistaticfirstorderscatteringmodel",
+        "Bistatic first order scattering model Documentation",
+        "Raphael Quast, Alexander Loew",
+        "Bistaticfirstorderscatteringmodel",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 # Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
 
 # If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
 
 # If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
+# texinfo_no_detailmenu = False
diff --git a/doc/examples.rst b/doc/examples.rst
index d04a3ca5..c4376380 100644
--- a/doc/examples.rst
+++ b/doc/examples.rst
@@ -9,4 +9,3 @@ A script that produces all examples is provided in the doc/examples directory. T
 * `Example 2 from Quast and Wagner (2016) <https://github.com/pygeo/rt1/tree/master/doc/examples/example02.ipynb>`_
 * `Example 3: usage of a linear combination for the phase function and BRDF <https://github.com/pygeo/rt1/tree/master/doc/examples/example_lin_comb.ipynb>`_
 * `Example 4: basic fitting example <https://github.com/pygeo/rt1/tree/master/doc/examples/example_fitting.ipynb>`_
-
diff --git a/doc/index.rst b/doc/index.rst
index 67390ae4..6a39dcfb 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -24,4 +24,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/doc/installation.rst b/doc/installation.rst
index efa29bf8..73914b27 100644
--- a/doc/installation.rst
+++ b/doc/installation.rst
@@ -27,4 +27,3 @@ You can also download the source code package from the `project website <https:/
     python setup.py install
 
 If might be that you might need administrator rights for this step, as the program tries to install into system library pathes. To install into a user specific directory please refer to the `pip` documentation.
-
diff --git a/doc/introduction.rst b/doc/introduction.rst
index ba14c2eb..d65a32c7 100644
--- a/doc/introduction.rst
+++ b/doc/introduction.rst
@@ -19,6 +19,3 @@ When you are using `RT1` as part of your publications, please give the developer
 
 * R.Quast and W.Wagner, `Analytical solution for first-order scattering in bistatic radiative transfer interaction problems of layered media`, Appl.Opt.55, 5379-5386 (2016)
 * R.Quast, C.Albergel, J.C.Calvet, W.Wagner, `A Generic First-Order Radiative Transfer Modelling Approach for the Inversion of Soil and Vegetation Parameters from Scatterometer Observations`,  doi:10.3390/rs11030285
-
-
-
diff --git a/doc/model_specification.rst b/doc/model_specification.rst
index 4b772d25..ea856c09 100644
--- a/doc/model_specification.rst
+++ b/doc/model_specification.rst
@@ -11,7 +11,7 @@ Evaluation Geometries
 
 From the general definition of the fn-coefficients :eq:`fn_coef_definition` it is apparent that they are in principle dependent on :math:`\theta_0,\phi_0,\theta_{ex}` and :math:`\phi_{ex}`.
 If the series-expansions (:eq:`brdf_expansion` and :eq:`p_expansion`) contain a high number of Legendre-polynomials, the resulting fn-coefficients turn out to be rather lengthy and moreover their evaluation might consume a lot of time.
-Since usually one is only interested in an evaluation with respect to a specific (a-priori known) geometry of the measurement-setup, the rt1-module incorporates a parameter that allows specifying the 
+Since usually one is only interested in an evaluation with respect to a specific (a-priori known) geometry of the measurement-setup, the rt1-module incorporates a parameter that allows specifying the
 geometry at which the results are being evaluated. This generally results in a considerable speedup for the fn-coefficient generation.
 
 The measurement-geometry is defined by the value of the :code:`geometry`-parameter of the RT1-class object:
@@ -132,8 +132,8 @@ Linear-combination of volume-scattering phase-functions is used to generate a co
    \hat{p}_{combined}(\theta_0,\phi_0,\theta_{ex},\phi_{ex}) = \sum_{n=0}^N  w_n * \hat{p}_n(\cos(\Theta_{a_n}))
    = \sum_{n=0}^N w_n * \sum_{k=0}^{K_n} \hat{P}_k(\cos(\Theta_{a_n})) * p^{(n)}_k
 
-where :math:`\hat{p}_n(\cos(\Theta_{a_n}))` denotes the scattering phase-functions to be combined, :math:`\cos(\Theta_{a_n})` denotes the individual scattering angles :eq:`general_scat_angle` used to define the 
-scattering phase-functions :math:`w_n` denotes the associated weighting-factors, :math:`p_k^{(n)}` denotes the :math:`\textrm{k}^{\textrm{th}}` Legendre-expansion-coefficient :eq:`p_expansion` of the 
+where :math:`\hat{p}_n(\cos(\Theta_{a_n}))` denotes the scattering phase-functions to be combined, :math:`\cos(\Theta_{a_n})` denotes the individual scattering angles :eq:`general_scat_angle` used to define the
+scattering phase-functions :math:`w_n` denotes the associated weighting-factors, :math:`p_k^{(n)}` denotes the :math:`\textrm{k}^{\textrm{th}}` Legendre-expansion-coefficient :eq:`p_expansion` of the
 :math:`\textrm{n}^{\textrm{th}}` phase-function and :math:`\hat{P}_k(x)` denotes the :math:`\textrm{k}^{\textrm{th}}` Legendre-polynomial.
 
 .. note::
@@ -194,8 +194,8 @@ Linear-combination of BRDF's is used to generate a combined surface-class elemen
    BRDF_{combined}(\theta_0,\phi_0,\theta_{ex},\phi_{ex}) = \sum_{n=0}^N  w_n * BRDF_n(\cos(\Theta_{a_n}))
    = \sum_{n=0}^N w_n * \sum_{k=0}^{K_n} \hat{P}_k(\cos(\Theta_{a_n})) * b^{(n)}_k
 
-where :math:`BRDF_n(\cos(\Theta_{a_n}))` denotes the BRDF's to be combined, :math:`\cos(\Theta_{a_n})` denotes the individual scattering angles :eq:`general_scat_angle` used to define the 
-BRDF's :math:`w_n` denotes the associated weighting-factors, :math:`b_k^{(n)}` denotes the :math:`\textrm{k}^{\textrm{th}}` Legendre-expansion-coefficient :eq:`brdf_expansion` of the 
+where :math:`BRDF_n(\cos(\Theta_{a_n}))` denotes the BRDF's to be combined, :math:`\cos(\Theta_{a_n})` denotes the individual scattering angles :eq:`general_scat_angle` used to define the
+BRDF's :math:`w_n` denotes the associated weighting-factors, :math:`b_k^{(n)}` denotes the :math:`\textrm{k}^{\textrm{th}}` Legendre-expansion-coefficient :eq:`brdf_expansion` of the
 :math:`\textrm{n}^{\textrm{th}}` BRDF and :math:`\hat{P}_k(x)` denotes the :math:`\textrm{k}^{\textrm{th}}` Legendre-polynomial.
 
 .. note::
@@ -208,7 +208,7 @@ BRDF's :math:`w_n` denotes the associated weighting-factors, :math:`b_k^{(n)}` d
    there is in principle no restriction on the weighting-factors for combination of BRDF's!
 
    It is however important to notice that the associated hemispherical reflectance :math:`R(\theta_0,\phi_0)` must always be lower or equal to 1.
-   In order to provide a simple tool that allows validating the above condition, the function :code:`RT1.Plots().hemreflect()` numerically evaluates 
+   In order to provide a simple tool that allows validating the above condition, the function :code:`RT1.Plots().hemreflect()` numerically evaluates
    the hemispherical reflectance using a simple Simpson-rule integration-scheme and generates a plot that displays :math:`R(\theta_0,\phi_0)`.
 
 
@@ -250,4 +250,4 @@ Current limitations
   For example the possibility of using delta-peaked scattering-distributions is contemplated for future versions of the code.
 
 + If the expansion-coefficients of the BRDF and volume-scattering phase-function exceed a certain number (approx. ncoefs > 20)
-  one might run into numerical precision errors.
\ No newline at end of file
+  one might run into numerical precision errors.
diff --git a/doc/numerical_evaluation.py b/doc/numerical_evaluation.py
index 1323f22f..4fa963a7 100644
--- a/doc/numerical_evaluation.py
+++ b/doc/numerical_evaluation.py
@@ -22,36 +22,71 @@
  http://stackoverflow.com/questions/20668689/integrating-2d-samples-on-a-rectangular-grid-using-scipy
 """
 
-# phase-function definitions in terms of spherical coordinate system angles (NOT zenith-angles)
-def HG(thetai,thetas,phii,phis,t):
-    return 1./(4.*np.pi)*(1.-t**2)/(1.+t**2 - 2.*t*(np.cos(thetai)*np.cos(thetas)+np.sin(thetai)*np.sin(thetas)*np.cos(phii-phis)))**(3./2.)
-
-def RAYLEIGH(thetai,thetas,phii,phis):
-    return (3./(16.*np.pi)*(1.+(np.cos(thetai)*np.cos(thetas)+np.sin(thetai)*np.sin(thetas)*np.cos(phii-phis))**2))
 
-def COSLOBE(thetai,thetas,phii,phis,t):
-    asdf=np.maximum((-np.cos(thetai)*np.cos(thetas)+np.sin(thetai)*np.sin(thetas)*np.cos(phii-phis))**t,0.)
+# phase-function definitions in terms of spherical coordinate system angles (NOT zenith-angles)
+def HG(thetai, thetas, phii, phis, t):
+    return (
+        1.0
+        / (4.0 * np.pi)
+        * (1.0 - t**2)
+        / (
+            1.0
+            + t**2
+            - 2.0
+            * t
+            * (
+                np.cos(thetai) * np.cos(thetas)
+                + np.sin(thetai) * np.sin(thetas) * np.cos(phii - phis)
+            )
+        )
+        ** (3.0 / 2.0)
+    )
+
+
+def RAYLEIGH(thetai, thetas, phii, phis):
+    return (
+        3.0
+        / (16.0 * np.pi)
+        * (
+            1.0
+            + (
+                np.cos(thetai) * np.cos(thetas)
+                + np.sin(thetai) * np.sin(thetas) * np.cos(phii - phis)
+            )
+            ** 2
+        )
+    )
+
+
+def COSLOBE(thetai, thetas, phii, phis, t):
+    asdf = np.maximum(
+        (
+            -np.cos(thetai) * np.cos(thetas)
+            + np.sin(thetai) * np.sin(thetas) * np.cos(phii - phis)
+        )
+        ** t,
+        0.0,
+    )
     return asdf
 
-            
+
 # parameters for example 1 and 2 of the paper:
-            
-CosP=5.                     # set the power of the cosine-lobe     
-HGt=0.7                     # set the asymmetry parameter of the Henyey Greenstein Function
-phii = 0.                   # set the incident direction to phi_i = 0.
+
+CosP = 5.0  # set the power of the cosine-lobe
+HGt = 0.7  # set the asymmetry parameter of the Henyey Greenstein Function
+phii = 0.0  # set the incident direction to phi_i = 0.
 
 omega = 0.3
 tau = 0.7
 
 
-
 # set incident directions for which the integral should be evaluated
 # notice that this is equal to the zenit-angle theta_0 since the arguments in the paper are theta_0 !
-inc = np.arange(5.,90.,10.)
+inc = np.arange(5.0, 90.0, 10.0)
 
-# define grid for integration 
-x=np.linspace(0.,np.pi/2.,3000)
-y=np.linspace(0.,2*np.pi,3000)
+# define grid for integration
+x = np.linspace(0.0, np.pi / 2.0, 3000)
+y = np.linspace(0.0, 2 * np.pi, 3000)
 
 # initialize array for solutions
 solCosRay = []
@@ -59,70 +94,95 @@ def COSLOBE(thetai,thetas,phii,phis,t):
 
 
 # ---- evaluation of first example
-print('start of evaluation of first example')
+print("start of evaluation of first example")
 for thetai in np.deg2rad(inc):
     # define the function that has to be integrated (i.e. Eq.20 in the paper)
     # notice the additional  np.sin(thetas)  which oritinates from integrating over theta_s instead of mu_s
     mu0 = np.cos(thetai)
-    def integfunkt(thetas,phis):
-        return np.array(np.sin(thetas)*2.*omega*np.exp(-tau/mu0)*mu0*np.cos(thetas)/(mu0-np.cos(thetas)) * (np.exp(-tau/mu0)-np.exp(-tau/np.cos(thetas))) *  RAYLEIGH(thetai,thetas,phii,phis)*COSLOBE(thetai,np.pi-thetas,np.pi,phis,CosP))
+
+    def integfunkt(thetas, phis):
+        return np.array(
+            np.sin(thetas)
+            * 2.0
+            * omega
+            * np.exp(-tau / mu0)
+            * mu0
+            * np.cos(thetas)
+            / (mu0 - np.cos(thetas))
+            * (np.exp(-tau / mu0) - np.exp(-tau / np.cos(thetas)))
+            * RAYLEIGH(thetai, thetas, phii, phis)
+            * COSLOBE(thetai, np.pi - thetas, np.pi, phis, CosP)
+        )
 
     # evaluate the integral using Simpson's Rule twice
-    z=integfunkt(x[:,None],y)
-    solCosRay = solCosRay + [simps(simps(z,y),x)]
+    z = integfunkt(x[:, None], y)
+    solCosRay = solCosRay + [simps(simps(z, y), x)]
 
 solCosRay = np.array(solCosRay)
 
 
-
 # ---- evaluation of second example
-print('start of evaluation of second example')
+print("start of evaluation of second example")
 for thetai in np.deg2rad(inc):
     # define the function that has to be integrated (i.e. Eq.20 in the paper)
     # notice the additional  np.sin(thetas)  which oritinates from integrating over theta_s instead of mu_s
     mu0 = np.cos(thetai)
-    def integfunkt(thetas,phis):
-        return np.array(np.sin(thetas)*2.*omega*np.exp(-tau/mu0)*mu0*np.cos(thetas)/(mu0-np.cos(thetas)) * (np.exp(-tau/mu0)-np.exp(-tau/np.cos(thetas))) *  HG(thetai,thetas,phii,phis,HGt)*COSLOBE(thetai,np.pi-thetas,np.pi,phis,CosP))
+
+    def integfunkt(thetas, phis):
+        return np.array(
+            np.sin(thetas)
+            * 2.0
+            * omega
+            * np.exp(-tau / mu0)
+            * mu0
+            * np.cos(thetas)
+            / (mu0 - np.cos(thetas))
+            * (np.exp(-tau / mu0) - np.exp(-tau / np.cos(thetas)))
+            * HG(thetai, thetas, phii, phis, HGt)
+            * COSLOBE(thetai, np.pi - thetas, np.pi, phis, CosP)
+        )
 
     # evaluate the integral using Simpson's Rule twice
-    z=integfunkt(x[:,None],y)
-    solCosHG = solCosHG + [simps(simps(z,y),x)]
+    z = integfunkt(x[:, None], y)
+    solCosHG = solCosHG + [simps(simps(z, y), x)]
 
 solCosHG = np.array(solCosHG)
 
 
-np.savetxt("../tests/example1_int.csv", [[i,j] for i,j in zip(inc,solCosRay)], delimiter = ",")
-np.savetxt("../tests/example2_int.csv", [[i,j] for i,j in zip(inc,solCosHG)], delimiter = ",")
-
-
+np.savetxt(
+    "../tests/example1_int.csv", [[i, j] for i, j in zip(inc, solCosRay)], delimiter=","
+)
+np.savetxt(
+    "../tests/example2_int.csv", [[i, j] for i, j in zip(inc, solCosHG)], delimiter=","
+)
 
 
 #  - - - - - - - - OPTIONAL GRAPHICAL EVALUATION OF TEST
 
 # ----- evaluation of model
-#from rt1.rt1 import RT1
-#from rt1.volume import Rayleigh
-#from rt1.volume import HenyeyGreenstein
-#from rt1.surface import CosineLobe
+# from rt1.rt1 import RT1
+# from rt1.volume import Rayleigh
+# from rt1.volume import HenyeyGreenstein
+# from rt1.surface import CosineLobe
 #
 #
 ## initialize output fields for faster processing
-#Itot = np.ones_like(inc)*np.nan
-#Isurf = np.ones_like(inc)*np.nan
-#Ivol = np.ones_like(inc)*np.nan
+# Itot = np.ones_like(inc)*np.nan
+# Isurf = np.ones_like(inc)*np.nan
+# Ivol = np.ones_like(inc)*np.nan
 #
-#Iint1 = np.ones_like(inc)*np.nan
-#Iint2 = np.ones_like(inc)*np.nan
+# Iint1 = np.ones_like(inc)*np.nan
+# Iint2 = np.ones_like(inc)*np.nan
 #
 #
 #
 ## ---- evaluation of first example
-#V = Rayleigh(tau=0.7, omega=0.3)
-#SRF = CosineLobe(ncoefs=10, i=5)
-#label = 'Example 1'
+# V = Rayleigh(tau=0.7, omega=0.3)
+# SRF = CosineLobe(ncoefs=10, i=5)
+# label = 'Example 1'
 #
-#fn = None
-#for i in xrange(len(inc)):
+# fn = None
+# for i in xrange(len(inc)):
 #    # set geometries
 #    mu_0 = np.cos(np.deg2rad(inc[i]))
 #    mu_ex = mu_0*1.
@@ -138,13 +198,13 @@ def integfunkt(thetas,phis):
 #
 #
 ## ---- evaluation of second example
-#V = HenyeyGreenstein(tau=0.7, omega=0.3, t=0.7, ncoefs=20)
-#SRF = CosineLobe(ncoefs=10, i=5)
-#label = 'Example 2'
+# V = HenyeyGreenstein(tau=0.7, omega=0.3, t=0.7, ncoefs=20)
+# SRF = CosineLobe(ncoefs=10, i=5)
+# label = 'Example 2'
 #
 #
-#fn = None
-#for i in xrange(len(inc)):
+# fn = None
+# for i in xrange(len(inc)):
 #    # set geometries
 #    mu_0 = np.cos(np.deg2rad(inc[i]))
 #    mu_ex = mu_0*1.
@@ -161,25 +221,19 @@ def integfunkt(thetas,phis):
 #
 #
 #
-#fig = plt.figure()
-#axnum = fig.add_subplot(1,1,1)
+# fig = plt.figure()
+# axnum = fig.add_subplot(1,1,1)
 #
 ## plot of numerical results
-#axnum.plot(inc,solCosRay, 'bo', label = "Example 1")
-#axnum.plot(inc,solCosHG, 'go', label = "Example 2")
+# axnum.plot(inc,solCosRay, 'bo', label = "Example 1")
+# axnum.plot(inc,solCosHG, 'go', label = "Example 2")
 #
-## plot of result from model calculation 
+## plot of result from model calculation
 ##  !!!!!! examples.py needs to be run first to assign inc and Iint
-#axnum.plot(inc,Iint1, label = "Example 1")
-#axnum.plot(inc,Iint2, label = "Example 2")
+# axnum.plot(inc,Iint1, label = "Example 1")
+# axnum.plot(inc,Iint2, label = "Example 2")
 #
 #
-#axnum.legend()
+# axnum.legend()
 #
-#plt.show()
-
-
-
-
-
-
+# plt.show()
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 66f88aae..a2954e31 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,1 +1,1 @@
-numpydoc
\ No newline at end of file
+numpydoc
diff --git a/doc/theory.rst b/doc/theory.rst
index b0e1e7f9..f3c28018 100644
--- a/doc/theory.rst
+++ b/doc/theory.rst
@@ -1,7 +1,7 @@
 Theory
 ======
 
-The RT1 module provides a method for calculating the scattered radiation from a 
+The RT1 module provides a method for calculating the scattered radiation from a
 uniformly illuminated rough surface covered by a homogeneous layer of tenuous media.
 The following sections are intended to give a general overview of the underlying theory of the
 RT1 module. A more general discussion on the derivation of the used results can be found in [QuWa16]_.
@@ -10,7 +10,7 @@ within the RT1-module are given in :ref:`cha_model_specification`.
 
 
 
-The utilized theoretical framework is based on applying the Radiative Transfer Equation (RTE) :eq:`RTE` to 
+The utilized theoretical framework is based on applying the Radiative Transfer Equation (RTE) :eq:`RTE` to
 the geometry shown in :numref:`problem_geometry`.
 
 .. math::
@@ -29,14 +29,14 @@ The individual variables are hereby defined as follows:
 - :math:`\hat{p}(\theta,\phi,\theta',\phi')` denotes the scattering phase-function of the constituents of the covering layer
 
 .. note::
-	To remain consistent with [QuWa16]_, the arguments of the functions :math:`\hat{p}(\theta,\phi,\theta',\phi')` and :math:`BRDF(\theta,\phi,\theta',\phi')` are defined as angles 
+	To remain consistent with [QuWa16]_, the arguments of the functions :math:`\hat{p}(\theta,\phi,\theta',\phi')` and :math:`BRDF(\theta,\phi,\theta',\phi')` are defined as angles
 	with respect to a spherical coordinate system in the following discussion. Within the RT1-module however, the functions are defined with respect to the associated zenith-angles!
- 
+
 	A relationship between the module-functions and the functions within the subsequent discussion is therefore given by:
- 
+
 	:code:`SRF.brdf(t_0,t_ex,p_0,p_ex)` :math:`\hat{=} ~BRDF(\pi - \theta_0, \phi_0, \theta_{ex},\phi_{ex}) \quad` and :math:`\mbox{}\quad` :code:`V.p(t_0,t_ex,p_0,p_ex)` :math:`\hat{=} ~\hat{p}(\pi - \theta_0, \phi_0, \theta_{ex},\phi_{ex})`
 
- 
+
 
 
 Problem Geometry and Boundary Conditions
@@ -73,7 +73,7 @@ The superscripts :math:`I^\pm` hereby indicate a separation between upwelling :m
 
 The additional specifications of the covering layer and the ground surface are summarized as follows:
 
-   
+
 Parameters used to describe the scattering properties of the covering layer
 ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
 
@@ -82,7 +82,7 @@ Parameters used to describe the scattering properties of the covering layer
 
 .. math::
    \hat{p}(\theta,\phi,\theta',\phi') \qquad \textrm{with} \qquad   \int\limits_0^{2\pi} ~ \int\limits_{0}^{\pi} \hat{p}(\theta,\phi,\theta',\phi') \sin(\theta') d\theta' d\phi' = 1
-   
+
 **Optical Depth:**
 
 .. math::
@@ -111,7 +111,7 @@ Parameters used to describe the scattering properties of the ground surface
 where :math:`R(\theta,\phi)` denotes the **Directional-Hemispherical Reflectance** of the ground surface.
 
 TBD: perhaps describe also normalization conditions for p and BRDF
-   
+
 First-order solution to the RTE
 --------------------------------
 
@@ -127,7 +127,7 @@ where the individual terms (representing the contributions to the scattered inte
 - :math:`I_{\textrm{volume}}`: radiation scattered once within the covering layer
 - :math:`I_{\textrm{interaction}}`: radiation scattered once by the ground surface and once within the covering layer
 - :math:`I_{svs}`: radiation scattered twice by the ground surface and once within the covering layer
-      (This contribution is assumed to be negligible due to the occurrence of second order surface-scattering) 
+      (This contribution is assumed to be negligible due to the occurrence of second order surface-scattering)
 
 After some algebraic manipulations the individual contributions are found to be given by (details can be found in [QuWa16]_):
 
@@ -151,17 +151,17 @@ After some algebraic manipulations the individual contributions are found to be
 Evaluation of the interaction-contribution
 -------------------------------------------
 
-In order to analytically evaluate the remaining integral appearing in the interaction-term, the BRDF and the scattering phase-function of the covering layer are approximated via a 
+In order to analytically evaluate the remaining integral appearing in the interaction-term, the BRDF and the scattering phase-function of the covering layer are approximated via a
 Legendre-series in a (possibly generalized) scattering angle of the form:
 
 .. math::
-   BRDF(\theta, \phi, \theta_{s}, \phi_{s}) = \sum_{n=0}^{N_b} b_n P_n(\cos(\Theta_{a_b})) 
+   BRDF(\theta, \phi, \theta_{s}, \phi_{s}) = \sum_{n=0}^{N_b} b_n P_n(\cos(\Theta_{a_b}))
    :label: brdf_expansion
 
 .. math::
-   \hat{p}(\theta, \phi, \theta_{s}, \phi_{s}) = \sum_{n=0}^{N_p} p_n P_n(\cos(\Theta_{a_p})) 
+   \hat{p}(\theta, \phi, \theta_{s}, \phi_{s}) = \sum_{n=0}^{N_p} p_n P_n(\cos(\Theta_{a_p}))
    :label: p_expansion
-   
+
 where :math:`P_n(x)` denotes the :math:`\textrm{n}^\textrm{th}` Legendre-polynomial and the generalized scattering angle :math:`\Theta_a` is defined via:
 
 .. math::
@@ -171,7 +171,7 @@ where :math:`P_n(x)` denotes the :math:`\textrm{n}^\textrm{th}` Legendre-polynom
 where :math:`\theta ,\phi` are the polar- and azimuth-angles of the incident radiation, :math:`\theta_{s}, \phi_{s}` are the polar- and azimuth-angles of the scattered radiation and :math:`a_1,a_2` and :math:`a_3`
 are constants that allow consideration of off-specular and anisotropic effects within the approximations.
 
- 
+
 Once the :math:`b_n` and :math:`p_n` coefficients are known, the method developed in [QuWa16]_ is used to analytically solve :math:`I_{\textrm{interaction}}`.
 
 This is done in two steps:
@@ -215,4 +215,4 @@ Second, :math:`I_{\textrm{interaction}}` is evaluated using the analytic solutio
 
 
 .. rubric:: References
-.. [QuWa16]  R.Quast and W.Wagner, "Analytical solution for first-order scattering in bistatic radiative transfer interaction problems of layered media," Appl.Opt.55, 5379-5386 (2016) 
+.. [QuWa16]  R.Quast and W.Wagner, "Analytical solution for first-order scattering in bistatic radiative transfer interaction problems of layered media," Appl.Opt.55, 5379-5386 (2016)
diff --git a/rt1/__init__.py b/rt1/__init__.py
index 4bc9df4c..a60877de 100644
--- a/rt1/__init__.py
+++ b/rt1/__init__.py
@@ -1,5 +1,13 @@
 """Import module for RT1 module"""
 
+try:
+    from colorama import Fore, Back, Style, init
+
+    init()
+    _colored = True
+except Exception:
+    _colored = False
+
 import logging
 import logging.handlers
 import sys
@@ -7,7 +15,7 @@
 import multiprocessing as mp
 from .general_functions import groupby_unsorted
 
-__version__ = "1.5.2"
+__version__ = "2.0"
 __author__ = "Raphael Quast"
 
 
@@ -53,7 +61,7 @@ def set_log_handler_level(level, name="rt1_consolehandler"):
     h.setLevel(level)
 
 
-def start_log_to_file(path, name="rt1_filehandler", level=logging.INFO):
+def start_log_to_file(path, name="rt1_filehandler", level=logging.DEBUG):
     """
     forward all log-messages to a file
     to close the file use the `stop_log_to_file()` method!
@@ -77,12 +85,10 @@ def start_log_to_file(path, name="rt1_filehandler", level=logging.INFO):
         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)
+        formatter = _get_logger_formatter(True, colored=False)
+        fh.setFormatter(formatter)
         fh.set_name(name)
         # initialize the file-handler with level 1 to get all infos
         fh.setLevel(level)
@@ -122,7 +128,9 @@ def stop_log_to_file(name="rt1_filehandler"):
 
 class WrappedFixedIndentingLog(logging.Formatter):
     def __init__(self, fmt=None, datefmt=None, style="%", indent=4):
-        super().__init__(fmt=fmt, datefmt=datefmt, style=style)
+        usefmt = "%(asctime)s - " + fmt
+
+        super().__init__(fmt=usefmt, datefmt=datefmt, style=style)
 
         self._indent = indent
 
@@ -130,12 +138,40 @@ def format(self, record):
         return indent(super().format(record), " " * self._indent).strip()
 
 
-def _get_logger_formatter(simple=True):
+class ColoredWrappedFixedIndentingLog(logging.Formatter):
+    def __init__(self, fmt=None, datefmt=None, style="%", indent=4):
+        super().__init__(fmt=fmt, datefmt=datefmt, style=style)
+
+        self._indent = indent
+
+        self.FORMATS = {
+            logging.DEBUG: Fore.CYAN + fmt + Style.RESET_ALL,
+            logging.INFO: Fore.GREEN + fmt + Style.RESET_ALL,
+            logging.WARNING: Fore.MAGENTA + fmt + Style.RESET_ALL,
+            logging.ERROR: Back.RED + fmt + Style.RESET_ALL,
+            logging.CRITICAL: Back.RED + fmt + Style.RESET_ALL,
+            _PROGRESS_LEVEL_NUM: Back.GREEN + fmt + Style.RESET_ALL,
+        }
+
+    def format(self, record):
+        log_fmt = (
+            Fore.LIGHTWHITE_EX
+            + "%(asctime)s"
+            + Style.RESET_ALL
+            + " - "
+            + self.FORMATS.get(record.levelno)
+        )
+        formatter = logging.Formatter(log_fmt, datefmt=self.datefmt)
+
+        return indent(formatter.format(record), " " * self._indent).strip()
+
 
+def _get_logger_formatter(simple=True, colored=False):
     if simple is False:
         logfmt = (
-            "%(asctime)s - "
-            + mp.current_process().name
+            # "%(asctime)s - "
+            # +
+            mp.current_process().name
             + ": "
             + "%(levelname)-8s ("
             + "%(filename)s:%(lineno)d - "
@@ -145,25 +181,31 @@ def _get_logger_formatter(simple=True):
         )
     else:
         logfmt = (
-            "%(asctime)s - "
-            +
+            # "%(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")
+    if colored:
+        formatter = ColoredWrappedFixedIndentingLog(
+            logfmt,
+            indent=51,
+            datefmt="%Y-%m-%d %H:%M:%S",
+        )
+    else:
+        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=21,
-    simple=True,
+    log_name="rt1", console_out=True, console_level=21, simple=True, colored=_colored
 ):
-
     logger = logging.getLogger(log_name)
 
     # check if the logger has a Handler, if yes, it's an already existing
@@ -172,7 +214,7 @@ def setup_logger(
         return logger
 
     logger.setLevel(logging.DEBUG)
-    formatter = _get_logger_formatter(simple)
+    formatter = _get_logger_formatter(simple, colored=colored)
 
     if console_out is True:
         # setup a console-handler that prints to sys.stdout
@@ -188,4 +230,4 @@ def setup_logger(
 
 
 # initialize a logger for rt1
-log = setup_logger(console_out=True, simple=True, log_name="rt1")
+log = setup_logger(console_out=True, simple=True, log_name="rt1", colored=_colored)
diff --git a/rt1/_containers.py b/rt1/_containers.py
new file mode 100644
index 00000000..58d0c37d
--- /dev/null
+++ b/rt1/_containers.py
@@ -0,0 +1,395 @@
+import copy
+
+
+class _var_container:
+    def __init__(self):
+        pass
+
+    def __getitem__(self, key):
+        return getattr(self, key)
+
+
+class _RT1_defdict:
+    def __init__(self):
+        self._variables = list()
+
+        self._vars = _var_container()
+
+    def __repr__(self):
+        return "\n".join(
+            [f"{name}: {repr(getattr(self.var, name))}" for name in self._variables]
+        )
+
+    def __getitem__(self, key):
+        return getattr(self.var, key)
+
+    def __setattr__(self, key, val):
+        if not key.startswith("_") and key not in self._variables:
+            # set the value of the corresponding property on item assignment
+            if isinstance(val, dict):
+                self.add_variable(key, **val)
+            elif isinstance(val, list):
+                self.add_variable(key, *val)
+            else:
+                raise AttributeError(
+                    "RT1 defdicts only support setting new variables"
+                    + " via lists or dicts!"
+                )
+        else:
+            object.__setattr__(self, key, val)
+
+    def __iter__(self):
+        return iter(self._variables)
+
+    @property
+    def var(self):
+        return self._vars
+
+    def add_variable(
+        self, name, fitQ=True, val=0.5, freq=None, minval=0.0, maxval=1.0, interpQ=False
+    ):
+        """
+        Add a new definition for a variable to the defdict.
+
+        Parameters
+        ----------
+        name : str
+            the name of the variable (as specified in set_V_SRF).
+        fitQ : bool, optional
+            indicator if the quantity should be fitted (True) or used as a constant
+            during the fit (False). The default is True.
+        val : float, optional
+            - If figQ is True:
+              - if float: The (constant) start-value for the fit.
+              - if "auxiliary": The start-values for the fit are evaluated via the
+              mean-values of each "dyn-group" of the dataset-column '<name>_start'.
+
+            - if fitQ is False:
+              - if float: The (constant) value that will be assigned to the variable.
+              - if "auxiliary": The corresponding column "<name>" of the provided
+              dataset will be used as values for the parameter.
+
+            The default is 0.5.
+        freq : str or None, optional
+            Only relevant if fitQ is True.
+
+            - if None, a constant value will be fitted
+            - if "manual", the DataFrame column "<name>_dyn" provided
+              in the dataset will be used to assign the temporal
+              variability within the fit
+            - if "index", a unique value will be fitted to each
+              unique index of the provided dataset
+            - if freq corresponds to a pandas offset-alias, it
+              will be used together with the dataset-index to
+              assign the temporal variability within the fit
+              (see http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases)
+            - if freq is an integer (N), the dataset will be grouped
+              such that each group contains N unique dataset-indexes
+              (in case an exact split is not possible, the split is
+               performed such that the groups are as similar as
+               possible)
+            - if "freq + manual" AND a dataset-column "key_dyn" is
+              provided, the the provided variability will be
+              superimposed onto the variability resulting
+              form the chosen offset-alias
+
+            The default is None.
+        minval, maxval : float, optional
+            Only relevant if fitQ is True.
+
+            The boundary-values used within the fit. The default is (0, 1).
+        interpQ : bool, optional
+            Indicator if the obtained values should be interpolated to the
+            dataset-timestamps via a quadratic interpolation function or if a
+            simple step-function should be used. (only affects parameters
+            where freq is not None). The default is False.
+        """
+
+        if name not in self._variables:
+            self._variables.append(name)
+        obj = _RT1_variable(
+            name=name,
+            fitQ=fitQ,
+            val=val,
+            freq=freq,
+            minval=minval,
+            maxval=maxval,
+            interpQ=interpQ,
+        )
+        object.__setattr__(
+            self.var,
+            name,
+            obj,
+        )
+
+    def remove_variable(self, name):
+        """
+        Remove a variable from the defdict.
+
+        Parameters
+        ----------
+        name : str
+            The name of the variable to remove.
+        """
+        if name in self._variables:
+            self._variables.pop(self._variables.index(name))
+        delattr(self.var, name)
+
+    @classmethod
+    def from_dict(cls, d, copy_vals=True):
+        """
+        Initialize a RT1-defdict object from a given dict.
+
+        Parameters
+        ----------
+        d : dict
+            a dict of the following shape:
+                {key : [fitQ, val, freq, ([minval], [maxval]), interpQ]}
+            or
+                {key : {fitQ=.., val=.., freq=.., minval=.., maxval=.., interpQ=..}}
+        copy_vals : bool
+            Indicator if a (deep)-copy of the values should be performed or not
+            The default is True
+
+        Returns
+        -------
+        defdict : _RT1_defdict
+            The RT1-defdict with all variables assigned according to the provided dict.
+
+        """
+
+        defdict = cls()
+        for key, val in d.items():
+            defdict.add_variable(key)
+            if isinstance(val, list):
+                defdict[key]._from_list(copy.deepcopy(val) if copy_vals else val)
+            elif isinstance(val, dict):
+                defdict[key]._from_dict(copy.deepcopy(val) if copy_vals else val)
+        return defdict
+
+    def to_dict(self, props="list"):
+        """
+        Convert the RT1-defdict to an ordinary dict
+
+        Parameters
+        ----------
+        props : str, optional
+            Indicator if lists or dicts should be used for the properties.
+            The default is "list".
+
+        Returns
+        -------
+        d : dict
+            A dict-representation of the definitions.
+
+        """
+        d = dict()
+        if props == "list":
+            for name in self._variables:
+                d[name] = getattr(self.var, name)._to_list()
+        elif props == "dict":
+            for name in self._variables:
+                d[name] = getattr(self.var, name)._to_dict()
+
+        return d
+
+    def items(self):
+        return self.to_dict().items()
+
+    def keys(self):
+        return [*self._variables]
+
+
+class _var:
+    def __init__(self, name, val):
+        self._name = name
+        self._val = val
+
+    def __repr__(self):
+        return f"{self._name}: {self._val}"
+
+    def __setitem__(self, val):
+        self._val = val
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def value(self):
+        return self._val
+
+    @value.setter
+    def value(self, val):
+        self._val = val
+
+
+class _RT1_variable:
+    _paramnames = ["fitQ", "val", "freq", "minval", "maxval", "interpQ"]
+
+    def __init__(
+        self,
+        name=None,
+        fitQ=None,
+        val=None,
+        freq=None,
+        minval=None,
+        maxval=None,
+        interpQ=None,
+    ):
+        self._name = _var("name", name)
+        self._fitQ = _var("fitQ", fitQ)
+        self._val = _var("val", val)
+        self._freq = _var("freq", freq)
+        self._minval = _var("minval", minval)
+        self._maxval = _var("maxval", maxval)
+        self._interpQ = _var("interpQ", interpQ)
+
+    def __repr__(self):
+        return str(self._props)
+
+    def __getitem__(self, key):
+        if isinstance(key, int):
+            return self._props[key]
+        else:
+            return object.__getattribute__(self, key).value
+
+    def __getattribute__(self, key):
+        if key in ["fitQ", "val", "freq", "minval", "maxval", "interpQ"]:
+            return object.__getattribute__(self, key).value
+        else:
+            return object.__getattribute__(self, key)
+
+    def __setitem__(self, key, val):
+        # set the value of the corresponding property on item assignment
+        if isinstance(key, int):
+            name = ["fitQ", "val", "freq", "mima", "interpQ"][key]
+            if name == "mima":
+                ([v0], [v1]) = val
+
+                object.__getattribute__(self, "minval").value = v0
+                object.__getattribute__(self, "maxval").value = v1
+
+            else:
+                object.__getattribute__(self, name).value = val
+        else:
+            object.__getattribute__(self, key).value = val
+
+    def __setattr__(self, key, val):
+        if key in ["fitQ", "val", "freq", "minval", "maxval", "interpQ"]:
+            # set the value of the corresponding property on item assignment
+            object.__getattribute__(self, key).value = val
+        else:
+            object.__setattr__(self, key, val)
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def fitQ(self):
+        return self._fitQ
+
+    @property
+    def val(self):
+        return self._val
+
+    @property
+    def freq(self):
+        return self._freq
+
+    @property
+    def minval(self):
+        return self._minval
+
+    @property
+    def maxval(self):
+        return self._maxval
+
+    @property
+    def interpQ(self):
+        return self._interpQ
+
+    @property
+    def _props(self):
+        return [
+            self.fitQ,
+            self.val,
+            self.freq,
+            ([self.minval], [self.maxval]),
+            self.interpQ,
+        ]
+
+    def _from_list(self, l):
+        """
+        Parse properties of a variable from a list
+
+        Parameters
+        ----------
+        l : list
+            A list of the specifications in the following shape:
+
+                [fitQ, val, freq, ([minval], [maxval]), interpQ]
+        """
+        assert isinstance(l, list), "The provided value must be a list!"
+        [
+            self.fitQ,
+            self.val,
+            self.freq,
+            [[self.minval], [self.maxval]],
+            self.interpQ,
+        ] = (
+            l + [None, None, None, [[None], [None]], None][len(l) :]
+        )
+
+    def _to_list(self):
+        """
+        Get a list of the properties
+
+        Returns
+        -------
+        list
+            [fitQ, val, freq, ([minval], [maxval]), interpQ]
+
+        """
+        return [
+            self.fitQ,
+            self.val,
+            self.freq,
+            ([self.minval], [self.maxval]),
+            self.interpQ,
+        ]
+
+    def _from_dict(self, d):
+        """
+        Parse properties of a variable from a dict
+
+        Parameters
+        ----------
+        d : dict
+            A dict of the specifications with the following keys:
+
+            - "fitQ", "val", "freq", "minval", "maxval", "interpQ"
+        """
+
+        for key, val in d.items():
+            setattr(self, key, val)
+
+    def _to_dict(self):
+        """
+        Get a dict of the properties
+
+        Returns
+        -------
+        list
+            A dict of the properties
+        """
+
+        return dict(
+            fitQ=self.fitQ.value,
+            val=self.val.value,
+            freq=self.freq.value,
+            minval=self.minval.value,
+            maxval=self.maxval.value,
+            interpQ=self.interpQ.value,
+        )
diff --git a/rt1/_rtprocess_writer.py b/rt1/_rtprocess_writer.py
new file mode 100644
index 00000000..bcd51097
--- /dev/null
+++ b/rt1/_rtprocess_writer.py
@@ -0,0 +1,788 @@
+import multiprocessing as mp
+import threading
+
+import ctypes
+from timeit import default_timer
+from datetime import timedelta
+import sys
+import pandas as pd
+
+from queue import Empty as QueueEmpty
+import os
+from pathlib import Path
+
+from contextlib import contextmanager
+from collections import defaultdict
+from itertools import count
+import signal
+
+from . import log
+from .rtresults import HDFaccessor
+from .general_functions import (
+    dt_to_hms,
+    update_progress,
+    groupby_unsorted,
+    isidentifier,
+    find_missing,
+)
+
+
+class RepeatTimer(threading.Thread):
+    """
+    A simple timer that executes a function after a given amount of time.
+    (as an asynchronous task)
+
+    to setup a scheduled execution of a given function use:
+
+        >>> r = RepeatTimer(1, lambda: print("hello"))
+        >>> r.start()
+
+    to stop the execution use:
+
+        >>> r.finished.set()
+    """
+
+    def __init__(self, interval, function, args=None, kwargs=None):
+        threading.Thread.__init__(self, name="RTprocess_print_thread")
+        self.interval = interval
+        self.function = function
+        self.args = args if args is not None else []
+        self.kwargs = kwargs if kwargs is not None else {}
+        self.finished = threading.Event()
+
+    def cancel(self):
+        """Stop the timer if it hasn't finished yet."""
+        self.finished.set()
+
+    def run(self):
+        while not self.finished.wait(self.interval):
+            self.function(*self.args, **self.kwargs)
+
+
+class RT1_processor(object):
+    def __init__(
+        self,
+        n_worker=1,
+        n_combiner=2,
+        n_writer=1,
+        dst_path=None,
+        HDF_kwargs=None,
+        write_chunk_size=1000,
+        out_cache_size=10,
+        min_itemsize=None,
+    ):
+        """
+        A class that takes care of combining results and writing them
+        to disk as a HDF-container.
+
+        Parameters
+        ----------
+        n_worker, n_combiner, n_writer: int
+            the number of workers, combiners and writers to use
+        dst_path : str
+            the path where the HDF-store will be saved
+        HDF_kwargs : dict, optional
+            a dict with keyword-arguments passed to the HDF-store initialization
+            - see `pandas.HDFstore` for details
+        write_chunk_size : int, optional
+            the chunk-size for writing results to dict.
+            e.g.: a set of N results is combined and the combined output
+             is written to disc to reduce io
+            The default is 1000.
+        out_cache_size : int, optional
+            the max. number of results cached for writing.
+            e.g.: the number of combined results scheduled to be written to
+            disc. once this treshold is reached, processing is throttled
+            to protect memory-overload...
+            The default is 10.
+        min_itemsize : dict
+            a dict with the minimum-number of characters used to save string-columns
+            within pytables.
+            for details, check:
+
+               - https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#storing-types
+               - https://github.com/PyTables/PyTables/issues/48
+
+            NOTE: variable-length strigns are not supported by pytables!
+                  -> in case variable lenght strings are encountered, you MUST specify
+                  min_itemsize to avoid truncating the strings!
+
+            the default is set to {"index": 99, "values": 99} which results in a
+            minimum length of 99 characters for any strings encountered in indexes
+            or values of the saved DataFrames
+        """
+        if min_itemsize is None:
+            self.min_itemsize = {"index": 99, "values": 99}
+        else:
+            self.min_itemsize = min_itemsize
+
+        signal.signal(signal.SIGINT, self.manual_shutdown)
+        signal.signal(signal.SIGTERM, self.manual_shutdown)
+
+        self.n_worker = n_worker
+        self.n_combiner = n_combiner
+        self.n_writer = n_writer
+
+        self.dst_path = dst_path
+
+        self.HDF_kwargs = dict(complevel=1, complib="blosc")
+        if HDF_kwargs is not None:
+            self.HDF_kwargs.update(HDF_kwargs)
+
+        self.write_chunk_size = write_chunk_size
+        self.out_cache_size = out_cache_size
+
+    def manual_shutdown(self, *args, **kwargs):
+        log.error("shutting down due to SIGTERM event")
+
+        signal.signal(signal.SIGINT, signal.SIG_IGN)
+        signal.signal(signal.SIGTERM, signal.SIG_IGN)
+        self.should_stop.set()
+        self._stop.set()
+        raise (SystemExit)
+
+    def _increase_cnt(self, err=False):
+        self.lock.acquire()
+        if err is False:
+            # increase the total counter
+            self.p_totcnt.value += 1
+            # increase the counter for successful fits
+            self.p_meancnt.value += 1
+        else:
+            # only increase the total counter
+            self.p_totcnt.value += 1
+        self.lock.release()
+
+    def print_progress(self):
+        # shut down the print-thread in case should_stop is set and the queue is empty
+        if (self.should_stop.is_set() and self.queue.empty()) or self._stop.is_set():
+            threading.current_thread().cancel()
+            print()  # print a newline
+
+        self.lock.acquire()
+        qsize = self.queue.qsize()
+        p_totcnt = self.p_totcnt.value + qsize
+        p_meancnt = self.p_meancnt.value + qsize
+        self.lock.release()
+
+        if self.p_meancnt.value == 0:
+            title = f"{'estimating time ...':<28}"
+            p_max = self.p_max.value if self.p_max.value else p_totcnt + 1
+            meantime = 0
+        else:
+            end = default_timer()
+            meantime = (end - self.p_start) / (p_meancnt)
+
+            if self.p_max.value is None:
+                p_max = p_totcnt + 1
+                remain = timedelta(seconds=meantime * 1000)
+                timesuffix = "for 1000 fits"
+            else:
+                p_max = self.p_max.value
+                remain = timedelta(seconds=meantime * (p_max - p_totcnt))
+                timesuffix = "remaining"
+
+            d, h, m, s = dt_to_hms(remain)
+            title = f"~ {d} {h:02}:{m:02}:{s:02} {timesuffix}"
+
+        msg = update_progress(
+            p_totcnt,
+            p_max,
+            title=title,
+            finalmsg=(
+                "finished! "
+                + f"({p_max} [{self.p_totcnt.value - self.p_meancnt.value}] fits)"
+            ),
+            progress2=p_totcnt - p_meancnt,
+        )
+
+        sys.stdout.write(
+            msg + f" Q={self.queue.qsize():03}" + f" O={self.out_queue.qsize():02}"
+        )
+        sys.stdout.flush()
+
+    def _combine_results(self, results):
+        outdict = dict()
+        for key, val in results.items():
+            if len(val) == 0:
+                continue
+            if isinstance(val[0], (pd.Series, pd.DataFrame)):
+                outdict[key] = pd.concat(val)
+            else:
+                outdict[key] = pd.Series(val)
+
+        self.out_queue.put(outdict)
+
+    def _combiner_process(self, init_func=None, init_args=None):
+        """
+        combine results before writing them to the HDF-store
+
+        any key starting with "const__" will be treated as a constant
+        property that is maintained for all configs of a MultiConfig object
+
+        """
+        if callable(init_func) and init_func is not None:
+            if init_args is None:
+                init_func()
+            else:
+                init_func(*init_args)
+
+        c = count()  # a counter to name threads
+        threads = []
+        while True:
+            if self.should_stop.is_set():
+                if not self.queue.empty():
+                    log.progress(
+                        "... combining remaining "
+                        + f"{self.queue.qsize()} processed results"
+                    )
+                else:
+                    log.progress("shutting down " + mp.current_process().name)
+                    break
+            if self._stop.is_set():
+                log.progress("shutting down " + mp.current_process().name)
+                break
+
+            try:
+                # caches for results
+                results = defaultdict(list)
+                # results counter
+                nres = 0
+
+                # append more results until cache-size is reached
+                while nres < self.write_chunk_size:
+                    try:
+                        # get values from the queue, timeout after 5 seconds
+                        val = self.queue.get(timeout=5)
+                        if val is None:
+                            log.debug("ignored a None in the queue")
+                            self._increase_cnt(err=True)
+                            continue
+                    except QueueEmpty:
+                        break
+
+                    for cfg, cfg_results in val.items():
+                        if cfg.startswith("const__"):
+                            results[cfg.lstrip("const__")].append(cfg_results)
+                        elif isinstance(cfg_results, dict):
+                            for cfg_key, cfg_val in cfg_results.items():
+                                results[cfg + "/" + cfg_key].append(cfg_val)
+                        else:
+                            results[cfg].append(cfg_results)
+
+                    nres += 1
+                    self._increase_cnt(err=False)
+
+                if len(results) > 0:
+                    t = threading.Thread(
+                        target=self._combine_results,
+                        args=(results,),
+                        name=f"{mp.current_process().name} - {next(c)}",
+                    )
+                    t.start()
+                    threads.append(t)
+
+            except Exception:
+                log.error("There was a problem combining results:", exc_info=True)
+
+        # wait for all threads to finish before exiting the combiner process
+        for n, t in enumerate(threads):
+            if t.is_alive():
+                log.progress(f"joining remaining thread... {t.name}")
+                t.join()
+
+    def _check_IDs(self, key="reader_arg", get_ID=None):
+        """
+        return a list of IDs that are not yet present in the hdf-container
+        located at "dst_path"
+
+        Parameters
+        ----------
+        key : str, optional
+            the key to use in the HDF-container. The default is "reader_arg".
+        get_ID : callable, optional
+            a custom callable that extracts the ID from the provided
+            IDs list.
+            The default is None in which case the following function is used:
+
+                >>> # extract filename from given path
+                >>> lambda i: i.split(os.sep)[-1].split(".")[0]
+
+        Returns
+        -------
+        new_IDs : set
+            a set containing the unique elements of "inp" whose IDs are not
+            already present in the output HDF container.
+
+        """
+
+        if Path(self.dst_path).exists():
+            log.progress("evaluating list of IDs to process...")
+            if get_ID is None:
+
+                def get_ID(i):
+                    ID = str(i["ID"]).split(os.sep)[-1].split(".")[0]
+                    if not isidentifier(str(ID)):
+                        return f"RT1_{ID}"
+                    else:
+                        return str(ID)
+
+            with HDFaccessor(self.dst_path) as fit_db:
+                # get all IDs that are already present in the HDF store
+                # found_IDs = store.select_column("reader_arg", "ID").values
+                # get a list of integers that have already been assigned to IDs
+                # found_ID_nums = store.select_column("reader_arg", "index").values
+
+                found_ID_nums = fit_db.IDs.index
+                found_IDs = fit_db.IDs.ID.values
+
+                # a list of bool's that indicate if the ID is already processed
+                process_Q = pd.Index(map(get_ID, self.arg_list)).isin(found_IDs)
+
+                # a counter that yields unique IDs that are not yet assigned
+                # in the HDF store
+                id_counter = find_missing(found_ID_nums)
+
+                # set the processing-args
+                # update ID to be a valid python-identifier
+                self.args_to_process = [
+                    {**arg, "_RT1_ID_num": next(id_counter)}
+                    for arg, q in zip(self.arg_list, process_Q)
+                    if not q
+                ]
+        else:
+            self.args_to_process = [
+                {**i, "_RT1_ID_num": n}
+                for i, n in zip(self.arg_list, range(len(self.arg_list)))
+            ]
+
+            log.info("no existing output-HDF file found...")
+            log.progress(f"processing all {len(self.args_to_process)} IDs!")
+            return
+        log.progress(
+            f"found {len(self.args_to_process)} missing and "
+            + f"{len(self.arg_list) - len(self.args_to_process)}"
+            + " existing IDs!"
+        )
+
+    def _save_file_process(self, init_func=None, init_args=None):
+        log_first_expected_rows = defaultdict(lambda: True)
+
+        if callable(init_func) and init_func is not None:
+            if init_args is None:
+                init_func()
+            else:
+                init_func(*init_args)
+
+        while True:
+            # continue until the out_queue is emptied, then break
+            if self.combiner_ready.is_set():
+                if not self.out_queue.empty():
+                    log.progress(
+                        f"writing remaining {self.out_queue.qsize()}"
+                        + " cached results to disk"
+                    )
+                else:
+                    log.progress("shutting down " + mp.current_process().name)
+                    break
+
+            if self._stop.is_set():
+                log.progress("shutting down " + mp.current_process().name)
+                break
+
+            # pause processing in case out_cache_size is reached
+            if self.out_queue.qsize() < self.out_cache_size:
+                self.should_wait.set()
+            elif self.should_wait.is_set():
+                self.should_wait.clear()
+                log.warning("\nqueue full... waiting")
+
+            # wait for results to appear in the out_queue
+            # timeout after 2 sec to check for break-condition
+            try:
+                out = self.out_queue.get(timeout=2)
+            except QueueEmpty:
+                continue
+
+            try:
+                self.write_lock.acquire()
+                with pd.HDFStore(self.dst_path, "a", **self.HDF_kwargs) as store:
+                    for key, val in out.items():
+                        # make sure the ID column is indexed for the reader_args
+                        if key == "reader_arg":
+                            data_columns = ["ID"]
+                        else:
+                            data_columns = []
+                        # don't index data-columns
+                        # don't index right away (will be done once all processes are
+                        # finished to maintain write-speed during processing)
+
+                        # evaluate the number of expected rows since it is
+                        # "highly recommended" to do this for very large datasets
+                        if self.p_max.value is None:
+                            expectedrows = None
+                        else:
+                            expectedrows = self.p_max.value * len(val)
+
+                        store.append(
+                            key,
+                            val,
+                            format="t",
+                            data_columns=data_columns,
+                            index=False,
+                            min_itemsize=self.min_itemsize,
+                            expectedrows=expectedrows,
+                        )
+
+                        if log_first_expected_rows[key]:
+                            log.debug(
+                                "expecting to get "
+                                + f"{self.p_max.value * len(val)} rows "
+                                + "in the HDF5-container for "
+                                + f"'{key}' based on {self.p_max.value}"
+                                + " results to process"
+                            )
+                            # don't log this multiple times
+                            # (even though it might change depending on
+                            #  the length of the dataframes to append)!
+                            log_first_expected_rows[key] = False
+
+                self.write_lock.release()
+            except Exception:
+                log.error("problem while writing data... exiting", exc_info=True)
+                self._stop.set()
+                self.should_wait.clear()
+                self.write_lock.release()
+
+    def _worker_process(self, *args):
+        self.should_wait.wait()
+
+        try:
+            if self.reader_func is not None:
+                # use provided reader-func...
+                data = self.reader_func(args[0])
+            else:
+                data = args[0]
+
+            if data is not None:
+                res = self.process_func(data, *args[1:])
+                self.queue.put(res)
+            else:
+                log.warning(f"loading {args} resulted in None... skipping")
+        except Exception:
+            # put None in the queue to indicate an erroneous fit
+            self.queue.put(None)
+
+            print()
+            log.error(f"problem while processing \n{args}", exc_info=True)
+
+    def _start_writer_process(self, init_func=None, init_args=None):
+        # define a process that writes the results to disc
+        procs = []
+        for i in range(self.n_writer):
+            ctx = mp.get_context("spawn")
+            writer = ctx.Process(
+                target=self._save_file_process,
+                args=(init_func, init_args),
+                name=f"RTprocess writer {i}",
+            )
+            procs.append(writer)
+            log.progress(f"starting {writer.name}")
+            writer.start()
+        return procs
+
+    def _start_combiner_process(self, init_func=None, init_args=None):
+        procs = []
+        for i in range(self.n_combiner):
+            # start thread to combine the results
+            ctx = mp.get_context("spawn")
+            t = ctx.Process(
+                target=self._combiner_process,
+                args=(init_func, init_args),
+                name=f"RTprocess combiner {i}",
+            )
+            procs.append(t)
+            # combiner_thread.setDaemon(True)
+            log.progress(f"starting {t.name}")
+            t.start()
+        return procs
+
+    def _setup_manager(self):
+        ctx = mp.get_context("spawn")
+        manager = ctx.Manager()
+
+        self.queue = manager.Queue()
+        self.out_queue = manager.Queue()
+
+        self.should_wait = manager.Event()
+        self.should_stop = manager.Event()
+        self._stop = manager.Event()
+
+        self.lock = manager.Lock()
+        self.write_lock = manager.Lock()
+
+        self.p_totcnt = manager.Value(ctypes.c_ulonglong, 0)
+        self.p_meancnt = manager.Value(ctypes.c_ulonglong, 0)
+
+        self.combiner_ready = manager.Event()
+
+        self.p_max = manager.Value(ctypes.c_ulonglong, None)
+
+        return manager
+
+    @classmethod
+    @contextmanager
+    def writer_pool(cls, init_func=None, init_args=None, *args, **kwargs):
+        try:
+            w = cls(*args, **kwargs)
+            manager = w._setup_manager()
+            writer = w._start_writer_process(init_func=init_func, init_args=init_args)
+            combiner = w._start_combiner_process(
+                init_func=init_func, init_args=init_args
+            )
+
+            yield w
+        finally:
+            w.stop(writer, combiner, manager)
+
+    def start_print_thread(self):
+        # start the timer for estimating the processing-time
+        self.p_start = default_timer()
+
+        print_thread = RepeatTimer(0.25, self.print_progress)
+        print_thread.start()
+        return print_thread
+
+    def run_starmap(
+        self,
+        arg_list=None,
+        process_func=None,
+        reader_func=None,
+        pool_kwargs=None,
+        starmap_args=None,
+        ID_getter=None,
+    ):
+        """
+
+
+        Parameters
+        ----------
+        arg_list : iterable, optional
+            list of arguments to be processed. if the HDF store already exists,
+            a check for existing IDs will be performed prior to processing and only
+            the non-existing IDs will be used!
+            The default is None.
+        process_func : callable, optional
+            the function to use for obtaining the results.
+            -> must return a dict of pandas.DataFrames !
+            The default is None.
+        reader_func : callable, optional
+            optional explicit specification of a reader-function.
+            If None, the arguments are directly forwarded to process_func
+            The default is None.
+        pool_kwargs : dict, optional
+            a dict of kwargs passed to the initialization of the multiprocessing.Pool.
+            The default is None.
+        starmap_args : iterable, optional
+            a list of additional arguments passed to the evaluation of
+            "process_func". The default is None.
+
+        Returns
+        -------
+        res : iterable
+            the final output of the process.
+
+        """
+
+        if pool_kwargs is None:
+            pool_kwargs = dict()
+        if starmap_args is None:
+            starmap_args = []
+
+        self.arg_list = arg_list
+        self.reader_func = reader_func
+        self.process_func = process_func
+
+        assert self.arg_list is not None, 'you must provide "arg_list"'
+
+        self._check_IDs(get_ID=ID_getter)
+        self.p_max.value = len(self.args_to_process)
+
+        if self.p_max.value == 0:
+            log.error("ALL IDs are already present in the HDF file!")
+            return
+
+        ctx = mp.get_context("spawn")
+        pool = ctx.Pool(self.n_worker, **pool_kwargs)
+        # with ctx.Pool(self.n_worker, **pool_kwargs) as pool:
+        try:
+            worker = pool.starmap_async(
+                self._worker_process,
+                zip(self.args_to_process, *starmap_args),
+            )
+
+            # do this after calling starmap_async to wait for the initializers
+            # to finish!
+            _ = self.start_print_thread()
+
+            res = worker.get()
+
+            print()  # print a newline
+            d, h, m, s = dt_to_hms(timedelta(seconds=default_timer() - self.p_start))
+            log.progress(
+                "finished processing "
+                + f"{len(self.args_to_process)} [{self.p_totcnt.value}] IDs!"
+                + f" ... it took {d} {h:02}:{m:02}:{s:02}"
+            )
+        finally:
+            pool.close()
+            pool.join()
+
+        return res
+
+    def stop(self, writer, combiner, manager):
+        # initialize shutdown
+        self.should_stop.set()
+        print()  # newline
+
+        # wait for cached results to be combined
+        for c in combiner:
+            c.join()
+        self.combiner_ready.set()
+
+        # wait for output-cache to be written to disc
+        for w in writer:
+            w.join()
+
+        # tell all processes to stop
+        self._stop.set()
+
+        manager.shutdown()
+
+        if hasattr(self, "p_start"):
+            d, h, m, s = dt_to_hms(timedelta(seconds=default_timer() - self.p_start))
+            log.progress(f"finished! ... it took {d} {h:02}:{m:02}:{s:02}")
+        else:
+            log.progress("exiting...")
+
+    @staticmethod
+    def create_index(dst_path, idx_levels=None, keys=None, kind="full", optlevel=6):
+        """
+        create an index for the given HDF-store to speed up querying.
+        (the index is stored to disk, so this only needs to be executed once!)
+
+        NOTICE: this can take quite some time for large datasets and
+                complex indexes (e.g. MultiIndex etc.) !
+
+        Parameters
+        ----------
+        dst_path : str or Path
+            the path to the HDF-store.
+        idx_levels : iterable, "all" or None, optional
+            if iterable: a list of index-levels to use when creating the index.
+                         only levels actually present in a dataset are
+                         considered! multiindexes are also possible,
+                         e.g.: (["ID", "date"])
+            if "all": all available index-levels found in the dataset are used
+            if None: only the first index-level found in the dataset is used
+            The default is None.
+        keys : iterable, optional
+            the keys to create an index for.
+            The default is None, in which case all keys are used!.
+        optlevel : int or None, default None
+            passed to "pandas.io.pytables.---FrameTable.create_index()"
+            Optimization level [1-9], if None, pytables defaults to 6.
+
+        kind : str or None, default None
+            passed to "pandas.io.pytables.---FrameTable.create_index()"
+            Kind of index, if None, pytables defaults to "medium".
+            can be one of ["ultralight", "light", "medium", "full"]
+
+        """
+        with pd.HDFStore(dst_path, "a") as store:
+            use_keys = keys if keys else store.keys()
+            use_keys = [key for key in use_keys if not key.endswith("/meta")]
+            log.progress(
+                f"attempting to create '{kind}' indexes with "
+                + f"optlevel {optlevel} for the columns:\n"
+                + "    \n".join(use_keys)
+            )
+            for key in use_keys:
+                s = store.get_storer(key)
+                s_levels = s.levels
+
+                if idx_levels is None:
+                    try:
+                        # if s_levels is iterable and idx_cols is None,
+                        # use the first level
+                        iter(s_levels)
+                        use_levels = [s.levels[0]]
+                    except TypeError:
+                        # else use whatever the level-identification available
+                        use_levels = [s.levels]
+
+                elif idx_levels == "all":
+                    try:
+                        # if s_levels is iterable and idx_cols is None,
+                        # use all available levels
+                        iter(s_levels)
+                        use_levels = s.levels
+                    except TypeError:
+                        # else use whatever the level-identification available
+                        use_levels = s.levels
+
+                else:
+                    use_levels = list(set(idx_levels) & set(s_levels))
+
+                if len(use_levels) > 0:
+                    log.progress(f'creating index-levels "{use_levels}" for "{key}"')
+                    s.create_index(
+                        list(map(str, use_levels)),
+                        kind=kind,
+                        optlevel=optlevel,
+                    )
+                else:
+                    log.warning(f'no index-level found for "{key}"... skipping')
+        log.progress("finished index-creation")
+
+    @staticmethod
+    def get_data(dst_path, key, config=None, **kwargs):
+        """
+        read data from the associated HDF file
+
+        Parameters
+        ----------
+        key : str
+            the key to use.
+        config : str, optional
+            the config to use.
+            The default is None, in which case "constant" keys are accessed
+        **kwargs :
+            kwargs passed to `pd.HDFStore.select()`.
+            (e.g. where, start, stop, columns, iterator, chunksize)
+        Returns
+        -------
+        res : pd.DataFrame
+            the extracted dataset.
+
+        """
+        with pd.HDFStore(dst_path, "r") as store:
+            keysplits = [key.lstrip("/").split("/") for key in store]
+            cfg_keys = (i for i in keysplits if len(i) > 1)
+            cfg_keys = groupby_unsorted(
+                cfg_keys, key=lambda x: x[0], get=lambda x: x[1], sort=True
+            )
+            const_keys = [i[0] for i in keysplits if len(i) == 1]
+
+            if config is None:
+                assert key in const_keys, (
+                    f"'{key}' not found in HDF-file\n"
+                    + "... available constant layers are"
+                    + f"\n    {const_keys}\n"
+                    + "... available config layers are \n    "
+                    + "\n    ".join(f"{key}: {val}" for key, val in cfg_keys.items())
+                )
+
+            usekey = f"{config}/{key}"
+            res = store.select(usekey, **kwargs)
+        return res
diff --git a/rt1/general_functions.py b/rt1/general_functions.py
index acd7f288..d8fd94a6 100644
--- a/rt1/general_functions.py
+++ b/rt1/general_functions.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 """helper functions that are used both in rtfits and rtplots"""
-from itertools import tee, islice
+from itertools import tee, islice, count, chain
 from collections import OrderedDict
+import keyword
 
 try:
     import numpy as np
@@ -9,6 +10,24 @@
     pass
 
 
+def isidentifier(ident: str) -> bool:
+    """
+    Determines if string is valid Python identifier.
+    taken from: https://stackoverflow.com/a/29586366/9703451
+    """
+
+    if not isinstance(ident, str):
+        raise TypeError("expected str, but got {!r}".format(type(ident)))
+
+    if not ident.isidentifier():
+        return False
+
+    if keyword.iskeyword(ident):
+        return False
+
+    return True
+
+
 def rectangularize(array, return_mask=False, dim=None, return_masked=False, dtype=None):
     """
     return a rectangularized version of the input-array by repeating the
@@ -224,7 +243,7 @@ def update_progress(
     adapted from: https://blender.stackexchange.com/a/30739
     """
 
-    length = 25  # the length of the progress bar
+    length = 20  # the length of the progress bar
     block = int(round(length * progress / max_prog))
     if progress2 is not None:
         msg = (
@@ -330,3 +349,76 @@ def interpolate_to_index(data, index, data_index=None, **interp1d_kwargs):
         f = interp1d(data_index.to_julian_date(), data.values, **kwargs)
         x = f(index.to_julian_date())
         return Series(x, index)
+
+
+def find_missing(lst=[], N=None):
+    """
+    return a generator that yields sorted unique integers that are NOT present
+    in the provided "lst" list.
+
+    returned numbers will "fill up" missing values in the sorted "lst",
+    starting at 0:
+
+        >>> list(find_missing([2, 5, 6, 34, 68], 5))
+        >>> [0, 1, 3, 4, 7]
+
+    if the max. of the provided list is smaller than "N" the list is
+    extended to obtain N unique numbers, e.g.:
+
+        >>> list(find_missing([0, 1, 2], 5))
+        >>> [3, 4, 5, 6, 7]
+
+    Parameters
+    ----------
+    lst : iterable, optional
+        an iterable with numbers that should be excluded in the list.
+        The default is [].
+    N : int, optional
+        If N is provided, the generator will be exhausted after N values have
+        been returned. (otherwise it will continue yielding results forever!)
+    Returns
+    -------
+    missingIDs : list
+        a list of N unique integers that are not present in "lst".
+
+    """
+    start = 0
+    # get the max. ID defined in "lst"
+    maxID = max(lst) if len(lst) > 0 else 0
+
+    if len(lst) > 0:
+        # check for IDs that can be used to "fill-up" the provided list
+        missingIDs = sorted(set(range(start, maxID)) - set(lst))
+    else:
+        missingIDs = []
+
+    # init a counter that yields numbers larger than the max ID found in "lst"
+    c = count(maxID + 1)
+
+    if N is not None:
+        return islice(chain(missingIDs, c), N)
+    else:
+        return chain(missingIDs, c)
+
+
+def chunkit(arr, chunk_size=10000):
+    """
+    split a array-like object into chunks of a pre-defined size
+
+    Parameters
+    ----------
+    arr : array-like
+        the data to split.
+    chunk_size : int, optional
+        the chunk-size. The default is 10000.
+
+    Yields
+    ------
+    array-like
+        the chunks of the input-array.
+    """
+    num_chunks = len(arr) // chunk_size
+    if len(arr) % chunk_size != 0:
+        num_chunks += 1
+    for i in range(num_chunks):
+        yield arr[i * chunk_size : (i + 1) * chunk_size]
diff --git a/rt1/processing_config.py b/rt1/processing_config.py
index c1e5a0dc..91ff7d8d 100644
--- a/rt1/processing_config.py
+++ b/rt1/processing_config.py
@@ -1,185 +1,11 @@
 """convenient default functions for use with rt1.rtfits.processfunc()"""
 
 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[2] == "manual":
-                    parameter_specs["fitted_dynamic_manual"].append(key)
-                elif val[2] == "index":
-                    parameter_specs["fitted_dynamic_index"].append(key)
-                elif isinstance(val[2], str):
-                    parameter_specs["fitted_dynamic_datetime"].append((key, val[2]))
-                elif isinstance(val[2], int):
-                    parameter_specs["fitted_dynamic_integer"].append((key, val[2]))
-        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):
     """
@@ -194,10 +20,6 @@ class rt1_processing_config(object):
     dumpfolder : str, optional
         the sub-folder in which the dump-files will be stored.
         The default is None.
-    error_dumpfolder : srt, optional
-        the sub-folder in which the error-dump-files will be stored.
-        if None and dumpfolder is provided, dumpfolder will be used!
-        The default is None.
     finalout_name : srt, optional
         the name of the hdf-file generated by finaloutput().
         The default is None.
@@ -231,17 +53,6 @@ class rt1_processing_config(object):
     ...         ...
     ...         return df, aux_data
     ...
-    ...     # customize the postprocess function
-    ...     def postprocess(self, fit, reader_arg):
-    ...         # do something with each fit and return the desired output
-    ...         return ...
-    ...
-    ...     # customize the finaloutput function
-    ...     def finaloutput(self, res):
-    ...         # do something with res
-    ...         # (res is a list of outputs from the postprocess() function)
-    ...         ...
-    ...
     ...     # customize the function that is used to catch exceptions
     ...     def exceptfunc(self, ex, reader_arg):
     ...         # do something with the catched exception ex.args:
@@ -251,7 +62,6 @@ 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"]
 
@@ -266,7 +76,7 @@ def __init__(self, **kwargs):
         # append all arguments passed as kwargs to the class
         # NOTICE: ALL definitions in the 'PROCESS_SPECS' section of the
         #         config-file will be passed as kwargs to the initialization
-        #          of this class!)
+        #         of this class!)
         for key, val in kwargs.items():
             setattr(self, key, val)
 
@@ -274,7 +84,7 @@ def get_names_ids(self, reader_arg):
         """
         A function that returns the file-name based on the passed reader_args
 
-        - the filenames are generated from the reader-argument `'gpi'`
+        - by default, the filenames are generated from the reader-argument "ID"
 
         Parameters
         ----------
@@ -283,13 +93,14 @@ def get_names_ids(self, reader_arg):
 
         Returns
         -------
+        feature_id : str
+            the ID that will be assigned to the fits-object (e.g. `fit.ID`)
         filename : str
-            the file-name that will be used to save the fit dump-files in case
-            the processing was successful
+            the file-name that will be used to save the pickle dump-files
         """
 
         # the ID used for indexing the processed sites
-        feature_id = reader_arg["gpi"]
+        feature_id = str(reader_arg["ID"])
 
         # the filename of the dump-file
         filename = f"{feature_id}.dump"
@@ -299,151 +110,66 @@ def get_names_ids(self, reader_arg):
             filename=filename,
         )
 
-    def check_dump_exists(self, reader_arg):
-        """
-        check if a dump of the fit already exists
-        (used to determine if the fit has already been evaluated to avoid
-         performing the same fit twice)
-
-        Parameters
-        ----------
-        reader_arg : dict
-            the reader-arg dict.
-
-        Raises
-        ------
-        rt1_file_already_exists
-            If a dump-file with the specified filename already exists
-            at "save_path / dumpfolder / dumps /"
-        """
-
-        # 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")
-
-    def dump_fit_to_file(self, fit, reader_arg, mini=True):
+    def preprocess(self, **kwargs):
         """
-        pickle to Fits-object to  "save_path / dumpfolder / filename.dump"
+        a (optional) function that is called PRIOR to processing
 
         Parameters
         ----------
-        fit : rt1.rtfits.Fits
-            the rtfits.Fits object.
-        reader_arg : dict
-            the reader-arg dict.
-        mini : bool, optional
-            indicator if a mini-dump should be performed or not.
-            (see rt1.rtfits.Fits.dump() for details)
-            The default is True.
+        kwargs :
+            kwargs obtained from
+
+            >>> RTprocess(...).run_processing(preprocess_kwargs=dict(...))
+
+        Returns:
+        --------
+        dict
+            a dict with the following keys defined:
+
+                - "reader_args" : a list of dicts that will be used as input
+                  for the call to the reader function (e.g. `reader_arg`)
+                - "pool_kwargs" : a dict of kwargs passed to the initialization
+                  of the multiprocessing.Pool used for processing
+                  (useful for providing initializers etc.)
+        --------
         """
-        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,
-                )
 
-    def preprocess(self, **kwargs):
-        """a function that is called PRIOR to processing"""
         return
 
     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 = [0.1, 0.2, 0.3, 0.4, 0.5]
-        # get the sig0-values of the data
-        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")
-
-        data = pd.DataFrame(dict(inc=inc, sig=sig), index=index)
-
-        aux_data = pd.DataFrame(dict(something=[1, 2, 3, 4, 5]))
-
-        return data, aux_data
-
-    def postprocess(self, fit, reader_arg=None):
         """
-        A function that is called AFTER processing of each site:
+        a function that is called for each site to obtain the dataset
 
-        - a pandas.DataFrame with the obtained parameters is returned.
-          The columns are multiindexes corresponding to::
+        it must return a pandas.DataFrame with the following keys defined:
+            - "inc" : the incidence-angle in radians
+            - "sig" : the backscattering coefficient values
 
-              columns = [feature_id,
-                         [param_1, param_2, ...]]
-
-        - in case "save_path" is provided:
-            - a dump of the fit object will be stored in the folder
-            - if the file already exists, a 'rt1_file_already_exists' error
-              will be raised
+        any additional return-values will be appended to the fit as `fit.aux_data`
 
+        >>> def reader(self, reader_arg):
+        >>>    data = pd.DataFrame(inc=[...]
+        >>>                        sig=[...],
+        >>>                        index = [a datetime-index])
+        >>>
+        >>>    aux_data = "any aux-data that should be appended"
+        >>>
+        >>>    return data, aux_data
 
         Parameters
         ----------
-        fit: rt1.rtfits.Fits object
-            The fits object.
-        reader_arg: dict
+        reader_arg : dict
             the arguments passed to the reader function.
 
         Returns
         -------
-        df: pandas.DataFrame
-            a pandas dataframe containing the fitted parameterss.
-
-        """
-        if reader_arg is None:
-            reader_arg = 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)
-
-        # add all constant (fitted) parameters as static layers
-        staticlayers = dict()
-        for key in params["fitted_const"]:
-            staticlayers[key] = fit.res_dict[key][0]
-
-        ret = postprocess_xarray(
-            fit=fit,
-            saveparams=["inc", "sig", *params["fitted_dynamic"]],
-            xindex=("ID", names_ids["feature_id"]),
-            staticlayers=staticlayers,
-        )
-
-        return ret
-
-    def finaloutput(self, res):
-        """
-        A function that is called after ALL sites are processed:
-
-        Parameters
-        ----------
-        res: list
-            A list of return-values from the "postprocess()" function.
-
-        Returns
-        -------
-        res: xarray.Dataset
-             the concatenated results
+        return_data : pandas.DataFrame
+            a pandas.DataFrame that will be used as "dataset" for the fit
+            (e.g. `fit.dataset`)
+        *aux_data :
+            any additional return-arguments will be attached to the fits-object
+            as `fit.aux_data`
         """
-
-        # concatenate the results
-        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:
-            log.info(
-                "both save_path and finalout_name must be specified... "
-                + "otherwise the final results can NOT be saved!"
-            )
-            return resxar
-        else:
-            # export netcdf file
-            resxar.to_netcdf(self.rt1_procsesing_respath / self.finalout_name)
+        assert False, "you must define a proper reader-function first!"
 
     def exceptfunc(self, ex, reader_arg):
         """
@@ -455,10 +181,10 @@ def exceptfunc(self, ex, reader_arg):
         - 'rt1_data_error'
             exceptions are ignored and the next site is processed
         - 'rt1_file_already_exists'
-            the already existing dump-file is loaded, the postprocess()
-            function is applied and the result is returned
+            the already existing dump-file is loaded, and the dict defining
+            the Fits-object is returned
         - for any other exceptions:
-            if `save_path` and `dumpfolder`or `error_dumpfolder` are specified
+            if `save_path` and `dumpfolder` are specified
             a dump of the exception-message is saved and the exception
             is ignored. otherwise the exception will be raised.
 
@@ -486,7 +212,7 @@ def exceptfunc(self, ex, reader_arg):
             raise_exception = False
 
         elif "rt1_file_already_exists" in ex.args:
-            # if the fit-dump file already exists, try loading the existing
+            # if a fit-dump file already exists, try loading the existing
             # file and apply post-processing (e.g. avoid re-processing results)
             raise_exception = False
 
@@ -497,7 +223,7 @@ def exceptfunc(self, ex, reader_arg):
 
             try:
                 fit = load(self.rt1_procsesing_dumppath / names_ids["filename"])
-                return self.postprocess(fit, reader_arg)
+                return fit._get_fit_to_hdf_dict()
             except Exception:
                 log.error(
                     "there has been a problem while loading the "
@@ -509,9 +235,15 @@ def exceptfunc(self, ex, reader_arg):
             # raised if there was a problem with the data, ignore and continue
             raise_exception = False
 
-            log.error(f"there was a DATA-problem for '{names_ids['filename']}'")
+            log.error(
+                f"there was a DATA-problem for '{names_ids['filename']}'"
+                + "\n"
+                + str([i for i in ex.args if i != "rt1_data_error"])
+            )
         else:
-            log.error(traceback.format_exc())
+            log.error(
+                f"something went wrong for: {reader_arg}\n" + traceback.format_exc()
+            )
 
         # in case `save_path` is specified, write ALL exceptions to a file
         # and continue processing WITHOUT raising the exception.
@@ -527,3 +259,90 @@ def exceptfunc(self, ex, reader_arg):
 
         # flush stdout to see output of child-processes
         sys.stdout.flush()
+
+    def mask_existing_HDF_IDs(self, arg_list, key="reader_arg", get_ID=None):
+        """
+        check if the IDs provided in "arg_list" are already present in the "fit_db.h5"
+        container and return a list that contains only the missing entries of "arg_list".
+
+        Parameters
+        ----------
+        key : str, optional
+            the key to use in the HDF-container. The default is "reader_arg".
+        get_ID : callable, optional
+            a custom callable that extracts the ID from the provided
+            IDs list.
+            The default is None in which case the following function is used:
+
+                >>> # extract filename from given path
+                >>> def get_ID(i):
+                >>>     ID = str(i["ID"]).split(os.sep)[-1].split(".")[0]
+                >>>     if not isidentifier(str(ID)):
+                >>>         return f"RT1_{ID}"
+                >>>     else:
+                >>>         return str(ID)
+
+        Returns
+        -------
+        new_IDs : set
+            a set containing the unique elements of "inp" whose IDs are not
+            already present in the output HDF container.
+
+        """
+        import os
+        from .general_functions import isidentifier, find_missing
+        from .rtresults import HDFaccessor
+        import pandas as pd
+
+        dst_path = self.rt1_procsesing_respath / "fit_db.h5"
+
+        if Path(dst_path).exists():
+            log.progress("Checking for already existing IDs in the `fit_db.h5`...")
+            if get_ID is None:
+
+                def get_ID(i):
+                    ID = str(i["ID"]).split(os.sep)[-1].split(".")[0]
+                    if not isidentifier(str(ID)):
+                        return f"RT1_{ID}"
+                    else:
+                        return str(ID)
+
+            with HDFaccessor(dst_path) as fit_db:
+                # get all IDs that are already present in the HDF store
+                # found_IDs = store.select_column("reader_arg", "ID").values
+                # get a list of integers that have already been assigned to IDs
+                # found_ID_nums = store.select_column("reader_arg", "index").values
+
+                found_ID_nums = fit_db.IDs.index
+                found_IDs = fit_db.IDs.ID.values
+
+                # a list of bool's that indicate if the ID is already processed
+                process_Q = pd.Index(map(get_ID, arg_list)).isin(found_IDs)
+
+                # a counter that yields unique IDs that are not yet assigned
+                # in the HDF store
+                id_counter = find_missing(found_ID_nums)
+
+                # set the processing-args
+                # update ID to be a valid python-identifier
+                args_to_process = [
+                    {**arg, "_RT1_ID_num": next(id_counter)}
+                    for arg, q in zip(arg_list, process_Q)
+                    if not q
+                ]
+        else:
+            args_to_process = [
+                {**i, "_RT1_ID_num": n} for i, n in zip(arg_list, range(len(arg_list)))
+            ]
+
+            log.info("no existing output-HDF file found...")
+            log.progress(f"processing all {len(args_to_process)} IDs!")
+            return args_to_process
+
+        log.progress(
+            f"Found {len(args_to_process)} missing and "
+            + f"{len(arg_list) - len(args_to_process)}"
+            + " existing IDs!"
+        )
+
+        return args_to_process
diff --git a/rt1/rt1.py b/rt1/rt1.py
index 7789bc34..3f9ef0fa 100644
--- a/rt1/rt1.py
+++ b/rt1/rt1.py
@@ -139,7 +139,6 @@ def __init__(
         int_Q=True,
         verbosity=1,
     ):
-
         assert isinstance(geometry, str), (
             "ERROR: geometry must be " + "a 4-character string"
         )
@@ -881,7 +880,7 @@ def surface_curv(self, dB=False, sig0=False):
                 * 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
+                    4.0 * self.V.tau**2 / self._mu_0**3 * np.sin(self.t_0) ** 2
                     - 2.0 * self.V.tau
                     - self._mu_0
                 )
@@ -916,12 +915,12 @@ def surface_curv(self, dB=False, sig0=False):
                     )
                 )
             elif sig0 is False and dB is True:
-                return 10.0 / 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.0
                     / np.log(10)
-                    * (I_curv / I_val - I_slope ** 2 / I_val ** 2 - self._mu_0 ** (-2))
+                    * (I_curv / I_val - I_slope**2 / I_val**2 - self._mu_0 ** (-2))
                 )
 
     def volume(self):
@@ -1004,7 +1003,7 @@ def volume_slope(self, dB=False, sig0=False):
                     * 2
                     * self.V.tau
                     * np.sin(self.t_0)
-                    / self._mu_0 ** 2
+                    / self._mu_0**2
                 )
                 * p_val
                 + (1.0 - np.exp(-(2 * self.V.tau / self._mu_0))) * p_slope
@@ -1081,7 +1080,7 @@ def volume_curv(self, dB=False, sig0=False):
             / 2.0
             * (
                 np.exp(-(2 * self.V.tau / self._mu_0))
-                * (2 * self.V.tau / self._mu_0 ** 3)
+                * (2 * self.V.tau / self._mu_0**3)
                 * (
                     np.sin(self.t_0) ** 2
                     + 1.0
@@ -1092,7 +1091,7 @@ def volume_curv(self, dB=False, sig0=False):
                     np.exp(-(2 * self.V.tau / self._mu_0))
                     * 4.0
                     * self.V.tau
-                    / self._mu_0 ** 2
+                    / self._mu_0**2
                     * np.sin(self.t_0)
                 )
                 * p_slope
@@ -1116,12 +1115,12 @@ def volume_curv(self, dB=False, sig0=False):
                     )
                 )
             elif sig0 is False and dB is True:
-                return 10.0 / 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.0
                     / np.log(10)
-                    * (I_curv / I_val - I_slope ** 2 / I_val ** 2 - self._mu_0 ** (-2))
+                    * (I_curv / I_val - I_slope**2 / I_val**2 - self._mu_0 ** (-2))
                 )
 
     def tot_slope(self, sig0=False, dB=False):
@@ -1205,12 +1204,12 @@ def tot_curv(self, sig0=False, dB=False):
                     )
                 )
             elif sig0 is False and dB is True:
-                return 10.0 / 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.0
                     / np.log(10)
-                    * (I_curv / I_val - I_slope ** 2 / I_val ** 2 - self._mu_0 ** (-2))
+                    * (I_curv / I_val - I_slope**2 / I_val**2 - self._mu_0 ** (-2))
                 )
 
     def interaction(self):
@@ -1682,7 +1681,6 @@ def jacobian(self, dB=False, sig0=False, param_list=["omega", "tau", "NormBRDF"]
 
         jac = []
         for key in param_list:
-
             if key == "omega":
                 jac += [(self._dsurface_domega() + self._dvolume_domega()) * norm]
             elif key == "tau":
diff --git a/rt1/rtfits.py b/rt1/rtfits.py
index bfa40b9c..909207a8 100644
--- a/rt1/rtfits.py
+++ b/rt1/rtfits.py
@@ -19,6 +19,7 @@
     split_into,
     update_progress,
     groupby_unsorted,
+    isidentifier,
 )
 
 from .rtplots import plot as rt1_plots
@@ -28,12 +29,14 @@
 from . import __version__ as _RT1_version
 from . import log
 from .rtmetrics import _metric_keys
+from ._containers import _RT1_defdict
 
 import copy
 from itertools import repeat, count, chain, groupby
 from functools import lru_cache, partial, wraps, update_wrapper
 from operator import itemgetter, add, methodcaller
 from datetime import datetime
+from pathlib import Path
 
 try:
     import cloudpickle
@@ -41,6 +44,10 @@
     log.warning("cloudpickle could not be imported, .dump() will not work!")
 
 
+def _check_multi(fit):
+    return fit.__class__.__name__ == "MultiFits"
+
+
 def load(path):
     """
     a convenience-function to load a Fits-object dumped with `fit.dump()`
@@ -57,6 +64,16 @@ def load(path):
 
     with open(path, "rb") as file:
         fit = pd.read_pickle(file)
+    if not hasattr(fit, "ID") or fit.ID is None:
+        if _check_multi(fit):
+            fit.set_ID(Path(path).stem)
+        else:
+            fit.ID = Path(path).stem
+
+    if _check_multi(fit):
+        fit.set_dataset(fit.dataset)
+        fit.set_aux_data(fit.aux_data)
+        fit.set_reader_arg(fit.reader_arg)
 
     return fit
 
@@ -198,12 +215,6 @@ class Fits(Scatter):
                     interaction-term
                     -> use with care! you must ensure that the provided
                     function evaluates correctly for the used definitions
-    _interp_vals: list, optional
-                  a list of keys corresponding to parameters whose values
-                  should be quadratically interpolated over the timespan
-                  instead of using a step-function that assigns the obtained
-                  value equally to all observations within the timespan.
-                  -> use with care! this might cause unexpected behaviour!
     verbose : int, optional
               the verbosity of the print-outputs (also passed to
               scipy.optimize.least_squares and rt1.RT1)
@@ -277,14 +288,14 @@ def __init__(
         lambda_backend=None,
         _fnevals_input=None,
         verbose=2,
+        ID="RT1_fit",
         **kwargs,
     ):
-
         self.sig0 = sig0
         self.dB = dB
         self.dataset = self._check_monotonic_dataset_index(dataset)
         self.set_V_SRF = copy.deepcopy(set_V_SRF)
-        self.defdict = copy.deepcopy(defdict)
+
         if lsq_kwargs is None:
             self.lsq_kwargs = dict()
         else:
@@ -300,9 +311,32 @@ def __init__(
 
         self.verbose = verbose
 
+        self.ID = ID
+
         # add plotfunctions
         self.plot = rt1_plots(self)
 
+        if defdict is None:
+            self._defdict = _RT1_defdict()
+        else:
+            self.defdict = defdict
+
+    @property
+    def defdict(self):
+        return self._defdict
+
+    @defdict.setter
+    def defdict(self, d):
+        if isinstance(d, _RT1_defdict):
+            self._defdict = d
+        elif d is None:
+            self._defdict = _RT1_defdict()
+        else:
+            if isinstance(d, (dict, pd.DataFrame)):
+                self._defdict = _RT1_defdict.from_dict(d)
+            else:
+                raise TypeError("defdicts can only be parsed from dicts!")
+
     def __update__(self):
         """needed for downward compatibility"""
 
@@ -331,7 +365,6 @@ def __update__(self):
             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()}
 
@@ -357,12 +390,13 @@ def __getstate__(self):
                 returndict[key] = None
 
             # save only a minimal subset of the fit-output
-            if hasattr(self, "fit_output"):
-                fit_props = dict()
-                fit_props["fit_success"] = self.fit_output.success
-                fit_props["fit_status"] = self.fit_output.status
-                fit_props["fit_optimality"] = self.fit_output.optimality
-
+            fit_props = dict()
+            if hasattr(self, "fit_output") and self.fit_output is not None:
+                for key in ["success", "status", "optimality"]:
+                    try:
+                        fit_props[f"fit_{key}"] = getattr(self.fit_output, key)
+                    except AttributeError:
+                        continue
             returndict["fit_output"] = fit_props
 
             return returndict
@@ -380,6 +414,18 @@ def __setattr__(self, attr, value):
 
         super().__setattr__(attr, value)
 
+    @property
+    def ID(self):
+        return self._ID
+
+    @ID.setter
+    def ID(self, ID):
+        if not isidentifier(str(ID)):
+            self._ID = f"RT1_{ID}"
+            log.debug(f"'{ID}' is not a valid identifier - using '{self._ID}'")
+        else:
+            self._ID = str(ID)
+
     @property
     def dataset(self):
         return self._dataset
@@ -395,7 +441,7 @@ def _check_monotonic_dataset_index(dataset):
             return None
 
         if not dataset.index.is_monotonic_increasing:
-            log.warning("dataset has been sorted to ensure a monotonic index!")
+            log.info("dataset has been sorted to ensure a monotonic index!")
             return dataset.sort_index()
         else:
             return dataset
@@ -427,8 +473,10 @@ def _cached_props(self):
             "_param_dyn_monotonic",
             "_get_excludesymbs",
             "metric",
-            "V", "SRF", "set_model",
-            "_assignvals_log_message"
+            "V",
+            "SRF",
+            "set_model",
+            "_assignvals_log_message",
         ]
 
         for i in ["tau", "omega", "N"]:
@@ -536,10 +584,8 @@ def param_dyn_dict(self):
         if self.dataset is None:
             return dict()
         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)
@@ -660,7 +706,9 @@ def param_dyn_df(self):
     @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_increasing for key, val in self.param_dyn_df.items()
+        }
 
     @property
     @lru_cache()
@@ -671,19 +719,18 @@ 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)
 
+        conclist = list()
         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
@@ -700,8 +747,10 @@ 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]
+        # always attach "sig" and "inc" keys
+
+        usekeys = list({*self.defdict, "sig", "inc"} & set(self.dataset))
+
         if "data_weights" in self.dataset:
             usekeys += ["data_weights"]
 
@@ -713,7 +762,14 @@ def _dataset_used(self):
                 key=itemgetter(0),
                 get=itemgetter(1),
             )
-        df = pd.DataFrame(df).rename(columns={"index": "orig_index"})
+
+        # make sure "orig_index" is correctly renamed in case
+        # the index-column has been explicitly re-named
+        indexname = (
+            self.dataset.index.name if self.dataset.index.name is not None else "index"
+        )
+
+        df = pd.DataFrame(df).rename(columns={indexname: "orig_index"})
         return df
 
     @property
@@ -978,8 +1034,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
 
@@ -1066,8 +1121,7 @@ def _startvaldict(self):
                     )
                     meanstartvals = list(
                         groupby_unsorted(
-                            zip(self._groupindex,
-                                self.dataset[key + "_start"]),
+                            zip(self._groupindex, self.dataset[key + "_start"]),
                             key=itemgetter(0),
                             get=itemgetter(1),
                         ).values()
@@ -1142,7 +1196,6 @@ def _timescaledict(self):
                 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 "
@@ -1181,7 +1234,7 @@ def _manual_dyn_df(self):
                     # 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"'
+                        + ' if defdict[{key}][2] is set to "manual"'
                     )
 
                     manual_dyn_df[f"{key}"] = self.dataset[f"{key}_dyn"]
@@ -1198,7 +1251,6 @@ def _manual_dyn_df(self):
                     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"'
@@ -1239,9 +1291,8 @@ def _param_R_dict(self):
         vsymb = self._Vsymb - angset
         srfsymb = self._SRFsymb - angset
 
-        paramset = (
-            set(map(str, self._startvaldict.keys()))
-            ^ set(map(str, self.fixed_dict.keys()))
+        paramset = set(map(str, self._startvaldict.keys())) ^ set(
+            map(str, self.fixed_dict.keys())
         )
 
         assert paramset >= (vsymb | srfsymb), (
@@ -1268,8 +1319,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
 
     @property
@@ -1281,8 +1331,7 @@ 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
@@ -1320,8 +1369,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
@@ -1418,8 +1466,7 @@ def res_df(self):
             vals[key] = val[~self.mask]
 
         resdf = (
-            pd.DataFrame(vals, list(chain(*self._orig_index))
-                         ).groupby(level=0).first()
+            pd.DataFrame(vals, list(chain(*self._orig_index))).groupby(level=0).first()
         )
 
         return resdf
@@ -1496,8 +1543,7 @@ def _assignvals(self, res_dict, interp_vals=None):
                     self._assignvals_log_message("interp_non_mono", 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
@@ -1602,9 +1648,9 @@ def _get_excludesymbs(self):
 
         return excludekeys
 
-    def _set_calc_values(self, R=None, res_dict=None, fixed_dict=None, interp_vals=None,
-                         assign=True):
-
+    def _set_calc_values(
+        self, R=None, res_dict=None, fixed_dict=None, interp_vals=None, assign=True
+    ):
         # ensure correct array-processing
         # res_dict = {key:val[:,np.newaxis] for
         #             key, val in self._assignvals(res_dict).items()}
@@ -1619,7 +1665,10 @@ def _set_calc_values(self, R=None, res_dict=None, fixed_dict=None, interp_vals=N
                 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] if key in res_dict else fixed_dict[key]
+                    for key in self._omega_symb
+                }
             )
 
         if self._tau_func is None:
@@ -1627,14 +1676,21 @@ def _set_calc_values(self, R=None, res_dict=None, fixed_dict=None, interp_vals=N
                 R.V.tau = res_dict["tau"]
         else:
             R.V.tau = self._tau_func(
-                **{key: res_dict[key] for key in self._tau_symb})
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_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"]
         else:
             R.SRF.NormBRDF = self._N_func(
-                **{key: res_dict[key] for key in self._N_symb}
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_dict[key]
+                    for key in self._N_symb
+                }
             )
 
         if "bsf" in res_dict:
@@ -1646,7 +1702,9 @@ def _set_calc_values(self, R=None, res_dict=None, fixed_dict=None, interp_vals=N
         # and the symbols used to define them must be removed)
 
         strparam_fn = {
-            str(key): val for key, val in res_dict.items() if key not in self._get_excludesymbs
+            str(key): val
+            for key, val in res_dict.items()
+            if key not in self._get_excludesymbs
         }
 
         # set the param-dict to the newly generated dict
@@ -1707,10 +1765,13 @@ def _calc_model(
         if interp_vals is None:
             interp_vals = self.interp_vals
 
-        R = self._set_calc_values(R=R, res_dict=res_dict,
-                                  fixed_dict=fixed_dict,
-                                  interp_vals=interp_vals,
-                                  assign=assign)
+        R = self._set_calc_values(
+            R=R,
+            res_dict=res_dict,
+            fixed_dict=fixed_dict,
+            interp_vals=interp_vals,
+            assign=assign,
+        )
 
         # calculate total backscatter-values
         if return_components is True:
@@ -1772,10 +1833,13 @@ def _calc_jac(
         if order is None:
             order = self._order
 
-        R = self._set_calc_values(R=R, res_dict=res_dict,
-                                  fixed_dict=fixed_dict,
-                                  interp_vals=interp_vals,
-                                  assign=True)
+        R = self._set_calc_values(
+            R=R,
+            res_dict=res_dict,
+            fixed_dict=fixed_dict,
+            interp_vals=interp_vals,
+            assign=True,
+        )
 
         # if tau, omega or NormBRDF have been provided in terms of symbols,
         # remove the symbols that are intended to be fitted (that are also
@@ -1832,7 +1896,10 @@ def _calc_jac(
             # 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}
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_dict[key]
+                    for key in self._tau_symb
+                }
             )
             if not np.isscalar(df_dx):
                 # flatten the array (except if it is a scalar)
@@ -1848,7 +1915,10 @@ def _calc_jac(
         # 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}
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_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)
@@ -1861,7 +1931,11 @@ def _calc_jac(
         # 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})
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_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)
 
@@ -1910,7 +1984,7 @@ def _get_data(self, prop):
                 return rectangularize(self._dataset_used[prop].values)
             elif prop == "mask":
                 _, mask = rectangularize(
-                    self._dataset_used.inc.values, return_mask=True
+                    self._dataset_used.orig_index.values, return_mask=True
                 )
                 if prop == "mask":
                     return mask
@@ -1979,7 +2053,10 @@ def _calc_slope_curv(
                 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] if key in res_dict else fixed_dict[key]
+                    for key in self._omega_symb
+                }
             )
 
         if self._tau_func is None:
@@ -1987,14 +2064,21 @@ def _calc_slope_curv(
                 R.V.tau = res_dict["tau"]
         else:
             R.V.tau = self._tau_func(
-                **{key: res_dict[key] for key in self._tau_symb})
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_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"]
         else:
             R.SRF.NormBRDF = self._N_func(
-                **{key: res_dict[key] for key in self._N_symb}
+                **{
+                    key: res_dict[key] if key in res_dict else fixed_dict[key]
+                    for key in self._N_symb
+                }
             )
 
         if "bsf" in res_dict:
@@ -2084,9 +2168,7 @@ def _init_V_SRF(self, props, setdict=None):
         SRF : a function of rt1.surface
             the used surface-scattering function.
         """
-
-        if setdict is None:
-            setdict = dict()
+        ignore_keys = ["omega", "tau", "NormBRDF", "bsf"]
 
         assert (
             "V_name" in props or "SRF_name" in props
@@ -2095,14 +2177,13 @@ def _init_V_SRF(self, props, setdict=None):
             "V_name" in props and "SRF_name" in props
         ), 'provide either "V_name" or "SRF_name" not both!'
 
-        ignore_keys = ['omega', 'tau', 'NormBRDF', 'bsf']
+        if setdict is None:
+            setdict = dict()
 
         set_dict = dict()
         free_symbols = set()
-        for key, val in props.items():
-            if key == "V_name" or key == "SRF_name":
-                continue
 
+        def checkval(key, val):
             # check if val is directly provided in setdict, if yes use it
             # (e.g. this means the key is simply another name for a parameter
             # and not an  equation)
@@ -2119,7 +2200,7 @@ def _init_V_SRF(self, props, setdict=None):
             # then replace them by the corresponding values in setdict
             else:
                 # convert to sympy expression (check doc for use of _clash)
-                useval = sp.sympify(val, _clash)
+                useval = sp.sympify(val, _clash, evaluate=False)
                 # in case parts of the expression are provided in setdict,
                 # replace them with the provided values
                 replacements = dict()
@@ -2130,6 +2211,29 @@ def _init_V_SRF(self, props, setdict=None):
 
                 if key not in ignore_keys:
                     free_symbols.update(set(useval.free_symbols))
+            return useval
+
+        # deal with linear-combinations
+        for prefix in ["V", "SRF"]:
+            if f"{prefix}_name" in props and props[f"{prefix}_name"].startswith(
+                f"LinComb{prefix}"
+            ):
+                choices = []
+                for val, initdict in props[f"{prefix}choices"]:
+                    useval = checkval(val, val)
+
+                    choices.append([useval, self._init_V_SRF(initdict)])
+
+                set_dict[f"{prefix}choices"] = choices
+
+        for key, val in props.items():
+            if key in ["V_name", "SRF_name", "SRFchoices", "Vchoices"]:
+                continue
+            if key in ["a"]:
+                # use the values directly without checking anything
+                set_dict[key] = val
+                continue
+            useval = checkval(key, val)
             set_dict[key] = useval
 
         if "V_name" in props:
@@ -2224,10 +2328,9 @@ def fun(params):
             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]
+            tot = self._calc_model(R=R, res_dict=newdict)
+
+            errs = (self.data_weights * (tot - self.data))[~self.mask]
 
             if intermediate_results is True:
                 self.intermediate_results["parameters"] += [newdict]
@@ -2475,8 +2578,7 @@ def _model_definition(self):
                 else:
                     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}"
                 except IndexError:
@@ -2502,8 +2604,7 @@ def _model_definition(self):
             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)):
@@ -2591,7 +2692,7 @@ def _model_definition(self):
         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:]
+                keys[: (len(keys) + 1) // 2], keys[(len(keys) + 1) // 2 :]
             ):
                 outstr += (
                     f" {key1:<15}= {lsqkw[key1]}".ljust(37)
@@ -2650,7 +2751,7 @@ def _reinit_object(cls, self, share_fnevals=False, share_auxdata=True, **kwargs)
             "sig0": self.sig0,
             "dB": self.dB,
             "dataset": self.dataset,
-            "defdict": self.defdict,
+            "defdict": self.defdict.to_dict(),
             "set_V_SRF": self.set_V_SRF,
             "lsq_kwargs": self.lsq_kwargs,
             "int_Q": self.int_Q,
@@ -2668,6 +2769,29 @@ def _reinit_object(cls, self, share_fnevals=False, share_auxdata=True, **kwargs)
 
         return fit
 
+    def _get_V_SRF_dict(self):
+        # convert set_V_SRF to a dict
+        if isinstance(self.set_V_SRF, dict):
+            return self.set_V_SRF
+        else:
+            try:
+                # init V and SRF with string-variables
+                V, SRF = self.set_V_SRF(**dict(zip(*[self.defdict._variables] * 2)))
+                return dict(V_props=V.init_dict, SRF_props=SRF.init_dict)
+            except TypeError:
+                log.error("Not all variables are specified in defdict!", exc_info=True)
+
+    def _get_init_dict(self):
+        args = {
+            "sig0": self.sig0,
+            "dB": self.dB,
+            "defdict": self.defdict.to_dict(),
+            "set_V_SRF": self._get_V_SRF_dict(),
+            "lsq_kwargs": self.lsq_kwargs,
+            "int_Q": self.int_Q,
+        }
+        return args
+
     def reinit_object(self, share_fnevals=False, share_auxdata=True, **kwargs):
         """
         initialize a new fits-object that share all attributes except
@@ -2704,6 +2828,204 @@ def reinit_object(self, share_fnevals=False, share_auxdata=True, **kwargs):
         """
         return self._reinit_object(self, **kwargs)
 
+    def _get_res_dict_df(self, ID=None):
+        """
+        get a data-frame that contains all fitted parameters
+        (smaller than fit.res_df which is always interpolated to the index)
+
+        Returns
+        -------
+        df : pandas.DataFrame
+            a pandas-dataframe representing the obtained parameters.
+
+        """
+
+        if ID is None:
+            ID = self.ID
+
+        # convert res_dict to a dataframe
+        if self.res_dict is not None:
+            if hasattr(self, "config_name"):
+                idxcols = [[ID], [self.config_name]]
+                names = ["ID", "cfg", "n"]
+            else:
+                idxcols = [[ID]]
+                names = ["ID", "n"]
+
+            dfs = []
+            for key, val in self.res_dict.items():
+                idx = pd.MultiIndex.from_product(
+                    [*idxcols, range(len(val))], names=names
+                )
+
+                # TODO implement this after the following pandas-bug is fixed:
+                # https://github.com/pandas-dev/pandas/issues/42070
+                # dfs.append(pd.DataFrame({key:pd.arrays.SparseArray(val)}, idx))
+
+                dfs.append(pd.DataFrame({key: val}, idx))
+
+            df = pd.concat(dfs, axis=1)
+            return df
+
+    @staticmethod
+    def _prepend_ID_to_index(df, ID, idx_name="date"):
+        df = df.copy()
+        newindex = pd.MultiIndex.from_product([[ID], df.index], names=["ID", idx_name])
+        df.index = newindex
+        return df
+
+    def _get_fit_to_hdf_dict(
+        self,
+        save_results=True,
+        save_res_df=True,
+        save_data=True,
+        save_auxdata=True,
+        ID=None,
+    ):
+        """
+        return a dict of pandas-dataframes suitable to fully re-create
+        a Fits (or MultiFits) object.
+        -> used to store Fit-objects in HDF-containers
+
+        Parameters
+        ----------
+        save_results : bool, optional
+            indicator if results should be saved. The default is True.
+        save_res_df : bool, optional
+            indicator if results re-indexed to the dataframe index
+            should be saved or not. The default is True.
+        save_data : bool, optional
+            indicator if the dataset should be saved. The default is True.
+        save_auxdata : bool, optional
+            indicator if auxdata should be saved. The default is True.
+        ID : int, optional
+            the index-number to use for indexing within the HDF-store
+            if None, "fit._RT1_ID_num" will be used if available, else "fit.ID"
+        Returns
+        -------
+        hf : dict
+            a dict containing all relevant information necessary to re-create
+            a Fits (or MultiFits) object.
+
+        """
+        if ID is None:
+            if hasattr(self, "_RT1_ID_num"):
+                ID = self._RT1_ID_num
+            else:
+                ID = self.ID
+        hf = dict()
+
+        # -------------- save INIT_DICT (always saved, different for MultiFits)
+        # defdicts are stored as categories to save memory
+        try:
+            if isinstance(self, MultiFits):
+                initdict = pd.concat(
+                    map(
+                        lambda arg: pd.DataFrame(
+                            [pd.Series(arg[1])],
+                            pd.MultiIndex.from_product(
+                                [[ID], [arg[0]]], names=["ID", "cfg"]
+                            ),
+                            dtype=str,
+                        ).astype("category"),
+                        self._get_init_dict(),
+                    )
+                )
+            else:
+                initdict = pd.DataFrame(
+                    [pd.Series(self._get_init_dict())], [ID], dtype=str
+                ).astype("category")
+                initdict.index.name = "ID"
+
+            hf["init_dict"] = initdict
+        except Exception:
+            log.debug(f"could not save 'init_dict' for fit {ID}")
+
+        # -------------- save READER_ARG   (always saved)
+        try:
+            df = pd.DataFrame(self.reader_arg, [ID])
+            df.index.name = "ID"
+            hf["const__reader_arg"] = df
+        except Exception:
+            log.debug(f"could not save 'reader_arg' for fit {ID}")
+            df = pd.DataFrame({"ID": ID}, [ID])
+            df.index.name = "ID"
+
+            hf["const__reader_arg"] = df
+
+        # -------------- save DATASET
+        try:
+            if save_data is True:
+                df = getattr(self, "dataset", None)
+                if df is not None:
+                    hf["dataset"] = Fits._prepend_ID_to_index(df, ID)
+            elif save_results is True:
+                # store only data that is required to re-create the results
+                if isinstance(self, MultiFits):
+                    dynkeys = set(
+                        key + "_dyn"
+                        for cfg, defdict in self.apply(lambda fit: self.defdict)
+                        for key, val in defdict.items()
+                        if val[0] and val[2] == "manual"
+                    )
+                else:
+                    dynkeys = set(
+                        key + "_dyn"
+                        for key, val in self.defdict.items()
+                        if val[0] and val[2] == "manual"
+                    )
+                df = getattr(self, "dataset", None)[dynkeys]
+
+                hf["dataset"] = Fits._prepend_ID_to_index(df, ID)
+        except Exception:
+            log.debug(f"could not save 'dataset' for fit {ID}")
+
+        # -------------- save AUX_DATA
+        if save_auxdata is True:
+            try:
+                df = getattr(self, "aux_data", None)
+                if df is not None:
+                    hf["aux_data"] = Fits._prepend_ID_to_index(df, ID)
+            except Exception:
+                log.debug(f"could not save 'aux_data' for fit {ID}")
+
+        # -------------- save RES_DICT (different for MultiFits)
+        if save_results is True:
+            try:
+                if isinstance(self, MultiFits):
+                    df = pd.concat(
+                        i[1]
+                        for i in self.apply(lambda fit: fit._get_res_dict_df(ID=ID))
+                    )
+                else:
+                    df = self._get_res_dict_df(ID=ID)
+
+                hf["res_dict"] = df
+            except Exception:
+                log.debug(f"could not save 'res_dict' for fit {ID}")
+
+        # -------------- save RES_DF (different for MultiFits)
+        if save_res_df is True:
+            try:
+                if isinstance(self, MultiFits):
+                    df = pd.concat(
+                        i[1]
+                        for i in self.apply(
+                            lambda fit: pd.concat(
+                                [fit.res_df], keys=[fit.config_name], names=["cfg"]
+                            )
+                        )
+                    )
+                    df = pd.concat([df], names=["ID"], keys=[ID])
+                else:
+                    df = Fits._prepend_ID_to_index(self.res_df, ID=ID)
+
+                hf["res_df"] = df
+            except Exception:
+                log.debug(f"could not save 'res_df' for fit {ID}")
+
+        return hf
+
     @property
     @lru_cache()  # cache this since we need a static reference!
     def metric(self):
@@ -2728,41 +3050,6 @@ def metric(self):
         return _metric_keys(self)
 
 
-def get_fitobject(self):
-    configs = self.config_names
-    if len(configs) > 0:
-        rt1_fits = []
-        for config in configs:
-            cfg = self.get_config(config)
-
-            rt1_fits.append(
-                [
-                    config,
-                    Fits(
-                        dataset=None,
-                        defdict=cfg["defdict"],
-                        set_V_SRF=cfg["set_V_SRF"],
-                        lsq_kwargs=cfg["lsq_kwargs"],
-                        **cfg["fits_kwargs"],
-                    ),
-                ]
-            )
-    else:
-        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"],
-        )
-
-    return rt1_fits
-
-
-# %%
-
-
 class _MultiAccessors:
     """
     a class to run functions and access properties on all `Fits` objects
@@ -2770,8 +3057,8 @@ class _MultiAccessors:
     """
 
     def __init__(self, FitContainer):
-
         self._FitContainer = FitContainer
+        ignore_keys = ["dataset", "aux_data", "plot"]
 
         # add all functions and attributes
         allattrs = list(i for i in dir(Fits()) if not i.startswith("__"))
@@ -2782,8 +3069,7 @@ def __init__(self, FitContainer):
                     self,
                     prop,
                     update_wrapper(
-                        partial(self._applyit, prop=prop), getattr(
-                            Fits(), prop)
+                        partial(self._applyit, prop=prop), getattr(Fits(), prop)
                     ),
                 )
             elif isinstance(getattr(Fits, prop), property):
@@ -2793,7 +3079,7 @@ def __init__(self, FitContainer):
                     property(self._getit(prop)),
                 )
 
-        for prop in (set(allattrs) ^ set(classattrs)) - set(["dataset", "aux_data"]):
+        for prop in (set(allattrs) ^ set(classattrs)) - set(ignore_keys):
             setattr(
                 _MultiAccessors,
                 prop,
@@ -2802,7 +3088,7 @@ def __init__(self, FitContainer):
 
     @property
     def config_fits(self):
-        return self._FitContainer.__dict__
+        return {i.config_name: i for i in self._FitContainer}
 
     def _getit(self, prop):
         def _get_all(self):
@@ -2838,7 +3124,43 @@ def _applyit(self, prop, *args, **kwargs):
 
 
 class _FitContainer:
-    pass
+    def __init__(self, parent):
+        self._parent = parent
+
+    def __setstate__(self, d):
+        # downward-compatibility for the _parent property
+        if "_parent" not in d:
+            parent = MultiFits()
+            for key, val in d.items():
+                if isinstance(val, Fits):
+                    parent.add_config(key, val)
+            d["_parent"] = parent
+        # -----------------------------------------------
+        self.__dict__ = d
+
+    def __iter__(self):
+        return (getattr(self, i) for i in self._parent.config_names)
+
+    def __getitem__(self, key):
+        if isinstance(key, str):
+            assert key in self._parent.config_names, (
+                f"config {key} not found, use one of:"
+                + f"\n{self._parent.config_names}"
+            )
+
+            return getattr(self, key)
+        elif isinstance(key, int):
+            n_names = len(self._parent.config_names)
+            assert key < n_names, (
+                f"cannot access config nr. {key}.... there are only "
+                + f"{n_names} configs available: \n{self._parent.config_names}"
+            )
+            return getattr(self, self._parent.config_names[key])
+
+        else:
+            raise AttributeError(
+                "configs can only be accessed using integers or strings"
+            )
 
 
 class MultiFits:
@@ -2870,11 +3192,15 @@ class MultiFits:
 
     """
 
-    def __init__(self, dataset=None, aux_data=None, reader_arg=None):
-
+    def __init__(
+        self,
+        dataset=None,
+        aux_data=None,
+        reader_arg=None,
+    ):
         self.config_names = []
 
-        self.configs = _FitContainer()
+        self.configs = _FitContainer(self)
         self._accessor = _MultiAccessors(self.configs)
 
         self.set_dataset(dataset)
@@ -2891,6 +3217,35 @@ def __setstate__(self, d):
             if not hasattr(fit, "config_name"):
                 fit.config_name = name
 
+    def _get_init_dict(self):
+        args = self.apply(lambda fit: fit._get_init_dict())
+        return args
+
+    def _get_fit_to_hdf_dict(
+        self, save_results=True, save_data=True, save_auxdata=True, ID=None
+    ):
+        return Fits._get_fit_to_hdf_dict(
+            self,
+            save_results=save_results,
+            save_data=save_data,
+            save_auxdata=save_auxdata,
+            ID=ID,
+        )
+
+    @property
+    def ID(self):
+        assert (
+            len(self.config_names) > 0
+        ), "NO config... the MultiFits ID is inherited from the first config!"
+
+        return getattr(self.configs, self.config_names[0]).ID
+
+    @ID.setter
+    def ID(self, ID):
+        raise AttributeError(
+            "use `set_ID` to set 'ID' on ALL configs of the MultiFits object!"
+        )
+
     @property
     def dataset(self):
         return self._dataset
@@ -2908,7 +3263,8 @@ def aux_data(self):
     @aux_data.setter
     def aux_data(self, value):
         raise AttributeError(
-            "use `set_aux_data` to set 'aux_data' on ALL configs of the MultiFits object!"
+            "use `set_aux_data` to set 'aux_data' on ALL configs"
+            + " of the MultiFits object!"
         )
 
     @property
@@ -2918,7 +3274,8 @@ def reader_arg(self):
     @reader_arg.setter
     def reader_arg(self, value):
         raise AttributeError(
-            "use `set_reader_arg` to set 'reader_arg' on ALL configs of the MultiFits object!"
+            "use `set_reader_arg` to set 'reader_arg' on ALL configs"
+            + " of the MultiFits object!"
         )
 
     def set_dataset(self, dataset):
@@ -2938,6 +3295,10 @@ def set_reader_arg(self, reader_arg):
         for name in self.config_names:
             getattr(self.configs, name).reader_arg = self._reader_arg
 
+    def set_ID(self, ID):
+        for i, name in enumerate(self.config_names):
+            getattr(self.configs, name).ID = ID
+
     @property
     def accessor(self):
         return self._accessor
@@ -2969,8 +3330,7 @@ def apply(self, f, use_config=None, **kwargs):
             )
         elif isinstance(use_config, str):
             return [
-                (use_config, f(
-                    fit=self.accessor.config_fits[use_config], **kwargs))
+                (use_config, f(fit=self.accessor.config_fits[use_config], **kwargs))
             ]
         elif isinstance(use_config, list):
             return (
@@ -2995,16 +3355,16 @@ def add_config(self, name, fit_object):
             the fit-object
         """
         if name in self.config_names:
-            log.warning(
-                f"the config {name} will be overwritten by the new definition!")
+            log.warning(f"the config {name} will be overwritten by the new definition!")
 
         assert str.isidentifier(
             name
         ), f"the name {name} is not a valid python identifier!"
 
         # make sure that the config-name does not overwrite any definitions
-        assert name not in set(self.__dict__), (
-            f"you can not use {name} as the name for a configuration!")
+        assert name not in set(
+            self.__dict__
+        ), f"you can not use {name} as the name for a configuration!"
         self.config_names.append(name)
 
         fit_object.dataset = self.dataset
@@ -3031,6 +3391,3 @@ def dump(self, path, mini=True):
 
         with open(path, "wb") as file:
             cloudpickle.dump(self, file)
-
-
-# %%
diff --git a/rt1/rtmetrics.py b/rt1/rtmetrics.py
index 12ad8b0d..d95fed00 100644
--- a/rt1/rtmetrics.py
+++ b/rt1/rtmetrics.py
@@ -21,7 +21,6 @@ class _metric_keys(object):
     """
 
     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()
@@ -135,7 +134,6 @@ def __init__(self):
 
 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'
 
@@ -247,11 +245,10 @@ 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)
+        return 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
@@ -328,12 +325,10 @@ def linregress(d1, d2):
             float values for slope, intercept, pearson, pvalue, stderr
 
         """
-        return dict(
-            zip(
-                ["slope", "intercept", "pearson", "pvalue", "stderr"],
-                stats.linregress(d1, d2),
-            )
-        )
+        try:
+            return stats.linregress(d1, d2)._asdict()
+        except ValueError:
+            return None
 
     @staticmethod
     def rmsd(d1, d2):
@@ -423,7 +418,8 @@ def mae(d1, d2):
     @staticmethod
     def mape(d1, d2):
         """
-        evaluates mean absolute percentage error of given Series d1 and d2 with respect to d1
+        evaluates mean absolute percentage error of given Series
+        d1 and d2 with respect to d1
 
         Parameters
         ----------
@@ -444,7 +440,8 @@ def mape(d1, d2):
     @staticmethod
     def std_ratio(d1, d2):
         """
-        evaluates standard deviation ratio of given Series d1 and d2: sigma_1 / sigma_2
+        evaluates standard deviation ratio of given Series
+        d1 and d2: sigma_1 / sigma_2
 
         Parameters
         ----------
@@ -464,8 +461,9 @@ def std_ratio(d1, d2):
     @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
+        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
         ----------
@@ -512,14 +510,54 @@ def scatterplot(cls, d1, d2, d1_name, d2_name):
 
         # 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)
+
+        # add linregress-line
+        intercept = metrics_dict["linregress"]["intercept"]
+        slope = metrics_dict["linregress"]["slope"]
+        stderr = metrics_dict["linregress"]["stderr"]
+
+        x = np.array([d1.min(), d1.max()])
+        ax1.plot(x, intercept + slope * x, c="k", ls="--")
+
+        # overplot gradient error
+        cint = intercept + slope * x.mean()
+        ax1.plot(
+            x,
+            [
+                (cint + (slope + stderr) * (x.min() - x.mean())),
+                (cint + (slope + stderr) * (x.max() - x.mean())),
+            ],
+            c="r",
+            ls="--",
+            alpha=0.5,
+        )
+
+        ax1.plot(
+            x,
+            [
+                (cint + (slope - stderr) * (x.min() - x.mean())),
+                (cint + (slope - stderr) * (x.max() - x.mean())),
+            ],
+            c="r",
+            ls="--",
+            alpha=0.5,
+        )
+
+        # overplot intercept error
+        # ("intercept_stderr" is available only in new scipy versions!)
+        if "intercept_stderr" in metrics_dict["linregress"]:
+            intercept_err = metrics_dict["linregress"]["intercept_stderr"]
+            ax1.plot(x, intercept + intercept_err + slope * x, c="g", ls=":")
+            ax1.plot(x, intercept - intercept_err + slope * x, c="g", ls=":")
+
         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)
@@ -544,20 +582,20 @@ def scatterplot(cls, d1, d2, d1_name, d2_name):
 
         # scale for higher cells
         metrics_table.scale(1, 1.5)
-
-        plt.show()
+        fig.tight_layout()
         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
+        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
+            dict that should be flattened, should contain float or dict values
         depth : integer
             recursion depth used for prefixing
 
@@ -581,8 +619,9 @@ def _flatten_dictionary(cls, dictionary, depth=0):
     @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
+        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
         ----------
@@ -622,7 +661,8 @@ def _metrics_table_dict_entry(cls, metrics_dict, depth=0):
         Returns
         -------
         string
-            multiline string containing all floating point entries of a dictionary and its sub-dictionaries
+            multiline string containing all floating point entries of a
+            dictionary and its sub-dictionaries
 
         """
         entries = ""
diff --git a/rt1/rtparse.py b/rt1/rtparse.py
index 1c65c43b..e066dc25 100644
--- a/rt1/rtparse.py
+++ b/rt1/rtparse.py
@@ -1,6 +1,7 @@
 """a module to parse .ini files"""
 
 from configparser import ConfigParser, ExtendedInterpolation
+from collections import defaultdict
 from datetime import datetime
 from functools import partial
 import sys
@@ -131,7 +132,6 @@ class RT1_configparser(object):
     """
 
     def __init__(self, configpath, interpolation=ExtendedInterpolation()):
-
         self.configpath = Path(configpath)
         self.interpolation = interpolation
         # setup config (allow empty values -> will result in None)
@@ -162,7 +162,6 @@ def __init__(self, configpath, interpolation=ExtendedInterpolation()):
         self._check_integrity()
 
     def _check_integrity(self):
-
         # check if required sections are defined
         required_sections = [
             "PROCESS_SPECS",
@@ -250,14 +249,19 @@ def _parse_dict(
         return parsed_dict
 
     def _parse_V_SRF(self, section):
-
         inp = self.config[section]
 
         parsed_dict = dict()
         for key in inp:
             val = None
-            if key == "ncoefs":
+            if key == "ncoefs" or key[1:] == "__ncoefs":
                 val = inp.getint(key)
+            elif key == "a" or key[1:] == "__a":
+                val = inp[key]
+                if val.startswith("[") and val.endswith("]"):
+                    val = list(map(float, val[1:-1].replace(" ", "").split(",")))
+                else:
+                    assert False, f"the passed V/SRF value for {key} is not a list"
             else:
                 # try to convert floats, if it fails return the string
                 try:
@@ -267,6 +271,20 @@ def _parse_V_SRF(self, section):
 
             parsed_dict[key] = val
 
+        for prefix in ["V", "SRF"]:
+            if (
+                f"{prefix}_name" in parsed_dict
+                and parsed_dict[f"{prefix}_name"] == f"LinComb{prefix}"
+            ):
+                choices = defaultdict(lambda: [1, dict()])
+                for key in list(parsed_dict):
+                    if key[0].isupper() and key[1:].startswith("__"):
+                        if key.endswith("__frac"):
+                            choices[key[0]][0] = parsed_dict.pop(key)
+                        else:
+                            choices[key[0]][1][key[3:]] = parsed_dict.pop(key)
+
+                parsed_dict[f"{prefix}choices"] = list(choices.values())
         return parsed_dict
 
     def _parse_defdict(self, section):
diff --git a/rt1/rtplots.py b/rt1/rtplots.py
index 7379b9d4..eb37667f 100644
--- a/rt1/rtplots.py
+++ b/rt1/rtplots.py
@@ -18,6 +18,7 @@
 from matplotlib.gridspec import GridSpec
 from matplotlib.gridspec import GridSpecFromSubplotSpec
 import matplotlib.ticker as ticker
+from matplotlib.collections import LineCollection
 
 try:
     from mpl_toolkits.mplot3d import Axes3D
@@ -188,9 +189,7 @@ def angsub(ti):
             for ti in plottis:
                 color = next(colors)
                 used_colors.append(color)
-                rad = getattr(X, funcname)(
-                    ti, thetass, 0.0, 0.0, param_dict=params
-                )
+                rad = getattr(X, funcname)(ti, thetass, 0.0, 0.0, param_dict=params)
                 if aprox is True:
                     # the use of np.pi-ti stems from the definition
                     # of legexpansion() in volume.py
@@ -531,7 +530,7 @@ def scatter(self, fit=None, mima=None, pointsize=0.5, regression=True, **kwargs)
             ax.text(
                 0.8,
                 0.1,
-                "$R^2$ = " + str(np.round(r_value ** 2, 2)),
+                "$R^2$ = " + str(np.round(r_value**2, 2)),
                 horizontalalignment="center",
                 verticalalignment="center",
                 transform=ax.transAxes,
@@ -738,6 +737,7 @@ def fit_timeseries(
             labels=list(lab),
             loc="upper left",
             ncol=5,
+            markerscale=3,
         )
 
         if ylim is not None:
@@ -1114,7 +1114,6 @@ def single_results(
 
         # function to generate colormap that fades between colors
         def CustomCmap(from_rgb, to_rgb):
-
             # from color r,g,b
             r1, g1, b1 = from_rgb
 
@@ -1137,7 +1136,6 @@ def CustomCmap(from_rgb, to_rgb):
         ax = fig.add_subplot(111)
 
         for m_i, m in enumerate(fit_numbers):
-
             if convertTodB is True:
                 y = 10.0 * np.log10(estimates[m][~fit.mask[m]])
             else:
@@ -1796,7 +1794,6 @@ def plotlines(
             styledict_dict,
             styledict_fullt0_dict,
         ):
-
             incs = np.rad2deg(sig0_vals["incs"])
 
             lines = []
@@ -1858,7 +1855,6 @@ def plotlines(
                 newincs = np.rad2deg(newsig0_vals["incs"])
 
                 for day in np.arange(0, dayrange, 1):
-
                     sortp = np.argsort(newincs[day])
 
                     linesfull += ax.plot(
@@ -2096,14 +2092,12 @@ def animate(
             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"))
             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"
@@ -2280,7 +2274,6 @@ def updatesliderboundary(evt, slider):
             "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
@@ -2591,10 +2584,36 @@ def analyzemodel(
             paramslider[key].label.set_horizontalalignment("left")
             paramslider[key].valtext.set_position([0.8, 0.5])
 
-        buttons = CheckButtons(buttonax, buttonlabels, [False for i in buttonlabels])
+        buttonvals = [False for i in buttonlabels]
+
+        # add a button to indicate the fit-results
+        if res_dict is not None:
+            buttonlabels += ["... indicate fit"]
+            buttonvals += [False]
+        buttons = CheckButtons(buttonax, buttonlabels, buttonvals)
 
         params = startparams.copy()
 
+        # indicate the fit-results with some lines
+        if res_dict is not None:
+            res = fit.calc(
+                fit.res_df,
+                inc,
+                return_components=False,
+                fixed_param=fit.dataset.groupby(level=0, axis=0).mean()[
+                    fit.fixed_dict.keys()
+                ],
+            )
+            res = dBsig0convert(res, inc, dB, sig0, fit.dB, fit.sig0)
+
+            lc = LineCollection(
+                np.stack((np.array([inc] * len(res)), res), axis=2),
+                lw=0.25,
+                ec=".5",
+                alpha=0.75,
+                zorder=-1,
+            )
+
         # define function to update lines based on slider-input
         def animate(value, key):
             # params = copy.deepcopy(startparams)
@@ -2616,12 +2635,20 @@ def animate(value, key):
             if int_Q is True:
                 lint.set_ydata(modelresult["inter"].T)
 
-            # poverprint boundaries
-            hatches = [r"//", r"\\ ", "+", "oo", "--", ".."]
+            # overprint boundaries
+            hatches = [r"//", r"\\\\", "+", "oo", "--", ".."]
             colors = ["C" + str(i) for i in range(10)]
-            ax.collections.clear()
+
+            for a in ax.collections:
+                a.remove()
+
             legendhandles = []
             for i, [key_i, key_Q] in enumerate(printvariationQ.items()):
+                if key_i == "... indicate fit":
+                    if key_Q is True:
+                        ax.add_collection(lc)
+                    continue
+
                 # replace label of key_i with provided label
                 if key_i in labels:
                     keylabel = labels[key_i]
@@ -2687,7 +2714,6 @@ def animate(value, key):
                     ]
 
                     if fillcomponents is True:
-
                         legendhandles += [
                             ax.fill_between(
                                 inc,
@@ -2732,8 +2758,21 @@ def animate(value, key):
                     leg1.remove()
 
         printvariationQ = {key: False for key in minparams}
+        if res_dict is not None:
+            printvariationQ["... indicate fit"] = False
 
         def buttonfunc(label):
+            if label == "... indicate fit":
+                if printvariationQ[label] is True:
+                    try:
+                        lc.remove()
+                    except Exception:
+                        pass
+                    printvariationQ[label] = False
+                else:
+                    printvariationQ[label] = True
+                    ax.add_collection(lc)
+                return
             # if labels of the buttons have been changed by the labels-argument
             # set the name to the corresponding key (= the actual parameter name)
             for key, val in labels.items():
@@ -2742,7 +2781,9 @@ def buttonfunc(label):
 
             # ax.collections.clear()
             if printvariationQ[label] is True:
-                ax.collections.clear()
+                for a in ax.collections:
+                    a.remove()
+                # ax.collections.clear()
                 printvariationQ[label] = False
             elif printvariationQ[label] is False:
                 printvariationQ[label] = True
@@ -2787,7 +2828,6 @@ def submit(val, key, minmax):
 
         textboxes_buttons = {}
         for i, [key, val] in enumerate(paramslider.items()):
-
             axbox0 = plt.axes(
                 [
                     val.ax.get_position().x0,
diff --git a/rt1/rtprocess.py b/rt1/rtprocess.py
index 28343ba2..81da9339 100644
--- a/rt1/rtprocess.py
+++ b/rt1/rtprocess.py
@@ -1,8 +1,6 @@
 import multiprocessing as mp
-from timeit import default_timer
-from datetime import datetime, timedelta
+from datetime import datetime
 from itertools import repeat, islice
-import ctypes
 import sys
 import traceback
 from textwrap import dedent
@@ -14,21 +12,16 @@
 import pandas as pd
 import numpy as np
 
-from .general_functions import dt_to_hms, update_progress, groupby_unsorted
+
+from ._rtprocess_writer import RT1_processor
+
+from .general_functions import groupby_unsorted, isidentifier
 from .rtparse import RT1_configparser
 from .rtresults import RTresults
-from .rtfits import load, MultiFits
+from .rtfits import MultiFits
 from . import log, _get_logger_formatter
 import logging
-
-
-try:
-    import xarray as xar
-except ModuleNotFoundError:
-    log.info(
-        "xarray could not be imported, "
-        + "NetCDF-features of RT1_results will not work!"
-    )
+import ctypes
 
 
 def _confirm_input(msg="are you sure?", stopmsg="STOP", callbackdict=None):
@@ -72,115 +65,17 @@ 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():
-            log.info(f'"{save_path}"\ndoes not exist... creating directory')
+            log.progress(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():
-                log.info(f'"{mkpath}"\ndoes not exist... creating directory')
+                log.progress(f'"{mkpath}"\ndoes not exist... creating directory')
                 mkpath.mkdir(parents=True, exist_ok=True)
 
 
-def _setup_cnt(N_items, ncpu):
-    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
-
-
-def _start_cnt():
-    return default_timer()
-
-
-def _increase_cnt(process_cnt, start, err=False):
-    if process_cnt is None:
-        return
-
-    p_totcnt, p_meancnt, p_max, p_time, p_ncpu, lock = process_cnt
-    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()
-            # increase the total counter
-            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
-            )
-            # 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)
-            d, h, m, s = dt_to_hms(remain)
-
-            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,
-            )
-
-        else:
-            # only increase the total counter
-            p_totcnt.value += 1
-            if p_meancnt.value == 0:
-                title = f"{'estimating time ...':<28}"
-            else:
-                # get the remaining time and update the progressbar
-                remain = timedelta(
-                    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"
-
-            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,
-            )
-
-            # log to file if an error occured during processing
-            log.debug(msg.strip())
-
-        if lock is not None:
-            # release the lock
-            lock.release()
-    except Exception:
-        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,
@@ -190,6 +85,7 @@ def __init__(
         proc_cls=None,
         parent_fit=None,
         init_kwargs=None,
+        hdf_export_kwargs=None,
     ):
         """
         A class to perform parallelized processing.
@@ -230,6 +126,10 @@ def __init__(
             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.
+        hdf_export_kwargs: dict, optional
+            a dict passed to fit._get_fit_to_hdf_dict()
+            (use it to set which variables to store in the hdf-container)
+            (e.g.: save_results, save_res_df, save_data, save_auxdata)
         """
         assert config_path is not None, "Please provide a config-path!"
         self.config_path = Path(config_path)
@@ -240,39 +140,41 @@ def __init__(
 
         self.autocontinue = autocontinue
 
-        self._postprocess = True
-
         self.copy = copy
 
+        if hdf_export_kwargs is None:
+            self.hdf_export_kwargs = dict()
+        else:
+            self.hdf_export_kwargs = hdf_export_kwargs
+
         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 !'
             self.init_kwargs = init_kwargs
 
-    def _listener_process(self, queue):
-
+    @staticmethod
+    def _listener_process(queue, dumppath, log_level=1):
         # 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"):
+        if dumppath is None:
             log.warning("no dumppath specified, log is not saved!")
             return
 
         datestring = datetime.now().strftime("%d_%m_%Y_%H_%M_%S")
-        path = self.dumppath / "cfg" / f"RT1_process({datestring}).log"
+        path = dumppath / "cfg" / f"RT1_process({datestring}).log"
 
         log.debug("listener process started for path: " + str(path))
 
         log2 = logging.getLogger()
-        log2.setLevel(1)
+        log2.setLevel(log_level)
         h = logging.FileHandler(path)
         h.setFormatter(_get_logger_formatter())
-        h.setLevel(1)
+        h.setLevel(log_level)
         log2.addHandler(h)
 
         while True:
@@ -287,7 +189,6 @@ def _listener_process(self, queue):
 
     @staticmethod
     def _worker_configurer(queue, loglevel=10):
-
         log.debug("configuring worker... ")
         log.setLevel(loglevel)
         h = logging.handlers.QueueHandler(queue)  # Just the one handler needed
@@ -357,7 +258,7 @@ def _remove_dumpfolder(self):
         # remove folderstructure
         shutil.rmtree(self.dumppath)
 
-        log.info(f'"{self.dumppath}"' + "\nhas successfully been removed.\n")
+        log.progress(f'"{self.dumppath}"' + "\nhas successfully been removed.\n")
 
     def setup(self):
         """
@@ -366,14 +267,14 @@ def setup(self):
           - copy modules and .ini files (if copy=True) (only from MainProcess!)
           - load modules and set parent-fit-object
         """
+
         try:
             self.cfg = RT1_configparser(self.config_path)
         except Exception:
             if mp.current_process().name == "MainProcess":
-                log.warning("no valid config file was found")
+                log.warning(f"no valid config file was found", exc_info=1)
             return
 
-
         # update specs with init_kwargs
         for section, init_defs in self.init_kwargs.items():
             assert (
@@ -381,14 +282,17 @@ def setup(self):
             ), f"the init_kwargs section {section} is not present in the .ini file!"
 
             for key, val in init_defs.items():
-                if key in self.cfg.config[section]:
+                if (
+                    key in self.cfg.config[section]
+                    and mp.current_process().name == "MainProcess"
+                ):
                     log.warning(
                         f'"{key} = {self.cfg.config[section][key]}" '
                         + "will be overwritten by the definition provided via "
                         + f'"init_kwargs[{section}]": "{key} = {val}" '
                     )
-                    # update the parsed config (for import of modules etc.)
-                    self.cfg.config[section][key] = val
+                # update the parsed config (for import of modules etc.)
+                self.cfg.config[section][key] = val
 
         specs = self.cfg.get_process_specs()
         self.dumppath = specs["save_path"] / specs["dumpfolder"]
@@ -396,7 +300,6 @@ def setup(self):
         if mp.current_process().name == "MainProcess":
             # init folderstructure and copy files only from main process
             if self.autocontinue is False:
-
                 if self.dumppath.exists():
                     _confirm_input(
                         msg=(
@@ -425,6 +328,8 @@ def setup(self):
                 ["results", "cfg", "dumps"],
             )
 
+        # TODO fix this properly!!
+        sys.path.append(str(self.dumppath / "cfg"))
         # copy modules and config-files and ensure that they are loaded from the
         # right direction (NO files will be overwritten!!)
         # copying is only performed in the main-process!
@@ -441,7 +346,6 @@ def setup(self):
         # only import proc_cls if it is not yet set
         # (to allow overriding already set properties during runtime)
         if self._proc_cls is None:
-
             log.debug(
                 f'processing config class "{proc_class_name}"'
                 + f'  will be imported from \n"{procmodule}"'
@@ -461,8 +365,6 @@ def setup(self):
         for key in [
             "preprocess",
             "reader",
-            "postprocess",
-            "finaloutput",
             "exceptfunc",
         ]:
             assert hasattr(
@@ -479,16 +381,13 @@ def _copy_cfg_and_modules(self):
         copypath = self.dumppath / "cfg" / self.cfg.configpath.name
         if mp.current_process().name == "MainProcess":
             if copypath.exists() and len(self.init_kwargs) == 0:
-
                 log.warning(
                     f'the file "{Path(*copypath.parts[-3:])} "'
                     + "already exists... NO copying is performed and the "
                     + "existing one is used!\n"
                 )
 
-                log.debug(
-                    f'{copypath.name} imported from \n"{copypath}"\n'
-                )
+                log.debug(f'{copypath.name} imported from \n"{copypath}"\n')
             else:
                 if len(self.init_kwargs) == 0:
                     # if no init_kwargs have been provided, copy the
@@ -531,7 +430,8 @@ def _copy_cfg_and_modules(self):
                         )
 
                         log.debug(
-                            f'{module_copypath.name} imported from \n"{module_copypath}"\n'
+                            f"{module_copypath.name} imported from \n"
+                            + f'"{module_copypath}"\n'
                         )
 
                     else:
@@ -541,7 +441,7 @@ def _copy_cfg_and_modules(self):
         del self.cfg
         self.cfg = RT1_configparser(copypath)
 
-    def _evalfunc(self, reader_arg=None, process_cnt=None):
+    def _evalfunc(self, reader_arg=None):
         """
         Initialize a Fits-instance and perform a fit.
         (used for parallel processing)
@@ -550,16 +450,11 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
         ----------
         reader_arg : dict
             A dict of arguments passed to the reader.
-        process_cnt : list
-            A list of shared-memory variables that are used to update the
-            progressbar
         Returns
         -------
-        The used 'rt1.rtfit.Fits' object or the output of 'postprocess()'
+        a dict containing all data and definitions needed to re-create the Fits object
         """
 
-        if process_cnt is not None:
-            start = _start_cnt()
         try:
             # if a reader (and no dataset) is provided, use the reader
             read_data = self.proc_cls.reader(reader_arg)
@@ -582,6 +477,10 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
                     + "must be a pandas DataFrame"
                 )
 
+            if dataset is None or len(dataset) == 0:
+                log.error("dataset is empty")
+                raise Exception("rt1_skip")
+
             # take proper care of MultiFit objects
             if isinstance(self.parent_fit, MultiFits):
                 # set the dataset
@@ -591,71 +490,69 @@ def _evalfunc(self, reader_arg=None, process_cnt=None):
                     self.parent_fit.set_aux_data(aux_data)
                 # append reader_arg to all configs
                 self.parent_fit.set_reader_arg(reader_arg)
+                # set ID for all configs
+                self.parent_fit.set_ID(
+                    self.proc_cls.get_names_ids(reader_arg)["feature_id"]
+                )
 
                 do_performfit = self.parent_fit.accessor.performfit()
                 _ = [doit() for doit in do_performfit.values()]
 
-                # if a post-processing function is provided, return its output,
-                # else return None
-                if self._postprocess and callable(self.proc_cls.postprocess):
-                    ret = dict(
-                        self.parent_fit.apply(
-                            self.proc_cls.postprocess, reader_arg=reader_arg
-                        )
-                    )
-                else:
-                    ret = dict()
-
-                if self._dump_fit and hasattr(self.proc_cls, "dump_fit_to_file"):
-                    self.proc_cls.dump_fit_to_file(
-                        self.parent_fit, reader_arg, mini=True
+                if self._dump_fit:
+                    self.parent_fit.dump(
+                        (
+                            self.dumppath
+                            / "dumps"
+                            / self.proc_cls.get_names_ids(reader_arg)["filename"]
+                        ),
+                        mini=True,
                     )
 
-                if process_cnt is not None:
-                    _increase_cnt(process_cnt, start, err=False)
+                return self.parent_fit._get_fit_to_hdf_dict(
+                    ID=reader_arg.get("_RT1_ID_num", None), **self.hdf_export_kwargs
+                )
 
             else:
-                fit = self.parent_fit.reinit_object(dataset=dataset)
+                fit = self.parent_fit.reinit_object(
+                    dataset=dataset,
+                    share_fnevals=True,
+                    ID=self.proc_cls.get_names_ids(reader_arg)["feature_id"],
+                )
+                # append reader_arg
+                fit.reader_arg = reader_arg
+
                 fit.performfit()
 
                 # append auxiliary data
                 if aux_data is not None:
                     fit.aux_data = aux_data
 
-                # append reader_arg
-                fit.reader_arg = reader_arg
-
-                if process_cnt is not None:
-                    _increase_cnt(process_cnt, start, err=False)
-
-                # if a post-processing function is provided, return its output,
-                # else return None
-                if self._postprocess and callable(self.proc_cls.postprocess):
-                    ret = self.proc_cls.postprocess(fit, reader_arg)
-                else:
-                    ret = None
-
                 # dump a fit-file
                 # save dumps AFTER prostprocessing has been performed
                 # (it might happen that some parts change during postprocessing)
-                if self._dump_fit and hasattr(self.proc_cls, "dump_fit_to_file"):
-                    self.proc_cls.dump_fit_to_file(fit, reader_arg, mini=True)
-            return ret
+                if self._dump_fit:
+                    fit.dump(
+                        (
+                            self.dumppath
+                            / "dumps"
+                            / self.proc_cls.get_names_ids(reader_arg)["filename"]
+                        ),
+                        mini=True,
+                    )
 
-        except Exception as ex:
+                return fit._get_fit_to_hdf_dict(
+                    ID=reader_arg.get("_RT1_ID_num", None), **self.hdf_export_kwargs
+                )
 
+        except Exception as ex:
             if callable(self.proc_cls.exceptfunc):
                 ex_ret = self.proc_cls.exceptfunc(ex, 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
 
-    def _initializer(self, initializer, queue, *args):
+    def _initializer(self, initializer, queue, proc_counter=None, *args):
         """
         decorate a provided initializer with the "_worker_configurer()" to
         enable logging from subprocesses while keeping the initializer a
@@ -664,11 +561,18 @@ def _initializer(self, initializer, queue, *args):
         """
 
         if queue is not None:
-            self._worker_configurer(queue)
+            RTprocess._worker_configurer(queue)
 
         if not mp.current_process().name == "MainProcess":
             # call setup() on each worker-process to ensure that the importer loads
             # all required modules from the desired locations
+
+            origname = mp.current_process().name
+            mp.current_process().name = "RTprocess-" + "".join(origname.split("-")[1:])
+            if proc_counter is not None:
+                proc_counter.value += 1
+                mp.current_process().name = f"RTprocess-{proc_counter.value}"
+
             log.progress("setting up RTprocess-worker")
             self.setup()
 
@@ -676,7 +580,7 @@ def _initializer(self, initializer, queue, *args):
             res = initializer(*args)
             return res
 
-    def processfunc(
+    def _processfunc(
         self,
         ncpu=1,
         print_progress=True,
@@ -684,8 +588,10 @@ def processfunc(
         pool_kwargs=None,
         preprocess_kwargs=None,
         queue=None,
-        dump_fit=True,
-        postprocess=True,
+        proc_counter=None,
+        dump_fit=False,
+        create_index=False,
+        **kwargs,
     ):
         """
         Evaluate a RT-1 model on a single core or in parallel using
@@ -700,15 +606,15 @@ def processfunc(
             (see for example: )
 
             >>> if __name__ == '__main__':
-                    fit.processfunc(...)
+                    fit._processfunc(...)
 
             In order to allow pickling the final rt1.rtfits.Fits object,
             it is required to store ALL definitions within a separate
-            file and call processfunc in the 'main'-file as follows:
+            file and call _processfunc in the 'main'-file as follows:
 
             >>> from specification_file import fit reader lsq_kwargs ... ...
                 if __name__ == '__main__':
-                    fit.processfunc(ncpu=5, reader=reader,
+                    fit._processfunc(ncpu=5, reader=reader,
                                     lsq_kwargs=lsq_kwargs, ... ...)
 
         Parameters
@@ -750,21 +656,24 @@ def processfunc(
         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
+            indicator if a pickle of the fit-object should be put in the
+            "dumps" folder for each finished fit.
+            The default is False
+        create_index : bool or dict, optional
+            indicator if the created HDF-store should be indexed or not.
+
+            - if True a index is created for the first index-column
+              (this can take quite some time but speeds up querying a lot!)
+            - if a dict is provided, it is used as kwargs for
+              `_rtprocess_writer.RT1_processor.create_index()`
+              (e.g. the defaults are:  idx_levels=None and keys=None)
 
         Returns
         -------
         res : list
-            A list of rt1.rtfits.Fits objects or a list of outputs of the
-            postprocess-function.
+            A list of rt1.rtfits.Fits objects
 
         """
-        self._postprocess = postprocess
         self._dump_fit = dump_fit
 
         if callable(self.proc_cls.preprocess):
@@ -780,113 +689,86 @@ def processfunc(
         # 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() '
+                'if "reader_args" is not passed directly to _processfunc() '
                 + ', the preprocess() function must return a key "reader_args"!'
             )
 
             reader_args = setupdict["reader_args"]
         else:
             assert "reader_args" not in setupdict, (
-                '"reader_args" is provided as argument to processfunc() '
+                '"reader_args" is provided as argument to _processfunc() '
                 + "AND via the return-dict of the preprocess() function!"
             )
 
-        log.info(f"processing {len(reader_args)} features")
-
-        if "pool_kwargs" in setupdict:
-            pool_kwargs = setupdict["pool_kwargs"]
-
-        if pool_kwargs is None:
-            pool_kwargs = dict()
+        if len(reader_args) == 0:
+            log.warning("There is nothing to process... (reader_args is empty)")
+            res = []
+        else:
+            log.info(f"attempting to process {len(reader_args)} features")
 
-        # 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 "pool_kwargs" in setupdict:
+                pool_kwargs = setupdict["pool_kwargs"]
 
-        # pre-evaluate the fn-coefficients if interaction terms are used
-        if isinstance(self.parent_fit, MultiFits):
-            for name, parent_fit in self.parent_fit.accessor.config_fits.items():
-                if parent_fit.int_Q is True:
-                    parent_fit._fnevals_input = parent_fit.R._fnevals
-        else:
-            if self.parent_fit.int_Q is True:
-                self.parent_fit._fnevals_input = self.parent_fit.R._fnevals
+            if pool_kwargs is None:
+                pool_kwargs = dict()
 
-        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(reader_args), ncpu=ncpu)
-        else:
-            process_cnt = None
-
-        if ncpu > 1:
-            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))
-                )
+            # 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.close()  # Marks the pool as closed.
-                pool.join()  # Waits for workers to exit.
-                res = res_async.get()
-        else:
-            log.info("start of single-core evaluation")
-            # force autocontinue=True when calling the initializer since setup() has
-            # already been called in the main process so all folders will already exist!
-            init_autocontinue = self.autocontinue
-            self.autocontinue = True
+            pool_kwargs["initargs"] = [
+                pool_kwargs.pop("initializer", None),
+                queue,
+                proc_counter,
+                *pool_kwargs.pop("initargs", []),
+            ]
+            pool_kwargs["initializer"] = self._initializer
 
-            # 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"]()
+            # pre-evaluate the fn-coefficients if interaction terms are used
+            if isinstance(self.parent_fit, MultiFits):
+                for name, parent_fit in self.parent_fit.accessor.config_fits.items():
+                    if parent_fit.int_Q is True:
+                        log.progress(
+                            f"pre-evaluating coefficients for config {name} ..."
+                        )
+                        parent_fit._fnevals_input = parent_fit.R._fnevals
+            else:
+                if self.parent_fit.int_Q is True:
+                    log.progress("pre-evaluating coefficients ...")
+                    self.parent_fit._fnevals_input = self.parent_fit.R._fnevals
 
-            self.autocontinue = init_autocontinue
+            # start processing
+            with RT1_processor.writer_pool(
+                n_worker=ncpu,
+                dst_path=self.proc_cls.rt1_procsesing_respath / "fit_db.h5",
+                **kwargs,
+            ) as w:
+                res = w.run_starmap(
+                    arg_list=reader_args,
+                    process_func=self._evalfunc,
+                    pool_kwargs=pool_kwargs,
+                    ID_getter=self._extract_ID,
+                )
 
-            res = []
-            for reader_arg in reader_args:
-                res.append(
-                    self._evalfunc(reader_arg=reader_arg, process_cnt=process_cnt)
+        if create_index:
+            if isinstance(create_index, dict):
+                RT1_processor.create_index(
+                    self.proc_cls.rt1_procsesing_respath / "fit_db.h5", **create_index
+                )
+            elif create_index is True:
+                RT1_processor.create_index(
+                    self.proc_cls.rt1_procsesing_respath / "fit_db.h5"
                 )
 
-        # TODO incorporate this in run_finalout
-        # call finaloutput if post-processing has been performed and a finaloutput
-        # function is provided
-        if self._postprocess and callable(self.proc_cls.finaloutput):
-            # in case of a multi-config, results will be dicts!
-            if isinstance(self.parent_fit, MultiFits):
-                # store original finalout_name
-                finalout_name, ending = self.proc_cls.finalout_name.split(".")
+        return res
 
-                for name in self.parent_fit.config_names:
-                    self.proc_cls.finalout_name = f"{finalout_name}__{name}.{ending}"
-                    self.proc_cls.finaloutput(
-                        (i.get(name) for i in res if i is not None)
-                    )
-                # reset initial finalout_name
-                self.proc_cls.finalout_name = f"{finalout_name}.{ending}"
-
-            # if len(self.cfg.config_names) > 0:
-            #     for i, res_i in enumerate(
-            #         zip_longest(*(i for i in res if i is not None))
-            #     ):
-            #         self.proc_cls.rt1_procsesing_respath = (
-            #             self.dumppath / "results" / self.parent_fit[i][0]
-            #         )
-            #         self.proc_cls.finaloutput(res_i)
-            else:
-                return self.proc_cls.finaloutput(res)
+    def _extract_ID(self, reader_arg):
+        ID = self.proc_cls.get_names_ids(reader_arg)["feature_id"]
+
+        if not isidentifier(str(ID)):
+            return f"RT1_{ID}"
         else:
-            return res
+            return str(ID)
 
     def run_processing(
         self,
@@ -895,9 +777,9 @@ def run_processing(
         reader_args=None,
         pool_kwargs=None,
         preprocess_kwargs=None,
-        logfile_level=1,
-        dump_fit=True,
-        postprocess=True,
+        logfile_level=21,
+        dump_fit=False,
+        create_index=False,
     ):
         """
         Start the processing
@@ -945,35 +827,59 @@ def run_processing(
 
             for information on the level values see:
                 https://docs.python.org/3/library/logging.html#logging-levels
+
+            The default is 21 (=logging.PROGRESS), in which case only
+            e.g. only RT1 progress-messages, warnings and errors will be logged
         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.
+            indicator if a pickle-dump file should be generated and put in the
+            "dumps" folder after finishing the fit.
+            -> this will generate a pickle (".dump") file for each fit so that
+            can be loaded and analyzed quickly.
+
+            useful for debug and initial configuration of a fit)
+            NOT suitable for long-term storage!!
+            The default is False
+        create_index : bool or dict, optional
+            indicator if the created HDF-store should be indexed or not.
+
+            - if True a index is created for the first index-column
+              (this can take quite some time but speeds up querying a lot!)
+            - if a dict is provided, it is used as kwargs for
+              `_rtprocess_writer.RT1_processor.create_index()`
+              (e.g. the defaults are:  idx_levels=None and keys=None)
+
         """
+        if hasattr(self, "proc_cls"):
+            if hasattr(self.proc_cls, "finalizer"):
+                self.proc_cls.finalizer()
 
         try:
-            if logfile_level is not None and ncpu > 1:
+            # initialize all necessary properties if setup was not yet called
+            self.setup()
+
+            if logfile_level is not None:
                 # 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])
+                manager = mp.Manager()
+                queue = manager.Queue(-1)
+                # a counter to name the multiprocess-workers
+                proc_counter = manager.Value(ctypes.c_ulonglong, 0)
+
+                listener = mp.Process(
+                    target=self._listener_process,
+                    args=[queue, self.dumppath, logfile_level],
+                    name="RTprocess logger",
+                )
 
                 # 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)
+                self._worker_configurer(queue, loglevel=logfile_level)
             else:
-                queue = None
-
-            # initialize all necessary properties if setup was not yet called
-            self.setup()
+                queue, proc_counter = None
 
-            if logfile_level is not None and ncpu > 1:
+            if logfile_level is not None:
                 # 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!
@@ -994,7 +900,6 @@ def run_processing(
                             self.dumppath / "cfg" / f"model_definition__{cfg_name}.txt",
                             "w",
                         ) as file:
-
                             outtxt = ""
                             if hasattr(self.proc_cls, "description"):
                                 outtxt += dedent(self.proc_cls.description)
@@ -1011,7 +916,6 @@ def run_processing(
                     with open(
                         self.dumppath / "cfg" / "model_definition.txt", "w"
                     ) as file:
-
                         outtxt = ""
                         if hasattr(self.proc_cls, "description"):
                             outtxt += dedent(self.proc_cls.description)
@@ -1024,15 +928,18 @@ def run_processing(
 
                         log.info(outtxt)
 
-            _ = self.processfunc(
+            _ = self._processfunc(
                 ncpu=ncpu,
                 print_progress=print_progress,
                 reader_args=reader_args,
                 pool_kwargs=pool_kwargs,
                 preprocess_kwargs=preprocess_kwargs,
                 queue=queue,
+                proc_counter=proc_counter,
                 dump_fit=dump_fit,
-                postprocess=postprocess,
+                init_func=self._worker_configurer,
+                init_args=(queue, logfile_level),
+                create_index=create_index,
             )
 
         except Exception as err:
@@ -1048,13 +955,11 @@ def run_processing(
             logging.captureWarnings(False)
 
             if logfile_level is not None:
-                if ncpu > 1:
+                # tell the queue to stop
+                queue.put_nowait(None)
 
-                    # tell the queue to stop
-                    queue.put_nowait(None)
-
-                    # stop the listener process
-                    listener.join()
+                # stop the listener process
+                listener.join()
 
                 # remove any remaining file-handler and queue-handlers after
                 # the processing is done
@@ -1071,14 +976,17 @@ def run_finaloutput(
         use_N_files=None,
         use_config=None,
         finalout_name=None,
-        finaloutput=None,
         postprocess=None,
         print_progress=True,
-        logfile_level=1,
-        fitlist=None
+        logfile_level=21,
+        fitlist=None,
+        save_path=None,
+        create_index=True,
+        use_dumps=False,
+        **kwargs,
     ):
         """
-        run postprocess and finaloutput for available .dump files
+        run postprocess for available .dump files
 
         Parameters
         ----------
@@ -1092,18 +1000,9 @@ def run_finaloutput(
             The default is None, in which case all configs are used!
         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
@@ -1121,32 +1020,46 @@ def run_finaloutput(
 
             for information on the level values see:
                 https://docs.python.org/3/library/logging.html#logging-levels
+
+            The default is 21 (=logging.PROGRESS), in which case only
+            e.g. only RT1 progress-messages, warnings and errors will be logged
         fitlist : list, optional
             optional way to provide a list of paths to dump-files directly
             (useful if RTprocess is used without a config attached)
             The default is None, in which case the dump-files will be
             identified according to the definitions in the config-file.
+        save_path : str, optional
+            the path where the finaloutput file will be stored
+            if None, the path provided in the ".ini" file will be used
+            (e.g. "dumpfolder / results / finalout_name.h5" )
+        create_index : bool or dict, optional
+            indicator if the created HDF-store should be indexed or not.
+
+            - if True a index is created for the first index-column
+              (this can take quite some time but speeds up querying a lot!)
+            - if a dict is provided, it is used as kwargs for
+              `_rtprocess_writer.RT1_processor.create_index()`
+              (e.g. the defaults are:  idx_levels=None and keys=None)
+        use_dumps : bool, optional
+            indicatior if the "fit_db.h5" file or the ".dump" files in the
+            "dumps" folder should be used to load the Fits-objects
+            The default is False in which case "fit_db.h5" is used
+        **kwargs :
+            additional kwargs passed to
+            `_rtprocess_writer.RT1_processor()`
+            (n_combiner, n_writer, HDF_kwargs, write_chunk_size,
+             out_cache_size)
         """
-        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
+        # check if provided postprocess function is a callable
+        if postprocess is not None:
+            assert callable(postprocess), "postprocess must be callable!"
 
+        try:
             # override finalout_name
             # do this BEFORE self.setup() is called !!
             if finalout_name is not None:
                 assert isinstance(finalout_name, str), "finalout_name must be a string"
-                # self.proc_cls.finalout_name = finalout_name
                 self.override_config(
                     PROCESS_SPECS=dict(finalout_name=finalout_name), override=False
                 )
@@ -1157,44 +1070,76 @@ def run_finaloutput(
             self.setup()
             self.autocontinue = initial_autocontinue
 
-            if logfile_level is not None and ncpu > 1:
+            # if "save-path" is not provided explicitly, use the path from the config
+            if save_path is None:
+                save_path = (
+                    self.proc_cls.rt1_procsesing_respath / self.proc_cls.finalout_name
+                )
+
+            if logfile_level is not None:
+                # start a listener-process that takes care of the logs from
+                # multiprocessing workers
+                manager = mp.Manager()
+                queue = manager.Queue(-1)
+                # a counter to name the multiprocess-workers
+                proc_counter = manager.Value(ctypes.c_ulonglong, 0)
+
+                listener = mp.Process(
+                    target=self._listener_process,
+                    args=[queue, self.dumppath, logfile_level],
+                    name="RTprocess logger",
+                )
+
+                # 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, loglevel=logfile_level)
+            else:
+                queue, proc_counter = None, None
+
+            if logfile_level is not None:
                 # 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 not fitlist:
-                fitlist = self._get_files(use_N_files=use_N_files)
-
-            return self._run_finalout(
+            if fitlist is None:
+                # use fits from .dump file if no fitlist is provided
+                fitlist = self._get_files(use_N_files=use_N_files, use_dumps=use_dumps)
+            res = self._run_finalout(
                 ncpu,
                 fitlist=fitlist,
+                save_path=save_path,
                 use_config=use_config,
-                finaloutput=finaloutput,
                 postprocess=postprocess,
                 queue=queue,
+                proc_counter=proc_counter,
                 print_progress=print_progress,
+                create_index=create_index,
+                init_func=self._worker_configurer,
+                init_args=(queue, logfile_level),
+                **kwargs,
             )
 
+            return res
+
         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)
+                # tell the queue to stop
+                queue.put_nowait(None)
 
-                    # stop the listener process
-                    listener.join()
+                # stop the listener process
+                listener.join()
 
-                # remove any remaining file-handler and queue-handlers after
-                # the processing is done
+                # remove any remaining file-and queue-handlers after 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!)
@@ -1202,224 +1147,508 @@ def run_finaloutput(
                     # remove the file-handler from the log
                     log.handlers.remove(h)
 
-    def _run_postprocess(
-        self, fitpath, use_config=None, process_cnt=None, postprocess=None
+    def _run_finalout(
+        self,
+        ncpu,
+        fitlist=None,
+        save_path=None,
+        use_config=None,
+        postprocess=None,
+        queue=None,
+        proc_counter=None,
+        print_progress=True,
+        create_index=True,
+        **kwargs,
     ):
-        if process_cnt is not None:
-            start = _start_cnt()
+        pool_kwargs = dict(
+            initializer=self._initializer,
+            initargs=[None, queue, proc_counter],
+        )
+
+        # pre-evaluate the fn-coefficients if interaction terms are used
+        # since finalout might involve calling "calc_model()" or similar
+        # functions that require a re-evaluation of the fn_coefficients
+        if hasattr(self, "parent_fit"):
+            if isinstance(self.parent_fit, MultiFits):
+                if use_config is None or use_config == "from_postprocess":
+                    cfgs = self.parent_fit.config_names
+                else:
+                    cfgs = use_config
+
+                for fit_name in cfgs:
+                    log.progress(f"pre-evaluation of coefficients for {fit_name}")
+                    parent_fit = self.parent_fit.accessor.config_fits[fit_name]
+                    if parent_fit.int_Q is True:
+                        parent_fit._fnevals_input = parent_fit.R._fnevals
+            else:
+                if self.parent_fit.int_Q is True:
+                    self.parent_fit._fnevals_input = self.parent_fit.R._fnevals
+
+        log.progress(f"start of finalout generation on {ncpu} cores")
+        with RT1_processor.writer_pool(
+            n_worker=ncpu, dst_path=save_path, **kwargs
+        ) as w:
+            res = w.run_starmap(
+                arg_list=fitlist,
+                reader_func=self._finalout_reader,
+                process_func=self._run_postprocess,
+                pool_kwargs=pool_kwargs,
+                starmap_args=[
+                    repeat(use_config),
+                    repeat(postprocess),
+                ],
+            )
+
+        if create_index:
+            if isinstance(create_index, dict):
+                RT1_processor.create_index(save_path, **create_index)
+            elif create_index is True:
+                RT1_processor.create_index(save_path)
 
-        # load fitobjects based on fit-paths
+        return res
+
+    def _finalout_reader(self, reader_arg, use_config=None):
+        ID = reader_arg["ID"]
+        # load fitobjects based on IDs
         try:
-            fit = load(fitpath)
-            if use_config is None and isinstance(fit, MultiFits):
+            fit = self._useres.load_fit(ID)
+            if fit is None:
+                return
+            if (use_config is None or use_config == "from_postprocess") and isinstance(
+                fit, MultiFits
+            ):
                 use_config = fit.config_names
 
-
             # assign pre-evaluated fn-coefficients
             if hasattr(self, "parent_fit"):
-                for config_name in use_config:
-                    config_fit = self.parent_fit.accessor.config_fits[config_name]
-                    if config_fit.int_Q is True:
-                        try:
-                            getattr(fit.configs, config_name)._fnevals_input = config_fit._fnevals_input
-                        except AttributeError:
-                            log.error("could not assign pre-evaluated fn-coefficients to " +
-                                      f"the config {config_name}... " +
-                                      "available configs of the loaded Fits object:" +
-                                      f" {fit.config_names}")
+                if isinstance(fit, MultiFits):
+                    for config_name in use_config:
+                        config_fit = self.parent_fit.configs[config_name]
+
+                        if config_fit.int_Q is True:
+                            try:
+                                fit.configs[
+                                    config_name
+                                ]._fnevals_input = config_fit._fnevals_input
+                            except AttributeError:
+                                log.error(
+                                    "could not assign pre-evaluated fn-coefficients to "
+                                    + f"the config {config_name}... "
+                                    + "available configs of the loaded Fits object:"
+                                    + f" {fit.config_names}"
+                                )
+
+                else:
+                    fit._fnevals_input = self.parent_fit._fnevals_input
 
+            # attach ID integer identifier to the Fits-object
+            fit._RT1_ID_num = reader_arg.get("_RT1_ID_num", None)
+            if isinstance(fit, MultiFits):
+                for cfg_fit in fit.configs:
+                    cfg_fit._RT1_ID_num = reader_arg.get("_RT1_ID_num", None)
 
+            return fit
         except Exception:
-            log.error(f"there was an error while loading {fitpath}")
-            _increase_cnt(process_cnt, start, err=True)
+            log.error(f"there was an error while loading {ID}", exc_info=True)
             return
-        # TODO fix reader_arg argument of postprocess
+
+    def _run_postprocess(self, fit, use_config=None, postprocess=None, *args):
+        # "from_postprocess" is used as indicator to use the function-output
+        #  directly (e.g. without using .apply to treat MultiFits objects etc.)
+        # -> only use if you know what you're doing!
+        if use_config == "from_postprocess":
+            use_func_directly = True
+            use_config = None
+        else:
+            use_func_directly = False
+
+        # apply postprocess function (with proper treatment of MultiFits)
         try:
             if isinstance(fit, MultiFits):
                 if postprocess is None:
+                    assert hasattr(self.proc_cls, "postprocess"), (
+                        "no explicit 'postprocess' function provided"
+                        + "and no 'postprocess' function found in processing_config class"
+                    )
                     ret = dict(
                         fit.apply(
                             self.proc_cls.postprocess,
                             use_config=use_config,
-                            reader_arg=fit.reader_arg,
                         )
                     )
+                elif use_func_directly:
+                    ret = postprocess(fit)
                 else:
                     ret = dict(
                         fit.apply(
                             postprocess,
                             use_config=use_config,
-                            reader_arg=fit.reader_arg,
                         )
                     )
             else:
+                if hasattr(fit, "config_name"):
+                    cfgname = fit.config_name
+                else:
+                    cfgname = "default"
+
                 # run postprocessing
                 if postprocess is None:
-                    ret = self.proc_cls.postprocess(fit, reader_arg=fit.reader_arg)
+                    if use_func_directly:
+                        ret = self.proc_cls.postprocess(fit)
+                    else:
+                        ret = {cfgname: self.proc_cls.postprocess(fit)}
                 else:
-                    ret = postprocess(fit, reader_arg=fit.reader_arg)
+                    if use_func_directly:
+                        ret = postprocess(fit)
+                    else:
+                        ret = {cfgname: postprocess(fit)}
+
+            if "const__reader_arg" not in ret and hasattr(fit, "reader_arg"):
+                # append reader_args
+                attrs = pd.DataFrame(fit.reader_arg, [fit._RT1_ID_num])
+                attrs.index.name = "ID"
 
-            if process_cnt is not None:
-                _increase_cnt(process_cnt, start, err=False)
+                # attrs = pd.DataFrame(fit.reader_arg, index=[0])
+                # attrs.set_index(self.proc_cls.ID_key, inplace=True)
+                ret["const__reader_arg"] = attrs
 
             return ret
 
         except Exception as ex:
             if hasattr(self, "proc_cls") and 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
 
-    def _get_files(self, use_N_files=None):
-        # get the relevant files that have to be processed for `run_finaloutput`
-
-        useres = getattr(RTresults(self.dumppath), self.proc_cls.dumpfolder)
+    def _get_files(self, use_N_files=None, use_dumps=False):
+        if not hasattr(self, "_useres"):
+            self._useres = getattr(
+                RTresults(self.dumppath, use_dumps=use_dumps), self.proc_cls.dumpfolder
+            )
         # get dump-files
-        dumpiter = useres.dump_files
+        dumpiter = self._useres.dump_files
 
         if use_N_files is not None:
-            fitlist = list(islice(dumpiter, use_N_files))
+            fitlist = list(map(lambda x: dict(ID=x), islice(dumpiter, use_N_files)))
         else:
-            fitlist = list(dumpiter)
+            fitlist = list(map(lambda x: dict(ID=x), dumpiter))
         return fitlist
 
-    def _run_finalout(
-        self,
-        ncpu,
-        fitlist=None,
-        use_config=None,
-        finaloutput=None,
-        postprocess=None,
-        queue=None,
-        print_progress=True,
-    ):
-
-        # override postprocess function
-        if postprocess is not None:
-            assert callable(postprocess), "postprocess must be callable!"
+    @staticmethod
+    def _defdict_parser(defdict):
+        """
+        get parameter-dynamics specifications from a given defdict
 
-        # don't call additional initializers, only use the default initializer
-        # (to enable subprocess-logging)
-        pool_kwargs = dict(initializer=self._initializer, initargs=[None, queue])
+        Parameters
+        ----------
+        defdict : dict
+            a defdict (e.g. fit.defdict)
 
-        # pre-evaluate the fn-coefficients if interaction terms are used
-        # since finalout might involve calling "calc_model()" or similar functions
-        # that require a re-evaluation of the fn_coefficients
-        if hasattr(self, "parent_fit"):
-            if isinstance(self.parent_fit, MultiFits):
-                if use_config is None:
-                    use_config = self.parent_fit.config_names
+        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 fit_name in use_config:
-                    parent_fit = self.parent_fit.accessor.config_fits[fit_name]
-                    if parent_fit.int_Q is True:
-                        parent_fit._fnevals_input = parent_fit.R._fnevals
+        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[2] == "manual":
+                        parameter_specs["fitted_dynamic_manual"].append(key)
+                    elif val[2] == "index":
+                        parameter_specs["fitted_dynamic_index"].append(key)
+                    elif isinstance(val[2], str):
+                        parameter_specs["fitted_dynamic_datetime"].append((key, val[2]))
+                    elif isinstance(val[2], int):
+                        parameter_specs["fitted_dynamic_integer"].append((key, val[2]))
             else:
-                if self.parent_fit.int_Q is True:
-                    self.parent_fit._fnevals_input = self.parent_fit.R._fnevals
+                if val[1] == "auxiliary":
+                    parameter_specs["auxiliary"].append(key)
+                else:
+                    parameter_specs["constant"].append(key)
 
-        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.progress(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(use_config),
-                        repeat(process_cnt),
-                        repeat(postprocess),
-                    ),
-                )
+        return parameter_specs
 
-                pool.close()  # Marks the pool as closed.
-                pool.join()  # Waits for workers to exit.
-                res = res_async.get()
-        else:
-            log.progress("start of single-core finalout generation")
-            # force autocontinue=True when calling the initializer since setup() has
-            # already been called in the main process so all folders will already exist!
-            init_autocontinue = self.autocontinue
-            self.autocontinue = True
+    def export_data_to_HDF(
+        self,
+        parameters=None,
+        metrics=None,
+        export_functions=None,
+        use_config=None,
+        dumpfolder=None,
+        use_nfiles=None,
+        ncpu=1,
+        sig_to_dB=False,
+        inc_to_degree=False,
+        pre_evaluate_fn_coefs=True,
+        finalout_name=None,
+        savepath=None,
+        **kwargs,
+    ):
+        """
+        a convenience-method to export parameters and performance-metrics
+        from a collection of rtfits.Fits objects into a HDF-store
 
-            # 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"]()
+        Parameters
+        ----------
+        parameters : list
+            a list of parameter-names to attach.
+            can be any parameter available in "fit.dataset", "fit.res_df", or
+            any of the calculated model-contributions, e.g.:
 
-            self.autocontinue = init_autocontinue
-            res = []
-            for fitpath in fitlist:
-                res.append(
-                    self._run_postprocess(
-                        fitpath=fitpath,
-                        use_config=use_config,
-                        process_cnt=process_cnt,
-                        postprocess=postprocess,
-                    )
+            >>> ["tot", "surf", "vol", "inter"]
+
+            or any parameter whose export-function has been provided via
+            cumstom defined "export_functions"
+
+        metrics : dict
+            a dict of metrics to calculate between model-parameters and dataset-keys
+
+            - key: the name to use for the metric in the returned dataset
+            - value: a tuple of the form:
+                     (metric, parameter 1, parameter 2)
+                     if `value = "custom"` the function provided in
+                     `export_functions`will be used
+
+            >>> dict(R = ("pearson", "sig", "tot"),
+            >>>      RMSD = ("rmsd", "sig", "tot"),
+            >>>      X = "custom"    # evaluated via export_functions["X"]
+            >>>     )
+
+
+        export_functions = dict, optional
+            a dict with functions that will be used to export the
+            parameter- and/or metric-values.
+
+            parameter-functions must return a pandas.DataFrame!
+            metric-functions must return an int or float
+
+            NOTE: this will override the default extraction-procedures!
+            NOTE: the functions must be pickleable for parallel processing!
+
+            >>> dict(sig=lambda fit: fit.dataset.sig)
+
+        use_config : list, optional
+            a list of configs to use in case a multi-config fit has been performed.
+            The default is None.
+        dumpfolder : str, optional
+            the dumpfolder to use. (must be specified if more than 1 result is found)
+            The default is None.
+        use_nfiles : int, optional
+            the number of files to process.
+            The default is None in which case ALL files are processed!
+        ncpu : int, optional
+            the number of cpu's to use. The default is 1.
+        sig_to_dB : bool, optional
+            indicator if sigma0 datasets ("sig", "tot", "surf", "vol", "inter")
+            should be converted to dB
+        inc_to_degrees : str, optional
+            indicator if incidence-angle datasets ("inc") should be converted to
+            degrees
+        pre_evaluate_fn_coefs : bool
+            indicator if the first Fits (or MultiFits) object should be used to
+            pre-evaluate fn-coefficients required to evaluate interaction-terms.
+            Note: if this is set to False, the coefficients have to be evaluated
+            for every single file which can cause a major reduction in speed!
+            The default is True.
+        finalout_name : str, optional
+            override the finalout_name provided in the ".ini" file
+            (only relevant if savepath is not specified)
+        save_path : str, optional
+            the path where the output file will be stored
+            if None, the path provided in the ".ini" file will be used
+            (e.g. "dumpfolder / results / < finalout_name >.h5" )
+        **kwargs :
+            additional kwargs passed to `RTprocess.run_finaloutput()`
+            and further to `_rtprocess_writer.RT1_processor()`
+            (n_combiner, n_writer, HDF_kwargs, write_chunk_size,
+             out_cache_size)
+        """
+        if not parameters:
+            parameters = []
+
+        if not metrics:
+            metrics = dict()
+
+        # ----- separate model-keys from the provided parameters
+        model_keys = set(parameters) & set(["tot", "surf", "vol", "inter"])
+        parameters = set(parameters) ^ model_keys
+
+        # ----- initialize a RTresults object for easy access to the list of dump-files
+        # don't use "self.dumppath" since it requires a call to setup() !
+        if not hasattr(self, "cfg"):
+            specs = RT1_configparser(self.config_path).get_process_specs()
+        else:
+            specs = self.cfg.get_process_specs()
+        res = RTresults(
+            specs["save_path"] / specs["dumpfolder"],
+            use_dumps=kwargs.get("use_dumps", False),
+        )
+
+        # ----- get dumpfolder to use
+        if not dumpfolder:
+            if len(res._paths) > 1:
+                log.error(
+                    "there is more than 1 possible dumpfolder!\n"
+                    + f"please explicitly specify one of:\n{list(res._paths)}"
                 )
-        log.progress("... generating finaloutput")
-
-        # in case no config is provided return the output to the console
-        if not hasattr(self, "proc_cls"):
-            if isinstance(res[0], dict):
-                out = dict()
-                if use_config is None:
-                    use_config = res[0].keys()
-                for name in use_config:
-                    if callable(finaloutput):
-                        out[name] = finaloutput((i.get(name) for i in res if i is not None))
+                return
             else:
-                if callable(finaloutput):
-                    out = finaloutput(res)
-                else:
-                    out = res
-
-            return out
-
-        # in case of a multi-config, results will be dicts!
-        # if isinstance(self.parent_fit, MultiFits):
-        if isinstance(res[0], dict):
-            # store original finalout_name
-            finalout_name, ending = self.proc_cls.finalout_name.split(".")
-
-            for name in use_config:
-                self.proc_cls.finalout_name = f"{finalout_name}__{name}.{ending}"
-                self.proc_cls.config_name = name
-                if callable(finaloutput):
-                    finaloutput((i.get(name) for i in res if i is not None))
-                elif hasattr(self.proc_cls, "finaloutput"):
-                    self.proc_cls.finaloutput(
-                        (i.get(name) for i in res if i is not None)
-                    )
+                dumpfolder = list(res._paths)[0]
+                log.progress(f"exporting parameters from '{dumpfolder}' dumpfolder")
 
-            # reset initial finalout_name
-            self.proc_cls.finalout_name = f"{finalout_name}.{ending}"
+        # ----- get list of paths to fit-objects
+        self._useres = getattr(res, dumpfolder)
+        # load the first fit-object to pre-load fn-coefficients
+        if hasattr(self._useres, "fit_db"):
+            fit0_ID = self._useres.fit_db.IDs.values.flat[0]
         else:
-            if callable(finaloutput):
-                return finaloutput(res)
-                log.info("finished generation of finaloutput!")
-            elif hasattr(self.proc_cls, "finaloutput"):
-                return self.proc_cls.finaloutput(res)
+            fit0_ID = 0
+        fit0 = self._useres.load_fit(fit0_ID)
+
+        fn_evals = None
+        if pre_evaluate_fn_coefs:
+            log.progress("... pre-evaluation of fn-coefficients")
+
+            if isinstance(fit0, MultiFits):
+                fn_evals = dict()
+                for name, fit_cfg in fit0.accessor.config_fits.items():
+                    if fit_cfg.int_Q is True:
+                        fn_evals[name] = fit_cfg.R._fnevals
+            elif fit0.int_Q is True:
+                fn_evals = fit0.R._fnevals
+
+        # ----- set postprocess and finalout functions
+        func = partial(
+            self._HDF_export_postprocess,
+            parameters=parameters,
+            metrics=metrics,
+            export_functions=export_functions,
+            model_keys=model_keys,
+            _fnevals_input=fn_evals,
+        )
+
+        # ----- run finalout generation
+        out = self.run_finaloutput(
+            ncpu=ncpu,
+            use_N_files=use_nfiles,
+            use_config=use_config,
+            finalout_name=finalout_name,
+            postprocess=func,
+            save_path=savepath,
+            **kwargs,
+        )
+
+        return out
+
+    @staticmethod
+    def _HDF_export_postprocess(
+        fit,
+        parameters=[],
+        metrics=None,
+        export_functions=None,
+        model_keys=[],
+        sig_to_dB=False,
+        inc_to_degree=False,
+        _fnevals_input=None,
+    ):
+        """
+        Parameters
+        ----------
+
+        fit: rt1.rtfits.Fits object
+            The fits object.
+        reader_arg: dict
+            the arguments passed to the reader function.
+        parameters : list
+            a list of parameter-names to attach.
+        metrics : dict
+            key: the name to use for the metric in the returned dataset
+            value: a tuple of the form:
+                  (metric, parameter 1, parameter 2)
+        export_fuinctions : dict
+            a dict of functions to use for exporting the parameter
+        model_keys : list
+            a list of keys that correspond to model-calculation results
+            (e.g. any of ["tot", "surf", "vol", "inter"])
+        _fnevals_input : dict or callable
+            pre-evaluated fnevals functions. in case fit is a MultiFits object:
+            a dict with the config-names and pre-evaluated fnevals functions
+        Returns
+        -------
+        df: pandas.DataFrame
+            a xarray.Dataset containing the fitted parameterss.
+
+        """
+
+        # assign pre-evaluated fn-coefficients
+        if _fnevals_input:
+            if hasattr(fit, "config_name"):
+                if fit.int_Q is True and fit.config_name in _fnevals_input:
+                    fit._fnevals_input = _fnevals_input[fit.config_name]
             else:
-                return res
+                fit._fnevals_input = _fnevals_input
 
-    def _postprocess_xarray(
-        self,
+        auxdata, staticlayers = dict(), dict()
+        if model_keys:
+            auxdata = fit.calc_model(return_components=True)[model_keys]
+
+        if export_functions:
+            for key, func in export_functions.items():
+                if key in parameters:
+                    val = func(fit)
+                    if isinstance(val, (int, float, np.number)):
+                        staticlayers[key] = val
+                        parameters.remove(key)
+                    else:
+                        auxdata[key] = val
+
+        ret = RTprocess._postprocess_getparams(
+            fit=fit,
+            saveparams=set(parameters) ^ set(auxdata) ^ set(model_keys),
+            xindex=("ID", fit._RT1_ID_num),
+            auxdata=pd.DataFrame(auxdata),
+            sig_to_dB=sig_to_dB,
+            inc_to_degree=inc_to_degree,
+            staticlayers=staticlayers,
+        )
+
+        # attach metrics if provided
+        if metrics:
+            metric_layer = dict()
+            for name, p in metrics.items():
+                if export_functions and name in export_functions and p == "custom":
+                    metric_layer[name] = export_functions[name](fit)
+                else:
+                    metric_layer[name] = getattr(
+                        getattr(getattr(fit.metric, p[1]), p[2]), p[0].lower()
+                    )
+            metric_layer = pd.DataFrame(metric_layer, index=[fit._RT1_ID_num])
+            metric_layer.index.name = "ID"
+
+            ret["metrics"] = metric_layer
+
+        return ret
+
+    @staticmethod
+    def _postprocess_getparams(
         fit,
         saveparams=None,
         xindex=("x", -9999),
@@ -1427,7 +1656,7 @@ def _postprocess_xarray(
         staticlayers=None,
         auxdata=None,
         sig_to_dB=False,
-        inc_to_degree=False
+        inc_to_degree=False,
     ):
         """
         the identification of parameters is as follows:
@@ -1470,12 +1699,6 @@ def _postprocess_xarray(
         inc_to_degree : bool
             indicator if incidence-angle values (e.g. "inc")
             should be converted to degrees
-
-        Returns
-        -------
-        dfxar : xarray.Dataset
-            a xarray-dataset with all layers defined according to the specs.
-
         """
         if saveparams is None:
             saveparams = []
@@ -1483,11 +1706,12 @@ def _postprocess_xarray(
         if staticlayers is None:
             staticlayers = dict()
 
-        defs = self._defdict_parser(fit.defdict)
+        ret = dict()  # dict that holds the return-values
+
+        defs = RTprocess._defdict_parser(fit.defdict)
 
         usedfs = []
         for key in saveparams:
-
             if key == "sig":
                 if fit.dB is False and sig_to_dB:
                     usedfs.append(10.0 * np.log10(fit.dataset.sig))
@@ -1502,13 +1726,15 @@ def _postprocess_xarray(
                 elif key in defs["fitted_const"]:
                     staticlayers[key] = fit.res_dict[key][0]
                 elif key in defs["constant"]:
-                    staticlayers[key] = fit.defdict[key][1]
+                    staticlayers[key] = fit.defdict[key].val.value
                 elif key in defs["auxiliary"]:
                     usedfs.append(fit.dataset[key])
             elif key in fit.dataset:
-                if (key in ["tot", "surf", "vol", "inter"] and
-                    fit.dB is False and
-                    sig_to_dB):
+                if (
+                    key in ["tot", "surf", "vol", "inter"]
+                    and fit.dB is False
+                    and sig_to_dB
+                ):
                     usedfs.append(10.0 * np.log10(fit.dataset[key]))
                 else:
                     usedfs.append(fit.dataset[key])
@@ -1523,374 +1749,34 @@ def _postprocess_xarray(
         if auxdata is not None and len(auxdata) > 0:
             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
-
-    @staticmethod
-    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[2] == "manual":
-                        parameter_specs["fitted_dynamic_manual"].append(key)
-                    elif val[2] == "index":
-                        parameter_specs["fitted_dynamic_index"].append(key)
-                    elif isinstance(val[2], str):
-                        parameter_specs["fitted_dynamic_datetime"].append((key, val[2]))
-                    elif isinstance(val[2], int):
-                        parameter_specs["fitted_dynamic_integer"].append((key, val[2]))
-            else:
-                if val[1] == "auxiliary":
-                    parameter_specs["auxiliary"].append(key)
-                else:
-                    parameter_specs["constant"].append(key)
-
-        return parameter_specs
-
-    def _export_postprocess(self,
-                            fit,
-                            reader_arg=None,
-                            parameters=[],
-                            metrics=None,
-                            export_functions=None,
-                            model_keys=[],
-                            index_col='ID',
-                            sig_to_dB=False,
-                            inc_to_degree=False,
-                            _fnevals_input=None
-                            ):
-        """
-        Parameters
-        ----------
-
-        fit: rt1.rtfits.Fits object
-            The fits object.
-        reader_arg: dict
-            the arguments passed to the reader function.
-        parameters : list
-            a list of parameter-names to attach.
-        metrics : dict
-            key: the name to use for the metric in the returned dataset
-            value: a tuple of the form:
-                  (metric, parameter 1, parameter 2)
-        export_fuinctions : dict
-            a dict of functions to use for exporting the parameter
-        model_keys : list
-            a list of keys that correspond to model-calculation results
-            (e.g. any of ["tot", "surf", "vol", "inter"])
-        index_col : str
-            the name of the reader-arg value to use as index
-        _fnevals_input : dict or callable
-            pre-evaluated fnevals functions. in case fit is a MultiFits object:
-            a dict with the config-names and pre-evaluated fnevals functions
-        Returns
-        -------
-        df: pandas.DataFrame
-            a xarray.Dataset containing the fitted parameterss.
-
-        """
-        if reader_arg is None:
-            reader_arg = fit.reader_arg
-
-        # assign pre-evaluated fn-coefficients
-        if _fnevals_input:
-            if hasattr(fit, "config_name"):
-                if fit.int_Q is True and fit.config_name in _fnevals_input:
-                    fit._fnevals_input = _fnevals_input[fit.config_name]
-
-        staticlayers = dict()
-        if metrics:
-            for name, p in metrics.items():
-                staticlayers[name] = getattr(
-                    getattr(getattr(fit.metric, p[1]), p[2]), p[0].lower())
-
-        auxdata = dict()
-        if model_keys:
-            auxdata = fit.calc_model(return_components=True)[model_keys]
-
-        if export_functions:
-            for key, func in export_functions.items():
-                auxdata[key] = func(fit)
-
-        ret = self._postprocess_xarray(
-            fit=fit,
-            saveparams=set(parameters) ^ set(auxdata) ^ set(model_keys),
-            xindex=(index_col, reader_arg[index_col]),
-            staticlayers=staticlayers,
-            auxdata=pd.DataFrame(auxdata),
-            sig_to_dB=sig_to_dB,
-            inc_to_degree=inc_to_degree
-        )
-
-        return ret
-
-    @staticmethod
-    def _export_finalout(res, savepath=None, attrs=None, descriptions=None,
-                         encoding=None, concat_dim="ID"):
-
-        resxar = xar.combine_nested([i for i in res if i is not None],
-                                    concat_dim=concat_dim)
-        if attrs:
-            for key, val in attrs.items():
-                resxar.attrs[key] = val
-
-        if descriptions:
-            for p, a in descriptions.items():
-                for key, val in a.items():
-                    getattr(resxar, p).attrs[key] = val
-
-        if savepath:
-            if not encoding:
-                encoding = {key: {"zlib": True, "complevel": 1}
-                            for key in resxar.data_vars}
-
-            log.info(
-                "export of NetCDF file at location:\n"
-                + '"'
-                + savepath
-                + '"'
-            )
+        if len(usedfs) > 0:
+            # combine all timeseries and set the proper index
+            df = pd.concat(usedfs, axis=1)
+            df.columns.names = ["param"]
+            df.index.names = ["date"]
 
-            resxar.to_netcdf(savepath, encoding=encoding)
-        else:
-            return resxar
-
-    def export_data(self, parameters=None, metrics=None,
-                    attributes=None, export_functions=None,
-                    index_col="ID", use_config=None, dumpfolder=None,
-                    use_nfiles=None, ncpu=1, sig_to_dB=False, inc_to_degree=False,
-                    pre_evaluate_fn_coefs=True, savepath=None):
-        """
-        a convenience-method to export parameters and performance-metrics
-        from a collection of rtfits.Fits objects
-
-        Parameters
-        ----------
-        parameters : list or dict
-            a list (or dict) of parameter-names to attach.
-            can be any parameter available in "fit.dataset", "fit.res_df", any of
-            the model-contributions, e.g.: ["tot", "surf", "vol", "inter"] or any
-            parameter whose export-function has been provided via "export_functions"
-
-            To attach descriptions, use a dict of dicts of the form:
-            >>> dict(sig = dict(long_name = "sigma0 data",
-            >>>                 units = "dB"),
-            >>>      inc = dict(long_name = "incidence angle",
-            >>>                 units = "degrees"))
-
-        metrics : dict
-            a dict of metrics to calculate between model-parameters and dataset-keys
-
-            - key: the name to use for the metric in the returned dataset
-            - value: a tuple of the form (description is optional):
-                     (metric, parameter 1, parameter 2, [description-dict])
-
-            >>> dict(R = ("pearson", "sig", "tot",
-            >>>           dict(long_name="sig0 pearson correlation")))
-
-        export_functions = dict, optional
-            a dict with functions that will be used to export the parameter-values
-            (Note that this will override the default extraction-procedures!)
-
-            >>> dict(sig=lambda fit: fit.dataset.sig)
-
-        attributes : dict, optional
-            additional attributes to attach to the returned xarray.Dataset
-        index_col : str
-            the name of the reader-arg value to use as index
-        use_config : list, optional
-            a list of configs to use in case a multi-config fit has been performed.
-            The default is None.
-        dumpfolder : str, optional
-            the dumpfolder to use. (must be specified if more than 1 result is found)
-            The default is None.
-        use_nfiles : int, optional
-            the number of files to process.
-            The default is None in which case ALL files are processed!
-        ncpu : int, optional
-            the number of cpu's to use. The default is 1.
-        sig_to_dB : bool, optional
-            indicator if sigma0 datasets ("sig", "tot", "surf", "vol", "inter")
-            should be converted to dB
-        inc_to_degrees : str, optional
-            indicator if incidence-angle datasets ("inc") should be converted to
-            degrees
-        pre_evaluate_fn_coefs : bool
-            indicator if the first Fits (or MultiFits) object should be used to
-            pre-evaluate fn-coefficients required to evaluate interaction-terms.
-            Note: if this is set to False, the coefficients have to be evaluated
-            for every single file which can cause a major reduction in speed!
-            The default is True.
-        savepath : str
-            the path to store the exported NetCDF files
-        Returns
-        -------
-        out : xarray.Dataset
-            a xarray Dataset of the exported data.
-
-        """
-
-        if not parameters:
-            parameters = []
-
-        if not metrics:
-            metrics = dict()
-
-        # ----- set descriptions (and separate model-keys)
-        descriptions = dict()
-        if isinstance(parameters, dict):
-            model_keys = dict()
-            descriptions.update(parameters)
-
-            for key in ["tot", "surf", "vol", "inter"]:
-                if key in parameters:
-                    model_keys[key] = parameters.pop(key)
-        else:
-            model_keys = set(parameters) & set(["tot", "surf", "vol", "inter"])
-            parameters = set(parameters) ^ model_keys
-
-        for key, val in metrics.items():
-            if len(val) == 4:
-                descriptions[key] = val[3]
-
-        # ----- initialize a RTresults object for easy access to the list of dump-files
-        res = RTresults(self.config_path)
-
-        # ----- get dumpfolder to use
-        if not dumpfolder:
-            if len(res._paths) > 1:
-                log.error("there is more than 1 possible dumpfolder!\n" +
-                          f"please explicitly specify one of:\n{list(res._paths)}")
-                return
+            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]])
             else:
-                dumpfolder = list(res._paths)[0]
-                log.progress(f"exporting parameters from '{dumpfolder}' dumpfolder")
-
-        # ----- get list of paths to fit-objects
-        useres = getattr(res, dumpfolder)
-        useres.scan_folder()
-        fitlist = list(islice(useres.dump_files, use_nfiles))
-
-        # load the first fit-object to pre-load fn-coefficients
-        fit0 = useres.load_fit(0)
-
-        fn_evals = None
-        if pre_evaluate_fn_coefs:
-            log.progress("... pre-evaluation of fn-coefficients")
-
-            if isinstance(fit0, MultiFits):
-                fn_evals = dict()
-                for name, fit_cfg in fit0.accessor.config_fits.items():
-                    if fit_cfg.int_Q is True:
-                        fn_evals[name] = fit_cfg.R._fnevals
-            elif fit0.int_Q is True:
-                fn_evals = fit0.R._fnevals
+                df = pd.concat([df], keys=[xindex[1]], names=[xindex[0]])
 
-        # ----- set postprocess and finalout functions
-        func = partial(self._export_postprocess,
-                       parameters=parameters,
-                       metrics=metrics,
-                       export_functions=export_functions,
-                       model_keys=model_keys,
-                       index_col=index_col,
-                       _fnevals_input=fn_evals)
-
-        finalout = partial(self._export_finalout,
-                           descriptions=descriptions,
-                           attrs=attributes,
-                           concat_dim=index_col)
-
-        # ----- run finalout generation
-        out = self.run_finaloutput(
-            ncpu=ncpu,
-            use_N_files=use_nfiles,
-            use_config=use_config,
-            finalout_name=None,
-            finaloutput=finalout,
-            postprocess=func,
-            print_progress=True,
-            logfile_level=1,
-            fitlist=fitlist
-        )
+            ret["dynamic"] = df
 
-        # ----- attach model definition strings as attributes
-        if isinstance(fit0, MultiFits):
-            # remove dataset since we are not interested in site-specific infos
-            fit0.set_dataset(None)
-            for key, val in out.items():
-                val.attrs['model_definition'] = (
-                    fit0.accessor.config_fits[key]._model_definition)
-        else:
-            fit0.dataset = None
-            out.attrs['model_definition'] = fit0._model_definition
-
-        # if savepath is provided, save NetCDF files to disc
-        if savepath:
-            savepath = Path(savepath)
-            if isinstance(out, dict):
-                parent = savepath.parent
-                name = savepath.stem
-                suffix = savepath.suffix
-                for key, val in out.items():
-                    val.to_netcdf(parent / (name + "__" + key + suffix))
+        if len(staticlayers) > 0:
+            if yindex is not None:
+                # set static layers
+                statics = pd.DataFrame(
+                    staticlayers,
+                    index=pd.MultiIndex.from_product(
+                        iterables=[[xindex[1]], [yindex[1]]], names=["x", "y"]
+                    ),
+                )
             else:
-                out.to_netcdf(savepath)
-
-        return out
+                # set static layers
+                statics = pd.DataFrame(staticlayers, index=[xindex[1]])
+                statics.index.name = xindex[0]
 
+            ret["static"] = statics
 
+        return ret
diff --git a/rt1/rtresults.py b/rt1/rtresults.py
index bdd0d060..91defbe2 100644
--- a/rt1/rtresults.py
+++ b/rt1/rtresults.py
@@ -1,12 +1,20 @@
 # -*- coding: utf-8 -*-
 
 from itertools import islice
+from functools import wraps, lru_cache
+from collections import defaultdict
+import weakref
 import os
 from pathlib import Path
+from ast import literal_eval
+import gc
+
+import pandas as pd
 from numpy.random import choice
 
 from .rtparse import RT1_configparser
-from .rtfits import load
+from .rtfits import load, Fits, MultiFits
+from .general_functions import isidentifier
 from . import log
 
 
@@ -19,6 +27,9 @@
     )
 
 
+from contextlib import contextmanager
+
+
 class RTresults(object):
     """
     A class to provide easy access to processed results.
@@ -77,10 +88,15 @@ class RTresults(object):
     ----------
     parent_path : str
         the parent-path where the results are stored.
+    use_dumps : bool
+        indicator if the dumps-folder and the .dump-files or
+        the "fit_db.h5" container should be used to load Fits objects etc.
+        The default is False in which case the "fit_db.h5" file is used
     """
 
-    def __init__(self, parent_path):
+    def __init__(self, parent_path, use_dumps=False):
         self._parent_path = Path(parent_path)
+        self._use_dumps = use_dumps
 
         self._paths = dict()
         if self._check_folderstructure(self._parent_path):
@@ -93,6 +109,22 @@ def __init__(self, parent_path):
                 if p.is_dir():
                     self._addresult(Path(p.path))
 
+    def __iter__(self):
+        return (getattr(self, i) for i in self._paths)
+
+    def __getitem__(self, key):
+        assert (
+            key in self._paths
+        ), f"'{key}' not found... use one of {list(self._paths)}"
+
+        return getattr(self, key)
+
+    def __repr__(self):
+        s = [f"Parent path = {self._parent_path}", "Results:"]
+        s += [f"    - {i}" for i in self._paths]
+
+        return "\n".join(s)
+
     @staticmethod
     def _check_folderstructure(path):
         return all(
@@ -111,22 +143,68 @@ def _addresult(self, path):
             setattr(
                 self,
                 path.stem,
-                self._RT1_fitresult(path.stem, path),
+                self._RT1_fitresult(path.stem, path, self._use_dumps),
             )
 
     class _RT1_fitresult(object):
-        def __init__(self, name, path):
+        def __init__(self, name, path, use_dumps):
             self.name = name
             self.path = Path(path)
+            self._use_dumps = use_dumps
             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")
+            self._hdf_paths = self._get_results(".h5")
 
         def __iter__(self):
             return self.dump_files
 
+        def __repr__(self):
+            s = [str(self.name)]
+            if len(self._nc_paths) > 0:
+                s += ["NetCDF files:"]
+                s += [f"    - {i}" for i in self._nc_paths]
+            if len(self._hdf_paths) > 0:
+                s += ["HDF5 files:"]
+                s += [f"    - {i}" for i in self._hdf_paths]
+
+            return "\n".join(s)
+
+        def rescan(self, use_dumps=None):
+            """
+            re-scan the folder and reset the cache
+
+            Parameters
+            ----------
+            use_dumps : bool, optional
+                optionally change if dump-files or the fit_db HDF store
+                is used to load fits. The default is None.
+            """
+
+            if use_dumps is not None:
+                self._use_dumps = use_dumps
+            self._nc_paths = self._get_results(".nc")
+            self._hdf_paths = self._get_results(".h5")
+            self._reset_fit_db_cache()
+
+        @property
+        @lru_cache()
+        def fit_db(self):
+            assert (
+                "fit_db" in self._hdf_paths
+            ), '"fit_db.h5" not found in results-folder'
+
+            return self.load_hdf("fit_db")
+
+        def _reset_fit_db_cache(self):
+            """
+            clear the cached handler for "fit_db"
+            """
+            print("clearing cached fit_db handler")
+            self.__class__.fit_db.fget.cache_clear()
+
         @staticmethod
         def _check_dump_filename(p):
             return p.endswith(".dump") and "error" not in p.split(os.sep)[-1]
@@ -147,6 +225,39 @@ def _get_results(self, ending):
 
             return results
 
+        def load_hdf(self, result_name=None, **kwargs):
+            """
+            open a HDF5 file stored in the "results"-folder with
+            the HDFaccessor class (the file is opened in read-only mode!)
+
+            Parameters
+            ----------
+            result_name : str, optional
+                The name of the HDF-file (without a .h5 extension).
+                If None, and only 1 file is available, the available file
+                will be laoded. The default is None.
+            **kwargs :
+                kwargs passed to the initialization of the HDFaccessor class
+
+            Returns
+            -------
+            HDFaccessor : the HDFaccessor instance for the given file
+            """
+
+            results = self._get_results(".h5")
+            assert len(results) > 0, "no HDF file in the results folder!"
+
+            assert len(results) == 1 or result_name in results, (
+                "there is more than 1 HDF file... "
+                + 'provide "result_name":\n    - '
+                + "\n    - ".join(results.keys())
+            )
+
+            if result_name is None:
+                result_name = list(results.keys())[0]
+
+            return HDFaccessor(results[result_name], **kwargs)
+
         def load_nc(self, result_name=None, use_xarray=True):
             """
             open a NetCDF file stored in the "results"-folder
@@ -188,24 +299,33 @@ def load_nc(self, result_name=None, use_xarray=True):
             file = xar.open_dataset(results[result_name])
             return file
 
-        def load_fit(self, ID=0, return_ID=False):
+        def _load_fit_from_HDF(self, ID):
+            if not self.fit_db.store.is_open:
+                self._reset_fit_db_cache()
+            fit = self.fit_db.load_fit(ID)
+
+            return fit
+
+        def load_fit(self, ID=None, return_ID=False):
             """
-            load one of the available .dump-files located in the "dumps"
-            folder.  (using rt1.rtfits.load() )
+            load one of the available Fits (or MultiFits) object from the data
+            stored in the "results/fit_db.h5" file
 
-            Notice: the dump-files are generated using cloudpickle.dump()
-            and might be platform and environment-specific!
+            > alternatively load the .dump-file if "load_dumps" is set to "True"
+            > during initialization of the RTresults class
+            > Notice: the dump-files are generated using cloudpickle.dump()
+            > and might be platform and environment-specific!
 
             Parameters
             ----------
             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 str:  The name of the Fits object to be loaded
+                If int:  load the nth file found
                 if Path: the full path to the .dump file
-                If None: load a random file from the folder
-                         NOTE: in order to load a random file, the folder
-                         must be indexed first! (this might take some time for
+                         > Notice: if a full path is provided, the .dump file is loaded!
+                If None: load a random Fits object
+                         NOTE: if "load_dump" is True the folder must be indexed before
+                         you  can load a random file! (this might take some time for
                          very large amounts of files)
                          > call `scan_folder()` to re-scan the folder contents
                 The default is 0.
@@ -218,28 +338,37 @@ def load_fit(self, ID=0, return_ID=False):
             fit : rt1.rtfits.Fits
                 the loaded rt1.rtfits.Fits result.
             """
-
-            if isinstance(ID, Path):
-                filepath = ID
+            if self._use_dumps is True or isinstance(ID, Path):
+                if isinstance(ID, Path):
+                    filepath = ID
+                else:
+                    if ID is None:
+                        if not hasattr(self, "_dump_file_list"):
+                            self.scan_folder()
+
+                        filepath = Path(choice(self._dump_file_list))
+
+                        log.info(
+                            f"loading random ID ({filepath.stem}) from "
+                            + f" {self._n_dump_files} available files"
+                        )
+                    elif isinstance(ID, int):
+                        try:
+                            filepath = Path(next(islice(self.dump_files, ID, None)))
+                        except StopIteration:
+                            log.error("there is no '.dump' file available!")
+                            return
+                    elif isinstance(ID, str):
+                        if not ID.endswith(".dump"):
+                            ID += ".dump"
+                        filepath = self._dump_path / (ID)
+
+                fit = load(filepath)
             else:
-                if ID is None:
-                    if not hasattr(self, "_dump_file_list"):
-                        self.scan_folder()
-
-                    filepath = Path(choice(self._dump_file_list))
-
-                    log.info(
-                        f"loading random ID ({filepath.stem}) from "
-                        + f" {self._n_dump_files} available files"
-                    )
-                elif isinstance(ID, int):
-                    filepath = Path(next(islice(self.dump_files, ID, None)))
-                elif isinstance(ID, str):
-                    if not ID.endswith(".dump"):
-                        ID += ".dump"
-                    filepath = self._dump_path / (ID)
-
-            fit = load(filepath)
+                assert (
+                    isinstance(ID, (str, int)) or ID is None
+                ), "please provide either a string, int or 'None' as ID"
+                fit = self._load_fit_from_HDF(ID)
 
             if return_ID is True:
                 return (fit, filepath.stem)
@@ -289,17 +418,26 @@ def load_cfg(self, cfg_name=None):
         @property
         def dump_files(self):
             """
-            a generator returning the paths to the available dump-files
+            if use_dumps is True:
+                a generator returning the paths to the available dump-files
+            if use_dumps is False:
+                a generator returning the IDs of the fits available in "fit_db.h5"
 
             NOTICE: only files that do NOT contain "error" in the filename and
             whose file-ending is ".dump" are returned!
             """
-            for entry in os.scandir(self._dump_path):
-                if (entry.name.endswith('.dump')
-                    and entry.is_file()
-                    and 'error' not in entry.name):
 
-                    yield entry.path
+            if self._use_dumps:
+                for entry in os.scandir(self._dump_path):
+                    if (
+                        entry.name.endswith(".dump")
+                        and entry.is_file()
+                        and "error" not in entry.name
+                    ):
+                        yield entry.path
+            else:
+                for ID in self.fit_db.IDs.ID:
+                    yield ID
 
         @property
         def dump_fits(self):
@@ -309,12 +447,17 @@ def dump_fits(self):
             NOTICE: only files that do NOT contain "error" in the filename and
             whose file-ending is ".dump" are returned!
             """
-            for entry in os.scandir(self._dump_path):
-                if (entry.name.endswith('.dump')
-                    and entry.is_file()
-                    and 'error' not in entry.name):
-
-                    yield self.load_fit(entry.name)
+            if self._use_dumps:
+                for entry in os.scandir(self._dump_path):
+                    if (
+                        entry.name.endswith(".dump")
+                        and entry.is_file()
+                        and "error" not in entry.name
+                    ):
+                        yield self.load_fit(entry.name)
+            else:
+                for ID in self.fit_db.IDs.ID:
+                    yield self.load_fit(ID)
 
         @property
         def NetCDF_variables(self):
@@ -327,3 +470,532 @@ def NetCDF_variables(self):
                 print(f"\n################ result:  {r}.nc")
                 with self.load_nc(r) as ncfile:
                     print(ncfile)
+
+
+class HDFaccessor(object):
+    def __init__(self, path, set_ID=False, **kwargs):
+        """
+        accessor class to read HDF5 files stored during RT1 processing
+
+        Parameters
+        ----------
+        path : str
+            the path to the HDF file.
+        set_ID : bool
+            indicator if the ID should be recovered from self.IDs when
+            accessing a dataset or not.
+        **kwargs :
+            kwargs passed to pd.HDFstore().
+
+        Returns
+        -------
+        None.
+
+        """
+        self.store = pd.HDFStore(path=path, mode="r", **kwargs)
+
+        self.datasets = _data_container()
+        self.set_ID = set_ID
+
+        if "reader_arg" in self.store:
+            try:
+                # use .values to avoid the following issue:
+                # https://github.com/pandas-dev/pandas/issues/42818
+                self.IDs = pd.DataFrame(
+                    dict(
+                        ID=self.store.select_column("reader_arg", "ID")
+                        .apply(self._get_ID)
+                        .values
+                    ),
+                    self.store.select_column("reader_arg", "index").values,
+                )
+
+            except KeyError:
+                # TODO this is just for downward-compatibility... remove it!
+                self.IDs = self.store.select_column("reader_arg", "index")
+        else:
+            self.IDs = None
+
+        cfgkeys = defaultdict(list)
+        for key in self.store.keys():
+            if key.endswith("/meta"):
+                continue
+
+            keysplit = key.lstrip("/").split("/")
+            if len(keysplit) == 2:
+                cfgkeys[keysplit[0]].append(keysplit[1])
+            elif len(keysplit) == 1:
+                setattr(self.datasets, keysplit[0], _subselector(self, key))
+            else:
+                setattr(self.datasets, key, _subselector(self, key))
+
+        for cfg, keys in cfgkeys.items():
+            setattr(self.datasets, cfg, _cfgselector(self, cfg, keys))
+
+        # make load_fit function public if "init_dict" is found in the store
+        if "init_dict" in self.store:
+            self.load_fit = self._load_fit
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        self.store.close()
+        gc.collect()
+
+    def __del__(self):
+        self.store.close()
+        gc.collect()
+
+    def __repr__(self):
+        s = [f"Path: {self.store._path}"]
+        if self.IDs is not None:
+            s += ["", f"Number of IDs: {len(self.IDs)}"]
+            s += ["Data keys:"]
+        s += [
+            (
+                f"    - {key.lstrip('/') :<15}"
+                + f" (nrows: {self.store.get_storer(key).nrows})"
+            )
+            for key in self.store
+            if not key.endswith("meta")
+        ]
+
+        return "\n".join(s)
+
+    def close(self):
+        self.store.close()
+        gc.collect()
+
+    @contextmanager
+    def _numeric_ID(self):
+        # a contextmanager to force using numeric IDs
+        setID = self.set_ID
+        self.set_ID = False
+        try:
+            yield
+        finally:
+            self.set_ID = setID
+
+    @staticmethod
+    def _get_ID(ID):
+        if not isidentifier(str(ID)):
+            return f"RT1_{ID}"
+        else:
+            return str(ID)
+
+    def _load_fit(self, ID):
+        with self._numeric_ID():
+            if ID is None:
+                ID = choice(self.IDs.values.flat)
+
+            if isinstance(ID, str):
+                ID = self._ID_to_IDnum(ID)
+
+            try:
+                init_dict = self.datasets.init_dict.get_id(ID)
+            except Exception:
+                print("'init_dict' not present... fit can not be loaded")
+                return
+
+            try:
+                dataset = self.datasets.dataset.get_id(ID)
+            except Exception:
+                dataset = None
+                pass
+
+            try:
+                aux_data = self.datasets.aux_data.get_id(ID)
+            except Exception:
+                aux_data = None
+                pass
+
+            try:
+                reader_arg = self.datasets.reader_arg.get_id(ID)
+                reader_arg = reader_arg.loc[ID].to_dict()
+                if "ID" not in reader_arg:
+                    reader_arg["ID"] = self.IDs.loc[ID].values[0]
+            except Exception:
+                reader_arg = dict(ID=self.IDs.loc[ID].values[0])
+                pass
+
+            try:
+                res_dict = self.datasets.res_dict.get_id(ID)
+            except Exception:
+                res_dict = None
+                pass
+
+            if "cfg" in init_dict.index.names:
+                mf = MultiFits()
+                for cfg, cfg_attrs in init_dict.loc[ID].iterrows():
+                    attrs = {key: literal_eval(val) for key, val in cfg_attrs.items()}
+
+                    fit = Fits(**attrs)
+
+                    if res_dict is not None:
+                        # use dropna(how="all", axis=1) to make sure that parameters that
+                        # are fitted in one config but not in another are not added as
+                        # empty lists!
+                        fit.res_dict = {
+                            key: val.dropna().to_list()
+                            for key, val in res_dict.loc[ID]
+                            .loc[cfg]
+                            .dropna(how="all", axis=1)
+                            .items()
+                        }
+
+                    mf.add_config(cfg, fit)
+
+                # attach shared properties
+                if dataset is not None:
+                    mf.set_dataset(dataset.loc[ID])
+                if aux_data is not None:
+                    mf.set_aux_data(aux_data.loc[ID])
+                if reader_arg is not None:
+                    mf.set_reader_arg(reader_arg)
+
+                mf.set_ID(self.IDs.loc[ID].values[0])
+                return mf
+
+            else:
+                attrs = {
+                    key: literal_eval(val) for key, val in init_dict.loc[ID].items()
+                }
+                fit = Fits(**attrs)
+                fit.ID = self.IDs.loc[ID].values[0]
+
+                if dataset is not None:
+                    fit.dataset = dataset.loc[ID]
+
+                if aux_data is not None:
+                    fit.aux_data = aux_data.loc[ID]
+
+                if reader_arg is not None:
+                    fit.reader_arg = reader_arg
+
+                if res_dict is not None:
+                    fit.res_dict = {
+                        key: val.dropna().to_list()
+                        for key, val in res_dict.loc[ID].items()
+                    }
+                return fit
+
+    @staticmethod
+    def _chunks(lst, n):
+        """Yield successive n-sized chunks from lst."""
+        for i in range(0, len(lst), n):
+            yield lst[i : i + n]
+
+    def _get_vals(self, key, chunksize, start=None, **kwargs):
+        # TODO
+        idxlevels = self.store.get_storer(key).levels
+        if idxlevels == 1:
+            idxname = "index"
+        else:
+            idxname = idxlevels[0]
+
+        assert self.IDs is not None, "no IDs found in HDF-store"
+        if start:
+            assert start < len(self.IDs), (
+                f"start={start} is bigger than the number"
+                + f" of IDs ({len(self.IDs)})"
+            )
+        use_ids = self._chunks(self.IDs.index[slice(start, None)], chunksize)
+        for i, ids in enumerate(use_ids):
+            indexes = self.store.select_as_coordinates(
+                key=key, where=f"{idxname} in ids"
+            )
+            data = self.store.get_storer(key).read(where=indexes, **kwargs)
+            yield data
+
+    def _get_vals_with_IDs(self, key, use_ids, chunksize=1, **kwargs):
+        # TODO
+        idxlevels = self.store.get_storer(key).levels
+        if idxlevels == 1:
+            idxname = "index"
+        else:
+            idxname = idxlevels[0]
+
+        assert self.IDs is not None, "no IDs found in HDF-store"
+        use_ids = self._chunks(use_ids, chunksize)
+
+        for i, ids in enumerate(use_ids):
+            indexes = self.store.select_as_coordinates(
+                key=key, where=f"{idxname} in ids"
+            )
+            data = self.store.get_storer(key).read(where=indexes, **kwargs)
+            yield data
+
+    def _get_nids(
+        self, key, nids, start=None, read_chunksize=None, print_progress=False, **kwargs
+    ):
+        """
+        get the data for "nids" ID's as a pandas.DataFrame
+
+        Parameters
+        ----------
+        nids : int
+            the number of IDs to return.
+        start : int, optional
+            the number to start from.
+            The default is None in which case the selection starts from the first result
+        read_chunksize : int, optional
+            the chunksize used for retrieving the data. The default is None.
+        print_progress : bool, optional
+            indicator if a progress-message should be printed.
+            The default is False.
+        **kwargs :
+            kwargs passed to `storer.read()`
+
+        Returns
+        -------
+        pandas.DataFrame
+        """
+
+        return next(
+            self._get_nids_iter(
+                key=key,
+                nids=nids,
+                start=start,
+                read_chunksize=read_chunksize,
+                print_progress=print_progress,
+                **kwargs,
+            )
+        )
+
+    def _ID_to_IDnum(self, ID):
+        return int(self.IDs[(self.IDs == ID).values].index[0])
+
+    def _get_id(self, key, n, **kwargs):
+        """
+        return the data for a single ID
+
+        Parameters
+        ----------
+        n : int or str
+            if int: return the n^th fit from the database
+            if str: return the fit that corresponds to the provided ID.
+        **kwargs :
+            kwargs passed to get_nids_iter().
+
+        Returns
+        -------
+        ret : pandas.DataFrame
+        """
+        assert isinstance(n, (int, str)), "ID can only be an integer or string!"
+
+        if isinstance(n, str):
+            n = self._ID_to_IDnum(n)
+
+        n = self.IDs.index.get_loc(n)
+        ret = next(self._get_nids_iter(key=key, nids=1, start=n, **kwargs))
+
+        return ret
+
+    def _get_nids_iter(
+        self, key, nids, start=None, read_chunksize=None, print_progress=False, **kwargs
+    ):
+        """
+        return an iterator that iteratively yields "nids" IDs from the given key
+        until all data has been returned
+
+        Parameters
+        ----------
+        nids : int or iterable
+            if int: the number of IDs to return.
+            if iterable: a list of IDs to use
+        start : int, optional
+            the index-number to start from.
+
+            e.g. equivalent to `IDs[start : start + nids]`
+
+            The default is None.
+        read_chunksize : int, optional
+            the chunksize used for retrieving the data. The default is None.
+        print_progress : bool, optional
+            indicator if a progress-message should be printed.
+            The default is False.
+        **kwargs :
+            kwargs passed to `storer.read()`
+
+        Yields
+        ------
+        data : pandas.DataFrame
+            the retrieved DataFrame.
+
+        """
+        nids = min(len(self.IDs), nids)
+
+        assert (
+            key in self.store
+        ), f"invalid key provided, use one of {self.store.keys()}"
+
+        if read_chunksize is None:
+            s = self.store.get_storer(key)
+
+            if isinstance(nids, int):
+                if s.is_multi_index:
+                    read_chunksize = min(10, nids)
+                else:
+                    read_chunksize = nids
+            elif isinstance(nids, str):
+                read_chunksize = 1
+            else:
+                read_chunksize = len(nids)
+
+        if isinstance(nids, int):
+            data_gen = self._get_vals(
+                key=key, chunksize=read_chunksize, start=start, **kwargs
+            )
+        elif isinstance(nids, str):
+            nids = 1
+            data_gen = self._get_vals_with_IDs(
+                key=key, chunksize=read_chunksize, use_ids=[nids], **kwargs
+            )
+        else:
+            use_ids = nids
+            nids = len(use_ids)
+            data_gen = self._get_vals_with_IDs(
+                key=key, chunksize=read_chunksize, use_ids=use_ids, **kwargs
+            )
+
+        ret = []
+        for i, d in enumerate(data_gen):
+            ret.append(d)
+
+            if (i + 1) % (nids // read_chunksize) == 0:
+                if nids % read_chunksize != 0:
+                    data = next(data_gen)
+                    ret.append(iloc_level(data, slice(nids % 10)))
+
+                data = pd.concat(ret)
+                if self.set_ID is True:
+                    if isinstance(data.index, pd.MultiIndex):
+                        data.index = data.index.set_levels(
+                            self.IDs.loc[data.index.levels[0]].ID, level=0
+                        )
+                    else:
+                        data["ID"] = self.IDs.loc[data.index]
+                        data = data.set_index("ID")
+                yield data
+
+                if nids % read_chunksize != 0:
+                    ret = [iloc_level(data, slice(nids % 10, None))]
+                else:
+                    ret = []
+
+            if print_progress:
+                print("reading", (i + 1) * read_chunksize)
+
+        if len(ret) > 0:
+            # make sure to return the last results (maybe less than the read-chunksize)
+            yield pd.concat(ret)
+
+
+class _data_container(object):
+    def __init__(self):
+        pass
+
+
+class _cfgselector(object):
+    def __init__(self, parent, cfg, keys):
+        # use weak references to ensure proper closing of the file-handlers
+        # if parent-class is deleted
+        self._parent = weakref.ref(parent)
+        self._cfg = cfg
+        self._keys = keys
+
+        for key in self._keys:
+            setattr(self, key, _subselector(parent, f"{cfg}/{key}"))
+
+
+class _subselector(object):
+    def __init__(self, parent, key):
+        # use weak references to ensure proper closing of the file-handlers
+        # if parent-class is deleted
+        self._parent = weakref.ref(parent)
+        self._key = key
+
+    def select(self, **kwargs):
+        ret = self._parent().store.select(self._key, **kwargs)
+
+        if self._parent().set_ID is True:
+            if isinstance(ret.index, pd.MultiIndex):
+                ret.index = ret.index.set_levels(
+                    self._parent().IDs.loc[ret.index.levels[0]].ID, level=0
+                )
+            else:
+                ret["ID"] = self._parent().IDs.loc[ret.index]
+                ret = ret.set_index("ID")
+
+        return ret
+
+    @wraps(HDFaccessor._get_nids_iter)
+    def get_nids_iter(
+        self, nids, start=None, read_chunksize=None, print_progress=False, **kwargs
+    ):
+        return self._parent()._get_nids_iter(
+            key=self._key,
+            nids=nids,
+            start=start,
+            read_chunksize=read_chunksize,
+            print_progress=print_progress,
+            **kwargs,
+        )
+
+    @wraps(HDFaccessor._get_nids)
+    def get_nids(
+        self, nids, start=None, read_chunksize=None, print_progress=False, **kwargs
+    ):
+        return self._parent()._get_nids(
+            key=self._key,
+            nids=nids,
+            start=start,
+            read_chunksize=read_chunksize,
+            print_progress=print_progress,
+            **kwargs,
+        )
+
+    @wraps(HDFaccessor._get_id)
+    def get_id(self, n, **kwargs):
+        return self._parent()._get_id(key=self._key, n=n, **kwargs)
+
+
+def _sanitize_index(df, idx):
+    # convert inputs to desired indexers
+    # (so that we can use integers and None directly)
+    try:
+        idx = list(idx)
+    except TypeError:
+        idx = [idx]
+
+    for level, i in enumerate(idx):
+        if i is None:
+            yield df.index.levels[level][slice(None)]
+        elif isinstance(i, int):
+            yield df.index.levels[level][slice(i, i + 1)]
+        else:
+            yield df.index.levels[level][i]
+
+
+def iloc_level(df, idx):
+    """
+    iloc a MultiIndexed pandas.DataFrame
+    Parameters
+    ----------
+    df : pandas.DataFrame
+        the MultiIndexed dataframe to use.
+    idx : iterable
+        the indexers to use for the MultiIndex levels.
+        can be (None, int, list or slice)
+
+        >>> idx = [level_1 indexer, level_2 indexer, ...]
+
+        if None: all index-values will be used
+        if int: the n^th index-value is selected
+
+    Returns
+    -------
+    pandas.DataFrame
+    """
+    return df.iloc[df.index.get_locs(list(_sanitize_index(df, idx)))]
diff --git a/rt1/scatter.py b/rt1/scatter.py
index 34b96dcb..0d9a1d5b 100644
--- a/rt1/scatter.py
+++ b/rt1/scatter.py
@@ -4,6 +4,7 @@
 import sympy as sp
 import numpy as np
 
+
 class Scatter(object):
     """The basis object for any Surface and Volume objects"""
 
diff --git a/rt1/surface.py b/rt1/surface.py
index 52e7d167..fda6d740 100644
--- a/rt1/surface.py
+++ b/rt1/surface.py
@@ -11,6 +11,9 @@
 class Surface(Scatter):
     """basic surface class"""
 
+    name = "RT1_Surface_base_class"
+    _param_names = ["NormBRDF", "a"]
+
     def __init__(self, **kwargs):
         # set scattering angle generalization-matrix to [1,1,1] if it is not
         # explicitly provided by the chosen class.
@@ -26,6 +29,49 @@ def __init__(self, **kwargs):
         self.hemreflect = partial(hemreflect, SRF=self)
         update_wrapper(self.hemreflect, hemreflect)
 
+    def __repr__(self):
+        try:
+            return (
+                self.name
+                + "("
+                + (",\n" + " " * (len(self.name) + 1)).join(
+                    [f"{param}={getattr(self, param)}" for param in self._param_names]
+                )
+                + ")"
+            )
+        except Exception:
+            return object.__repr__(self)
+
+    @property
+    def init_dict(self):
+        if self.name.startswith("LinCombSRF"):
+            d = dict()
+            for key in self._param_names:
+                val = self.__dict__[key]
+                if isinstance(val, sp.Basic):
+                    d[key] = str(val)
+                else:
+                    d[key] = val
+            d["SRF_name"] = "LinCombSRF"
+            srfchoices = []
+            for frac, srf in d["SRFchoices"]:
+                if isinstance(frac, sp.Basic):
+                    srfchoices.append([str(frac), srf.init_dict])
+                else:
+                    srfchoices.append([frac, srf.init_dict])
+
+            d["SRFchoices"] = srfchoices
+        else:
+            d = dict()
+            for key in self._param_names:
+                val = self.__dict__[key]
+                if isinstance(val, sp.Basic):
+                    d[key] = str(val)
+                else:
+                    d[key] = val
+            d["SRF_name"] = self.name
+        return d
+
     @lru_cache()
     def _lambda_func(self, *args):
         # define sympy objects
@@ -83,7 +129,7 @@ def brdf(self, t_0, t_ex, p_0, p_ex, param_dict={}):
                 0.1,
                 0.1,
                 0.1,
-                **{key: 0.12 for key in param_dict.keys()}
+                **{key: 0.12 for key in param_dict.keys()},
             ),
             np.ndarray,
         ):
@@ -388,7 +434,7 @@ def brdf_theta_diff(
                     0.1,
                     0.1,
                     0.1,
-                    **{key: 0.12 for key in param_dict.keys()}
+                    **{key: 0.12 for key in param_dict.keys()},
                 ),
                 np.ndarray,
             ):
@@ -419,9 +465,9 @@ class LinCombSRF(Surface):
     """
 
     name = "LinCombSRF"
+    _param_names = ["SRFchoices", "NormBRDF"]
 
     def __init__(self, SRFchoices=None, **kwargs):
-
         super(LinCombSRF, self).__init__(**kwargs)
 
         self.SRFchoices = SRFchoices
@@ -438,7 +484,6 @@ def _func(self):
         """define phase function as sympy object for later evaluation"""
         return self._SRFcombiner()._func
 
-
     def _set_legexpansion(self):
         """set legexpansion to the combined legexpansion"""
 
@@ -500,7 +545,6 @@ def __init__(self, **kwargs):
         # evaluation of combined expansion in legendre-polynomials
         dummylegexpansion = []
         for i in range(0, len(equal_a)):
-
             SRFdummy = BRDFfunction()
             # select SRF choices where a parameter is equal
             SRFequal = np.take(self.SRFchoices, equal_a[i], axis=0)
@@ -509,7 +553,6 @@ def __init__(self, **kwargs):
             SRFdummy.ncoefs = max([SRF[1].ncoefs for SRF in SRFequal])
             # loop over phase-functions with equal a-parameter
             for SRF in SRFequal:
-
                 # set parameters based on chosen phase-functions and evaluate
                 # combined legendre-expansion
                 SRFdummy.a = SRF[1].a
@@ -542,7 +585,9 @@ class Isotropic(Surface):
                Normalization-factor used to scale the BRDF,
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
+
     name = "Isotropic"
+    _param_names = ["NormBRDF"]
 
     def __init__(self, **kwargs):
         super(Isotropic, self).__init__(**kwargs)
@@ -588,7 +633,9 @@ class CosineLobe(Surface):
                Normalization-factor used to scale the BRDF,
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
+
     name = "CosineLobe"
+    _param_names = ["i", "ncoefs", "NormBRDF", "a"]
 
     def __init__(self, ncoefs=None, i=None, a=[1.0, 1.0, 1.0], **kwargs):
         assert ncoefs is not None, (
@@ -684,7 +731,9 @@ class HenyeyGreenstein(Surface):
                Normalization-factor used to scale the BRDF,
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
+
     name = "HenyeyGreenstein"
+    _param_names = ["t", "ncoefs", "NormBRDF", "a"]
 
     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!"
@@ -715,15 +764,15 @@ def _func(self):
 
         return (
             1.0
-            * (1.0 - self.t ** 2.0)
-            / ((sp.pi) * (1.0 + self.t ** 2.0 - 2.0 * self.t * x) ** 1.5)
+            * (1.0 - self.t**2.0)
+            / ((sp.pi) * (1.0 + self.t**2.0 - 2.0 * self.t * x) ** 1.5)
         )
 
     @property
     @lru_cache()
     def legcoefs(self):
         n = sp.Symbol("n")
-        return 1.0 * (1.0 / (sp.pi)) * (2.0 * n + 1) * self.t ** n
+        return 1.0 * (1.0 / (sp.pi)) * (2.0 * n + 1) * self.t**n
 
 
 class HG_nadirnorm(Surface):
@@ -748,7 +797,9 @@ class HG_nadirnorm(Surface):
                Normalization-factor used to scale the BRDF,
                i.e.  BRDF = NormBRDF * f(t_0,p_0,t_ex,p_ex)
     """
+
     name = "HG_nadirnorm"
+    _param_names = ["t", "ncoefs", "NormBRDF", "a"]
 
     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!"
@@ -766,6 +817,8 @@ def __init__(self, t=None, ncoefs=None, a=[1.0, 1.0, 1.0], **kwargs):
             "Error: Generalization-parameter list must " + "contain 3 values"
         )
 
+        self._param_names = ["t", "ncoefs", "NormBRDF", "a"]
+
     @property
     @lru_cache()
     def _func(self):
@@ -778,25 +831,25 @@ def _func(self):
         x = self.scat_angle(theta_0, theta_ex, phi_0, phi_ex, a=self.a)
 
         nadir_hemreflect = 4 * (
-            (1.0 - self.t ** 2.0)
+            (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)
+                    (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.t**2.0
+                * sp.sqrt(1.0 + self.t**2.0 - 2.0 * self.a[0] * self.t)
             )
         )
 
         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)
+            (1.0 - self.t**2.0)
+            / ((sp.pi) * (1.0 + self.t**2.0 - 2.0 * self.t * x) ** 1.5)
         )
 
         return func
@@ -812,25 +865,22 @@ def _func_numeric(self, theta_0, theta_ex, phi_0, phi_ex, **kwargs):
         x = self._scat_angle_numeric(theta_0, theta_ex, phi_0, phi_ex, a=self.a)
 
         nadir_hemreflect = 4 * (
-            (1.0 - t ** 2.0)
+            (1.0 - t**2.0)
             * (
                 1.0
                 - t * (-t + self.a[0])
-                - np.sqrt(
-                    (1 + t ** 2 - 2 * self.a[0] * t) * (1 + t ** 2)
-                )
+                - np.sqrt((1 + t**2 - 2 * self.a[0] * t) * (1 + t**2))
             )
             / (
                 2.0
                 * self.a[0] ** 2.0
-                * t ** 2.0
-                * np.sqrt(1.0 + t ** 2.0 - 2.0 * self.a[0] * t)
+                * t**2.0
+                * np.sqrt(1.0 + t**2.0 - 2.0 * self.a[0] * t)
             )
         )
 
         func = (1.0 / nadir_hemreflect) * (
-            (1.0 - t ** 2.0)
-            / ((np.pi) * (1.0 + t ** 2.0 - 2.0 * t * x) ** 1.5)
+            (1.0 - t**2.0) / ((np.pi) * (1.0 + t**2.0 - 2.0 * t * x) ** 1.5)
         )
 
         return func
@@ -839,25 +889,25 @@ def _func_numeric(self, theta_0, theta_ex, phi_0, phi_ex, **kwargs):
     @lru_cache()
     def legcoefs(self):
         nadir_hemreflect = 4 * (
-            (1.0 - self.t ** 2.0)
+            (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)
+                    (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.t**2.0
+                * sp.sqrt(1.0 + self.t**2.0 - 2.0 * self.a[0] * self.t)
             )
         )
 
         n = sp.Symbol("n")
         legcoefs = (1.0 / nadir_hemreflect) * (
-            (1.0 / (sp.pi)) * (2.0 * n + 1) * self.t ** n
+            (1.0 / (sp.pi)) * (2.0 * n + 1) * self.t**n
         )
 
         return legcoefs
diff --git a/rt1/volume.py b/rt1/volume.py
index 580f4120..4c6d13bb 100644
--- a/rt1/volume.py
+++ b/rt1/volume.py
@@ -10,6 +10,9 @@
 class Volume(Scatter):
     """basic volume class"""
 
+    name = "RT1_Volume_base_class"
+    _param_names = ["omega", "tau", "a"]
+
     def __init__(self, **kwargs):
         self.omega = kwargs.pop("omega", None)
         self.tau = kwargs.pop("tau", None)
@@ -24,6 +27,49 @@ def __init__(self, **kwargs):
         self.polarplot = partial(polarplot, X=self)
         update_wrapper(self.polarplot, polarplot)
 
+    def __repr__(self):
+        try:
+            return (
+                self.name
+                + "("
+                + (",\n" + " " * (len(self.name) + 1)).join(
+                    [f"{param}={getattr(self, param)}" for param in self._param_names]
+                )
+                + ")"
+            )
+        except Exception:
+            return object.__repr__(self)
+
+    @property
+    def init_dict(self):
+        if self.name.startswith("LinCombV"):
+            d = dict()
+            for key in self._param_names:
+                val = self.__dict__[key]
+                if isinstance(val, sp.Basic):
+                    d[key] = str(val)
+                else:
+                    d[key] = val
+            d["V_name"] = "LinCombV"
+            srfchoices = []
+            for frac, srf in d["Vchoices"]:
+                if isinstance(frac, sp.Basic):
+                    srfchoices.append([str(frac), srf.init_dict])
+                else:
+                    srfchoices.append([frac, srf.init_dict])
+
+            d["Vchoices"] = srfchoices
+        else:
+            d = dict()
+            for key in self._param_names:
+                val = self.__dict__[key]
+                if isinstance(val, sp.Basic):
+                    d[key] = str(val)
+                else:
+                    d[key] = val
+            d["V_name"] = self.name
+        return d
+
     @lru_cache()
     def _lambda_func(self, *args):
         # define sympy objects
@@ -38,7 +84,6 @@ def _lambda_func(self, *args):
         pfunc = sp.lambdify(args, self._func, modules=["numpy", "sympy"])
         return pfunc
 
-
     def p(self, t_0, t_ex, p_0, p_ex, param_dict={}):
         """
         Calculate numerical value of the volume-scattering phase-function
@@ -81,7 +126,7 @@ def p(self, t_0, t_ex, p_0, p_ex, param_dict={}):
                 0.1,
                 0.1,
                 0.1,
-                **{key: 0.12 for key in param_dict.keys()}
+                **{key: 0.12 for key in param_dict.keys()},
             ),
             np.ndarray,
         ):
@@ -254,7 +299,7 @@ def p_theta_diff(
                     0.1,
                     0.1,
                     0.1,
-                    **{key: 0.12 for key in param_dict.keys()}
+                    **{key: 0.12 for key in param_dict.keys()},
                 ),
                 np.ndarray,
             ):
@@ -332,7 +377,6 @@ def legexpansion(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"
@@ -432,7 +476,9 @@ class LinCombV(Volume):
                (Volume-objects) and the associated weighting-factors
                (floats) of the linear-combination.
     """
+
     name = "LinCombV"
+    _param_names = ["Vchoices", "tau", "omega"]
 
     def __init__(self, Vchoices=None, **kwargs):
         super(LinCombV, self).__init__(**kwargs)
@@ -515,7 +561,6 @@ def __init__(self, **kwargs):
         # evaluation of combined expansion in legendre-polynomials
         dummylegexpansion = []
         for i in range(0, len(equal_a)):
-
             Vdummy = Phasefunction()
             # select V choices where a parameter is equal
             Vequal = np.take(self.Vchoices, equal_a[i], axis=0)
@@ -524,7 +569,6 @@ def __init__(self, **kwargs):
             Vdummy.ncoefs = max([V[1].ncoefs for V in Vequal])
             # loop over phase-functions with equal a-parameter
             for V in Vequal:
-
                 # set parameters based on chosen phase-functions and evaluate
                 # combined legendre-expansion
                 Vdummy.a = V[1].a
@@ -567,7 +611,9 @@ class Rayleigh(Volume):
         scat_angle() of the BRDF
         (http://rt1.readthedocs.io/en/latest/theory.html#equation-general_scat_angle)
     """
+
     name = "Rayleigh"
+    _param_names = ["tau", "omega"]
 
     def __init__(self, **kwargs):
         super(Rayleigh, self).__init__(**kwargs)
@@ -588,7 +634,7 @@ def _func(self):
         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)
-        return 3.0 / (16.0 * sp.pi) * (1.0 + x ** 2.0)
+        return 3.0 / (16.0 * sp.pi) * (1.0 + x**2.0)
 
     @property
     @lru_cache()
@@ -632,7 +678,9 @@ class HenyeyGreenstein(Volume):
         scat_angle() of the BRDF
         (http://rt1.readthedocs.io/en/latest/theory.html#equation-general_scat_angle)
     """
+
     name = "HenyeyGreenstein"
+    _param_names = ["tau", "omega", "ncoefs", "t", "a"]
 
     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!"
@@ -658,8 +706,8 @@ def _func(self):
         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)
-        func = (1.0 - self.t ** 2.0) / (
-            (4.0 * sp.pi) * (1.0 + self.t ** 2.0 - 2.0 * self.t * x) ** 1.5
+        func = (1.0 - self.t**2.0) / (
+            (4.0 * sp.pi) * (1.0 + self.t**2.0 - 2.0 * self.t * x) ** 1.5
         )
 
         return func
@@ -671,8 +719,8 @@ def _func_numeric(self, theta_0, theta_ex, phi_0, phi_ex, **kwargs):
         else:
             t = self.t
         x = self._scat_angle_numeric(theta_0, theta_ex, phi_0, phi_ex, self.a)
-        func = (1.0 - t ** 2.0) / (
-            (4.0 * np.pi) * (1.0 + t ** 2.0 - 2.0 * t * x) ** 1.5
+        func = (1.0 - t**2.0) / (
+            (4.0 * np.pi) * (1.0 + t**2.0 - 2.0 * t * x) ** 1.5
         )
 
         return func
@@ -685,7 +733,7 @@ def legcoefs(self):
         needs to be a function that can be later evaluated by subsituting 'n'
         """
         n = sp.Symbol("n")
-        legcoefs = (1.0 / (4.0 * sp.pi)) * (2.0 * n + 1) * self.t ** n
+        legcoefs = (1.0 / (4.0 * sp.pi)) * (2.0 * n + 1) * self.t**n
         return legcoefs
 
 
@@ -716,7 +764,9 @@ class HGRayleigh(Volume):
         scat_angle() of the BRDF
         (http://rt1.readthedocs.io/en/latest/theory.html#equation-general_scat_angle)
     """
+
     name = "HGRayleigh"
+    _param_names = ["tau", "omega", "ncoefs", "t", "a"]
 
     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!"
@@ -751,10 +801,10 @@ def _func(self):
             / (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)
+                / (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)
             )
         )
 
@@ -771,11 +821,11 @@ def legcoefs(self):
                 3.0
                 / (8.0 * sp.pi)
                 * 1.0
-                / (2.0 + self.t ** 2)
+                / (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 + 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,
             ),
@@ -783,12 +833,12 @@ def legcoefs(self):
                 3.0
                 / (8.0 * sp.pi)
                 * 1.0
-                / (2.0 + self.t ** 2)
+                / (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
+                    + (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.cfg b/setup.cfg
index ac9a46f5..df2d03de 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -11,4 +11,4 @@ upload-dir = doc/_build/html
 inplace=1
 
 [metadata]
-long_description = file: README.md
\ No newline at end of file
+long_description = file: README.md
diff --git a/setup.py b/setup.py
index f7380460..6dac7526 100644
--- a/setup.py
+++ b/setup.py
@@ -7,52 +7,46 @@
 """
 
 from setuptools import setup, find_packages
-#from setuptools import find_packages
-from rt1 import __version__
-
-setup(name='rt1',
-
-      version=__version__,
-
-      description='RT1 - bistatic single scattering radiative transfer model',
-
-      packages=find_packages(),  # ['rt1'],
-      package_dir={'rt1': 'rt1'},
-      include_package_data=False,
-
-      author="Raphael Quast",
-      author_email='raphael.quast@geo.tuwien.ac.at',
-      maintainer='Raphael Quast',
-      maintainer_email='raphael.quast@geo.tuwien.ac.at',
-
-      #~ license='APACHE 2',
-
-      url='https://github.com/TUW-GEO/rt1',
-
-      long_description=('A module to perform forward-simulation and ' +
-                        'parameter-inversion of incidence-angle dependent ' +
-                        'backscatter observations based on a first-order ' +
-                        'radiative-transfer model describing a rough surface' +
-                        'covered by a homogeneous layer of scattering' +
-                        'media.'),
-
-      install_requires=["numpy>=1.16", "sympy>=1.4", "scipy>=1.2",
-                        "pandas>=0.24", "matplotlib>=3.0"],
-      extras_require={'full' : ["symengine>=0.4", "xarray>=0.16"]},
-
-      keywords=["physics", "radiative transfer"],
-
-      # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
-      classifiers=[
-          'Intended Audience :: Science/Research',
-          'Topic :: Scientific/Engineering :: Atmospheric Science',
-
-          # Pick your license as you wish (should match "license" above)
-          #~ 'License :: OSI Approved :: MIT License',
-
-          'Programming Language :: Python :: 3.7'
-      ],
-
-      )
 
+# from setuptools import find_packages
+from rt1 import __version__
 
+setup(
+    name="rt1",
+    version=__version__,
+    description="RT1 - bistatic single scattering radiative transfer model",
+    packages=find_packages(),  # ['rt1'],
+    package_dir={"rt1": "rt1"},
+    include_package_data=False,
+    author="Raphael Quast",
+    author_email="raphael.quast@geo.tuwien.ac.at",
+    maintainer="Raphael Quast",
+    maintainer_email="raphael.quast@geo.tuwien.ac.at",
+    # ~ license='APACHE 2',
+    url="https://github.com/TUW-GEO/rt1",
+    long_description=(
+        "A module to perform forward-simulation and "
+        + "parameter-inversion of incidence-angle dependent "
+        + "backscatter observations based on a first-order "
+        + "radiative-transfer model describing a rough surface"
+        + "covered by a homogeneous layer of scattering"
+        + "media."
+    ),
+    install_requires=[
+        "numpy>=1.16",
+        "sympy>=1.4",
+        "scipy>=1.2",
+        "pandas>=0.24",
+        "matplotlib>=3.0",
+    ],
+    extras_require={"full": ["symengine>=0.4", "xarray>=0.16", "pytables>=3.6.1"]},
+    keywords=["physics", "radiative transfer"],
+    # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+    classifiers=[
+        "Intended Audience :: Science/Research",
+        "Topic :: Scientific/Engineering :: Atmospheric Science",
+        # Pick your license as you wish (should match "license" above)
+        # ~ 'License :: OSI Approved :: MIT License',
+        "Programming Language :: Python :: 3.7",
+    ],
+)


Test patch:
diff --git a/tests/check_leg.py b/tests/check_leg.py
deleted file mode 100644
index 40faa2fa..00000000
--- a/tests/check_leg.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import sympy as sp
-import numpy as np
-
-"""
-check sympy.Sum function for a Legendre series
-it iss shown that he Sum function results in a different result
-than when using summing up the arguments individually!
-"""
-
-
-
-n=sp.Symbol('n')
-
-f1 = []
-f2 = []
-cum = 0.
-#  results in differences after coefficient #8
-for nmax in range(20):
-
-    fak1 = (((2.*n+1.)*15.*sp.sqrt(sp.pi))/(16.*sp.gamma(((sp.Rational(7.)-n)/sp.Rational(2.)))*sp.gamma((sp.Rational(8.)+n)/sp.Rational(2.)))).expand()
-    fak2=sp.legendre(n, 7.37721344327046e-17)  # use a tiny number of X which then should result in basically zero terms for every second polynomial
-    r1=fak1.xreplace({n:nmax}).evalf()
-    r2=fak2.xreplace({n:nmax}).evalf()
-    f1.append(r1)
-    f2.append(r2)
-    cum += r1*r2  #... here we sum up the products individually --> approach A
-
-    expr=sp.Sum((fak1*fak2).doit(), (n, 0, nmax))  # ... here we sum up the product of the factors using sympy.Sum
-
-    print 'n, sympy.sum(), cum: ', nmax, expr.evalf(), cum
-
-f1 = np.asarray(f1).astype('float')
-f2 = np.asarray(f2).astype('float')
-res = f1*f2
-
-
-print sp.__version__
-
-"""
-results in:
-n, sympy.sum(), cum:  0 0.0833333333333333 0.0833333333333333
-n, sympy.sum(), cum:  1 0.0833333333333333 0.0833333333333333
-n, sympy.sum(), cum:  2 -0.0468750000000000 -0.0468750000000000
-n, sympy.sum(), cum:  3 -0.0468750000000000 -0.0468750000000000
-n, sympy.sum(), cum:  4 0.00585937500000000 0.00585937500000000
-n, sympy.sum(), cum:  5 0.00585937500000001 0.00585937500000001
-n, sympy.sum(), cum:  6 0.000569661458333343 0.000569661458333343
-n, sympy.sum(), cum:  7 0.000569661458333343 0.000569661458333343
-n, sympy.sum(), cum:  8 0.000569661458333343 0.000137329101562509    <<---- look at differences here!  solved! was due to old sympy version
-n, sympy.sum(), cum:  9 0.000569661458333343 0.000137329101562509
-n, sympy.sum(), cum:  10 0.000569661458333343 4.72068786621187e-5
-n, sympy.sum(), cum:  11 0.000569661458333343 4.72068786621187e-5
-n, sympy.sum(), cum:  12 0.000569661458333343 1.98880831400646e-5
-n, sympy.sum(), cum:  13 0.000569661458333343 1.98880831400646e-5
-n, sympy.sum(), cum:  14 0.000569661458333343 9.58889722825025e-6
-n, sympy.sum(), cum:  15 0.000569661458333343 9.58889722825025e-6
-n, sympy.sum(), cum:  16 0.000569661458333343 5.09410165251230e-6
-n, sympy.sum(), cum:  17 0.000569661458333343 5.09410165251230e-6
-n, sympy.sum(), cum:  18 0.000569661458333343 2.91259978743577e-6
-n, sympy.sum(), cum:  19 0.000569661458333343 2.91259978743577e-6
-"""
diff --git a/tests/example1.json b/tests/example1.json
index 23321305..dd5aa804 100644
--- a/tests/example1.json
+++ b/tests/example1.json
@@ -1,1 +1,1 @@
-{"surf": [0.24659696394160649, 0.2387850409108048, 0.21664999482533132, 0.18377841038663301, 0.14518947826412917, 0.10619355697160474, 0.071285956836038486, 0.043368940990236214, 0.023474218311473161, 0.010991785972633677, 0.0042574687127541391, 0.001262115592320884, 0.00024574884804613153, 2.161544962044147e-05, 1.9429724733030765e-07, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], "int": [NaN, 0.010670524699475197, 0.010598013460808145, 0.010476970382883897, 0.010307138240055658, 0.010088217889136273, 0.0098199344159855262, 0.0095021285818855314, 0.0091348766672908824, 0.0087186406958046428, 0.0082544489988222534, 0.0077441041985372343, 0.0071904121198195165, 0.0065974211890178708, 0.0059706579205085174, 0.0053173405018534098, 0.0046465494779979813, 0.0039693319182251482, 0.003298712263358281, 0.0026495769232825934, 0.0020383857703328515, 0.0014826328687597404, 0.00099991562646150169, 0.00060635491987894898, 0.00031393242286450866, 0.00012619327960150319, 3.2353277686777618e-05, 3.2226189876112438e-06, 1.9320093400622364e-08, 2.5023346682096586e-22], "phii": 0, "phiex": 3.141592653589793, "vol": [0.013489629449527663, 0.013498498560452715, 0.013525126067283948, 0.013569571402117758, 0.01363192988552959, 0.013712326771101914, 0.013810908164731244, 0.013927828001280894, 0.014063229915089227, 0.014217222399783021, 0.014389845084891249, 0.014581023226433868, 0.014790506582178824, 0.015017787701730068, 0.015261993338612128, 0.015521741341348006, 0.015794954440544327, 0.01607862286776611, 0.016368512046653554, 0.016658824600406283, 0.016941857526665469, 0.017207764327889605, 0.017444669375747155, 0.017639623804067511, 0.017781211516127063, 0.017864632148691467, 0.01789829650635804, 0.017904702093191609, 0.017904931049504524, 0.017904931097838223], "inc": [0.0, 0.053563552906032867, 0.10712710581206573, 0.16069065871809859, 0.21425421162413147, 0.26781776453016432, 0.32138131743619719, 0.37494487034223006, 0.42850842324826294, 0.48207197615429581, 0.53563552906032863, 0.5891990819663615, 0.64276263487239438, 0.69632618777842725, 0.74988974068446013, 0.803453293590493, 0.85701684649652587, 0.91058039940255875, 0.96414395230859162, 1.0177075052146245, 1.0712710581206573, 1.1248346110266902, 1.178398163932723, 1.231961716838756, 1.2855252697447888, 1.3390888226508217, 1.3926523755568545, 1.4462159284628875, 1.4997794813689203, 1.5533430342749532]}
\ No newline at end of file
+{"surf": [0.24659696394160649, 0.2387850409108048, 0.21664999482533132, 0.18377841038663301, 0.14518947826412917, 0.10619355697160474, 0.071285956836038486, 0.043368940990236214, 0.023474218311473161, 0.010991785972633677, 0.0042574687127541391, 0.001262115592320884, 0.00024574884804613153, 2.161544962044147e-05, 1.9429724733030765e-07, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], "int": [NaN, 0.010670524699475197, 0.010598013460808145, 0.010476970382883897, 0.010307138240055658, 0.010088217889136273, 0.0098199344159855262, 0.0095021285818855314, 0.0091348766672908824, 0.0087186406958046428, 0.0082544489988222534, 0.0077441041985372343, 0.0071904121198195165, 0.0065974211890178708, 0.0059706579205085174, 0.0053173405018534098, 0.0046465494779979813, 0.0039693319182251482, 0.003298712263358281, 0.0026495769232825934, 0.0020383857703328515, 0.0014826328687597404, 0.00099991562646150169, 0.00060635491987894898, 0.00031393242286450866, 0.00012619327960150319, 3.2353277686777618e-05, 3.2226189876112438e-06, 1.9320093400622364e-08, 2.5023346682096586e-22], "phii": 0, "phiex": 3.141592653589793, "vol": [0.013489629449527663, 0.013498498560452715, 0.013525126067283948, 0.013569571402117758, 0.01363192988552959, 0.013712326771101914, 0.013810908164731244, 0.013927828001280894, 0.014063229915089227, 0.014217222399783021, 0.014389845084891249, 0.014581023226433868, 0.014790506582178824, 0.015017787701730068, 0.015261993338612128, 0.015521741341348006, 0.015794954440544327, 0.01607862286776611, 0.016368512046653554, 0.016658824600406283, 0.016941857526665469, 0.017207764327889605, 0.017444669375747155, 0.017639623804067511, 0.017781211516127063, 0.017864632148691467, 0.01789829650635804, 0.017904702093191609, 0.017904931049504524, 0.017904931097838223], "inc": [0.0, 0.053563552906032867, 0.10712710581206573, 0.16069065871809859, 0.21425421162413147, 0.26781776453016432, 0.32138131743619719, 0.37494487034223006, 0.42850842324826294, 0.48207197615429581, 0.53563552906032863, 0.5891990819663615, 0.64276263487239438, 0.69632618777842725, 0.74988974068446013, 0.803453293590493, 0.85701684649652587, 0.91058039940255875, 0.96414395230859162, 1.0177075052146245, 1.0712710581206573, 1.1248346110266902, 1.178398163932723, 1.231961716838756, 1.2855252697447888, 1.3390888226508217, 1.3926523755568545, 1.4462159284628875, 1.4997794813689203, 1.5533430342749532]}
diff --git a/tests/parallel_processing_config.py b/tests/parallel_processing_config.py
index 510da043..c43ba111 100644
--- a/tests/parallel_processing_config.py
+++ b/tests/parallel_processing_config.py
@@ -1,9 +1,10 @@
 import os
+
 # disable numpy's internal multithreading to avoid parallel overhead
-os.environ['OPENBLAS_NUM_THREADS'] = '1'
-os.environ['MKL_NUM_THREADS'] = '1'
-os.environ['OMP_NUM_THREADS'] = '1'
-os.environ['NUMEXPR_NUM_THREADS'] = '1'
+os.environ["OPENBLAS_NUM_THREADS"] = "1"
+os.environ["MKL_NUM_THREADS"] = "1"
+os.environ["OMP_NUM_THREADS"] = "1"
+os.environ["NUMEXPR_NUM_THREADS"] = "1"
 
 from rt1.processing_config import rt1_processing_config
 
@@ -17,17 +18,14 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
 
     def reader(self, reader_arg):
-
-        self.check_dump_exists(reader_arg)
-
         # initialize a reader
-        if reader_arg['gpi'] in [1,2,3]:
-            index = pd.date_range('1.1.2017', '1.1.2018', freq='D')
+        if reader_arg["ID"] in [1, 2, 3]:
+            index = pd.date_range("1.1.2017", "1.1.2018", freq="D")
             ndata = len(index)
-            df = pd.DataFrame(dict(sig=np.random.rand(ndata),
-                                   inc=np.random.rand(ndata)),
-                              index)
-        if reader_arg['gpi'] == 4:
+            df = pd.DataFrame(
+                dict(sig=np.random.rand(ndata), inc=np.random.rand(ndata)), index
+            )
+        if reader_arg["ID"] == 4:
             df = pd.DataFrame()
 
-        return df
\ No newline at end of file
+        return df
diff --git a/tests/sig0_dB.dump b/tests/sig0_dB.dump
deleted file mode 100644
index 2458ae47..00000000
Binary files a/tests/sig0_dB.dump and /dev/null differ
diff --git a/tests/sig0_linear.dump b/tests/sig0_linear.dump
deleted file mode 100644
index 22541a35..00000000
Binary files a/tests/sig0_linear.dump and /dev/null differ
diff --git a/tests/speed.py b/tests/speed.py
index 367d7d05..39278952 100644
--- a/tests/speed.py
+++ b/tests/speed.py
@@ -1,5 +1,6 @@
 import sys
-sys.path.append('..')
+
+sys.path.append("..")
 
 from rt1.rt1 import RT1
 from rt1.volume import Rayleigh
@@ -10,35 +11,32 @@
 import time
 
 
-
-
-
 # some speed benchmarking ...
 
 S = CosineLobe(ncoefs=10, i=5)
 V = Rayleigh(tau=0.7, omega=0.3)
 
-I0 = 1.
+I0 = 1.0
 
-theta_i = np.pi/2.
+theta_i = np.pi / 2.0
 theta_ex = 0.234234
-phi_i = np.pi/2.
-phi_ex = 0.
+phi_i = np.pi / 2.0
+phi_ex = 0.0
 
-RT = RT1(I0, theta_i, theta_ex, phi_i, phi_ex, RV=V, SRF=S, geometry='vvvv')
-#~ print RT.fn
-#~ print len(RT.fn)
+RT = RT1(I0, theta_i, theta_ex, phi_i, phi_ex, RV=V, SRF=S, geometry="vvvv")
+# ~ print RT.fn
+# ~ print len(RT.fn)
 
-#~ start = time.time()
-#~ for i in xrange(10):
-    #~ for n in xrange(len(RT.fn)):
-        #~ RT._get_fn(n, theta_i, phi_i, theta_ex, phi_ex)
-#~ end = time.time()
-#~ print('Time for get_fn (10x): ', end - start)
+# ~ start = time.time()
+# ~ for i in xrange(10):
+# ~ for n in xrange(len(RT.fn)):
+# ~ RT._get_fn(n, theta_i, phi_i, theta_ex, phi_ex)
+# ~ end = time.time()
+# ~ print('Time for get_fn (10x): ', end - start)
 
 
 start = time.time()
 for i in xrange(10):
     RT.interaction()
 end = time.time()
-print('Time for interaction (10x): ', end - start)
+print("Time for interaction (10x): ", end - start)
diff --git a/tests/test_config.ini b/tests/test_config.ini
index d3cb9693..9dfe752e 100644
--- a/tests/test_config.ini
+++ b/tests/test_config.ini
@@ -5,13 +5,10 @@
 # set dataset-properties (e.g. if the input-dataset is in dB or not)
 sig0     = True
 dB       = False
-# define how the index of the results should be set (first, last or mean)
-setindex = first
 # indicator if interaction term should be evaluated
 int_Q          = True
 # backend to use for symbolic evaluation of the interaction term functions
 lambda_backend = symengine
-interp_vals     =   tau,   omega
 _fnevals_input
 # the verbosity (e.g. the level of print outputs)
 verbose = 1
@@ -105,6 +102,3 @@ list__int__li = [1,  2,3,4,  5  ]
 list__bool__lb = [True, False, 1, 0]
 list__ls = [a,B  ,  c, AbCd#  ]
 list__datetime__ldt = [22-1-17 12:34, 24-3-19 11:13]   fmt= %d-%m-%y %H:%M
-
-
-
diff --git a/tests/test_config_multi.ini b/tests/test_config_multi.ini
index 49d86c55..5ec91bab 100644
--- a/tests/test_config_multi.ini
+++ b/tests/test_config_multi.ini
@@ -5,13 +5,10 @@
 # set dataset-properties (e.g. if the input-dataset is in dB or not)
 sig0     = True
 dB       = False
-# define how the index of the results should be set (first, last or mean)
-setindex = first
 # indicator if interaction term should be evaluated
 int_Q          = True
 # backend to use for symbolic evaluation of the interaction term functions
 lambda_backend = symengine
-interp_vals     =   tau,   omega
 _fnevals_input
 # the verbosity (e.g. the level of print outputs)
 verbose = 1
@@ -121,4 +118,4 @@ t      = .25
 [#config cfg_1 RT1_SRF]
 ncoefs = 5
 [#config cfg_1 least_squares_kwargs]
-ftol      = 1.e-3
\ No newline at end of file
+ftol      = 1.e-3
diff --git a/tests/test_configparser.py b/tests/test_configparser.py
index 566ee780..2f0433f0 100644
--- a/tests/test_configparser.py
+++ b/tests/test_configparser.py
@@ -14,136 +14,161 @@
 
 class TestCONFIGPARSER(unittest.TestCase):
     def setUp(self):
-        self.configpath = os.path.join(os.path.dirname(__file__),
-                                       'test_config.ini')
-
+        self.configpath = os.path.join(os.path.dirname(__file__), "test_config.ini")
 
     def test_load_cfg(self):
         cfg = RT1_configparser(self.configpath)
 
-        #----------------------------------------- check parsed configdicts
-        lsq_kwargs = {'verbose': 0,
-                      'ftol': 0.0001,
-                      'gtol': 0.0001,
-                      'xtol': 0.0001,
-                      'max_nfev': 20,
-                      'method': 'trf',
-                      'tr_solver': 'lsmr',
-                      'x_scale': 'jac'}
-
-        defdict = {'omega': [True, 0.05, '2M', ([0.01], [0.5]), True],
-                   't_v': [True, 0.25, None, ([0.01], [0.5]), False],
-                    't_s': [True, 0.25, None, ([0.01], [0.5]), False],
-                    'N': [True, 0.1, 'index', ([0.01], [0.2]), False],
-                    'tau': [True, 0.5, '3M', ([0.01], [1.5]), True],
-                    'bsf': [True, 0.05, 'A', ([0.01], [1.0]), False],
-                    'tau_multip': [False, 0.5]}
-
-        set_V_SRF = {'V_props': {'V_name': 'HenyeyGreenstein',
-                                 'tau': 'tau * tau_multip',
-                                 'omega': 'omega',
-                                 't': 't_v',
-                                 'ncoefs': 10},
-                     'SRF_props': {'SRF_name': 'HG_nadirnorm',
-                                   'NormBRDF': 'N',
-                                   't': 't_s',
-                                   'ncoefs': 10}}
-
-        fits_kwargs = {'sig0': True,
-                       'dB': False,
-                       'int_Q': True,
-                       'lambda_backend': 'symengine',
-                       '_fnevals_input': None,
-                       'verbose' : 1}
+        # ----------------------------------------- check parsed configdicts
+        lsq_kwargs = {
+            "verbose": 0,
+            "ftol": 0.0001,
+            "gtol": 0.0001,
+            "xtol": 0.0001,
+            "max_nfev": 20,
+            "method": "trf",
+            "tr_solver": "lsmr",
+            "x_scale": "jac",
+        }
+
+        defdict = {
+            "omega": [True, 0.05, "2M", ([0.01], [0.5]), True],
+            "t_v": [True, 0.25, None, ([0.01], [0.5]), False],
+            "t_s": [True, 0.25, None, ([0.01], [0.5]), False],
+            "N": [True, 0.1, "index", ([0.01], [0.2]), False],
+            "tau": [True, 0.5, "3M", ([0.01], [1.5]), True],
+            "bsf": [True, 0.05, "A", ([0.01], [1.0]), False],
+            "tau_multip": [False, 0.5],
+        }
+
+        set_V_SRF = {
+            "V_props": {
+                "V_name": "HenyeyGreenstein",
+                "tau": "tau * tau_multip",
+                "omega": "omega",
+                "t": "t_v",
+                "ncoefs": 10,
+            },
+            "SRF_props": {
+                "SRF_name": "HG_nadirnorm",
+                "NormBRDF": "N",
+                "t": "t_s",
+                "ncoefs": 10,
+            },
+        }
+
+        fits_kwargs = {
+            "sig0": True,
+            "dB": False,
+            "int_Q": True,
+            "lambda_backend": "symengine",
+            "_fnevals_input": None,
+            "verbose": 1,
+        }
 
         configdicts = cfg.get_config()
 
         for key, val in lsq_kwargs.items():
-            assert key in configdicts['lsq_kwargs'], f'error in lsq_kwargs {key}'
-            assert val == configdicts['lsq_kwargs'][key], f'error in lsq_kwargs {key}'
+            assert key in configdicts["lsq_kwargs"], f"error in lsq_kwargs {key}"
+            assert val == configdicts["lsq_kwargs"][key], f"error in lsq_kwargs {key}"
 
         for key, val in defdict.items():
-            assert key in configdicts['defdict'], f'{key} not in defdict'
+            assert key in configdicts["defdict"], f"{key} not in defdict"
             for i, val_i in enumerate(val):
-                assert val_i == configdicts['defdict'][key][i], f'error in defdict {key}'
+                assert (
+                    val_i == configdicts["defdict"][key][i]
+                ), f"error in defdict {key}"
 
         for key, val in set_V_SRF.items():
-            assert key in configdicts['set_V_SRF'], f'error in set_V_SRF {key}'
+            assert key in configdicts["set_V_SRF"], f"error in set_V_SRF {key}"
 
             for key_i, val_i in val.items():
-                assert key_i in configdicts['set_V_SRF'][key], f'error in set_V_SRF["{key}"]["{key_i}"]'
+                assert (
+                    key_i in configdicts["set_V_SRF"][key]
+                ), f'error in set_V_SRF["{key}"]["{key_i}"]'
 
-                assert val_i == configdicts['set_V_SRF'][key][key_i], f'error in set_V_SRF["{key}"]["{key_i}"]'
+                assert (
+                    val_i == configdicts["set_V_SRF"][key][key_i]
+                ), f'error in set_V_SRF["{key}"]["{key_i}"]'
 
         for key, val in fits_kwargs.items():
-            assert key in configdicts['fits_kwargs'], f'error in fits_kwargs {key}'
-            assert val == configdicts['fits_kwargs'][key], f'error in fits_kwargs {key}'
-
-        #----------------------------------------- check parsed process_specs
-
-        process_specs = {'finalout_name' : 'results.nc',
-                         'dumpfolder' : 'dump01',
-                         'save_path' : Path('tests/proc_test'),
-                         'f0' : 1245.,
-                         'f1' : 5.4,
-                         'i0' : 1,
-                         'i1' : 5,
-                         'b0' : False,
-                         'b1' : True,
-                         'd0' : datetime(2020,3,23),
-                         'd1' : datetime(2017,1,22,12,34),
-                         'lf' : [.1,.2,.3,.4,.5],
-                         'li' : [1,2,3,4,5],
-                         'lb' : [True, False, True, False],
-                         'ls' : ['a','B','c','AbCd#'],
-                         'ldt': [datetime(2017,1,22,12,34),
-                                 datetime(2019,3,24,11,13)]}
+            assert key in configdicts["fits_kwargs"], f"error in fits_kwargs {key}"
+            assert val == configdicts["fits_kwargs"][key], f"error in fits_kwargs {key}"
+
+        # ----------------------------------------- check parsed process_specs
+
+        process_specs = {
+            "finalout_name": "results.nc",
+            "dumpfolder": "dump01",
+            "save_path": Path("tests/proc_test"),
+            "f0": 1245.0,
+            "f1": 5.4,
+            "i0": 1,
+            "i1": 5,
+            "b0": False,
+            "b1": True,
+            "d0": datetime(2020, 3, 23),
+            "d1": datetime(2017, 1, 22, 12, 34),
+            "lf": [0.1, 0.2, 0.3, 0.4, 0.5],
+            "li": [1, 2, 3, 4, 5],
+            "lb": [True, False, True, False],
+            "ls": ["a", "B", "c", "AbCd#"],
+            "ldt": [datetime(2017, 1, 22, 12, 34), datetime(2019, 3, 24, 11, 13)],
+        }
 
         process_specs_parsed = cfg.get_process_specs()
 
         for key, val in process_specs.items():
-
             parsedval = process_specs_parsed[key]
 
-            assert key in process_specs_parsed, f'error in PROCESS_SPECS {key}'
-            assert val == parsedval, f'error in PROCESS_SPECS {key} ({val} != {parsedval})'
-
-            if key in ['f0', 'f1']:
-                assert isinstance(parsedval, float), f'{key} = {parsedval} is not parsed as float!'
-            if key in ['i0', 'i1']:
-                assert isinstance(parsedval, int), f'{key} = {parsedval} is not parsed as int!'
-            if key in ['b0', 'b1']:
-                assert isinstance(parsedval, bool), f'{key} = {parsedval} is not parsed as bool!'
-            if key in ['d0', 'd1']:
-                assert isinstance(parsedval, datetime), f'{key} = {parsedval} is not parsed as datetime!'
-
-
-
-        #----------------------------------------- check fitobjects
+            assert key in process_specs_parsed, f"error in PROCESS_SPECS {key}"
+            assert (
+                val == parsedval
+            ), f"error in PROCESS_SPECS {key} ({val} != {parsedval})"
+
+            if key in ["f0", "f1"]:
+                assert isinstance(
+                    parsedval, float
+                ), f"{key} = {parsedval} is not parsed as float!"
+            if key in ["i0", "i1"]:
+                assert isinstance(
+                    parsedval, int
+                ), f"{key} = {parsedval} is not parsed as int!"
+            if key in ["b0", "b1"]:
+                assert isinstance(
+                    parsedval, bool
+                ), f"{key} = {parsedval} is not parsed as bool!"
+            if key in ["d0", "d1"]:
+                assert isinstance(
+                    parsedval, datetime
+                ), f"{key} = {parsedval} is not parsed as datetime!"
+
+        # ----------------------------------------- check fitobjects
 
         fit = cfg.get_fitobject()
-        V = fit._init_V_SRF(fit.set_V_SRF['V_props'])
-        SRF = fit._init_V_SRF(fit.set_V_SRF['SRF_props'])
-
-        assert V.t == sp.Symbol('t_v'), 'V.t assigned incorrectly'
-        assert V.tau == sp.Symbol('tau') * sp.Symbol('tau_multip'), 'V.tau assigned incorrectly'
-        assert V.omega == sp.Symbol('omega'), 'V.omega assigned incorrectly'
-        assert SRF.t == sp.Symbol('t_s'), 'SRF.t assigned incorrectly'
-        assert SRF.NormBRDF == sp.Symbol('N'), 'SRF.NormBRDF assigned incorrectly'
-
-
-
-        #----------------------------------------- check imported module
+        V = fit._init_V_SRF(fit.set_V_SRF["V_props"])
+        SRF = fit._init_V_SRF(fit.set_V_SRF["SRF_props"])
+
+        assert V.t == sp.Symbol("t_v"), "V.t assigned incorrectly"
+        assert V.tau == sp.Symbol("tau") * sp.Symbol(
+            "tau_multip"
+        ), "V.tau assigned incorrectly"
+        assert V.omega == sp.Symbol("omega"), "V.omega assigned incorrectly"
+        assert SRF.t == sp.Symbol("t_s"), "SRF.t assigned incorrectly"
+        assert SRF.NormBRDF == sp.Symbol("N"), "SRF.NormBRDF assigned incorrectly"
+
+        # ----------------------------------------- check imported module
         cfg_module_dict = cfg.get_all_modules()
-        assert 'processing_cfg' in cfg_module_dict, 'modules not correctly parsed'
-        cfg_module = cfg_module_dict['processing_cfg']
-
-        assert hasattr(cfg_module, 'processing_cfg'), 'modules not correctly parsed'
+        assert "processing_cfg" in cfg_module_dict, "modules not correctly parsed"
+        cfg_module = cfg_module_dict["processing_cfg"]
 
+        assert hasattr(cfg_module, "processing_cfg"), "modules not correctly parsed"
 
-        cfg_module_direct = cfg.get_module('processing_cfg')
-        assert hasattr(cfg_module_direct, 'processing_cfg'), 'direct-module load not working'
+        cfg_module_direct = cfg.get_module("processing_cfg")
+        assert hasattr(
+            cfg_module_direct, "processing_cfg"
+        ), "direct-module load not working"
 
 
 if __name__ == "__main__":
-    unittest.main()
\ No newline at end of file
+    unittest.main()
diff --git a/tests/test_env.yml b/tests/test_env.yml
index 1bd0485e..39d45aa1 100644
--- a/tests/test_env.yml
+++ b/tests/test_env.yml
@@ -11,6 +11,7 @@ dependencies:
   - matplotlib
   - cloudpickle
   - python-symengine
+  - pytables
 
   - coveralls
   - pytest
diff --git a/tests/test_examples_int.py b/tests/test_examples_int.py
index dcdaca96..427804fb 100644
--- a/tests/test_examples_int.py
+++ b/tests/test_examples_int.py
@@ -1,4 +1,4 @@
-    # -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 
 """
 test examples given in paper by comparison against reference solution.
@@ -14,29 +14,28 @@
 import os
 import sys
 
-#sys.path.append(os.path.join('..', 'mintrend'))
-sys.path.append('..')
+# sys.path.append(os.path.join('..', 'mintrend'))
+sys.path.append("..")
 from rt1.rt1 import RT1
 from rt1.volume import Rayleigh, HenyeyGreenstein
 from rt1.surface import CosineLobe
 
 
 class TestExamples(unittest.TestCase):
-
     def setUp(self):
         # read reference solutions for backscattering case
-        fname1 = os.path.dirname(__file__) + os.sep + 'example1_int.csv'
-        x1 = np.loadtxt(fname1, delimiter=',', skiprows=0)
+        fname1 = os.path.dirname(__file__) + os.sep + "example1_int.csv"
+        x1 = np.loadtxt(fname1, delimiter=",", skiprows=0)
         self.inc1 = x1[:, 0]
         self.int_num_1 = x1[:, 1]
 
-        fname2 = os.path.dirname(__file__) + os.sep + 'example2_int.csv'
-        x2 = np.loadtxt(fname2, delimiter=',', skiprows=0)
+        fname2 = os.path.dirname(__file__) + os.sep + "example2_int.csv"
+        x2 = np.loadtxt(fname2, delimiter=",", skiprows=0)
         self.inc2 = x2[:, 0]
         self.int_num_2 = x2[:, 1]
 
     def test_example_1_int(self):
-        print('Testing Example 1 ...')
+        print("Testing Example 1 ...")
 
         inc = self.inc1
 
@@ -45,25 +44,40 @@ def test_example_1_int(self):
         # 11 instead of 10 coefficients used to assure 7 digit precision
         SRF = CosineLobe(ncoefs=11, i=5, NormBRDF=np.array([np.pi]))
 
-        R = RT1(1., np.deg2rad(inc), np.deg2rad(inc),
-                np.zeros_like(inc), np.full_like(inc, np.pi),
-                V=V, SRF=SRF, geometry='mono')
+        R = RT1(
+            1.0,
+            np.deg2rad(inc),
+            np.deg2rad(inc),
+            np.zeros_like(inc),
+            np.full_like(inc, np.pi),
+            V=V,
+            SRF=SRF,
+            geometry="mono",
+        )
 
         self.assertTrue(np.allclose(self.int_num_1, R.calc()[3]))
 
     def test_example_2_int(self):
-        print('Testing Example 2 ...')
+        print("Testing Example 2 ...")
 
         inc = self.inc2
 
         # ---- evaluation of second example
-        V = HenyeyGreenstein(tau=np.array([0.7]), omega=np.array([0.3]),
-                             t=0.7, ncoefs=20)
+        V = HenyeyGreenstein(
+            tau=np.array([0.7]), omega=np.array([0.3]), t=0.7, ncoefs=20
+        )
         SRF = CosineLobe(ncoefs=10, i=5, NormBRDF=np.array([np.pi]))
 
-        R = RT1(1., np.deg2rad(inc), np.deg2rad(inc),
-                np.zeros_like(inc), np.full_like(inc, np.pi),
-                V=V, SRF=SRF, geometry='mono')
+        R = RT1(
+            1.0,
+            np.deg2rad(inc),
+            np.deg2rad(inc),
+            np.zeros_like(inc),
+            np.full_like(inc, np.pi),
+            V=V,
+            SRF=SRF,
+            geometry="mono",
+        )
 
         self.assertTrue(np.allclose(self.int_num_2, R.calc()[3], atol=1e-6))
 
diff --git a/tests/test_fit_db.h5 b/tests/test_fit_db.h5
new file mode 100644
index 00000000..a4b6274b
Binary files /dev/null and b/tests/test_fit_db.h5 differ
diff --git a/tests/test_parallel_processing.py b/tests/test_parallel_processing.py
index 1c131fa5..34620003 100644
--- a/tests/test_parallel_processing.py
+++ b/tests/test_parallel_processing.py
@@ -1,25 +1,64 @@
 from pathlib import Path
+import shutil
 import unittest
 import unittest.mock as mock
 from rt1.rtprocess import RTprocess
 from rt1.rtresults import RTresults
 from rt1.rtfits import MultiFits
 import warnings
+import pytest
 
 warnings.simplefilter("ignore")
 
 
 # use "test_0_---"   "test_1_---"   to ensure test-order
 
+# # set this to False to avoid deleting results & processing-folders!
+cleanup_before = False
+cleanup_after = False
+
+
+# define an export_function (used in test_92_export_results)
+# (must be defined outside of __main__ to be pickleable!)
+def export_sig2(fit):
+    return fit.dataset.sig**2
+
 
 class TestRTfits(unittest.TestCase):
+    @classmethod
+    def setup_class(cls):
+        """... called before the testing starts"""
+        if not cleanup_before:
+            return
+
+        folders = ["proc_test", "proc_test2", "proc_multi"]
+
+        for folder in folders:
+            p = Path(f"tests/{folder}")
+            if p.exists():
+                print(f"removing existing folder '{folder}' before starting tests")
+                shutil.rmtree(p)
+
+    @classmethod
+    def teardown_class(cls):
+        """cleanup after the test finished"""
+        if not cleanup_after:
+            return
+
+        folders = ["proc_test", "proc_test2", "proc_multi"]
+
+        for folder in folders:
+            p = Path(f"tests/{folder}")
+            if p.exists():
+                print(f"removing folder '{folder}' to cleanup after testing")
+                shutil.rmtree(p)
+
     def test_0_parallel_processing(self):
-        reader_args = [dict(gpi=i) for i in [1, 2, 3, 4]]
+        reader_args = [dict(ID=i) for i in [1, 2, 3, 4]]
         config_path = Path(__file__).parent.absolute() / "test_config.ini"
 
         proc = RTprocess(config_path, autocontinue=True)
-
-        proc.run_processing(ncpu=4, reader_args=reader_args)
+        proc.run_processing(ncpu=4, reader_args=reader_args, dump_fit=True)
 
         # run again to check what happens if files already exist
         proc.run_processing(ncpu=4, reader_args=reader_args)
@@ -42,31 +81,101 @@ def test_0_parallel_processing(self):
         ).exists(), "copying did not work"
 
     def test_1_rtresults(self):
-
         results = RTresults("tests/proc_test")
+        dumpresults = RTresults("tests/proc_test", use_dumps=True)
+
         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()
+        _ = results.dump01.load_cfg()
 
-        with results.dump01.load_nc() as ncfile:
-            processed_ids = list(ncfile.ID.values)
+        # 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"
+        # 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
+        # # check if NetCDF_variables works as expected
+        # results.dump01.NetCDF_variables
 
         # remove the save_path directory
         # print('deleting save_path directory...')
         # shutil.rmtree(results._parent_path)
 
+        fit_db = results.dump01.fit_db
+
+        assert not any(
+            fit_db.IDs.duplicated()
+        ), "there are duplicated IDs in the HDF container!"
+
+        # check if all ID's are present in the HDF-container
+        assert all(
+            i in fit_db.IDs.ID.values for i in ["RT1_1", "RT1_2", "RT1_3"]
+        ), "HDF-container does not contain all IDs"
+
+        # check if dumped-properties are equal for pickles and HDF-containers
+        for fit in dumpresults.dump01.dump_fits:
+            fit_hdf = fit_db.load_fit(fit.ID)
+            assert fit_hdf.dataset.equals(
+                fit.dataset
+            ), "datasets of HDF-container and pickle-dumps are not equal!"
+            assert fit_hdf.res_df.equals(
+                fit.res_df
+            ), "res_df of HDF-container and pickle-dumps are not equal!"
+
+        # check accessing fits via ID
+        id_fit = fit_db.load_fit("RT1_1")
+        assert id_fit.ID == "RT1_1", "the ID of the loaded fit is not OK"
+
+        # try accessing the data directly
+        alldata = fit_db.datasets.dataset.select()
+        assert len(alldata.index.levels[0]) == 3, "accessing full dataset was not OK"
+
+        data0 = fit_db.datasets.dataset.get_id(0)
+        assert len(data0.index.levels[0]) == 1, "dataset-selection was not OK"
+        assert data0.index.levels[0][0] == 0, "dataset-selection was not OK"
+
+        for i in ["RT1_1", "RT1_2", "RT1_3"]:
+            dataID = fit_db.datasets.dataset.get_id(i)
+            assert (
+                len(dataID.index.levels[0]) == 1
+            ), "dataset-selection via ID was not OK"
+            assert (
+                fit_db.IDs.loc[dataID.index.levels[0][0]].ID == i
+            ), "dataset-selection via ID was not OK"
+
+        data2 = fit_db.datasets.dataset.get_nids(2)
+        assert len(data2.index.levels[0]) == 2, "multiple dataset-selection was not OK"
+        assert all(
+            fit_db.IDs.index[:2].isin(data2.index.levels[0])
+        ), "multiple dataset-selection was not OK"
+
+        # check data-generator
+        data_iter = fit_db.datasets.dataset.get_nids_iter(2)
+
+        data_iter_1 = next(data_iter)
+        assert len(data_iter_1.index.levels[0]) == 2, "dataset generator was not OK"
+        assert all(
+            fit_db.IDs.index[:2].isin(data_iter_1.index.levels[0])
+        ), "dataset generator was not OK"
+
+        data_iter_2 = next(data_iter)
+        assert len(data_iter_2.index.levels[0]) == 1, "dataset generator was not OK"
+        assert all(
+            fit_db.IDs.index[2:].isin(data_iter_2.index.levels[0])
+        ), "dataset generator was not OK"
+
+        with self.assertRaises(StopIteration):
+            _ = next(data_iter)
+
+        # make sure to close the HDF-file so that the folders can be savely deleted
+        fit_db.close()
+
     def test_2_single_core_processing(self):
-        reader_args = [dict(gpi=i) for i in [1, 2, 3, 4]]
+        reader_args = [dict(ID=i) for i in [1, 2, 3, 4]]
         config_path = Path(__file__).parent.absolute() / "test_config.ini"
 
         # mock inputs as shown here: https://stackoverflow.com/a/37467870/9703451
@@ -88,7 +197,7 @@ def test_2_single_core_processing(self):
         ), "user-input REMOVE did not work"
 
         with mock.patch("builtins.input", side_effect=["REMOVE", "Y"]):
-            proc = RTprocess(config_path, autocontinue=False, copy=False)
+            proc = RTprocess(config_path, autocontinue=False)
             proc.run_processing(ncpu=1, reader_args=reader_args)
 
         # ----------------------------------------- check if files have been copied
@@ -101,12 +210,12 @@ def test_2_single_core_processing(self):
         assert Path(
             "tests/proc_test/dump01/dumps"
         ).exists(), "folder-generation did not work"
-        assert not Path(
+        assert Path(
             "tests/proc_test/dump01/cfg/test_config.ini"
-        ).exists(), "NOT copying did not work"
-        assert not Path(
+        ).exists(), "copying did not work"
+        assert Path(
             "tests/proc_test/dump01/cfg/parallel_processing_config.py"
-        ).exists(), "NOT copying did not work"
+        ).exists(), "copying did not work"
 
         # remove the save_path directory
         # print('deleting save_path directory...')
@@ -114,7 +223,7 @@ def test_2_single_core_processing(self):
 
     def test_3_parallel_processing_init_kwargs(self):
         # test overwriting keyword-args from .ini file
-        reader_args = [dict(gpi=i) for i in [1, 2, 3, 4]]
+        reader_args = [dict(ID=i) for i in [1, 2, 3, 4]]
         config_path = Path(__file__).parent.absolute() / "test_config.ini"
 
         proc = RTprocess(config_path, autocontinue=True)
@@ -145,89 +254,59 @@ def test_3_parallel_processing_init_kwargs(self):
             "tests/proc_test2/dump02/cfg/parallel_processing_config.py"
         ).exists(), "copying did not work"
 
-    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]]
-
-        proc = RTprocess(config_path)
-        proc.override_config(
-            PROCESS_SPECS=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"
-
     def test_5_multiconfig(self):
-
         config_path = Path(__file__).parent.absolute() / "test_config_multi.ini"
-        reader_args = [dict(gpi=i) for i in [1, 2, 3, 4]]
+        reader_args = [dict(ID=i) for i in [1, 2, 3, 4]]
 
         proc = RTprocess(config_path)
-        proc.run_processing(ncpu=4, reader_args=reader_args, postprocess=True)
+        proc.run_processing(ncpu=4, reader_args=reader_args)
 
         for cfg in ["cfg_0", "cfg_1"]:
-            # check if all folders are properly initialized
-            # assert Path(
-            #     f"tests/proc_multi/dump01/dumps/{cfg}"
-            # ).exists(), "multiconfig folder generation did not work"
-            # assert Path(
-            #     f"tests/proc_multi/dump01/dumps/{cfg}"
-            # ).exists(), "multiconfig folder generation did not work"
-
             # check if model-definition files are written correctly
             assert Path(
                 f"tests/proc_multi/dump01/cfg/model_definition__{cfg}.txt"
             ).exists(), "multiconfig model_definition.txt export did not work"
 
-            # # check if netcdf have been exported
+            # # # check if netcdf have been exported
             # assert Path(
-            #     f"tests/proc_multi/dump01/results/{cfg}/results.nc"
+            #     f"tests/proc_multi/dump01/results/results__{cfg}.nc"
             # ).exists(), "multiconfig NetCDF export did not work"
-            # check if netcdf have been exported
-            assert Path(
-                f"tests/proc_multi/dump01/results/results__{cfg}.nc"
-            ).exists(), "multiconfig NetCDF export did not work"
 
-    def test_7_multiconfig_finalout(self):
-        config_path = Path(__file__).parent.absolute() / "test_config_multi.ini"
+    def test_6_multiconfig_rtresults(self):
+        results = RTresults("tests/proc_multi")
+        assert hasattr(results, "dump01"), "dumpfolder not found by RTresults"
 
-        proc = RTprocess(config_path)
-        proc.run_finaloutput(
-            ncpu=1,
-            finalout_name="ncpu1.nc",
-        )
+        _ = [i for i in results.dump01.dump_files]
 
-        proc.run_finaloutput(
-            ncpu=3,
-            finalout_name="ncpu3.nc",
-        )
+        fit = results.dump01.load_fit()
+        _ = results.dump01.load_cfg()
 
-        for cfg in ["cfg_0", "cfg_1"]:
-            # check if all folders are properly initialized
-            assert Path(
-                f"tests/proc_multi/dump01/results/ncpu1__{cfg}.nc"
-            ).exists(), "multiconfig finaloutput with ncpu=1 did not work"
-            assert Path(
-                f"tests/proc_multi/dump01/results/ncpu3__{cfg}.nc"
-            ).exists(), "multiconfig finaloutput with ncpu=3 did not work"
+        fit_db = results.dump01.fit_db
+
+        # check if all ID's are present in the HDF-container
+        assert all(
+            i in fit_db.IDs.ID.values for i in ["RT1_1", "RT1_2", "RT1_3"]
+        ), "HDF-container does not contain all IDs"
+
+        # check if dumped-properties are equal for pickles and HDF-containers
+        for fit in results.dump01.dump_fits:
+            fit_hdf = fit_db.load_fit(fit.ID)
+            _ = fit_hdf.dataset[list(fit.dataset)]
+            assert fit_hdf.dataset.equals(
+                fit.dataset
+            ), "datasets of HDF-container and pickle-dumps are not equal!"
+
+            for fit_cfg in fit.configs:
+                hdf_res_df = fit_hdf.configs[fit_cfg.config_name].res_df
+                # make sure column-order is the same
+                hdf_res_df = hdf_res_df[list(fit_cfg.res_df)]
+
+                assert hdf_res_df.equals(
+                    fit_cfg.res_df
+                ), "res_df of HDF-container and pickle-dumps are not equal!"
+
+        # make sure to close the HDF-file so that the folders can be savely deleted
+        fit_db.close()
 
     def test_8_multiconfig_rtresults(self):
         res = RTresults("tests/proc_multi")
@@ -267,51 +346,102 @@ def test_9_multiconfig_props(self):
         assert fit.SRF.ncoefs == 5, "multiconfig props not correct"
         assert fit.lsq_kwargs["ftol"] == 0.001, "multiconfig props not correct"
 
-    def test_91_export_results(self):
-        for folder in ["proc_test", "proc_test2", "proc_test3", "proc_multi"]:
-            proc = RTprocess(f"tests/{folder}")
+    def test_92_export_results(self):
+        for folder in ["proc_test", "proc_test2", "proc_multi"]:
+            # select the first subfolder and find the .ini file used
+            res = list(RTresults(f"tests/{folder}"))
+            assert len(res) == 1, "there's more than one result-folder???"
+            res = res[0]
+            configpath = res.load_cfg().configpath
+
+            proc = RTprocess(configpath)
+
+            parameters = ["t_s", "tau", "sig2"]
+            export_functions = dict(sig2=export_sig2)
+
+            metrics = dict(R=("pearson", "sig", "tot"), RMSD=("rmsd", "sig", "tot"))
+
+            proc.export_data_to_HDF(
+                parameters=parameters,
+                metrics=metrics,
+                export_functions=export_functions,
+                ncpu=1,
+                finalout_name="export_ncpu1.h5",
+            )
+
+            proc.export_data_to_HDF(
+                parameters=parameters,
+                metrics=metrics,
+                export_functions=export_functions,
+                ncpu=3,
+                finalout_name="export_ncpu3.h5",
+            )
+
+            for export_name in ["export_ncpu1", "export_ncpu3"]:
+                data = res.load_hdf(export_name)
+
+                # check if all ID's are present in the HDF-container
+                assert all(
+                    i in data.IDs.ID.values for i in ["RT1_1", "RT1_2", "RT1_3"]
+                ), "HDF-container does not contain all IDs"
+
+                # some basic checks if Fits and MultiFits are correctly exported
+
+                if folder in ["proc_test", "proc_test2"]:
+                    configs = ["default"]  # single-config is called "default"
+                else:
+                    configs = ["cfg_0", "cfg_1"]
+
+                for cfg in configs:
+                    cols = list(getattr(data.datasets, cfg).dynamic.get_id(0).columns)
+                    assert cols == ["tau", "sig2"], "dynamic columns are not OK"
+
+                    cols = list(getattr(data.datasets, cfg).static.get_id(0).columns)
+                    assert cols == ["t_s"], "static columns are not OK"
 
-            parameters = dict(t_s=dict(long_name="bare soil directionality"),
-                              tau=dict(long_name="optical depth"),
-                              sig2=dict(long_name="sigma0 squared")
-                              )
+                    cols = list(getattr(data.datasets, cfg).metrics.get_id(0).columns)
+                    assert cols == ["R", "RMSD"], "metrics columns are not OK"
 
-            metrics = dict(R=["pearson", "sig", "tot",
-                              dict(long_name="sig0 pearson correlation")],
-                           RMSD=["rmsd", "sig", "tot",
-                              dict(long_name="sig0 RMSD")]
-                           )
+    # this is needed to disable log-capturing during testing
+    @pytest.fixture(autouse=True)
+    def caplog(self, caplog):
+        self.caplog = caplog
 
-            export_functions = dict(sig2=lambda fit: fit.dataset.sig**2)
+    # def test_log_to_file(self):
+    #     logpath = Path("tests/proc_test/testlog.log")
+    #     # temporarily set the log-capture level to 0 (e.g. allow all logs)
+    #     with self.caplog.at_level(0):
+    #         start_log_to_file(logpath, 0)
+    #         log.error(str(log.handlers))
+    #         log.error("error message")
+    #         log.warning("warning message")
+    #         log.debug("debug message")
+    #         log.info("info message")
+    #         log.progress("progress message")
 
-            attributes = dict(info="some info")
+    #         log.progress("a multiline\nmessage nice!")
 
-            res = proc.export_data(parameters=parameters,
-                                   metrics=metrics,
-                                   export_functions=export_functions,
-                                   attributes=attributes,
-                                   index_col='gpi')
+    #     stop_log_to_file()
 
-            if folder != "proc_multi":
-                # make single-fit results a dict as well so that they can be treated
-                # in the same way as multi-fits
-                res = dict(single_fit=res)
+    #     assert logpath.exists(), "the logfile does not exist!"
 
-            for cfg_name, useres in res.items():
-                assert useres.R.dims == ('gpi',), "metric dim is wrong"
-                assert useres.RMSD.dims == ('gpi',), "metric dim is wrong"
+    #     with open(logpath, "r") as file:
+    #         msgs = [line.split(mp.current_process().name)[-1].strip()
+    #                 for line in file.readlines()]
 
-                assert useres.t_s.dims == ('gpi',), "static parameter dim is wrong"
-                assert useres.tau.dims == ('gpi', 'date'), "dynamic parameter dim is wrong"
-                assert useres.sig2.dims == ('gpi', 'date'), "dynamic parameter dim is wrong"
+    #     expected_msgs = ["ERROR   error message",
+    #                      "WARNING warning message",
+    #                      "DEBUG   debug message",
+    #                      "INFO    info message",
+    #                      "PROG.   progress message",
+    #                      "PROG.   a multiline",
+    #                      "message nice!"]
 
-                # check if attributes are correctly attached
-                assert useres.attrs['info'] == attributes['info'], "attributes not correctly attached"
-                for key, attrs in parameters.items():
-                    for name, a in attrs.items():
-                        assert useres[key].attrs[name] == a, "attributes not correctly attached"
+    #     # skip the first message since it comes from starting the file-handler
+    #     for i, msg in enumerate(msgs[1:]):
+    #         assert msg == expected_msgs[i], (
+    #             f"log message {i} not OK:\n {msgs}")
 
-                assert "model_definition" in useres.attrs, "model_definition not correctly attached"
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_rt1.py b/tests/test_rt1.py
index 062cf4fc..e81d6fdf 100644
--- a/tests/test_rt1.py
+++ b/tests/test_rt1.py
@@ -4,9 +4,11 @@
 import numpy as np
 
 import sys
-sys.path.append('..')
+
+sys.path.append("..")
 from rt1.rt1 import RT1
 from rt1.volume import Rayleigh, HenyeyGreenstein
+
 # from rt1.coefficients import RayleighIsotropic
 from rt1.surface import Isotropic, CosineLobe
 
@@ -14,52 +16,52 @@
 
 
 class TestRT1(unittest.TestCase):
-
     def setUp(self):
-        self.I0 = 1.
-        self.t_0 = np.deg2rad(60.)
-        self.t_ex = np.deg2rad(60.)
-        self.p_0 = np.ones_like(self.t_0) * 0.
-        self.p_ex = np.ones_like(self.t_0) * 0.
+        self.I0 = 1.0
+        self.t_0 = np.deg2rad(60.0)
+        self.t_ex = np.deg2rad(60.0)
+        self.p_0 = np.ones_like(self.t_0) * 0.0
+        self.p_ex = np.ones_like(self.t_0) * 0.0
         self.V = Rayleigh(tau=np.array([0.7]), omega=np.array([0.3]))
         self.S = Isotropic()
         # self.C = RayleighIsotropic()
 
     def test_init(self):
-        RT = RT1(self.I0, self.t_0, self.t_ex, self.p_0, self.p_ex,
-                 V=self.V, SRF=self.S)
+        RT = RT1(
+            self.I0, self.t_0, self.t_ex, self.p_0, self.p_ex, V=self.V, SRF=self.S
+        )
         self.assertTrue(RT.t_0 == self.t_0)
 
     def test_calc(self):
         # just try to get it running simply without further testing
-        RT = RT1(self.I0, self.t_0, self.t_ex, self.p_0, self.p_ex,
-                 V=self.V, SRF=self.S)
+        RT = RT1(
+            self.I0, self.t_0, self.t_ex, self.p_0, self.p_ex, V=self.V, SRF=self.S
+        )
         Itot, Isurf, Ivol, Iint = RT.calc()
         self.assertTrue(np.allclose(Itot, Isurf + Ivol + Iint))
 
-        V = Rayleigh(tau=np.array([0.]), omega=np.array([0.]))
-        RT = RT1(self.I0, self.t_0, self.t_ex, self.p_0, self.p_ex,
-                 V=V, SRF=self.S)
+        V = Rayleigh(tau=np.array([0.0]), omega=np.array([0.0]))
+        RT = RT1(self.I0, self.t_0, self.t_ex, self.p_0, self.p_ex, V=V, SRF=self.S)
         Itot, Isurf, Ivol, Iint = RT.calc()
-        self.assertEqual(Ivol, 0.)
-        self.assertEqual(Iint, 0.)  # todo gives nan
+        self.assertEqual(Ivol, 0.0)
+        self.assertEqual(Iint, 0.0)  # todo gives nan
         self.assertEqual(Itot, Isurf)
-        self.assertTrue(Isurf > 0.)
+        self.assertTrue(Isurf > 0.0)
 
     def test_surface(self):
-        V = Rayleigh(tau=np.array([0.]), omega=np.array([0.]))
-        t_0 = np.deg2rad(60.)
-        RT = RT1(4., t_0, self.t_ex, self.p_0, self.p_ex, V=V, SRF=self.S)
+        V = Rayleigh(tau=np.array([0.0]), omega=np.array([0.0]))
+        t_0 = np.deg2rad(60.0)
+        RT = RT1(4.0, t_0, self.t_ex, self.p_0, self.p_ex, V=V, SRF=self.S)
         Itot, Isurf, Ivol, Iint = RT.calc()
-        self.assertTrue(np.allclose(Isurf, 2. / np.pi, 15))
+        self.assertTrue(np.allclose(Isurf, 2.0 / np.pi, 15))
 
     def test_volume(self):
-        t_0 = np.deg2rad(60.)
-        t_ex = np.deg2rad(60.)
-        V = Rayleigh(tau=np.array([0.]), omega=np.array([0.]))
+        t_0 = np.deg2rad(60.0)
+        t_ex = np.deg2rad(60.0)
+        V = Rayleigh(tau=np.array([0.0]), omega=np.array([0.0]))
         RT = RT1(self.I0, t_0, t_ex, self.p_0, self.p_ex, V=V, SRF=self.S)
         Itot, Isurf, Ivol, Iint = RT.calc()
-        self.assertEqual(Ivol, 0.)
+        self.assertEqual(Ivol, 0.0)
 
     def test_fn_coefficients_RayIso(self):
         # test if calculation of fn coefficients is correct
@@ -68,20 +70,20 @@ def test_fn_coefficients_RayIso(self):
         # and isotropic surface scattering phase function
         S = Isotropic()
         V = Rayleigh(tau=np.array([0.7]), omega=np.array([0.3]))
-        t_0 = np.deg2rad(60.)
-        t_ex = np.deg2rad(60.)
-        p_0 = 0.
+        t_0 = np.deg2rad(60.0)
+        t_ex = np.deg2rad(60.0)
+        p_0 = 0.0
         RT = RT1(self.I0, t_0, t_ex, p_0, self.p_ex, V=V, SRF=S)
 
         # the reference solutions should be (see rayleighisocoefficients.pdf)
-        f0 = 3. / (16. * np.pi) * (3. - np.cos(t_0)**2.)
-        f1 = 0.
-        f2 = 3. / (16. * np.pi) * (3. * np.cos(t_0)**2. - 1.)
+        f0 = 3.0 / (16.0 * np.pi) * (3.0 - np.cos(t_0) ** 2.0)
+        f1 = 0.0
+        f2 = 3.0 / (16.0 * np.pi) * (3.0 * np.cos(t_0) ** 2.0 - 1.0)
         # and all others are 0.
 
-        self.assertTrue(np.allclose([f0, f1, f2],
-                                    RT._fnevals(t_0, p_0,
-                                                t_ex, self.p_ex)))
+        self.assertTrue(
+            np.allclose([f0, f1, f2], RT._fnevals(t_0, p_0, t_ex, self.p_ex))
+        )
 
     def test_fn_coefficients_RayCosine(self):
         # test if calculation of fn coefficients is correct
@@ -96,34 +98,34 @@ def test_fn_coefficients_RayCosine(self):
         # tests are using full Volume phase function, but only
         # ncoef times the coefficients from the surface
 
-        t_0 = np.pi / 2.
+        t_0 = np.pi / 2.0
         t_ex = 0.234234
-        p_0 = np.pi / 2.
-        p_ex = 0.
+        p_0 = np.pi / 2.0
+        p_ex = 0.0
 
-        RT = RT1(self.I0, t_0, t_ex, p_0, p_ex, V=V, SRF=S, geometry='vvvv')
+        RT = RT1(self.I0, t_0, t_ex, p_0, p_ex, V=V, SRF=S, geometry="vvvv")
         res = RT._fnevals(t_0, p_0, t_ex, p_ex)
 
         # ncoefs = 1
         # analtytical solution for ncoefs = 1 --> n=0
 
-        a0 = (3. / (16. * np.pi)) * (4. / 3.)
-        a2 = (3. / (16. * np.pi)) * (2. / 3.)
-        b0 = (15. * np.sqrt(np.pi)) / (16. * gamma(3.5) * gamma(4.))
+        a0 = (3.0 / (16.0 * np.pi)) * (4.0 / 3.0)
+        a2 = (3.0 / (16.0 * np.pi)) * (2.0 / 3.0)
+        b0 = (15.0 * np.sqrt(np.pi)) / (16.0 * gamma(3.5) * gamma(4.0))
 
         # print 'a0:', a0, V._get_legcoef(0)
         # print 'a2:', a2, V._get_legcoef(2)
         # print 'b0: ', b0, S._get_legcoef(0)
 
-        ref0 = 1. / 4. * b0 * (8. * a0 - a2 - 3. * a2 * np.cos(2. * t_0))
-        ref2 = 3. / 4. * a2 * b0 * (1. + 3. * np.cos(2. * t_0))
+        ref0 = 1.0 / 4.0 * b0 * (8.0 * a0 - a2 - 3.0 * a2 * np.cos(2.0 * t_0))
+        ref2 = 3.0 / 4.0 * a2 * b0 * (1.0 + 3.0 * np.cos(2.0 * t_0))
 
         self.assertTrue(np.allclose([ref0, ref2], [res[0], res[2]]))
 
         # ncoefs = 2
         # first and third coef should be the same as for ncoefs=1
         S = CosineLobe(ncoefs=2, i=5, NormBRDF=np.array([np.pi]))
-        RT = RT1(self.I0, t_0, t_ex, p_0, p_ex, V=V, SRF=S, geometry='ffff')
+        RT = RT1(self.I0, t_0, t_ex, p_0, p_ex, V=V, SRF=S, geometry="ffff")
         res2 = RT._fnevals(t_0, p_0, t_ex, p_ex)
 
         self.assertTrue(np.allclose([ref0, ref2], [res2[0], res2[2]]))
@@ -135,21 +137,20 @@ def test_fn_coefficients_HGIso(self):
         # and isotropic surface scattering phase function
         S = Isotropic()
 
-        t_0 = 0.
-        t_ex = 0.
-        p_0 = 0.
+        t_0 = 0.0
+        t_ex = 0.0
+        p_0 = 0.0
         p_ex = np.pi
 
-        V = HenyeyGreenstein(omega=np.array([0.2]),
-                             tau=np.array([1.7]),
-                             t=0.7, ncoefs=1)
-
+        V = HenyeyGreenstein(
+            omega=np.array([0.2]), tau=np.array([1.7]), t=0.7, ncoefs=1
+        )
 
-        RT = RT1(self.I0, t_0, t_ex, p_0, p_ex, V=V, SRF=S, geometry='ffff')
+        RT = RT1(self.I0, t_0, t_ex, p_0, p_ex, V=V, SRF=S, geometry="ffff")
 
         r = RT._fnevals(t_0, p_0, t_ex, p_ex)
 
-        self.assertTrue(np.allclose(r[0], 1. / (2. * np.pi)))
+        self.assertTrue(np.allclose(r[0], 1.0 / (2.0 * np.pi)))
 
         # V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=2)
         # RT = RT1(self.I0, mu_0, mu_ex, phi_0, phi_ex, V=V, SRF=S)
@@ -163,109 +164,109 @@ def test_fn_coefficients_HGIso(self):
 # tests with I0=0 and extreme angles
 # zero order solution ???
 
-    # def test_x(self):
-        # A = AR1()
-        # x = A.x(self.N, self.std, self.phi, method='slow')
-        # y = A.x(self.N, self.std, self.phi, mean=0.5, method='slow')
+# def test_x(self):
+# A = AR1()
+# x = A.x(self.N, self.std, self.phi, method='slow')
+# y = A.x(self.N, self.std, self.phi, mean=0.5, method='slow')
 #
-        # self.assertEqual(len(x), self.N)
-        # self.assertTrue(np.abs(x.mean()) < 1.e-9)
-        # self.assertTrue(np.abs(np.abs(y.mean())-0.5) < 1.e-9)
+# self.assertEqual(len(x), self.N)
+# self.assertTrue(np.abs(x.mean()) < 1.e-9)
+# self.assertTrue(np.abs(np.abs(y.mean())-0.5) < 1.e-9)
 #
-    # def test_methods(self):
-        # seed = 12345
-        # A = AR1()
-        # x = A.x(self.N, self.std, self.phi, method='slow', seed=seed)
-        # y = A.x(self.N, self.std, self.phi, seed=seed)
-        # z = A.x(self.N, self.std, self.phi, method='fast', seed=seed)
-        # self.assertTrue(np.array_equal(y,x))
-        # print x / y
-        # self.assertTrue(np.allclose(x/y, np.ones(self.N).astype('float'),
-        #                               rtol=1.E-9))
+# def test_methods(self):
+# seed = 12345
+# A = AR1()
+# x = A.x(self.N, self.std, self.phi, method='slow', seed=seed)
+# y = A.x(self.N, self.std, self.phi, seed=seed)
+# z = A.x(self.N, self.std, self.phi, method='fast', seed=seed)
+# self.assertTrue(np.array_equal(y,x))
+# print x / y
+# self.assertTrue(np.allclose(x/y, np.ones(self.N).astype('float'),
+#                               rtol=1.E-9))
 #
-    # def test_acorr(self):
-        # A = AR1()
-        # x = A.x(self.N, self.std, self.phi, method='slow')
-        # c = acorr_1_lag1(x)
-        # self.assertTrue(np.abs(1.-c[0]/1.)<0.000000001)  # autocorrelation
-        # print c
-        # self.assertTrue(c[1] < 1.)
-        # self.assertEqual(c[1], A.rho1_1d(x))
+# def test_acorr(self):
+# A = AR1()
+# x = A.x(self.N, self.std, self.phi, method='slow')
+# c = acorr_1_lag1(x)
+# self.assertTrue(np.abs(1.-c[0]/1.)<0.000000001)  # autocorrelation
+# print c
+# self.assertTrue(c[1] < 1.)
+# self.assertEqual(c[1], A.rho1_1d(x))
 #
-        # for phi in [0.2, 0.5, 0.75, 1.]:
-        # # with noise
-        # x = A.x(self.N, self.std, phi, method='slow')
-        # c = acorr_1_lag1(x)
-        # self.assertAlmostEqual(c[1], phi,places=1)
+# for phi in [0.2, 0.5, 0.75, 1.]:
+# # with noise
+# x = A.x(self.N, self.std, phi, method='slow')
+# c = acorr_1_lag1(x)
+# self.assertAlmostEqual(c[1], phi,places=1)
 #
-        # # without noise
-        # x = A.x(self.N, 0.0000000000001, phi, method='slow')
-        # c = acorr_1_lag1(x)
-        # self.assertAlmostEqual(c[1], phi,places=1)
+# # without noise
+# x = A.x(self.N, 0.0000000000001, phi, method='slow')
+# c = acorr_1_lag1(x)
+# self.assertAlmostEqual(c[1], phi,places=1)
 #
-    # def test_fast(self):
-        # # test that fast method produces the same results
-        # N = 1000
-        # sigma = 0.1
-        # phi = 0.5
-        # seed = 12345
+# def test_fast(self):
+# # test that fast method produces the same results
+# N = 1000
+# sigma = 0.1
+# phi = 0.5
+# seed = 12345
 #
-        # A = AR1()
-        # B = AR1()
+# A = AR1()
+# B = AR1()
 #
-        # a = A.x(N, sigma, phi, seed=seed, method='slow')
-        # b = B.x(N, sigma, phi, seed=seed, method='fast')
+# a = A.x(N, sigma, phi, seed=seed, method='slow')
+# b = B.x(N, sigma, phi, seed=seed, method='fast')
 #
-        # self.assertTrue(np.array_equal(a,b))
+# self.assertTrue(np.array_equal(a,b))
 #
 #
 #
 #
-    # def test_fit(self):
-        # # test fit with low noise
-        # A = AR1()
-        # x = A.x(self.N, 0.01, self.phi, method='slow')
+# def test_fit(self):
+# # test fit with low noise
+# A = AR1()
+# x = A.x(self.N, 0.01, self.phi, method='slow')
 #
-        # # retrieve parameters
-        # phi, std_r = A.fit(x)
-        # print 'phi, self.phi: ', phi, self.phi
-        # print 'std, self.std: ', std_r, self.std
-        # #self.assertAlmostEqual(phi, self.phi, places=1)
-        # todo: more robust testing of correct results!
-        # # we only check for PHI, as an error in PHI will automatically
-        # # propagate into an error in SIGMA
-        # #self.assertAlmostEqual(std_r, self.std,places=1)
+# # retrieve parameters
+# phi, std_r = A.fit(x)
+# print 'phi, self.phi: ', phi, self.phi
+# print 'std, self.std: ', std_r, self.std
+# #self.assertAlmostEqual(phi, self.phi, places=1)
+# todo: more robust testing of correct results!
+# # we only check for PHI, as an error in PHI will automatically
+# # propagate into an error in SIGMA
+# #self.assertAlmostEqual(std_r, self.std,places=1)
 #
-        # self.assertEqual(phi, A.rho1_1d(x))
-        # self.assertEqual(std_r, (np.sqrt(x.var()*(1.-phi**2.))))
+# self.assertEqual(phi, A.rho1_1d(x))
+# self.assertEqual(std_r, (np.sqrt(x.var()*(1.-phi**2.))))
 #
-        # # check for masked arrays
-        # y = np.hstack((x,[np.nan,np.nan]))
-        # y = np.ma.array(y, mask=np.isnan(y))
-        # phiy, std_ry = A.fit(y)
-        # ratio = np.abs(1.-phi/phiy)
-        # self.assertTrue(ratio < 0.1/100.)  # better than 0.1%s
-        # ratio = np.abs(1.-std_r/std_ry)
-        # self.assertTrue(ratio < 0.1/100.)  # better than 0.1%s
+# # check for masked arrays
+# y = np.hstack((x,[np.nan,np.nan]))
+# y = np.ma.array(y, mask=np.isnan(y))
+# phiy, std_ry = A.fit(y)
+# ratio = np.abs(1.-phi/phiy)
+# self.assertTrue(ratio < 0.1/100.)  # better than 0.1%s
+# ratio = np.abs(1.-std_r/std_ry)
+# self.assertTrue(ratio < 0.1/100.)  # better than 0.1%s
 #
-        # # test for 2D data (singular0 processing)
-        # ny = 5
-        # nx = 3
-        # X = np.ones((self.N,ny,nx))*np.nan
-        # for i in xrange(ny):
-        # for j in xrange(nx):
-        # X[:,i,j] = x[:]
+# # test for 2D data (singular0 processing)
+# ny = 5
+# nx = 3
+# X = np.ones((self.N,ny,nx))*np.nan
+# for i in xrange(ny):
+# for j in xrange(nx):
+# X[:,i,j] = x[:]
 #
 #
-        # PHI, STD = A.fit(X)
+# PHI, STD = A.fit(X)
 #
-        # self.assertTrue(np.all(PHI==phi))
-        # self.assertTrue(np.all(np.abs(1.-STD/std_r) < 0.001)) #0.1% accuracy
+# self.assertTrue(np.all(PHI==phi))
+# self.assertTrue(np.all(np.abs(1.-STD/std_r) < 0.001)) #0.1% accuracy
 
-        # parallel processing
-        #PHI1, STD1 = A.fit(X, nproc=4)
-        # self.assertTrue(np.all(PHI1==phi))
-        # self.assertTrue(np.all(np.abs(1.-STD1/std_r) < 0.001)) #0.1% accuracy
+# parallel processing
+# PHI1, STD1 = A.fit(X, nproc=4)
+# self.assertTrue(np.all(PHI1==phi))
+# self.assertTrue(np.all(np.abs(1.-STD1/std_r) < 0.001)) #0.1% accuracy
 
 
 if __name__ == "__main__":
diff --git a/tests/test_rtfit_dumps.py b/tests/test_rtfit_dumps.py
index 26943f73..dca1c8b6 100644
--- a/tests/test_rtfit_dumps.py
+++ b/tests/test_rtfit_dumps.py
@@ -6,51 +6,50 @@
 
 import unittest
 import numpy as np
-import cloudpickle
 import matplotlib.pyplot as plt
-import copy
 import os
 
+from rt1.rtresults import HDFaccessor
+from rt1.rtfits import load
+
+
 class TestDUMPS(unittest.TestCase):
     def setUp(self):
-        self.sig0_dB_path = os.path.dirname(__file__) + os.sep + "sig0_dB.dump"
-        self.sig0_linear_path = os.path.dirname(__file__) + os.sep + "sig0_linear.dump"
-
-    def load_data(self, path):
-        with open(path, 'rb') as file:
-            fit = cloudpickle.load(file)
-        return fit
+        self.fit_dB_path = os.path.dirname(__file__) + os.sep + "test_fit_db.h5"
 
+        self.sig0_dB_ID = "sig0_dB"
+        self.sig0_linear_ID = "sig0_linear"
 
-#        self.assertTrue(
-#            err < errdict[key],
-#            msg='derived error' + str(err) + 'too high for ' + str(key))
+    def load_data(self, ID):
+        with HDFaccessor(self.fit_dB_path) as fit_db:
+            fit = fit_db.load_fit(ID)
+        return fit
 
     def test_rtplots(self):
-
-        for path, msg in zip([self.sig0_dB_path, self.sig0_linear_path],
-                             ['dB', 'linear']):
-
-            print(f'testing plotfunctions for {msg} fit')
-            fit = self.load_data(path)
+        for ID, msg in zip([self.sig0_dB_ID, self.sig0_linear_ID], ["dB", "linear"]):
+            print(f"testing plotfunctions for {msg} fit")
+            fit = self.load_data(ID)
 
             # call performfit to re-initialize _fnevals functions
             # and evaluate intermediate results
             # (they might have been removed if symeninge has been used)
-            fit.lsq_kwargs['verbose'] = 0
-            fit.performfit(intermediate_results=True,
-                           print_progress=True)
+            fit.lsq_kwargs["verbose"] = 0
+            fit.performfit(intermediate_results=True, print_progress=True)
 
             # get list of available plot-methods
-            method_list = [func for func in dir(fit.plot) if
-                           callable(getattr(fit.plot, func)) and not func.startswith("__")]
+            method_list = [
+                func
+                for func in dir(fit.plot)
+                if callable(getattr(fit.plot, func)) and not func.startswith("__")
+            ]
 
             for function_name in method_list:
-                print(f'... {function_name}')
-                if function_name == 'printsig0analysis':
+                print(f"... {function_name}")
+                if function_name == "printsig0analysis":
                     # check 'dataset' index slider
                     f, s1, s2 = fit.plot.__getattribute__(function_name)(
-                            range2=2, range1=1, use_index='dataset')
+                        range2=2, range1=1, use_index="dataset"
+                    )
                     # check update functions
                     s1.set_val(1)
                     s2.set_val(1)
@@ -58,23 +57,22 @@ def test_rtplots(self):
 
                     # check 'groups' index slider
                     f, s1, s2 = fit.plot.__getattribute__(function_name)(
-                            range2=2, range1=1, use_index='groups')
+                        range2=2, range1=1, use_index="groups"
+                    )
                     # check update functions
                     s1.set_val(1)
                     s2.set_val(1)
                     plt.close(f)
 
-
-                elif function_name == 'analyzemodel':
-                    f, sliders, txt_but = fit.plot.__getattribute__(
-                        function_name)()
+                elif function_name == "analyzemodel":
+                    f, sliders, txt_but = fit.plot.__getattribute__(function_name)()
 
                     # check update functions
                     for key, s in sliders.items():
-                        s.set_val((s.valmax - s.valmin)/2.)
+                        s.set_val((s.valmax - s.valmin) / 2.0)
 
                     for key, b in txt_but.items():
-                        if key == 'buttons':
+                        if key == "buttons":
                             # the initial status is ALL OFF
                             stat = b.get_status()
                             for i in range(len(stat)):
@@ -87,28 +85,30 @@ def test_rtplots(self):
                             self.assertTrue(~np.all(b.get_status()))
                         else:
                             # set the boundaries of the parameters
-                            if 'min' in key:
+                            if "min" in key:
                                 b.set_val(0.02)
-                            if 'max' in key:
+                            if "max" in key:
                                 b.set_val(0.99)
                     plt.close(f)
 
-                elif function_name == 'intermediate_residuals':
+                elif function_name == "intermediate_residuals":
                     # check default (e.g. pandas datetime-offset)
-                    f = fit.plot.__getattribute__(function_name)(fmt='%d.%b %Y')
+                    f = fit.plot.__getattribute__(function_name)(fmt="%d.%b %Y")
                     plt.close(f)
                     # check grouping with respect to incidence angles and
                     # convert the labels to degrees
                     f = fit.plot.__getattribute__(function_name)(
-                        grp=('inc', 10),
-                        label_formatter=lambda x,y:round(np.rad2deg(x),2))
+                        grp=("inc", 10),
+                        label_formatter=lambda x, y: round(np.rad2deg(x), 2),
+                    )
                     plt.close(f)
                     # check grouping with respect to datetimes
-                    f = fit.plot.__getattribute__(function_name)(grp='groups')
+                    f = fit.plot.__getattribute__(function_name)(grp="groups")
                     plt.close(f)
                     # check grouping with respect to the dataset index
                     f = fit.plot.__getattribute__(function_name)(
-                        grp='dataset', plottype='2D', fmt='%Y %b %d (%H:%M)')
+                        grp="dataset", plottype="2D", fmt="%Y %b %d (%H:%M)"
+                    )
                     plt.close(f)
 
                 else:
@@ -116,38 +116,51 @@ def test_rtplots(self):
                     plt.close(f)
 
     def test_performfit(self):
-        for path, msg in zip([self.sig0_dB_path, self.sig0_linear_path],
-                             ['dB', 'linear']):
-
-            print(f'testing plotfunctions for {msg} fit')
-            fit = self.load_data(path)
+        for ID, msg in zip([self.sig0_dB_ID, self.sig0_linear_ID], ["dB", "linear"]):
+            print(f"testing plotfunctions for {msg} fit")
+            fit = self.load_data(ID)
 
             old_results = fit.res_dict
             # print model definition
             fit.model_definition
-            print('testing performfit')
-            fit.lsq_kwargs['verbose'] = 0
-            fit.performfit(intermediate_results=True,
-                           print_progress=True)
+            print("testing performfit")
+            fit.lsq_kwargs["verbose"] = 0
+            fit.performfit(intermediate_results=True, print_progress=True)
 
             # call _cache_info() to make coveralls happy
             fit._cache_info()
             fit.R._cache_info()
 
             # try to dump the file again (without fit-details)
-            fit.dump(os.path.join(os.path.dirname(__file__), 'testdump1.dump'),
-                     mini=True)
+            fit.dump(
+                os.path.join(os.path.dirname(__file__), "testdump1.dump"), mini=True
+            )
             # try to dump the file again (with fit-details)
-            fit.dump(os.path.join(os.path.dirname(__file__), 'testdump2.dump'),
-                     mini=False)
+            fit.dump(
+                os.path.join(os.path.dirname(__file__), "testdump2.dump"), mini=False
+            )
 
             for key, val in old_results.items():
-                self.assertTrue(np.allclose(fit.res_dict[key],
-                                            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()}')
+                self.assertTrue(
+                    np.allclose(
+                        fit.res_dict[key], old_results[key], atol=1e-3, rtol=1e-3
+                    ),
+                    msg=f"fitted values for {msg} fit of {key} differ by "
+                    + f"{np.subtract(fit.res_dict[key], old_results[key]).mean()}",
+                )
+
+    def test_load_and_dump_fits(self):
+        # load fit from HDF-container
+        fit = self.load_data(self.sig0_dB_ID)
+        # pickle-dump the fit
+        fit.dump(os.path.dirname(__file__) + os.sep + "dump_test.dump")
+
+        # load the fit from the pickled result
+        load_fit = load(os.path.dirname(__file__) + os.sep + "dump_test.dump")
 
+        # perform the fit to see if it works
+        load_fit.performfit()
 
 
 if __name__ == "__main__":
-    unittest.main()
\ No newline at end of file
+    unittest.main()
diff --git a/tests/test_rtfits_performfit.py b/tests/test_rtfits_performfit.py
index ec3a1764..7ad7a859 100644
--- a/tests/test_rtfits_performfit.py
+++ b/tests/test_rtfits_performfit.py
@@ -20,60 +20,54 @@
 from rt1.surface import HenyeyGreenstein as HGsurface
 import pandas as pd
 
-class TestRTfits(unittest.TestCase):
 
-    def performfit(self,
-                   sig0,
-                   dB,
-                   Nmeasurements,
-                   mininc,
-                   maxinc,
-                   minincnum,
-                   maxincnum,
-                   omin,
-                   omax,
-                   taumin,
-                   taumax,
-                   rmin,
-                   rmax,
-                   tmin,
-                   tmax,
-                   ):
-        '''
+class TestRTfits(unittest.TestCase):
+    def performfit(
+        self,
+        sig0,
+        dB,
+        Nmeasurements,
+        mininc,
+        maxinc,
+        minincnum,
+        maxincnum,
+        omin,
+        omax,
+        taumin,
+        taumax,
+        rmin,
+        rmax,
+        tmin,
+        tmax,
+    ):
+        """
         fucntion adapted from doc/examples/examples_fitting.py
 
         the values are generated using np.random.seed(0) for all
         calls of np.random...
-        '''
+        """
 
         # ---------------------------------------------------------------------
         # ------------------------- DATA-GENERATION ---------------------------
 
         # generate N arrays of incidence-angles that contain maxincnum
         # values between mininc and maxinc
-        inc = np.array([np.deg2rad(np.linspace(mininc,
-                                               maxinc,
-                                               maxincnum))]
-                       * Nmeasurements)
+        inc = np.array(
+            [np.deg2rad(np.linspace(mininc, maxinc, maxincnum))] * Nmeasurements
+        )
 
         np.random.seed(0)  # reset seed to have a reproducible test
         # generate random samples of parameters
-        omegadata = np.random.uniform(low=omin,
-                                      high=omax,
-                                      size=(Nmeasurements,))
+        omegadata = np.random.uniform(low=omin, high=omax, size=(Nmeasurements,))
 
         np.random.seed(0)  # reset seed to have a reproducible test
-        taudata = np.random.uniform(low=taumin,
-                                    high=taumax,
-                                    size=(Nmeasurements,))
+        taudata = np.random.uniform(low=taumin, high=taumax, size=(Nmeasurements,))
 
         np.random.seed(0)  # reset seed to have a reproducible test
-        rdata = np.random.uniform(low=rmin,
-                                  high=rmax, size=(Nmeasurements,))
+        rdata = np.random.uniform(low=rmin, high=rmax, size=(Nmeasurements,))
 
         np.random.seed(0)  # reset seed to have a reproducible test
-        tdata = np.random.uniform(low=tmin,
-                                  high=tmax, size=(Nmeasurements,))
+        tdata = np.random.uniform(low=tmin, high=tmax, size=(Nmeasurements,))
 
         # set tau of Neq measurements to be equal and fit only a single param.
         # for tau to all datasets
@@ -86,8 +80,9 @@ def performfit(self,
             Neq = int(Nmeasurements / 5)
 
             np.random.seed(0)  # reset seed to have a reproducible test
-            equal_tau_selects = np.random.choice(range(Nmeasurements),
-                                                 size=Neq + 1, replace=False)
+            equal_tau_selects = np.random.choice(
+                range(Nmeasurements), size=Neq + 1, replace=False
+            )
 
         for i in equal_tau_selects:
             # for i in [1,3,5,7,9,11,14,16,17]:
@@ -98,13 +93,22 @@ def performfit(self,
         # dataset since they will be changed to randomly generated values!
 
         V_data = Rayleigh(tau=0.1, omega=0.1)
-        SRF_data = HGsurface(ncoefs=10, t=sp.var('t_data'), a=[1., 1., 1.])
+        SRF_data = HGsurface(ncoefs=10, t=sp.var("t_data"), a=[1.0, 1.0, 1.0])
 
         # setup rt1-object
         # (since the fn-coefficients must still be calculated, one must
         #  specify the arrays for the parameters afterwards)
-        R_data = RT1(1., 0., 0., 0., 0., V=V_data, SRF=SRF_data,
-                     geometry='mono', param_dict={'t_data': .5})
+        R_data = RT1(
+            1.0,
+            0.0,
+            0.0,
+            0.0,
+            0.0,
+            V=V_data,
+            SRF=SRF_data,
+            geometry="mono",
+            param_dict={"t_data": 0.5},
+        )
 
         # specify parameters and incidence-angles
         R_data.t_0 = inc
@@ -113,36 +117,36 @@ def performfit(self,
         R_data.V.omega = omegadata[:, np.newaxis]
         R_data.V.tau = taudata[:, np.newaxis]
         R_data.SRF.NormBRDF = rdata[:, np.newaxis]
-        R_data.param_dict = {'t_data': tdata[:, np.newaxis]}
+        R_data.param_dict = {"t_data": tdata[:, np.newaxis]}
         # calculate the data and add some random noise
         data = R_data.calc()[0]
 
         np.random.seed(0)  # reset seed to have a reproducible test
-        noise = np.random.uniform(low=-np.max(data) / 50.,
-                                  high=np.max(data) / 50., size=data.shape)
+        noise = np.random.uniform(
+            low=-np.max(data) / 50.0, high=np.max(data) / 50.0, size=data.shape
+        )
         data = data + noise
 
         if sig0 is True:
             # convert the calculated results do sigma0
-            signorm = 4. * np.pi * np.cos(inc)
+            signorm = 4.0 * np.pi * np.cos(inc)
             data = signorm * data
 
         if dB is True:
             # convert the calculated results to dB
-            data = 10. * np.log10(data)
+            data = 10.0 * np.log10(data)
 
         # define the mask for selecting non-rectangular arrays of data (this is
         # done to show that fitting also works for non-rectangular datasets)
 
         np.random.seed(0)  # reset seed to have a reproducible test
-        inc_lengths = np.random.randint(minincnum,
-                                        maxincnum,
-                                        Nmeasurements)
+        inc_lengths = np.random.randint(minincnum, maxincnum, Nmeasurements)
         selects = []
         for i in range(Nmeasurements):
             np.random.seed(0)  # reset seed to have a reproducible test
-            selects += [np.random.choice(range(maxincnum), inc_lengths[i],
-                                         replace=False)]
+            selects += [
+                np.random.choice(range(maxincnum), inc_lengths[i], replace=False)
+            ]
 
         # generate dataset of the shape [ [inc_0, data_0], [inc_1, data_1], ..]
         # with inc_i and data_i being arrays of varying length
@@ -151,65 +155,70 @@ def performfit(self,
         dfinc = [j for i, inc_i in enumerate(inc) for j in inc_i[selects[i]]]
         dfsig = [j for i, data_i in enumerate(data) for j in data_i[selects[i]]]
 
-        dataset = pd.DataFrame({'inc':dfinc, 'sig':dfsig},
-                               index=pd.to_datetime(dfindex, unit='D'))
+        dataset = pd.DataFrame(
+            {"inc": dfinc, "sig": dfsig}, index=pd.to_datetime(dfindex, unit="D")
+        )
         # ---------------------------------------------------------------------
         # ------------------------------- FITTING -----------------------------
 
         def set_V_SRF(t1, N, tau, omega, **kwargs):
             V = Rayleigh(omega=omega, tau=tau)
-            SRF = HGsurface(ncoefs=10, t=t1, NormBRDF=N, a=[1., 1., 1.])
+            SRF = HGsurface(ncoefs=10, t=t1, NormBRDF=N, a=[1.0, 1.0, 1.0])
             return V, SRF
 
-
         # specify additional arguments for scipy.least_squares
         lsq_kwargs = {
-                'ftol': 1e-8,
-                'gtol': 1e-8,
-                'xtol': 1e-8,
-                'max_nfev': 500,
-                'method': 'trf',
-                'tr_solver': 'lsmr',
-                'x_scale': 'jac'}
-
-
+            "ftol": 1e-8,
+            "gtol": 1e-8,
+            "xtol": 1e-8,
+            "max_nfev": 500,
+            "method": "trf",
+            "tr_solver": "lsmr",
+            "x_scale": "jac",
+        }
 
         # select random numbers within the boundaries as sart-values
         np.random.seed(0)  # reset seed to have a reproducible test
         ostart = (omax - omin) * np.random.random() + omin
         np.random.seed(0)  # reset seed to have a reproducible test
         tstart = (tmax - tmin) * np.random.random() + tmin
-        taustart = (taumax -
-                    taumin) * np.random.random() + taumin
-
+        taustart = (taumax - taumin) * np.random.random() + taumin
 
         # fit only a single parameter to the datasets that have equal tau
         _, fittau_dyn = np.unique(dataset.index, return_inverse=True)
-        fittau_dyn[np.isin(fittau_dyn, equal_tau_selects)] = \
-            fittau_dyn[np.isin(fittau_dyn, equal_tau_selects)][0]
+        fittau_dyn[np.isin(fittau_dyn, equal_tau_selects)] = fittau_dyn[
+            np.isin(fittau_dyn, equal_tau_selects)
+        ][0]
         # add manual parameter dynamics for tau
-        dataset['tau_dyn'] = fittau_dyn
+        dataset["tau_dyn"] = fittau_dyn
 
         # specify the treatment of the parameters in the retrieval procedure
         defdict = {
-                    't1': [True, tstart, 'D', ([tmin], [tmax])],
-                    'N': [False, 'auxiliary'],
-                    'tau': [True, taustart, 'manual', ([taumin], [taumax])],
-                    'omega': [True, ostart, None, ([omin], [omax])],
-                    'bsf':[False, 0.]
-                    }
+            "t1": [True, tstart, "D", ([tmin], [tmax])],
+            "N": [False, "auxiliary"],
+            "tau": [True, taustart, "manual", ([taumin], [taumax])],
+            "omega": [True, ostart, None, ([omin], [omax])],
+            "bsf": [False, 0.0],
+        }
 
         # append auxiliary datasets
 
-        N_auxdata = pd.DataFrame({'N':rdata}, pd.unique(dataset.index)).loc[dataset.index]
-        dataset['N'] = N_auxdata
+        N_auxdata = pd.DataFrame({"N": rdata}, pd.unique(dataset.index)).loc[
+            dataset.index
+        ]
+        dataset["N"] = N_auxdata
 
         # initialize fit-class
-        testfit = Fits(sig0=sig0, dB=dB,
-                       dataset=dataset, defdict=defdict,
-                       set_V_SRF=set_V_SRF, lsq_kwargs=lsq_kwargs,
-                       setindex='mean',
-                       int_Q=True, verbose=2)
+        testfit = Fits(
+            sig0=sig0,
+            dB=dB,
+            dataset=dataset,
+            defdict=defdict,
+            set_V_SRF=set_V_SRF,
+            lsq_kwargs=lsq_kwargs,
+            int_Q=True,
+            verbose=2,
+        )
 
         # print model definition
         testfit.model_definition
@@ -223,15 +232,14 @@ def set_V_SRF(t1, N, tau, omega, **kwargs):
         slops, curvs = testfit._calc_slope_curv()
 
         # provide true-values for comparison of fitted results
-        truevals = {'tau': taudata,
-                    'omega': omegadata,
-                    't1': tdata,
-                    }
-
+        truevals = {
+            "tau": taudata,
+            "omega": omegadata,
+            "t1": tdata,
+        }
 
         # ----------- calculate R^2 values and errors of parameters -----------
 
-
         # sicne fit[0].fun gives the residuals weighted with respect to
         # weights, the model calculation can be gained via
         # estimates = fit[0].fun/weights + measurements
@@ -241,41 +249,33 @@ def set_V_SRF(t1, N, tau, omega, **kwargs):
         estimates = testfit.fit_output.fun + measures
 
         # evaluate linear regression to get r-value etc.
-        slope, intercept, r_value, p_value, std_err = linregress(estimates,
-                                                                 measures)
+        slope, intercept, r_value, p_value, std_err = linregress(estimates, measures)
         # calculate R^2 value
         r2 = r_value**2
 
         # check if r^2 between original and fitted data is > 0.95
-        self.assertTrue(r2 > 0.95, msg=f'r^2 condition not  met , R={r2:4f}')
+        self.assertTrue(r2 > 0.95, msg=f"r^2 condition not  met , R={r2:4f}")
 
         # set mean-error values for the derived parameters
         if sig0 is True and dB is False:
-            errdict = {'tau': 0.03,
-                       'omega': 0.008,
-                       't1': 0.05}
+            errdict = {"tau": 0.03, "omega": 0.008, "t1": 0.05}
 
         if sig0 is False and dB is False:
-            errdict = {'tau': 0.03,
-                       'omega': 0.01,
-                       't1': 0.08}
+            errdict = {"tau": 0.03, "omega": 0.01, "t1": 0.08}
 
         if sig0 is True and dB is True:
-            errdict = {'tau': 0.03,
-                       'omega': 0.01,
-                       't1': 0.09}
+            errdict = {"tau": 0.03, "omega": 0.01, "t1": 0.09}
 
         if sig0 is False and dB is True:
-            errdict = {'tau': 0.03,
-                       'omega': 0.01,
-                       't1': 0.09}
+            errdict = {"tau": 0.03, "omega": 0.01, "t1": 0.09}
 
         fittedvals = testfit.res_df
         for key in truevals:
             err = abs(fittedvals[key] - truevals[key]).mean()
             self.assertTrue(
                 err < errdict[key],
-                msg='derived error' + str(err) + 'too high for ' + str(key))
+                msg="derived error" + str(err) + "too high for " + str(key),
+            )
 
         return truevals, r2
 
@@ -355,6 +355,7 @@ def test_I_dB(self):
             tmax=0.5,
         )
 
+
 if __name__ == "__main__":
     unittest.main()
 
diff --git a/tests/test_rtmetrics.py b/tests/test_rtmetrics.py
index 831f34b0..a3796053 100644
--- a/tests/test_rtmetrics.py
+++ b/tests/test_rtmetrics.py
@@ -1,4 +1,5 @@
 import unittest
+import os
 from itertools import combinations
 
 import pandas as pd
@@ -6,43 +7,51 @@
 import matplotlib.pyplot as plt
 
 from rt1.rtmetrics import RTmetrics
-from rt1 import rtfits
+from rt1.rtresults import HDFaccessor
 
 
 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])
+        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}
+            "pearson": 0.13745925602259895,
+            "spearman": 0.10303030303030303,
+            "linregress": {
+                "slope": 0.2014594067161711,
+                "intercept": 27.29877405497224,
+                "rvalue": 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')
+        fit_dB_path = os.path.dirname(__file__) + os.sep + "test_fit_db.h5"
+
+        with HDFaccessor(fit_dB_path) as fit_db:
+            fit = fit_db.load_fit("sig0_dB")
+
+        return fit
 
     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':
+            if metric == "linregress":
                 continue
 
             res = getattr(RTmetrics, metric)(d1, d2)
@@ -51,47 +60,50 @@ def test_metrics(self):
     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'])
+        linregress_expected = expected_values["linregress"]
 
-    def test_fit_metric(self):
+        self.assertAlmostEqual(linregress["slope"], linregress_expected["slope"])
+        self.assertAlmostEqual(
+            linregress["intercept"], linregress_expected["intercept"]
+        )
+        self.assertAlmostEqual(linregress["rvalue"], linregress_expected["rvalue"])
+        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):
-
+        for [p1, s1], [p2, s2] in combinations(fit.metric._all_keys.items(), 2):
             metric_fit_params = getattr(getattr(fit.metric, p1), p2)
+            singular_data = (
+                metric_fit_params.d1.nunique() == 1
+                or metric_fit_params.d2.nunique() == 1
+            )
 
             # loop through all possible metrics
             for metric in RTmetrics.metrics_registry:
+                # don't attempt linear regression calculations if there is only a single
+                # unique value in one of the datasets (to avoid linregress issues)
+                if singular_data and metric == "linregress":
+                    continue
+
                 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}')]
+                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':
+                if s1 == "calc_model":
                     d1 = getattr(d1(return_components=True), p1)
                 else:
                     d1 = getattr(d1, p1)
 
                 d2 = getattr(fit, s2)
-                if s2 == 'calc_model':
+                if s2 == "calc_model":
                     d2 = getattr(d2(return_components=True), p2)
                 else:
                     d2 = getattr(d2, p2)
@@ -99,12 +111,14 @@ def test_fit_metric(self):
                 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')
+                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':
+                if metric == "linregress":
                     for key, val in fit_metric.items():
                         if np.isnan(val):
                             assert np.isnan(fit_func[key]), assertmsg
@@ -116,11 +130,20 @@ def test_fit_metric(self):
                     else:
                         assert fit_metric == fit_func, assertmsg
 
+            # check additional methods
+            if not singular_data:
+                # avoid scatterplots for singular datasets
+                f = metric_fit_params.scatterplot()
+                plt.close(f)
+
+            _ = metric_fit_params.allmetrics
+
+            _ = metric_fit_params.metrics_table
+
     def test_scatterplot(self):
         plt.ion()
         fit = self.mock_fit()
-        _ = RTmetrics.scatterplot(fit.res_df.SM, fit.res_df.omega,
-                                  "SM", "omega")
+        _ = RTmetrics.scatterplot(fit.res_df.SM, fit.res_df.omega, "SM", "omega")
 
 
 if __name__ == "__main__":
diff --git a/tests/test_scatter.py b/tests/test_scatter.py
index babd2ac5..0058bcfc 100644
--- a/tests/test_scatter.py
+++ b/tests/test_scatter.py
@@ -5,32 +5,32 @@
 
 import sys
 
-sys.path.append('..')
+sys.path.append("..")
 from rt1.scatter import Scatter
 
-class TestScatter(unittest.TestCase):
-
 
+class TestScatter(unittest.TestCase):
     def test_p(self):
         S = Scatter()
-        theta_0 = np.pi/2.
+        theta_0 = np.pi / 2.0
         theta_s = 0.234234
-        phi_0 = np.pi/2.
-        phi_s = 0.
+        phi_0 = np.pi / 2.0
+        phi_s = 0.0
 
-        p = S.scat_angle(theta_0, theta_s, phi_0, phi_s, a=[-1.,1.,1.])   # cos(theta)=0
-        self.assertAlmostEqual(p, 0.,10)
+        p = S.scat_angle(
+            theta_0, theta_s, phi_0, phi_s, a=[-1.0, 1.0, 1.0]
+        )  # cos(theta)=0
+        self.assertAlmostEqual(p, 0.0, 10)
 
-        theta_0 = 0.
-        theta_s = 0.
-        phi_0 = np.pi/2.
+        theta_0 = 0.0
+        theta_s = 0.0
+        phi_0 = np.pi / 2.0
         phi_s = 0.12345
-        p = S.scat_angle(theta_0, theta_s, phi_0, phi_s, a=[-.7,1.,1.])   # cos(theta)=-1
-        self.assertAlmostEqual(p, -.7,10)
+        p = S.scat_angle(
+            theta_0, theta_s, phi_0, phi_s, a=[-0.7, 1.0, 1.0]
+        )  # cos(theta)=-1
+        self.assertAlmostEqual(p, -0.7, 10)
 
 
 if __name__ == "__main__":
     unittest.main()
-
-
-
diff --git a/tests/test_surface.py b/tests/test_surface.py
index 065b0734..9d2e23c2 100644
--- a/tests/test_surface.py
+++ b/tests/test_surface.py
@@ -5,65 +5,71 @@
 
 import sys
 
-sys.path.append('..')
+sys.path.append("..")
 # from rt1.volume import Rayleigh
 # from rt1.rt1 import RT1
 
-from rt1.surface import Isotropic, CosineLobe, HenyeyGreenstein, LinCombSRF, \
-    HG_nadirnorm
+from rt1.surface import (
+    Isotropic,
+    CosineLobe,
+    HenyeyGreenstein,
+    LinCombSRF,
+    HG_nadirnorm,
+)
 from scipy import special as sc
+
 # import sympy as sp
 import matplotlib.pyplot as plt
 from scipy.integrate import simps
 from itertools import product
 
-class TestSurface(unittest.TestCase):
 
+class TestSurface(unittest.TestCase):
     def test_isotropic(self):
         S = Isotropic()
-        t_0 = np.pi / 2.
+        t_0 = np.pi / 2.0
         t_ex = 0.234234
-        p_0 = np.pi / 2.
-        p_ex = 0.
+        p_0 = np.pi / 2.0
+        p_ex = 0.0
 
-        self.assertEqual(S.brdf(t_0, t_ex, p_0, p_ex), 1. / np.pi)
-        self.assertEqual(S.brdf(t_0, t_ex, p_0, p_ex), 1. / np.pi)
-        self.assertEqual(S.brdf(t_0, t_ex, p_0, p_ex), 1. / np.pi)
+        self.assertEqual(S.brdf(t_0, t_ex, p_0, p_ex), 1.0 / np.pi)
+        self.assertEqual(S.brdf(t_0, t_ex, p_0, p_ex), 1.0 / np.pi)
+        self.assertEqual(S.brdf(t_0, t_ex, p_0, p_ex), 1.0 / np.pi)
 
     def test_cosine(self):
         S = CosineLobe(ncoefs=10, i=5)
-        t_0 = np.pi / 2.
+        t_0 = np.pi / 2.0
         t_ex = 0.234234
-        p_0 = np.pi / 2.
-        p_ex = 0.
+        p_0 = np.pi / 2.0
+        p_ex = 0.0
 
-        self.assertAlmostEqual(S.scat_angle(t_0, t_ex, p_0, p_ex, S.a), 0., 15)
-        self.assertAlmostEqual(S.brdf(t_0, t_ex, p_0, p_ex), 0., 20)
+        self.assertAlmostEqual(S.scat_angle(t_0, t_ex, p_0, p_ex, S.a), 0.0, 15)
+        self.assertAlmostEqual(S.brdf(t_0, t_ex, p_0, p_ex), 0.0, 20)
 
-        t_0 = 0.
-        t_ex = np.deg2rad(60.)
-        p_0 = 0.
-        p_ex = 0.
-        self.assertAlmostEqual(S.scat_angle(t_0, t_ex, p_0, p_ex, S.a),
-                               0.5, 15)
-        self.assertAlmostEqual(S.brdf(t_0, t_ex, p_0, p_ex), 0.5**5. / np.pi,
-                               10)
+        t_0 = 0.0
+        t_ex = np.deg2rad(60.0)
+        p_0 = 0.0
+        p_ex = 0.0
+        self.assertAlmostEqual(S.scat_angle(t_0, t_ex, p_0, p_ex, S.a), 0.5, 15)
+        self.assertAlmostEqual(S.brdf(t_0, t_ex, p_0, p_ex), 0.5**5.0 / np.pi, 10)
 
     def test_cosine_coeff(self):
         # test legcoefs for example in paper
         n = 10
         S = CosineLobe(ncoefs=n, i=5)
         for i in range(n):
-            z1 = 120. * (1. / 128. + i / 64.) / np.sqrt(np.pi)
-            z2 = sc.gamma((7. - i) * 0.5) * sc.gamma((8. + i) * 0.5)
+            z1 = 120.0 * (1.0 / 128.0 + i / 64.0) / np.sqrt(np.pi)
+            z2 = sc.gamma((7.0 - i) * 0.5) * sc.gamma((8.0 + i) * 0.5)
             self.assertAlmostEqual(S._get_legcoef(i), z1 / z2)
 
-        self.assertAlmostEqual(S._get_legcoef(0),
-                               15. / (16. * sc.gamma(3.5) * sc.gamma(4.) *
-                                      np.sqrt(np.pi)))
-        self.assertAlmostEqual(S._get_legcoef(2),
-                               75. / (16. * sc.gamma(2.5) * sc.gamma(5.) *
-                                      np.sqrt(np.pi)))
+        self.assertAlmostEqual(
+            S._get_legcoef(0),
+            15.0 / (16.0 * sc.gamma(3.5) * sc.gamma(4.0) * np.sqrt(np.pi)),
+        )
+        self.assertAlmostEqual(
+            S._get_legcoef(2),
+            75.0 / (16.0 * sc.gamma(2.5) * sc.gamma(5.0) * np.sqrt(np.pi)),
+        )
 
     def test_expansion_cosine_lobe(self):
         # theta_i = np.pi/2.
@@ -75,21 +81,22 @@ def test_expansion_cosine_lobe(self):
         ncoefs = 10
         # means coefficients 0...9; i=5 is for the example in the paper
         S = CosineLobe(ncoefs=ncoefs, i=5)
+        init_dict = S.init_dict
 
         # input parameters are set in a way that COS_THETA = 1
         # and therefore only the legendre coefficients should be returned
         # which is always 1 in case that cos_THETA=1
         t_0 = 0.1234
-        t_ex = np.pi / 2.
+        t_ex = np.pi / 2.0
         p_0 = 0.3456
-        p_ex = 0.
+        p_ex = 0.0
 
-        r = S._eval_legpoly(t_0, t_ex, p_0, p_ex, geometry='ffff')
+        r = S._eval_legpoly(t_0, t_ex, p_0, p_ex, geometry="ffff")
 
         # calculate reference solution
         refs = []
         for k in range(ncoefs):
-            refs.append(S._get_legcoef(k) * 1.)
+            refs.append(S._get_legcoef(k) * 1.0)
         ref = np.array(refs).sum()
         self.assertAlmostEqual(r, ref, 15)
 
@@ -101,56 +108,72 @@ def test_normalization(self):
         N = 20
         t_0 = np.random.random(N) * np.pi
         t_ex = np.random.random(N) * np.pi
-        p_0 = np.pi / 4.
-        p_ex = np.pi / 4.
+        p_0 = np.pi / 4.0
+        p_ex = np.pi / 4.0
 
         for i in range(N):
-            I = Isotropic()
-            self.assertTrue(np.allclose(I.brdf(t_0[i], t_ex[i], p_0, p_ex),
-                                        1. / np.pi))
+            Iso = Isotropic()
+            init_dict = Iso.init_dict
+
+            self.assertTrue(
+                np.allclose(Iso.brdf(t_0[i], t_ex[i], p_0, p_ex), 1.0 / np.pi)
+            )
 
             ncoefs = 10
             C = CosineLobe(ncoefs=ncoefs, i=0)
-            self.assertTrue(np.allclose(C.brdf(t_0[i], t_ex[i], p_0, p_ex),
-                                        1. / np.pi))
+            init_dict = C.init_dict
+
+            self.assertTrue(
+                np.allclose(C.brdf(t_0[i], t_ex[i], p_0, p_ex), 1.0 / np.pi)
+            )
 
             H = HenyeyGreenstein(t=0, ncoefs=5)
-            self.assertTrue(np.allclose(H.brdf(t_0[i], t_ex[i], p_0, p_ex),
-                                        1. / np.pi))
+            init_dict = H.init_dict
 
+            self.assertTrue(
+                np.allclose(H.brdf(t_0[i], t_ex[i], p_0, p_ex), 1.0 / np.pi)
+            )
 
-            LCH = LinCombSRF([[.5, HenyeyGreenstein(t=0, ncoefs=5)],
-                              [.5, HenyeyGreenstein(t=0, ncoefs=5)]])
-            self.assertTrue(np.allclose(LCH.brdf(t_0[i], t_ex[i], p_0, p_ex),
-                                        1. / np.pi))
+            LCH = LinCombSRF(
+                [
+                    [0.5, HenyeyGreenstein(t=0, ncoefs=5)],
+                    [0.5, HenyeyGreenstein(t=0, ncoefs=5)],
+                ]
+            )
+            init_dict = LCH.init_dict
 
-    def test_HGnadirnorm_norm(self):
+            self.assertTrue(
+                np.allclose(LCH.brdf(t_0[i], t_ex[i], p_0, p_ex), 1.0 / np.pi)
+            )
 
+    def test_HGnadirnorm_norm(self):
         def hemreflect(theta_0, phi_0, BRDF, simps_N=100):
-            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)
 
             def integfunkt(theta_s, phi_s):
-                return np.sin(theta_s) * np.cos(theta_s) * BRDF(
-                    theta_0, theta_s, phi_0, phi_s)
+                return (
+                    np.sin(theta_s)
+                    * np.cos(theta_s)
+                    * BRDF(theta_0, theta_s, phi_0, phi_s)
+                )
+
             # evaluate the integral using Simpson's Rule twice
             z = integfunkt(x[:, None], y)
             return simps(simps(z, y), x)
 
-
-        for t in [.1,.2,.3,.4,.5,.6,.7]:
+        for t in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]:
             H_nad = HG_nadirnorm(t=t, ncoefs=10)
+            init_dict = H_nad.init_dict
 
             nadirnorm = hemreflect(0, 0, H_nad.brdf)
 
-            self.assertTrue(np.allclose(nadirnorm, 1.))
-
-
-
-
+            self.assertTrue(np.allclose(nadirnorm, 1.0))
 
     def test_hemreflect_polarplot_SRF(self):
-        SRF = HenyeyGreenstein(t=0.5, ncoefs=15, NormBRDF=.3)
+        SRF = HenyeyGreenstein(t=0.5, ncoefs=15, NormBRDF=0.3)
+        init_dict = SRF.init_dict
+
         pl = SRF.polarplot()
         plt.close(pl)
 
diff --git a/tests/test_volume.py b/tests/test_volume.py
index 0701c508..394463a3 100644
--- a/tests/test_volume.py
+++ b/tests/test_volume.py
@@ -5,91 +5,104 @@
 
 import sys
 
-sys.path.append('..')
+sys.path.append("..")
 from rt1.volume import Volume
 from rt1.volume import Rayleigh, HenyeyGreenstein, HGRayleigh, LinCombV
 import matplotlib.pyplot as plt
 from scipy.integrate import simps
 from itertools import product
 
-class TestVolume(unittest.TestCase):
 
+class TestVolume(unittest.TestCase):
     def test_init(self):
         V = Volume(omega=0.2, tau=1.7)
+        init_dict = V.init_dict
+
         self.assertEqual(V.omega, 0.2)
         self.assertEqual(V.tau, 1.7)
 
     def test_rayleigh(self):
         V = Rayleigh(omega=0.2, tau=1.7)
-        t_0 = np.pi/2.
+        t_0 = np.pi / 2.0
         t_ex = 0.234234
-        p_0 = np.pi/2.
-        p_ex = 0.
+        p_0 = np.pi / 2.0
+        p_ex = 0.0
 
-        p = V.p(t_0, t_ex, p_0, p_ex)   # cos=0
+        p = V.p(t_0, t_ex, p_0, p_ex)  # cos=0
 
-        self.assertEqual(p, (3./(16.*np.pi)))
+        self.assertEqual(p, (3.0 / (16.0 * np.pi)))
 
     def test_HenyeyGreenstein(self):
-        V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=20)
-        self.assertEqual(V.t,0.7)
-        t_0 = np.pi/2.
+        V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
+        init_dict = V.init_dict
+
+        self.assertEqual(V.t, 0.7)
+        t_0 = np.pi / 2.0
         t_ex = 0.234234
-        p_0 = np.pi/2.
-        p_ex = 0.
+        p_0 = np.pi / 2.0
+        p_ex = 0.0
 
-        #--> cos(THETA) = 0
+        # --> cos(THETA) = 0
         p = V.p(t_0, t_ex, p_0, p_ex)
-        self.assertAlmostEqual(p, (1.-0.7*0.7)/(4.*np.pi*(1.+0.7*0.7)**1.5),10)
+        self.assertAlmostEqual(
+            p, (1.0 - 0.7 * 0.7) / (4.0 * np.pi * (1.0 + 0.7 * 0.7) ** 1.5), 10
+        )
 
     def test_HGRayleigh(self):
         V = HGRayleigh(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
+        init_dict = V.init_dict
         self.assertEqual(V.t, 0.7)
-        t_0 = np.pi/2.
+        t_0 = np.pi / 2.0
         t_ex = 0.234234
-        p_0 = np.pi/2.
-        p_ex = 0.
+        p_0 = np.pi / 2.0
+        p_ex = 0.0
 
-        Vr= Rayleigh(omega=0.2, tau=1.7)
-        Vhg = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=20)
+        Vr = Rayleigh(omega=0.2, tau=1.7)
+        init_dict = Vr.init_dict
+        Vhg = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
+        init_dict = Vhg.init_dict
 
         p = V.p(t_0, t_ex, p_0, p_ex)
         phg = Vhg.p(t_0, t_ex, p_0, p_ex)
         pr = Vr.p(t_0, t_ex, p_0, p_ex)
 
-        self.assertAlmostEqual(p, 4.*np.pi*(4/(2+.7**2))*1/2*phg*pr,10)
+        self.assertAlmostEqual(
+            p, 4.0 * np.pi * (4 / (2 + 0.7**2)) * 1 / 2 * phg * pr, 10
+        )
 
     def test_HenyeyGreenstein_coefficients(self):
-        V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=20)
-        self.assertEqual(V._get_legcoef(0),1./(4.*np.pi))
-        self.assertEqual(V._get_legcoef(1),3.*0.7/(4.*np.pi))
-
+        V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
+        init_dict = V.init_dict
+        self.assertEqual(V._get_legcoef(0), 1.0 / (4.0 * np.pi))
+        self.assertEqual(V._get_legcoef(1), 3.0 * 0.7 / (4.0 * np.pi))
 
     def test_polarplot_V(self):
-        V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=20)
+        V = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
+        init_dict = V.init_dict
         pl = V.polarplot()
         plt.close(pl)
 
-
     def test_V_normalization(self):
-
         V1 = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
         V2 = HGRayleigh(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
         V3 = Rayleigh(omega=0.2, tau=1.7)
 
-        V4 = LinCombV([[.5, Rayleigh()],
-                      [.5, HenyeyGreenstein(t=0.7,ncoefs=20)]],
-                     omega=0.2, tau=1.7)
+        V4 = LinCombV(
+            [[0.5, Rayleigh()], [0.5, HenyeyGreenstein(t=0.7, ncoefs=20)]],
+            omega=0.2,
+            tau=1.7,
+        )
 
-        for V in [V1,V2,V3,V4]:
+        for V in [V1, V2, V3, V4]:
+            init_dict = V.init_dict
             # set incident (zenith-angle) directions for which the integral
             # should be evaluated!
             incnum = np.linspace(0, np.pi, 5)
-            pincnum = np.linspace(0, 2*np.pi, 5)
+            pincnum = np.linspace(0, 2 * np.pi, 5)
 
             # define grid for integration
-            x = np.linspace(0., np.pi, 500)
-            y = np.linspace(0., 2 * np.pi, 500)
+            x = np.linspace(0.0, np.pi, 500)
+            y = np.linspace(0.0, 2 * np.pi, 500)
 
             # initialize array for solutions
             sol = []
@@ -102,42 +115,35 @@ def test_V_normalization(self):
                 # 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) * V.p(theta_0, theta_s,
-                                                 phi_0, phi_s)
+                    return np.sin(theta_s) * V.p(theta_0, theta_s, phi_0, phi_s)
+
                 # evaluate the integral using Simpson's Rule twice
                 z = integfunkt(x[:, None], y)
                 sol += [simps(simps(z, y), x)]
 
-                self.assertTrue(np.allclose(sol, 1.))
-
-
+                self.assertTrue(np.allclose(sol, 1.0))
 
     def test_LinCombV(self):
-        Vr= Rayleigh(omega=0.2, tau=1.7)
-        Vhg = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=20)
+        Vr = Rayleigh(omega=0.2, tau=1.7)
+        Vhg = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
 
-        V = LinCombV([[.5, Vr],
-                      [.5, Vhg]], omega=0.2, tau=1.7)
+        V = LinCombV([[0.5, Vr], [0.5, Vhg]], omega=0.2, tau=1.7)
 
+        init_dict = V.init_dict
 
-        Vr= Rayleigh(omega=0.2, tau=1.7)
-        Vhg = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7,ncoefs=20)
+        Vr = Rayleigh(omega=0.2, tau=1.7)
+        Vhg = HenyeyGreenstein(omega=0.2, tau=1.7, t=0.7, ncoefs=20)
 
         incnum = np.linspace(0, np.pi, 3)
-        pincnum = np.linspace(0, 2*np.pi, 3)
-
-        for t_0, t_ex, p_0, p_ex in product(incnum, pincnum,
-                                            incnum, pincnum):
+        pincnum = np.linspace(0, 2 * np.pi, 3)
 
+        for t_0, t_ex, p_0, p_ex in product(incnum, pincnum, incnum, pincnum):
             p = V.p(t_0, t_ex, p_0, p_ex)
             phg = Vhg.p(t_0, t_ex, p_0, p_ex)
             pr = Vr.p(t_0, t_ex, p_0, p_ex)
 
-            self.assertAlmostEqual(p, .5*(phg + pr),10)
-
+            self.assertAlmostEqual(p, 0.5 * (phg + pr), 10)
 
 
 if __name__ == "__main__":
     unittest.main()
-
-