diff --git a/LICENSE b/LICENSE
index 46fc28fa366e776732f85e9bda033be9b1b90789..dd9a0a3a225982cf5a117cdd0a2b0a52b8a936be 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2022 Valentin Bruch
+Copyright (c) 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/package/LICENSE b/package/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..dd9a0a3a225982cf5a117cdd0a2b0a52b8a936be
--- /dev/null
+++ b/package/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/package/README.md b/package/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2c086d666c8fe1e99ca12cee04f9e8adc69419d5
--- /dev/null
+++ b/package/README.md
@@ -0,0 +1,7 @@
+# FRTRG Kondo
+Package implementing Floquet real-time renormalization group method for the
+Kondo model as discussed in <https://arxiv.org/abs/2206.06263>.
+
+The following parts of this module can be used directly from the command line:
+* gen\_data
+* plot
diff --git a/package/pyproject.toml b/package/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..f8c85d7974ae74103fea7b64e7ffff0eaab5f38b
--- /dev/null
+++ b/package/pyproject.toml
@@ -0,0 +1,36 @@
+[build-system]
+requires = ["setuptools>=61.0", "numpy>=1.19"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "frtrg_kondo"
+version = "0.14.3"
+authors = [
+  { name="Valentin Bruch", email="valentin.bruch@rwth-aachen.de" },
+]
+description = "Floquet real-time renoralization group analysis of the Kondo model"
+readme = "README.md"
+requires-python = ">=3.7"
+classifiers = [
+  "Programming Language :: Python :: 3",
+  "Development Status :: 3 - Alpha",
+  "License :: OSI Approved :: MIT License",
+  "Operating System :: OS Independent",
+  "Intended Audience :: Science/Research",
+  "Topic :: Scientific/Engineering :: Physics",
+  "Natural Language :: English",
+]
+dependencies = [
+  "numpy",
+  "pandas",
+  "sqlalchemy",
+  "tables",
+  "scipy",
+]
+
+[project.optional-dependencies]
+plot = ["matplotlib"]
+
+[project.urls]
+"Homepage" = "https://git-ce.rwth-aachen.de/valentin.bruch/frtrglib"
+"Bug Tracker" = "https://git-ce.rwth-aachen.de/valentin.bruch/frtrglib/-/issues"
diff --git a/package/setup.py b/package/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..b707a46ffa1222f45e98399cb02bfb276934f0cc
--- /dev/null
+++ b/package/setup.py
@@ -0,0 +1,50 @@
+# build with:
+
+from setuptools import setup, Extension
+import numpy as np
+from os import environ
+
+def main():
+    compiler_args = ['-O3','-Wall','-Wextra','-std=c11']
+    linker_args = []
+    include_dirs = [np.get_include()]
+    libraries = ['lapack']
+
+    if 'CBLAS' in environ:
+        compiler_args += ['-DCBLAS']
+        libraries += ['cblas']
+        #libraries += ['mkl_rt']
+    else:
+        libraries += ['blas']
+
+    if 'LAPACK_C' in environ:
+        compiler_args += ['-DLAPACK_C']
+
+    parallel_modifiers = ('PARALLEL', 'PARALLEL_EXTRAPOLATION', 'PARALLEL_EXTRA_DIMS')
+
+    need_omp = False
+    for modifier in parallel_modifiers:
+        if modifier in environ:
+            compiler_args += ['-D' + modifier]
+            need_omp = True
+
+    if need_omp:
+        compiler_args += ['-fopenmp']
+        linker_args += ['-fopenmp']
+
+    if 'DEBUG' in environ:
+        compiler_args += ['-DDEBUG']
+
+    module = Extension(
+            "frtrg_kondo.rtrg_c",
+            sources = ['src/frtrg_kondo/rtrg_c.c'],
+            include_dirs = include_dirs,
+            libraries = libraries,
+            extra_compile_args = compiler_args,
+            extra_link_args = linker_args
+            )
+    setup(ext_modules = [module])
+
+
+if __name__ == '__main__':
+    main()
diff --git a/package/src/frtrg_kondo/__init__.py b/package/src/frtrg_kondo/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b1d5727ce7cfd77f58b3ef499df020267f1ce21
--- /dev/null
+++ b/package/src/frtrg_kondo/__init__.py
@@ -0,0 +1,20 @@
+"""
+FRTRG Kondo package:
+Floquet real-time renormalization group for periodically driven Kondo model.
+
+Modules defined here
+* kondo: implements RG equations for the Kondo model
+* rtrg: defines data types for Floquet matrices
+* compact_rtrg: use special symmetries for more efficient handling of some
+  data types of rtrg.py
+* reservoirmatrix.py: data type for vertices in RG equations
+* settings: global settings used in this package.
+* data_management.py: data management module
+* gen_data: generate data, should be called directly
+* main: show plots from saved data, should be called directly
+* rtrg_c: functions required by rtrg implemented in C using BLAS
+* rtrg_cublas: same functions as in rtrg_c using CUBLAS
+* drive_gate_voltage: very basic extension of kondo module to case of driven
+  coupling (or gate voltage in a quantum dot setup)
+"""
+__version__ = "0.14.3"
diff --git a/package/src/frtrg_kondo/compact_rtrg.py b/package/src/frtrg_kondo/compact_rtrg.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a4da6e31c0d59da8a8565e3347406750ba1f4ba
--- /dev/null
+++ b/package/src/frtrg_kondo/compact_rtrg.py
@@ -0,0 +1,576 @@
+# Copyright 2021 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, module defining RG objects describing Floquet matrices with
+special symmetry
+
+Module defining class SymRGfunction for the special symmetric case of driving
+fulfilling  V(t + T/2) = - V(t)  with T = 2π/Ω = driving period.
+
+See also: rtrg.py
+
+TODO: This file has not been tested after the rest of the Kondo module was rewritten!
+"""
+
+from frtrg_kondo.rtrg import *
+
+OVERWRITE_LEFT = bytes((1,))
+OVERWRITE_RIGHT = bytes((2,))
+OVERWRITE_BOTH = bytes((3,))
+
+
+class SymRGfunction(RGfunction):
+    '''
+    Subclass of RGfunction for Floquet matrices representing functions which in
+    time-domain fulfill  f(t + T/2) = ±f(t).
+    '''
+    # Flags:
+    # 1 << 0 : matrix C contiguous
+    # 1 << 1 : matrix F contiguous
+    INVC_COUNTER = [0 for i in range(1 << 2)]
+    # Flags:
+    # 1 << 0 = 0x01 : symmetric
+    # 1 << 1 = 0x02 : matrix 1 C contiguous
+    # 1 << 2 = 0x04 : matrix 1 F contiguous
+    # 1 << 3 = 0x08 : matrix 2 C contiguous
+    # 1 << 4 = 0x10 : matrix 2 F contiguous
+    MMC_COUNTER = [0 for i in range(1 << 5)]
+
+    def __init__(self, global_properties, values, symmetry=0, **kwargs):
+        self.global_properties = global_properties
+        self.symmetry = symmetry
+        self.voltage_shifts = 0
+        assert self.global_properties.voltage_branches is None
+
+        self.submatrix00 = None
+        self.submatrix01 = None
+        self.submatrix10 = None
+        self.submatrix11 = None
+        for (key, value) in kwargs.items():
+            setattr(self, key, value)
+        if (type(values) == str and values == 'identity'):
+            self.symmetry = 1
+            self.submatrix00 = np.identity(self.nmax+1, dtype=np.complex128)
+            self.submatrix11 = np.identity(self.nmax, dtype=np.complex128)
+        elif values is not None:
+            self.values = values
+
+    @property
+    def values(self):
+        values = np.zeros((2*self.nmax+1, 2*self.nmax+1), dtype=np.complex128)
+        if self.submatrix00 is not None:
+            values[0::2,0::2] = self.submatrix00
+        if self.submatrix01 is not None:
+            values[0::2,1::2] = self.submatrix01
+        if self.submatrix10 is not None:
+            values[1::2,0::2] = self.submatrix10
+        if self.submatrix11 is not None:
+            values[1::2,1::2] = self.submatrix11
+        return values
+
+    @values.setter
+    def values(self, values):
+        values = np.asarray(values, np.complex128)
+        assert values.ndim == 2
+        assert values.shape[0] == values.shape[1] == 2*self.nmax+1
+        self.submatrix00 = values[0::2,0::2]  # (n+1)x(n+1) matrix for +
+        self.submatrix01 = values[0::2,1::2]  # (n+1)x n    matrix for -
+        self.submatrix10 = values[1::2,0::2]  #    n x(n+1) matrix for -
+        self.submatrix11 = values[1::2,1::2]  #    n x n    matrix for +
+        if np.abs(self.submatrix00).max() < 1e-15:
+            self.submatrix00 = None
+        if np.abs(self.submatrix01).max() < 1e-15:
+            self.submatrix01 = None
+        if np.abs(self.submatrix10).max() < 1e-15:
+            self.submatrix10 = None
+        if np.abs(self.submatrix11).max() < 1e-15:
+            self.submatrix11 = None
+        assert (self.submatrix00 is None and self.submatrix11 is None) or (self.submatrix01 is None and self.submatrix10 is None)
+        self.energy_shifted_copies.clear()
+
+    def copy(self):
+        '''
+        Copy only values, take a reference to global_properties.
+        '''
+        return SymRGfunction(
+                self.global_properties,
+                values = None,
+                symmetry = self.symmetry,
+                submatrix00 = None if self.submatrix00 is None else self.submatrix00.copy(),
+                submatrix01 = None if self.submatrix01 is None else self.submatrix01.copy(),
+                submatrix10 = None if self.submatrix10 is None else self.submatrix10.copy(),
+                submatrix11 = None if self.submatrix11 is None else self.submatrix11.copy(),
+            )
+
+    def floquetConjugate(self):
+        '''
+        For a Floquet matrix A(E)_{nm} this returns the C-transform
+            C A(E)_{nm} C = A(-E*)_{-n,-m}
+        with the superoperator C defined by
+            C x := x^\dag.
+        This uses the symmetry of self if self has a symmetry. If this
+        C-transform leaves self invariant, this function will return a
+        copy of self, but never a reference to self.
+
+        This can only be evaluated if the energy of self lies on the
+        imaginary axis.
+        '''
+        if self.symmetry == 1:
+            return self.copy()
+        elif self.symmetry == -1:
+            return -self
+        assert abs(self.energy.real) < 1e-12
+        return SymRGfunction(
+                self.global_properties,
+                values = None,
+                submatrix00 = None if self.submatrix00 is None else np.conjugate(self.submatrix00[::-1,::-1]),
+                submatrix01 = None if self.submatrix01 is None else np.conjugate(self.submatrix01[::-1,::-1]),
+                submatrix10 = None if self.submatrix10 is None else np.conjugate(self.submatrix10[::-1,::-1]),
+                submatrix11 = None if self.submatrix11 is None else np.conjugate(self.submatrix11[::-1,::-1]),
+            )
+
+    def __matmul__(self, other):
+        '''
+        Convolution (or product in Floquet space) of two RG functions.
+        Other must be of type SymRGfunction.
+
+        Note: This is only approximately associative, as long the function
+        converges to 0 for |n| of order of nmax.
+        '''
+        if not isinstance(other, SymRGfunction):
+            if isinstance(other, RGfunction):
+                return self.toRGfunction() @ other
+            return NotImplemented
+        assert self.global_properties is other.global_properties
+        res00 = None
+        res11 = None
+        res01 = None
+        res10 = None
+        symmetry = self.symmetry * other.symmetry;
+        if (self.submatrix00 is not None and other.submatrix00 is not None):
+            assert (self.submatrix11 is not None and other.submatrix11 is not None)
+            res00 = rtrg_c.multiply_extended(other.submatrix00.T, self.submatrix00.T, self.padding//2, symmetry, self.clear_corners//2).T
+            res11 = rtrg_c.multiply_extended(other.submatrix11.T, self.submatrix11.T, self.padding//2, symmetry, self.clear_corners//2).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix00.T.flags.c_contiguous << 3) | (self.submatrix00.T.flags.f_contiguous << 4) | (other.submatrix00.T.flags.c_contiguous << 1) | (other.submatrix00.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix11.T.flags.c_contiguous << 3) | (self.submatrix11.T.flags.f_contiguous << 4) | (other.submatrix11.T.flags.c_contiguous << 1) | (other.submatrix11.T.flags.f_contiguous << 2)]  += 1
+        elif (self.submatrix01 is not None and other.submatrix01 is not None):
+            assert (self.submatrix10 is not None and other.submatrix10 is not None)
+            res00 = rtrg_c.multiply_extended(other.submatrix10.T, self.submatrix01.T, self.padding//2, symmetry, self.clear_corners//2).T
+            res11 = rtrg_c.multiply_extended(other.submatrix01.T, self.submatrix10.T, self.padding//2, symmetry, self.clear_corners//2).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix01.flags.c_contiguous << 3) | (self.submatrix01.flags.f_contiguous << 4) | (other.submatrix10.flags.c_contiguous << 1) | (other.submatrix10.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix10.flags.c_contiguous << 3) | (self.submatrix10.flags.f_contiguous << 4) | (other.submatrix01.flags.c_contiguous << 1) | (other.submatrix01.flags.f_contiguous << 2)]  += 1
+        elif (self.submatrix00 is not None and other.submatrix01 is not None):
+            assert (self.submatrix11 is not None and other.submatrix10 is not None)
+            res01 = rtrg_c.multiply_extended(other.submatrix01.T, self.submatrix00.T, self.padding//2, symmetry, self.clear_corners//2).T
+            res10 = rtrg_c.multiply_extended(other.submatrix10.T, self.submatrix11.T, self.padding//2, symmetry, self.clear_corners//2).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix00.T.flags.c_contiguous << 3) | (self.submatrix00.T.flags.f_contiguous << 4) | (other.submatrix01.T.flags.c_contiguous << 1) | (other.submatrix01.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix11.T.flags.c_contiguous << 3) | (self.submatrix11.T.flags.f_contiguous << 4) | (other.submatrix10.T.flags.c_contiguous << 1) | (other.submatrix10.T.flags.f_contiguous << 2)]  += 1
+        elif (self.submatrix01 is not None and other.submatrix00 is not None):
+            assert (self.submatrix10 is not None and other.submatrix11 is not None)
+            res01 = rtrg_c.multiply_extended(other.submatrix11.T, self.submatrix01.T, self.padding//2, symmetry, self.clear_corners//2).T
+            res10 = rtrg_c.multiply_extended(other.submatrix00.T, self.submatrix10.T, self.padding//2, symmetry, self.clear_corners//2).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix01.T.flags.c_contiguous << 3) | (self.submatrix01.T.flags.f_contiguous << 4) | (other.submatrix11.T.flags.c_contiguous << 1) | (other.submatrix11.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix10.T.flags.c_contiguous << 3) | (self.submatrix10.T.flags.f_contiguous << 4) | (other.submatrix00.T.flags.c_contiguous << 1) | (other.submatrix00.T.flags.f_contiguous << 2)]  += 1
+        return SymRGfunction(
+                self.global_properties,
+                values = None,
+                submatrix00 = res00,
+                submatrix01 = res01,
+                submatrix10 = res10,
+                submatrix11 = res11,
+                symmetry = self.symmetry * other.symmetry,
+            )
+
+    def __rmatmul__(self, other):
+        if isinstance(other, SymRGfunction):
+            return other @ self
+        if isinstance(other, RGfunction):
+            return other @ self.toRGfunction()
+        return NotImplemented
+
+    def __imatmul__(self, other):
+        if not isinstance(other, RGfunction):
+            return NotImplemented
+        assert self.global_properties is other.global_properties
+        self.symmetry *= other.symmetry
+        symmetry = self.symmetry;
+        if (self.submatrix00 is not None and other.submatrix00 is not None):
+            assert (self.submatrix11 is not None and other.submatrix11 is not None)
+            self.submatrix00 = rtrg_c.multiply_extended(other.submatrix00.T, self.submatrix00.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            self.submatrix11 = rtrg_c.multiply_extended(other.submatrix11.T, self.submatrix11.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix00.T.flags.c_contiguous << 3) | (self.submatrix00.T.flags.f_contiguous << 4) | (other.submatrix00.T.flags.c_contiguous << 1) | (other.submatrix00.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix11.T.flags.c_contiguous << 3) | (self.submatrix11.T.flags.f_contiguous << 4) | (other.submatrix11.T.flags.c_contiguous << 1) | (other.submatrix11.T.flags.f_contiguous << 2)]  += 1
+        elif (self.submatrix01 is not None and other.submatrix01 is not None):
+            assert (self.submatrix10 is not None and other.submatrix10 is not None)
+            self.submatrix00 = rtrg_c.multiply_extended(other.submatrix10.T, self.submatrix01.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            self.submatrix11 = rtrg_c.multiply_extended(other.submatrix01.T, self.submatrix10.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix01.T.flags.c_contiguous << 3) | (self.submatrix01.T.flags.f_contiguous << 4) | (other.submatrix10.T.flags.c_contiguous << 1) | (other.submatrix10.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix10.T.flags.c_contiguous << 3) | (self.submatrix10.T.flags.f_contiguous << 4) | (other.submatrix01.T.flags.c_contiguous << 1) | (other.submatrix01.T.flags.f_contiguous << 2)]  += 1
+            self.submatrix01 = None
+            self.submatrix10 = None
+        elif (self.submatrix00 is not None and other.submatrix01 is not None):
+            assert (self.submatrix11 is not None and other.submatrix10 is not None)
+            self.submatrix01 = rtrg_c.multiply_extended(other.submatrix01.T, self.submatrix00.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            self.submatrix10 = rtrg_c.multiply_extended(other.submatrix10.T, self.submatrix11.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix00.T.flags.c_contiguous << 3) | (self.submatrix00.T.flags.f_contiguous << 4) | (other.submatrix01.T.flags.c_contiguous << 1) | (other.submatrix01.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix11.T.flags.c_contiguous << 3) | (self.submatrix11.T.flags.f_contiguous << 4) | (other.submatrix10.T.flags.c_contiguous << 1) | (other.submatrix10.T.flags.f_contiguous << 2)]  += 1
+            self.submatrix00 = None
+            self.submatrix11 = None
+        elif (self.submatrix01 is not None and other.submatrix00 is not None):
+            assert (self.submatrix10 is not None and other.submatrix11 is not None)
+            self.submatrix01 = rtrg_c.multiply_extended(other.submatrix11.T, self.submatrix01.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            self.submatrix10 = rtrg_c.multiply_extended(other.submatrix00.T, self.submatrix10.T, self.padding//2, symmetry, self.clear_corners//2, OVERWRITE_LEFT).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix01.T.flags.c_contiguous << 3) | (self.submatrix01.T.flags.f_contiguous << 4) | (other.submatrix11.T.flags.c_contiguous << 1) | (other.submatrix11.T.flags.f_contiguous << 2)]  += 1
+                SymRGfunction.MMC_COUNTER[(symmetry != 0) | (self.submatrix10.T.flags.c_contiguous << 3) | (self.submatrix10.T.flags.f_contiguous << 4) | (other.submatrix00.T.flags.c_contiguous << 1) | (other.submatrix00.T.flags.f_contiguous << 2)]  += 1
+        return self
+
+    def __add__(self, other):
+        '''
+        Add other to self. If other is a scalar or a scalar function of energy
+        represented by an array of values at self.energies, this treats other
+        as an identity (or diagonal) Floquet matrix.
+        Other must be a scalar or array of same shape as self.energies or an
+        RGfunction of the same shape and energies as self.
+        '''
+        if isinstance(other, SymRGfunction):
+            assert self.global_properties is other.global_properties
+            symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            assert (self.submatrix00 is None and other.submatrix00 is None) or (self.submatrix01 is None and other.submatrix01 is None)
+            return SymRGfunction(
+                    self.global_properties,
+                    values = None,
+                    submatrix00 = other.submatrix00 if self.submatrix00 is None else (self.submatrix00 if other.submatrix00 is None else self.submatrix00 + other.submatrix00),
+                    submatrix01 = other.submatrix01 if self.submatrix01 is None else (self.submatrix01 if other.submatrix01 is None else self.submatrix01 + other.submatrix01),
+                    submatrix10 = other.submatrix10 if self.submatrix10 is None else (self.submatrix10 if other.submatrix10 is None else self.submatrix10 + other.submatrix10),
+                    submatrix11 = other.submatrix11 if self.submatrix11 is None else (self.submatrix11 if other.submatrix11 is None else self.submatrix11 + other.submatrix11),
+                    symmetry = symmetry,
+                )
+        elif isinstance(other, RGfunction):
+            return self.toRGfunction() + other
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            assert self.submatrix01 is None and self.submatrix10 is None
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            res00 = self.submatrix00.copy()
+            res11 = self.submatrix11.copy()
+            symmetry = 0
+            if isinstance(other, Number):
+                if self.symmetry == 1 and other.imag == 0:
+                    symmetry = 1
+                elif self.symmetry == -1 and other.real == 0:
+                    symmetry = -1
+            try:
+                res00[np.diag_indices(self.nmax+1)] += other
+                res11[np.diag_indices(self.nmax+1)] += other
+            except:
+                res00[np.diag_indices(self.nmax+1)] += other[0::2]
+                res11[np.diag_indices(self.nmax+1)] += other[1::2]
+            return SymRGfunction(
+                    self.global_properties,
+                    values = None,
+                    submatrix00 = res00,
+                    submatrix01 = None,
+                    submatrix10 = None,
+                    submatrix11 = res11,
+                    symmetry = symmetry,
+                )
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+
+    def __sub__(self, other):
+        if isinstance(other, SymRGfunction):
+            assert self.global_properties is other.global_properties
+            symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            assert (self.submatrix00 is None and other.submatrix00 is None) or (self.submatrix01 is None and other.submatrix01 is None)
+            return SymRGfunction(
+                    self.global_properties,
+                    values = None,
+                    submatrix00 = (None if other.submatrix00 is None else -other.submatrix00) if self.submatrix00 is None else (self.submatrix00 if other.submatrix00 is None else self.submatrix00 - other.submatrix00),
+                    submatrix01 = (None if other.submatrix01 is None else -other.submatrix01) if self.submatrix01 is None else (self.submatrix01 if other.submatrix01 is None else self.submatrix01 - other.submatrix01),
+                    submatrix10 = (None if other.submatrix10 is None else -other.submatrix10) if self.submatrix10 is None else (self.submatrix10 if other.submatrix10 is None else self.submatrix10 - other.submatrix10),
+                    submatrix11 = (None if other.submatrix11 is None else -other.submatrix11) if self.submatrix11 is None else (self.submatrix11 if other.submatrix11 is None else self.submatrix11 - other.submatrix11),
+                    symmetry = symmetry,
+                )
+        if isinstance(other, RGfunction):
+            return self.toRGfunction() - other
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            assert self.submatrix01 is None and self.submatrix10 is None
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            res00 = self.submatrix00.copy()
+            res11 = self.submatrix11.copy()
+            symmetry = 0
+            if isinstance(other, Number):
+                if self.symmetry == 1 and other.imag == 0:
+                    symmetry = 1
+                elif self.symmetry == -1 and other.real == 0:
+                    symmetry = -1
+            try:
+                res00[np.diag_indices(self.nmax+1)] -= other
+                res11[np.diag_indices(self.nmax+1)] -= other
+            except:
+                res00[np.diag_indices(self.nmax+1)] -= other[0::2]
+                res11[np.diag_indices(self.nmax+1)] -= other[1::2]
+            return SymRGfunction(
+                    self.global_properties,
+                    values = None,
+                    submatrix00 = res00,
+                    submatrix01 = None,
+                    submatrix10 = None,
+                    submatrix11 = res11,
+                    symmetry = symmetry,
+                )
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+
+    def __neg__(self):
+        '''
+        Return a copy of self with inverted sign of self.values.
+        '''
+        return SymRGfunction(
+                self.global_properties,
+                values = None,
+                submatrix00 = None if self.submatrix00 is None else -self.submatrix00,
+                submatrix01 = None if self.submatrix01 is None else -self.submatrix01,
+                submatrix10 = None if self.submatrix10 is None else -self.submatrix10,
+                submatrix11 = None if self.submatrix11 is None else -self.submatrix11,
+                symmetry = self.symmetry,
+            )
+
+    def __iadd__(self, other):
+        if isinstance(other, SymRGfunction):
+            assert self.global_properties is other.global_properties
+            self.symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            assert (self.submatrix00 is None and other.submatrix00 is None) or (self.submatrix01 is None and other.submatrix01 is None)
+            if (self.submatrix01 is None):
+                self.submatrix00 += other.submatrix00
+                self.submatrix11 += other.submatrix11
+            else:
+                self.submatrix01 += other.submatrix01
+                self.submatrix10 += other.submatrix10
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            assert self.submatrix01 is None and self.submatrix10 is None
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            if self.submatrix00 is None:
+                self.submatrix00 = np.zeros((self.nmax+1, self.nmax+1), np.complex128)
+            if self.submatrix11 is None:
+                self.submatrix11 = np.zeros((self.nmax, self.nmax), np.complex128)
+            try:
+                self.submatrix00[np.diag_indices(self.nmax+1)] += other
+                self.submatrix11[np.diag_indices(self.nmax)] += other
+            except:
+                self.submatrix00[np.diag_indices(self.nmax+1)] += other[0::2]
+                self.submatrix11[np.diag_indices(self.nmax)] += other[1::2]
+            if not isinstance(other, Number) or not ((self.symmetry == 1 and other.imag == 0) or (self.symmetry == -1 and other.real == 0)):
+                self.symmetry = 0
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+        return self
+
+    def __isub__(self, other):
+        if isinstance(other, SymRGfunction):
+            assert self.global_properties is other.global_properties
+            self.symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            assert (self.submatrix00 is None and other.submatrix00 is None) or (self.submatrix01 is None and other.submatrix01 is None)
+            if (self.submatrix01 is None):
+                self.submatrix00 -= other.submatrix00
+                self.submatrix11 -= other.submatrix11
+            else:
+                self.submatrix01 -= other.submatrix01
+                self.submatrix10 -= other.submatrix10
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            assert self.submatrix01 is None and self.submatrix10 is None
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            try:
+                self.submatrix00[np.diag_indices(self.nmax+1)] -= other
+                self.submatrix11[np.diag_indices(self.nmax+1)] -= other
+            except:
+                self.submatrix00[np.diag_indices(self.nmax+1)] -= other[0::2]
+                self.submatrix11[np.diag_indices(self.nmax+1)] -= other[1::2]
+            self.symmetry = 0
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+        return self
+
+    def __mul__(self, other):
+        '''
+        Multiplication by scalar or SymRGfunction.
+        If other is a SymRGfunction, this calculates the matrix product
+        (equivalent to __matmul__). If other is a scalar, this multiplies
+        self.values by other.
+        '''
+        if isinstance(other, RGfunction):
+            return self @ other
+        if isinstance(other, Number):
+            if other.imag == 0:
+                symmetry = self.symmetry
+            elif other.real == 0:
+                symmetry = -self.symmetry
+            else:
+                symmetry = 0
+            return SymRGfunction(
+                    self.global_properties,
+                    values = None,
+                    submatrix00 = None if self.submatrix00 is None else other*self.submatrix00,
+                    submatrix01 = None if self.submatrix01 is None else other*self.submatrix01,
+                    submatrix10 = None if self.submatrix10 is None else other*self.submatrix10,
+                    submatrix11 = None if self.submatrix11 is None else other*self.submatrix11,
+                    symmetry = symmetry,
+                )
+        return NotImplemented
+
+    def __imul__(self, other):
+        '''
+        In-place multiplication by scalar or SymRGfunction.
+        If other is a SymRGfunction, this calculates the matrix product
+        (equivalent to __matmul__). If other is a scalar, this multiplies
+        self.values by other.
+        '''
+        if isinstance(other, SymRGfunction):
+            self @= other
+        elif isinstance(other, Number):
+            if other.imag != 0:
+                if other.real == 0:
+                    self.symmetry = -self.symmetry
+                else:
+                    self.symmetry = 0
+            if (self.submatrix00 is not None):
+                self.submatrix00 *= other
+            if (self.submatrix01 is not None):
+                self.submatrix01 *= other
+            if (self.submatrix10 is not None):
+                self.submatrix10 *= other
+            if (self.submatrix11 is not None):
+                self.submatrix11 *= other
+        else:
+            return NotImplemented
+        return self
+
+    def __rmul__(self, other):
+        '''
+        Reverse multiplication by scalar or RGfunction.
+        If other is an RGfunction, this calculates the matrix product
+        (equivalent to __matmul__). If other is a scalar, this multiplies
+        self.values by other.
+        '''
+        if isinstance(other, RGfunction):
+            return other @ self
+        if isinstance(other, Number):
+            if other.imag == 0:
+                symmetry = self.symmetry
+            elif other.real == 0:
+                symmetry = -self.symmetry
+            else:
+                symmetry = 0
+            return self * other
+        return NotImplemented
+
+    def __truediv__(self, other):
+        '''
+        Divide self by other, which must be a scalar.
+        '''
+        if isinstance(other, Number):
+            return self * (1/other)
+        return NotImplemented
+
+    def __itruediv__(self, other):
+        '''
+        Divide self in-place by other, which must be a scalar.
+        '''
+        if isinstance(other, Number):
+            self *= (1/other)
+            return self
+        return NotImplemented
+
+    def __radd__(self, other):
+        return self + other
+
+    def __rsub__(self, other):
+        return -self + other
+
+    def __repr__(self):
+        return 'SymRGfunction{ %s,\n00: %s\n01: %s\n10: %s\n11: %s }'%(self.energy, self.submatrix00.__repr__(), self.submatrix01.__repr__(), self.submatrix10.__repr__(), self.submatrix11.__repr__())
+
+    def __getitem__(self, arg):
+        raise NotImplementedError
+
+    def __eq__(self, other):
+        return ( self.global_properties is other.global_properties ) \
+                and (self.submatrix00 is other.submatrix00 or np.allclose(self.submatrix00, other.submatrix00)) \
+                and (self.submatrix01 is other.submatrix01 or np.allclose(self.submatrix01, other.submatrix01)) \
+                and (self.submatrix10 is other.submatrix10 or np.allclose(self.submatrix10, other.submatrix10)) \
+                and (self.submatrix11 is other.submatrix11 or np.allclose(self.submatrix11, other.submatrix11)) \
+                and self.symmetry == other.symmetry
+
+    def k2lambda(self, shift_matrix=None):
+        '''
+        Assume that self is K_n^m(E) = K_n(E-mΩ).
+        Then calculate Λ_n^m(E) such that (approximately)
+
+                     m    [                   m-k    ]
+            δ   = Σ Λ (E) [ (E-(m-n)Ω) δ   - K   (E) ] .
+             n0   k  k    [             kn    n-k    ]
+
+        This calculates the propagator from an effective Liouvillian.
+        Some of the linear systems of equation which we need to solve here are
+        overdetermined. This means that we can in general only get an
+        approximate solution because an exact solution does not exist.
+
+        TODO: implement direct energy shift?
+        '''
+        assert shift_matrix is None
+        assert self.submatrix01 is None and self.submatrix10 is None
+        invert = -self
+        invert.submatrix00[np.diag_indices(self.nmax+1)] += self.energy + self.omega*np.arange(-self.nmax, self.nmax+1, 2)
+        invert.submatrix11[np.diag_indices(self.nmax)] += self.energy + self.omega*np.arange(-self.nmax+1, self.nmax+1, 2)
+        invert.symmetry = -1 if self.symmetry == -1 else 0
+        return invert.inverse()
+
+    def inverse(self):
+        '''
+        For a given object self = A try to calculate B such that
+        A @ B = identity
+        with identity[n,k] = δ(n,0).
+
+        Some of the linear systems of equation which we need to solve here are
+        overdetermined. This means that we can in general only get an
+        approximate solution because an exact solution does not exist.
+        '''
+
+        assert self.submatrix01 is None and self.submatrix10 is None
+        assert self.submatrix00 is not None and self.submatrix11 is not None
+        try:
+            res00 = rtrg_c.invert_extended(self.submatrix00.T, self.padding//2, round(LAZY_INVERSE_FACTOR*self.padding/2)).T
+            res11 = rtrg_c.invert_extended(self.submatrix11.T, self.padding//2, round(LAZY_INVERSE_FACTOR*self.padding/2)).T
+            if settings.logger.level == settings.logging.DEBUG:
+                SymRGfunction.INVC_COUNTER[self.submatrix00.T.flags.c_contiguous | (self.submatrix00.T.flags.f_contiguous << 1)]  += 1
+                SymRGfunction.INVC_COUNTER[self.submatrix11.T.flags.c_contiguous | (self.submatrix11.T.flags.f_contiguous << 1)]  += 1
+        except:
+            settings.logger.exception("padded inversion failed in compact RTRG", file=sys.stderr)
+            res00 = np.linalg.inv(self.submatrix00)
+            res11 = np.linalg.inv(self.submatrix11)
+        return SymRGfunction(
+                self.global_properties,
+                values = None,
+                submatrix00 = res00,
+                submatrix01 = None,
+                submatrix10 = None,
+                submatrix11 = res11,
+                symmetry = self.symmetry,
+            )
+
+    def toRGfunction(self):
+        return RGfunction(self.global_properties, self.values, symmetry=self.symmetry)
+
+    def shift_energies(self, n=0):
+        raise ValueError("shift_energies is not defined for SymRGfunction")
diff --git a/package/src/frtrg_kondo/data_management.py b/package/src/frtrg_kondo/data_management.py
new file mode 100644
index 0000000000000000000000000000000000000000..5d1f6a05527fea2c565fb0ab7d31106fa8bf2693
--- /dev/null
+++ b/package/src/frtrg_kondo/data_management.py
@@ -0,0 +1,668 @@
+# Copyright 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, data management module
+
+This file contains functions and classes to manage data generated using the
+kondo module.
+
+General concepts:
+* All metadata are stored in an SQL database.
+* Floquet matrices are stored in HDF5 files.
+* Each HDF5 file can contain multiple data points. Data points can be added to
+  HDF5 files.
+* Each HDF5 file contains a table of metadata for the data points stored in
+  this file.
+* Data points are identified in HDF5 files by a hash generated from their full
+  Floquet matrices at the end of the RG flow.
+* The SQL database stores the directory, filename, and hash where the Floquet
+  matrices are stored.
+
+Implementation:
+* pandas for accessing the SQL database and managing the full table of metadata
+* pytables for HDF5 files
+* a file "filename.lock" is temporarily created when writing to a HDF5 file.
+"""
+
+import os
+import tables as tb
+import pathlib
+from time import sleep
+from datetime import datetime
+import numpy as np
+import pandas as pd
+import sqlalchemy as db
+import random
+import warnings
+from frtrg_kondo import settings
+
+# We use hashs as identifiers for data points in HDF5 files. These hashs are
+# often not valid python names, which causes a warning. We ignore this warning.
+warnings.simplefilter("ignore", tb.NaturalNameWarning)
+
+def random_string(length : int):
+    """
+    Generate random strings of alphanumerical characters with given length.
+    """
+    res = ""
+    for _ in range(length):
+        x = random.randint(0, 61)
+        if x < 10:
+            res += chr(x + 48)
+        elif x < 36:
+            res += chr(x + 55)
+        else:
+            res += chr(x + 61)
+    return res
+
+
+def replace_all(string:str, replacements:dict):
+    """
+    Apply all replacements to string
+    """
+    for old, new in replacements.items():
+        string = string.replace(old, new)
+    return string
+
+
+class KondoExport:
+    """
+    Class for saving Kondo object to file.
+    Example usage:
+    >>> kondo = Kondo(...)
+    >>> kondo.run(...)
+    >>> KondoExport(kondo).save_h5("data/frtrg-01.h5")
+    """
+    METHOD_ENUM = tb.Enum(('unknown', 'mu', 'J', 'J-compact-1', 'J-compact-2', 'mu-reference', 'J-reference', 'mu-extrap-voltage', 'J-extrap-voltage'))
+    SOLVER_METHOD_ENUM = tb.Enum(('unknown', 'RK45', 'RK23', 'DOP853', 'Radau', 'BDF', 'LSODA', 'other'))
+
+    def __init__(self, kondo):
+        self.kondo = kondo
+
+    @property
+    def hash(self):
+        """
+        hash based on Floquet matrices in Kondo object
+        """
+        try:
+            return self._hash
+        except AttributeError:
+            self._hash = self.kondo.hash()[:40]
+            return self._hash
+
+    @property
+    def metadata(self):
+        """
+        dictionary of metadata
+        """
+        # Determine method
+        if self.kondo.unitary_transformation:
+            if self.kondo.compact == 2:
+                method = 'J-compact-2'
+            elif self.kondo.compact == 1:
+                method = 'J-compact-1'
+            else:
+                method = 'J'
+        else:
+            method = 'mu'
+
+        # Collect solver flags
+        solver_flags = 0
+        try:
+            if self.kondo.simplified_initial_conditions:
+                solver_flags |= DataManager.SOLVER_FLAGS["simplified_initial_conditions"]
+        except AttributeError:
+            pass
+        for (key, value) in self.kondo.global_settings.items():
+            if value:
+                try:
+                    solver_flags |= DataManager.SOLVER_FLAGS[key.lower()]
+                except KeyError:
+                    pass
+
+        version = self.kondo.global_settings["VERSION"]
+        return dict(
+                hash = self.hash,
+                omega = self.kondo.omega,
+                energy = self.kondo.energy,
+                version_major = version[0],
+                version_minor = version[1],
+                lazy_inverse_factor = self.kondo.global_settings["LAZY_INVERSE_FACTOR"],
+                git_commit_count = version[2],
+                git_commit_id = version[3],
+                method = method,
+                timestamp = datetime.utcnow().timestamp(),
+                solver_method = getattr(self.kondo, 'solveopts', {}).get('method', 'unknown'),
+                solver_tol_abs = getattr(self.kondo, 'solveopts', {}).get('atol', -1),
+                solver_tol_rel = getattr(self.kondo, 'solveopts', {}).get('rtol', -1),
+                d = self.kondo.d,
+                vdc = self.kondo.vdc,
+                vac = self.kondo.vac,
+                nmax = self.kondo.nmax,
+                padding = self.kondo.padding,
+                voltage_branches = self.kondo.voltage_branches,
+                resonant_dc_shift = self.kondo.resonant_dc_shift,
+                solver_flags = solver_flags,
+                )
+
+    @property
+    def main_results(self):
+        """
+        dictionary of main results: DC current, DC conductance, AC current (absolute value and phase)
+        """
+        results = dict(
+                dc_current = np.nan,
+                dc_conductance = np.nan,
+                ac_current_abs = np.nan,
+                ac_current_phase = np.nan
+                )
+        nmax = self.kondo.nmax
+        try:
+            results['dc_current'] = self.kondo.gammaL[nmax, nmax].real
+        except:
+            pass
+        try:
+            results['dc_conductance'] = self.kondo.deltaGammaL[nmax, nmax].real
+        except:
+            pass
+        if nmax == 0:
+            results['ac_current_abs'] = 0
+        else:
+            try:
+                results['ac_current_abs'] = np.abs(self.kondo.gammaL[nmax-1, nmax])
+                results['ac_current_phase'] = np.angle(self.kondo.gammaL[nmax-1, nmax])
+            except:
+                pass
+        return results
+
+    def data(self, include='all'):
+        """
+        dictionary of Floquet matrices as numpy arrays.
+
+        Argument include takes the following values:
+        "all":      save all data (Floquet matrices including voltage shifts)
+        "reduced":  exclude voltage shifts and yL
+        "observables: save only gamma, gammaL, deltaGammaL, excluding voltage
+                    shifts
+        "minimal":  save only central column of Floquet matrices for gamma,
+                    gammaL, deltaGammaL, excluding voltage
+        """
+        if include == 'all':
+            save = dict(
+                    gamma = self.kondo.gamma.values,
+                    z = self.kondo.z.values,
+                    gammaL = self.kondo.gammaL.values,
+                    deltaGammaL = self.kondo.deltaGammaL.values,
+                    deltaGamma = self.kondo.deltaGamma.values,
+                    yL = self.kondo.yL.values,
+                    g2 = self.kondo.g2.to_numpy_array(),
+                    g3 = self.kondo.g3.to_numpy_array(),
+                    current = self.kondo.current.to_numpy_array(),
+                    )
+        elif include == 'reduced':
+            if self.kondo.voltage_branches:
+                vb = self.kondo.voltage_branches
+                save = dict(
+                        gamma = self.kondo.gamma[vb],
+                        z = self.kondo.z[vb],
+                        gammaL = self.kondo.gammaL.values,
+                        deltaGammaL = self.kondo.deltaGammaL.values,
+                        deltaGamma = self.kondo.deltaGamma[min(vb,1)],
+                        g2 = self.kondo.g2.to_numpy_array()[:,:,vb],
+                        g3 = self.kondo.g3.to_numpy_array()[:,:,vb],
+                        current = self.kondo.current.to_numpy_array(),
+                        )
+            else:
+                save = dict(
+                        gamma = self.kondo.gamma.values,
+                        z = self.kondo.z.values,
+                        gammaL = self.kondo.gammaL.values,
+                        deltaGammaL = self.kondo.deltaGammaL.values,
+                        deltaGamma = self.kondo.deltaGamma.values,
+                        g2 = self.kondo.g2.to_numpy_array(),
+                        g3 = self.kondo.g3.to_numpy_array(),
+                        current = self.kondo.current.to_numpy_array(),
+                        )
+        elif include == 'observables':
+            if self.kondo.voltage_branches:
+                vb = self.kondo.voltage_branches
+                save = dict(
+                        gamma = self.kondo.gamma[vb],
+                        gammaL = self.kondo.gammaL.values,
+                        deltaGammaL = self.kondo.deltaGammaL.values,
+                        )
+            else:
+                save = dict(
+                        gamma = self.kondo.gamma.values,
+                        gammaL = self.kondo.gammaL.values,
+                        deltaGammaL = self.kondo.deltaGammaL.values,
+                        )
+        elif include == 'minimal':
+            nmax = self.kondo.nmax
+            if self.kondo.voltage_branches:
+                vb = self.kondo.voltage_branches
+                save = dict(
+                        gamma = self.kondo.gamma[vb,:,nmax],
+                        gammaL = self.kondo.gammaL[:,nmax],
+                        deltaGammaL = self.kondo.deltaGammaL[:,nmax],
+                        )
+            else:
+                save = dict(
+                        gamma = self.kondo.gamma[:,nmax],
+                        gammaL = self.kondo.gammaL[:,nmax],
+                        deltaGammaL = self.kondo.deltaGammaL[:,nmax],
+                        )
+        else:
+            raise ValueError("Unknown value for include: " + include)
+        return save
+
+    def save_npz(self, filename, include="all"):
+        """
+        Save data in binary numpy format.
+        """
+        np.savez(filename, **self.metadata, **self.data(include))
+
+    def save_h5(self, filename, include='all', overwrite=False):
+        """
+        Save data in HDF5 file.
+
+        Returns absolute path to filename where data have been saved.
+        If overwrite is False and a file would be overwritten, append a random
+        string to the end of the filename.
+        """
+        os.sync()
+        while os.path.exists(filename + '.lock'):
+            try:
+                settings.logger.warning('File %s is locked, waiting 0.5s'%filename)
+                sleep(0.5)
+            except KeyboardInterrupt:
+                answer = input('Ignore lock file? Then type "yes": ')
+                if answer.lower() == "yes":
+                    break
+                answer = input('Save with filename extended by random string? (Yn): ')
+                if answer.lower()[0] != "n":
+                    return self.save_h5(filename + random_string(8) + ".h5", include, overwrite)
+        pathlib.Path(filename + '.lock').touch()
+        try:
+            file_exists = os.path.exists(filename)
+            h5file = None
+            while h5file is None:
+                try:
+                    h5file = tb.open_file(filename, "a")
+                except tb.exceptions.HDF5ExtError:
+                    settings.logger.warning('Error opening file %s, waiting 0.5s'%filename)
+                    sleep(0.5)
+            try:
+                if file_exists:
+                    try:
+                        h5file.is_visible_node('/data/' + self.hash)
+                        settings.logger.warning("Hash exists in file %s!"%filename)
+                        return self.save_h5(filename + random_string(8) + ".h5", include, overwrite)
+                    except tb.exceptions.NoSuchNodeError:
+                        pass
+                    metadata_table = h5file.get_node("/metadata/mdtable")
+                else:
+                    # create new file
+                    metadata_parent = h5file.create_group(h5file.root, "metadata", "Metadata")
+                    metadata_table = h5file.create_table(metadata_parent,
+                            'mdtable',
+                            dict(
+                                idnum = tb.Int32Col(),
+                                hash = tb.StringCol(40),
+                                omega = tb.Float64Col(),
+                                energy = tb.ComplexCol(16),
+                                version_major = tb.Int16Col(),
+                                version_minor = tb.Int16Col(),
+                                git_commit_count = tb.Int16Col(),
+                                git_commit_id = tb.Int32Col(),
+                                timestamp = tb.Time64Col(),
+                                method = tb.EnumCol(KondoExport.METHOD_ENUM, 'unknown', 'int8'),
+                                solver_method = tb.EnumCol(KondoExport.SOLVER_METHOD_ENUM, 'unknown', 'int8'),
+                                solver_tol_abs = tb.Float64Col(),
+                                solver_tol_rel = tb.Float64Col(),
+                                d = tb.Float64Col(),
+                                vdc = tb.Float64Col(),
+                                vac = tb.Float64Col(),
+                                nmax = tb.Int16Col(),
+                                padding = tb.Int16Col(),
+                                voltage_branches = tb.Int16Col(),
+                                resonant_dc_shift = tb.Int16Col(),
+                                solver_flags = tb.Int16Col(),
+                                lazy_inverse_factor = tb.Float64Col(),
+                            )
+                        )
+                    h5file.create_group(h5file.root, "data", "Floquet matrices")
+                    h5file.flush()
+
+                # Save metadata
+                row = metadata_table.row
+                idnum = metadata_table.shape[0]
+                row['idnum'] = idnum
+                metadata = self.metadata
+                row['method'] = KondoExport.METHOD_ENUM[metadata.pop('method')]
+                row['solver_method'] = KondoExport.SOLVER_METHOD_ENUM[metadata.pop('solver_method')]
+                if include != "all":
+                    metadata["solver_flags"] |= DataManager.SOLVER_FLAGS["reduced"]
+                for key, value in metadata.items():
+                    try:
+                        row[key] = value
+                    except KeyError:
+                        pass
+                row.append()
+
+                # save data
+                datagroup = h5file.create_group("/data/", self.hash)
+                data = self.data(include)
+                for key, value in data.items():
+                    h5file.create_array(datagroup, key, value)
+                h5file.flush()
+            finally:
+                h5file.close()
+        finally:
+            os.remove(filename + ".lock")
+        return os.path.abspath(filename)
+
+
+class KondoImport:
+    """
+    Class for importing Kondo objects that were saved with KondoExport.
+    Example usage:
+    >>> kondo, = KondoImport.read_from_h5("data/frtrg-01.h5", "94f81d2b49df15912798d95cae8e108d75c637c2")
+    >>> print(kondo.gammaL[kondo.nmax, kondo.nmax])
+    """
+    def __init__(self, metadata, datanode, h5file, owns_h5file=False):
+        self.metadata = metadata
+        self._datanode = datanode
+        self._h5file = h5file
+        self._owns_h5file = owns_h5file
+
+    def __del__(self):
+        if self._owns_h5file:
+            settings.logger.info("closing h5file")
+            self._h5file.close()
+
+    @classmethod
+    def read_from_h5(cls, filename, khash):
+        h5file = tb.open_file(filename, "r")
+        datanode = h5file.get_node('/data/' + khash)
+        metadatatable = h5file.get_node('/metadata/mdtable')
+        counter = 0
+        for row in metadatatable.where(f"hash == '{khash}'"):
+            metadata = {key:row[key] for key in metadatatable.colnames}
+            item = cls(metadata, datanode, h5file)
+            yield item
+            counter += 1
+        if counter == 1:
+            item._owns_h5file = True
+        else:
+            settings.logger.warning("h5file will not be closed automatically")
+
+    @classmethod
+    def read_all_from_h5(cls, filename):
+        h5file = tb.open_file(filename)
+        metadatatable = h5file.get_node('/metadata/mdtable')
+        counter = 0
+        for row in metadatatable:
+            metadata = {key:row[key] for key in metadatatable.colnames}
+            datanode = h5file.get_node('/data/' + row.hash)
+            item = cls(metadata, datanode, h5file)
+            yield item
+            counter += 1
+        if counter == 1:
+            item._owns_h5file = True
+        else:
+            settings.logger.warning("h5file will not be closed automatically")
+
+    def __getitem__(self, name):
+        if name in self.metadata:
+            return self.metadata[name]
+        if name in self._datanode:
+            return self._datanode[name].read()
+        raise KeyError("Unknown key: %s"%name)
+
+    def __getattr__(self, name):
+        if name in self.metadata:
+            return self.metadata[name]
+        if name in self._datanode:
+            return self._datanode[name].read()
+        raise AttributeError("Unknown attribute name: %s"%name)
+
+
+
+class DataManager:
+    '''
+    Database structure
+    tables:
+        datapoints (single data point)
+    '''
+    SOLVER_FLAGS = dict(
+            contains_flow = 0x001,
+            reduced = 0x002,
+            deleted = 0x004,
+            simplified_initial_conditions = 0x008,
+            enforce_symmetric = 0x010,
+            check_symmetries = 0x020,
+            ignore_symmetries = 0x040,
+            extrapolate_voltage = 0x080,
+            use_cublas = 0x100,
+            use_reference_implementation = 0x200,
+            )
+
+    def __init__(self):
+        self.version = settings.VERSION
+        self.engine = db.create_engine(settings.DB_CONNECTION_STRING, future=True, echo=False)
+
+        self.metadata = db.MetaData()
+        try:
+            self.table = db.Table('datapoints', self.metadata, autoload=True, autoload_with=self.engine)
+        except db.exc.NoSuchTableError:
+            with self.engine.begin() as connection:
+                settings.logger.info('Creating database table datapoints')
+                self.table = db.Table(
+                        'datapoints',
+                        self.metadata,
+                        db.Column('id', db.INTEGER(), primary_key=True),
+                        db.Column('hash', db.CHAR(40)),
+                        db.Column('version_major', db.SMALLINT()),
+                        db.Column('version_minor', db.SMALLINT()),
+                        db.Column('git_commit_count', db.SMALLINT()),
+                        db.Column('git_commit_id', db.INTEGER()),
+                        db.Column('timestamp', db.TIMESTAMP()),
+                        db.Column('method', db.Enum('unknown', 'mu', 'J', 'J-compact-1', 'J-compact-2', 'mu-reference', 'J-reference')),
+                        db.Column('solver_method', db.Enum('unknown', 'RK45', 'RK23', 'DOP853', 'Radau', 'BDF', 'LSODA', 'other')),
+                        db.Column('solver_tol_abs', db.FLOAT()),
+                        db.Column('solver_tol_rel', db.FLOAT()),
+                        db.Column('omega', db.FLOAT()),
+                        db.Column('d', db.FLOAT()),
+                        db.Column('vdc', db.FLOAT()),
+                        db.Column('vac', db.FLOAT()),
+                        db.Column('energy_re', db.FLOAT()),
+                        db.Column('energy_im', db.FLOAT()),
+                        db.Column('lazy_inverse_factor', db.FLOAT()),
+                        db.Column('dc_current', db.FLOAT()),
+                        db.Column('ac_current_abs', db.FLOAT()),
+                        db.Column('ac_current_phase', db.FLOAT()),
+                        db.Column('dc_conductance', db.FLOAT()),
+                        db.Column('nmax', db.SMALLINT()),
+                        db.Column('padding', db.SMALLINT()),
+                        db.Column('voltage_branches', db.SMALLINT()),
+                        db.Column('resonant_dc_shift', db.SMALLINT()),
+                        db.Column('solver_flags', db.SMALLINT()), # unfortunately SET is not available in SQLite
+                        db.Column('dirname', db.String(256)),
+                        db.Column('basename', db.String(128)),
+                    )
+                self.table.create(bind=connection)
+
+    def insert_from_h5file(self, filename):
+        raise NotImplementedError()
+        basename = os.path.basename(filename)
+        dirname = os.path.dirname(filename)
+        # TODO
+
+    def insert_in_db(self, filename : str, kondo : KondoExport):
+        '''
+        Save metadata in database for data stored in filename.
+        '''
+        metadata = kondo.metadata
+        metadata.update(kondo.main_results)
+        energy = metadata.pop('energy')
+        metadata.update(
+                    energy_re = energy.real,
+                    energy_im = energy.imag,
+                    timestamp = datetime.fromtimestamp(metadata.pop("timestamp")).isoformat().replace('T', ' '),
+                    dirname = os.path.dirname(filename),
+                    basename = os.path.basename(filename),
+                )
+        frame = pd.DataFrame(metadata, index=[0])
+        frame.to_sql(
+                'datapoints',
+                self.engine,
+                if_exists='append',
+                index=False,
+                )
+        try:
+            del self.df_table
+        except AttributeError:
+            pass
+
+    def import_from_db(self, db_string, replace_base_path={}):
+        """
+        e.g. replace_base_path = {'/path/on/cluster/to/data':'/path/to/local/data'}
+        """
+        raise NotImplementedError()
+        # TODO: rewrite
+        import_engine = db.create_engine(db_string, future=True, echo=False)
+        import_metadata = db.MetaData()
+        import_table = db.Table('datapoints', import_metadata, autoload=True, autoload_with=import_engine)
+        with import_engine.begin() as connection:
+            import_df_table = pd.read_sql_table('datapoints', connection, index_col='id')
+        valid_indices = []
+        for idx in import_df_table.index:
+            import_df_table.dirname[idx] = replace_all(import_df_table.dirname[idx], replace_base_path)
+            # TODO: rewrite this
+            selection = self.df_table.basename == import_df_table.basename[idx]
+            if not any(self.df_table.dirname[selection] == import_df_table.dirname[idx]):
+                valid_indices.append(idx)
+        settings.logger.info('Importing %d entries'%len(valid_indices))
+        import_df_table.loc[valid_indices].to_sql(
+                'datapoints',
+                self.engine,
+                if_exists='append',
+                index=False,
+                )
+
+    def save_h5(self, kondo : KondoExport, filename : str = None, include='all', overwrite=False):
+        '''
+        Save all data in given filename and keep metadata in database.
+        '''
+        if filename is None:
+            filename = os.path.join(settings.BASEPATH, settings.FILENAME)
+        if not isinstance(kondo, KondoExport):
+            kondo = KondoExport(kondo)
+        filename = kondo.save_h5(filename, include, overwrite)
+        self.insert_in_db(filename, kondo)
+
+    def cache_df_table(self, min_version=(0,5,-1)):
+        settings.logger.debug('DataManager: cache df_table', flush=True)
+        with self.engine.begin() as connection:
+            df_table = pd.read_sql_table('datapoints', connection, index_col='id')
+        selection = (df_table.solver_flags & DataManager.SOLVER_FLAGS['deleted']) == 0
+        selection &= (df_table.version_major > min_version[0]) | ( (df_table.version_major == min_version[0]) & (df_table.version_minor >= min_version[1]) )
+        selection &= df_table.energy_re == 0
+        selection &= df_table.energy_im == 0
+        if len(min_version) > 2 and min_version[2] > 0:
+            selection &= df_table.git_commit_count >= min_version[2]
+        self.df_table = df_table[selection]
+
+    def __getattr__(self, name):
+        if name == 'df_table':
+            self.cache_df_table()
+            return self.df_table
+
+    def load(self, db_id):
+        '''
+        db_id is the id in the database (starts counting from 1)
+        '''
+        raise NotImplementedError
+        row = self.df_table.loc[db_id]
+        path = os.path.join(row.dirname, row.basename)
+        kondo = ...
+        kondo.solveopts = dict(
+                method = row.solver_method,
+                rtol = row.solver_tol_rel,
+                atol = row.solver_tol_abs,
+            )
+        return kondo
+
+    def list(self, min_version=(14,0,-1,-1), **parameters):
+        '''
+        Print and return DataFrame with selection of physical parameters.
+        '''
+        selection = (self.df_table.version_major > min_version[0]) | (self.df_table.version_major == min_version[0]) & (self.df_table.version_minor >= min_version[1])
+        selection &= self.df_table.energy_re == 0
+        selection &= self.df_table.energy_im == 0
+        if len(min_version) > 2 and min_version[2] > 0:
+            selection &= self.df_table.git_commit_count >= min_version[2]
+        for key, value in parameters.items():
+            if value is None:
+                continue
+            try:
+                selection &= self.df_table[key] == value
+            except KeyError:
+                settings.logger.warning("Unknown key: %s"%key)
+        if selection is True:
+            result = self.df_table
+        else:
+            result = self.df_table.loc[selection]
+        return result
+
+    def load_from_table(self, table, load_flow=False, load_old_files=True):
+        '''
+        Extend table by adding a "solver" column.
+        '''
+        solvers = []
+        reduced_table = table
+        for idx, row in table.iterrows():
+            old_file = load_old_files and row.version_major == 0 and row.version_minor < 6
+            loader = Solver.load_old_file if old_file else Solver.load
+            try:
+                solvers.append(loader(os.path.join(row.dirname, row.basename), load_flow))
+            except FileNotFoundError:
+                settings.logger.exception('Could not find file: "%s" / "%s"'%(row.dirname, row.basename))
+                reduced_table = reduced_table.drop(idx)
+            except AssertionError:
+                settings.logger.exception('Error while loading file: "%s" / "%s"'%(row.dirname, row.basename))
+                reduced_table = reduced_table.drop(idx)
+        return reduced_table.assign(solver = solvers)
+
+    def list_kondo(self, **kwargs):
+        '''
+        Returns a DataFrame with an extra column "solvers" with the filters
+        from kwargs applied (see documentation of DataManager.list for the
+        filters).
+        '''
+        return self.load_from_table(self.list(**kwargs))
+
+    def clean_database(self):
+        '''
+        Flag all database entries as 'deleted' for which no solver file can be found.
+        Delete duplicated entries.
+        '''
+        raise NotImplementedError
+        with self.engine.begin() as connection:
+            full_df_table = pd.read_sql_table('datapoints', connection, index_col='id')
+        remove_indices = []
+        for idx, row in full_df_table.iterrows():
+            path = os.path.join(row.dirname, row.basename)
+            if not os.path.exists(path):
+                path += '.npz'
+            if not os.path.exists(path):
+                settings.logger.warning('File does not exist:', path, idx)
+                row.solver_flags |= DataManager.SOLVER_FLAGS['deleted']
+                stmt = db.update(self.table).where(self.table.c.id == idx).values(solver_flags = row.solver_flags)
+                with self.engine.begin() as connection:
+                    connection.execute(stmt)
+
+def list_data(**kwargs):
+    table = DataManager().list(**kwargs)
+    print(result[['method', 'vdc', 'vac', 'omega', 'nmax', 'voltage_branches', 'padding', 'dc_current', 'dc_conductance', 'ac_current_abs']])
diff --git a/package/src/frtrg_kondo/drive_gate_voltage.py b/package/src/frtrg_kondo/drive_gate_voltage.py
new file mode 100644
index 0000000000000000000000000000000000000000..2596b84349c0dd03ed25e966f84a85ac310ced8e
--- /dev/null
+++ b/package/src/frtrg_kondo/drive_gate_voltage.py
@@ -0,0 +1,167 @@
+# Copyright 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, variant of kondo model for oscillating coupling
+
+Oscillations in the coupling between a quantum dot and its reservoirs can be
+the result of an oscillating gate voltage in a quantum dot. This module
+includes a basic implementation of this idea. The RG flow is not converging!
+"""
+from frtrg_kondo.kondo import *
+
+class KondoG(Kondo):
+    """
+    Variant of Kondo where absolute value of J oscillates.
+    This may be the result of an oscillating gate voltage for a quantum dot in
+    the Kondo regime or of direct driving of the tunneling barrier.
+    """
+    def __init__(self, j_driving_param, **options):
+        super().__init__(**options)
+        self.j_driving_param = j_driving_param
+
+    def initialize(self,
+            **solveopts : 'keyword arguments passed to solver',
+            ):
+        '''
+        Arguments:
+        **solveopts: keyword arguments passed to the solver. Most relevant
+                are rtol and atol.
+
+        Get initial conditions for Γ, Z and G2 by numerically solving the
+        equilibrium RG equations from E=0 to E=iD and for all required Re(E).
+        Initialize G3, Iγ, δΓ, δΓγ.
+        '''
+
+        assert self.compact == 0
+        sqrtxx = np.sqrt(self.xL*(1-self.xL))
+        symmetry = 0 if settings.IGNORE_SYMMETRIES else 1
+
+
+        #### Initial conditions from exact results at T=V=0
+        # Get Γ, Z and J (G2) for T=V=0.
+        gamma0, z0, j0 = solveTV0_Utransformed(d=self.d, properties=self.global_properties, **solveopts)
+
+        # Write T=V=0 results to Floquet index n=0.
+        gammavalues = np.zeros(self.shape(), dtype=np.complex128)
+        zvalues = np.zeros(self.shape(), dtype=np.complex128)
+        jvalues = np.zeros(self.shape(), dtype=np.complex128)
+
+        # construct diagonal matrices
+        diag_idx = (..., *np.diag_indices(2*self.nmax+1))
+        if self.resonant_dc_shift:
+            gammavalues[diag_idx] = gamma0[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+            zvalues[diag_idx] = z0[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+            jvalues[diag_idx] = j0[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+        else:
+            gammavalues[diag_idx] = gamma0
+            zvalues[diag_idx] = z0
+            jvalues[diag_idx] = j0
+
+        # Create Γ and Z with the just calculated initial values.
+        self.gamma = RGfunction(self.global_properties, gammavalues, symmetry=symmetry)
+        self.z = RGfunction(self.global_properties, zvalues, symmetry=symmetry)
+
+        # Create G2 from J:    G2_{ij} = - 2 sqrt(x_i x_j) J
+        self.g2 = ReservoirMatrix(self.global_properties, symmetry=symmetry)
+        j_rgfunction = RGfunction(self.global_properties, jvalues, symmetry=symmetry)
+        self.g2[0,0] = -2*self.xL * j_rgfunction
+        self.g2[1,1] = -2*(1-self.xL) * j_rgfunction
+        # Coefficients are given by the Bessel function of the first kind.
+        init_matrix = gen_init_matrix(self.nmax, 0, resonant_dc_shift=self.resonant_dc_shift)
+        init_matrix[np.arange(0, 2*self.nmax), np.arange(1, 2*self.nmax+1)] = self.j_driving_param
+        init_matrix[np.arange(1, 2*self.nmax+1), np.arange(0, 2*self.nmax)] = self.j_driving_param.conjugate()
+        j_LR = np.einsum('ij,...j->...ij', init_matrix, j0[...,2*self.resonant_dc_shift:])
+        j_RL = np.einsum('ji,...j->...ij', init_matrix.conjugate(), j0[...,:j0.shape[-1]-2*self.resonant_dc_shift])
+        j_LR = RGfunction(self.global_properties, j_LR)
+        j_RL = RGfunction(self.global_properties, j_RL)
+        self.g2[0,1] = -2*sqrtxx * j_LR
+        self.g2[1,0] = -2*sqrtxx * j_RL
+
+
+        ## Initial conditions for G3
+        # G3 ~ Jtilde^2  with  Jtilde = Z J
+        # Every entry of G3 will be of the following form (up to prefactors):
+        self.g3 = ReservoirMatrix(self.global_properties, symmetry=-symmetry)
+        g3_entry = np.zeros(self.shape(), dtype=np.complex128)
+        g3_entry[diag_idx] = 1j*np.pi * (jvalues[diag_idx]*zvalues[diag_idx])**2
+        g3_entry = RGfunction(self.global_properties, g3_entry, symmetry=-symmetry)
+        self.g3[0,0] = 2*self.xL * g3_entry
+        self.g3[1,1] = 2*(1-self.xL) * g3_entry
+        g30 = 1j*np.pi*(z0*j0)**2
+        g3_LR = np.einsum('ij,...j->...ij', init_matrix, g30[...,2*self.resonant_dc_shift:])
+        g3_RL = np.einsum('ji,...j->...ij', init_matrix.conjugate(), g30[...,:g30.shape[-1]-2*self.resonant_dc_shift])
+        g3_LR = RGfunction(self.global_properties, g3_LR)
+        g3_RL = RGfunction(self.global_properties, g3_RL)
+        self.g3[0,1] = 2*sqrtxx * g3_LR
+        self.g3[1,0] = 2*sqrtxx * g3_RL
+
+
+        ## Initial conditions for current I^{γ=L} = J0 (1 - Jtilde)
+        if self.voltage_branches:
+            current_entry = np.diag( 2*sqrtxx * jvalues[self.voltage_branches][diag_idx] * (1 - jvalues[self.voltage_branches][diag_idx] * zvalues[self.voltage_branches][diag_idx] ) )
+        else:
+            current_entry = np.diag( 2*sqrtxx * jvalues[diag_idx] * (1 - jvalues[diag_idx] * zvalues[diag_idx] ) )
+        current_entry = RGfunction(self.global_properties, current_entry, symmetry=-symmetry)
+        self.current = ReservoirMatrix(self.global_properties, symmetry=-symmetry)
+        self.current[0,0] = 0*current_entry
+        self.current[0,0].symmetry = -symmetry
+        self.current[1,1] = self.current[0,0].copy()
+        if self.voltage_branches:
+            i0 = j0[self.voltage_branches] * (1 - j0[self.voltage_branches]*z0[self.voltage_branches])
+        else:
+            i0 = j0 * (1 - j0*z0)
+        i_LR = np.einsum('ij,...j->...ij', init_matrix, i0[2*self.resonant_dc_shift:])
+        i_RL = np.einsum('ji,...j->...ij', init_matrix.conjugate(), i0[:i0.size-2*self.resonant_dc_shift])
+        i_LR = RGfunction(self.global_properties, i_LR)
+        i_RL = RGfunction(self.global_properties, i_RL)
+        self.current[0,1] =  2*sqrtxx * i_LR
+        self.current[1,0] = -2*sqrtxx * i_RL
+
+        ## Initial conditions for voltage-variation of Γ: δΓ
+        self.deltaGamma = RGfunction(
+                self.global_properties,
+                np.zeros((3,2*self.nmax+1,2*self.nmax+1) if self.voltage_branches else self.shape(), dtype=np.complex128),
+                symmetry = symmetry
+                )
+
+        ## Initial conditions for voltage-variation of current-Γ: δΓ_L
+        self.deltaGammaL = RGfunction(
+                self.global_properties,
+                np.zeros((2*self.nmax+1, 2*self.nmax+1), dtype=np.complex128),
+                symmetry = symmetry
+                )
+        if self.resonant_dc_shift:
+            if self.voltage_branches:
+                self.deltaGammaL.values[diag_idx] = 3*np.pi*sqrtxx**2 * (j0[self.voltage_branches,self.resonant_dc_shift:-self.resonant_dc_shift]*z0[self.voltage_branches,self.resonant_dc_shift:-self.resonant_dc_shift])**2
+            else:
+                self.deltaGammaL.values[diag_idx] = 3*np.pi*sqrtxx**2 * (j0[self.resonant_dc_shift:-self.resonant_dc_shift]*z0[self.resonant_dc_shift:-self.resonant_dc_shift])**2
+        else:
+            if self.voltage_branches:
+                diag_values = 3*np.pi*sqrtxx**2 * (j0[self.voltage_branches]*z0[self.voltage_branches])**2
+            else:
+                diag_values = 3*np.pi*sqrtxx**2 * (j0*z0)**2
+            self.deltaGammaL.values[diag_idx] = diag_values
+            del diag_values
+
+
+        ### Derivative of full current
+        self.yL = RGfunction(
+                self.global_properties,
+                np.zeros((2*self.nmax+1,2*self.nmax+1), dtype=np.complex128),
+                symmetry=-symmetry
+                )
+
+        ### Full current, also includes AC current
+        self.gammaL = (self.vdc + self.omega*self.resonant_dc_shift) * self.deltaGammaL.reduced()
+        if self.voltage_branches:
+            gammaL_AC = 3*np.pi*sqrtxx**2 * self.vac/2 * (j0[self.voltage_branches]*z0[self.voltage_branches])**2
+        else:
+            gammaL_AC = 3*np.pi*sqrtxx**2 * self.vac/2 * (j0*z0)**2
+        if self.resonant_dc_shift:
+            gammaL_AC = gammaL_AC[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+        idx = (np.arange(1, 2*self.nmax+1), np.arange(2*self.nmax))
+        self.gammaL.values[idx] = gammaL_AC[...,1:]
+        idx = (np.arange(0, 2*self.nmax), np.arange(1, 2*self.nmax+1))
+        self.gammaL.values[idx] = gammaL_AC[...,:-1]
+
+        self.global_properties.energy = 1j*self.d
diff --git a/package/src/frtrg_kondo/gen_data.py b/package/src/frtrg_kondo/gen_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..010438add10dc5867b10953106765b05d5e85aae
--- /dev/null
+++ b/package/src/frtrg_kondo/gen_data.py
@@ -0,0 +1,345 @@
+#!/usr/bin/env python3
+
+# Copyright 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, script for generating and saving data.
+
+See help function of parser in main() for documentation.
+"""
+
+import multiprocessing as mp
+import argparse
+import os
+import numpy as np
+from logging import _levelToName
+from frtrg_kondo import settings
+from frtrg_kondo.data_management import DataManager
+from frtrg_kondo.kondo import Kondo
+
+
+def gen_option_iter(steps=None, scales=None, **options):
+    """
+    Interpret given options to swipe over parameters.
+
+    Arguments:
+        steps: number of steps for each swipe dimension
+        scales: spacing (linear or logarithmic) for each swipe dimension
+        **options: arguments for Kondo(...) taken from an argument parser.
+            These are not the options for Kondo.run(...).
+
+    Interpretation of options is documented in the help function of this
+    script (parser.epilog in main()).
+    """
+    iter_variables = {}
+    if steps is None or len(steps) == 0:
+        max_length = 1
+        for key, value in options.items():
+            if type(value) != list or key == "fourier_coef":
+                continue
+            if len(value) == 1:
+                options[key], = value
+            elif max_length == 1:
+                max_length = len(value)
+                iter_variables[key] = (0, value)
+            else:
+                assert max_length == len(value)
+                iter_variables[key] = (0, value)
+        for key in iter_variables.keys():
+            options.pop(key)
+        steps = [max_length]
+    else:
+        if scales is None:
+            scales = len(steps) * ["linear"]
+        elif isinstance(scales, str):
+            scales = len(steps)*[scales]
+        elif len(scales) == 1:
+            scales *= len(steps)
+        for key, value in options.items():
+            if type(value) != list or key == "fourier_coef":
+                continue
+            if len(value) == 1:
+                options[key], = value
+            elif len(value) == 2:
+                if scales[0] in ("lin", "linear"):
+                    iter_variables[key] = (0, np.linspace(value[0], value[1], steps[0], dtype=type(value[0])))
+                elif scales[0] in ("log", "logarithmic"):
+                    iter_variables[key] = (0, np.logspace(np.log10(value[0]), np.log10(value[1]), steps[0], dtype=type(value[0])))
+                else:
+                    raise ValueError("Unexpected value for parameters \"scales\": %s"%scales[0])
+            elif len(value) == 3:
+                dim = round(value[2])
+                assert 0 <= dim < len(steps)
+                if scales[dim] in ("lin", "linear"):
+                    iter_variables[key] = (dim, np.linspace(value[0], value[1], steps[dim], dtype=type(value[0])))
+                elif scales[dim] == "log":
+                    iter_variables[key] = (dim, np.logspace(np.log10(value[0]), np.log10(value[1]), steps[dim], dtype=type(value[0])))
+                else:
+                    raise ValueError("Unexpected value for parameters \"scales\": %s"%scales[dim])
+            else:
+                raise ValueError("Array parameters must be of the form (start, stop, dim)")
+        for key in iter_variables.keys():
+            options.pop(key)
+    for index in np.ndindex(*steps):
+        for key, (dim, array) in iter_variables.items():
+            options[key] = array[index[dim]]
+        settings.logger.debug('step %s/%s: '%(index, steps) + ', '.join('%s=%s'%(key, value) for (key, value) in options.items() if key in iter_variables))
+        yield options.copy()
+
+
+def main():
+    """
+    Generate and save data for FRTRG applied to the Kondo model.
+    This program can generate single data points, multiple explicitly
+    specified data points, or swipes over parameters.
+
+    Energies are generally defined in units of Tkrg, the Kondo temperature
+    as integration constant of the RG equations. This is related to the
+    more conventional definition of the Kondo temperature by G(V=Tk)=e²/h
+    (differential conductance drops to half its universal value when the DC
+    bias voltage equals Tk) by Tk = 3.30743526735 Tkrg.
+    """
+    parser = argparse.ArgumentParser(
+            description = main.__doc__.replace("\n    ", "\n"),
+            epilog = """
+There are two ways to generate multiple data points for different
+parameters. All arguments which accept a list of values as input
+(except fourier_coef) can be used to provide multiple values.
+
+1.  Provide all values explicitly. Options should be given either 1 or n
+    values where n is the number of data points.
+
+Example:
+    python gen_data.py --method=J --nmax 10 10 11 11 --omega=10 --vac 1 2 3 4
+
+2.  Swipe over N different parameters p_0,...,p_{N-1} independently, where
+    parameter p_i takes n_i different values in linear or logarithmic
+    spacing. In this case parameter p_i gets the three arguments (minimum
+    value of p_i, maximum value of p_i, and index i of the parameter):
+    --p_i p_i_min p_i_max i
+    If the index (i) is not given, the default value 0 is assumed.
+    The numbers of values per parameter are defined by
+    --steps n_0 n_1 ... n_N.
+    This will iterate over n_0 × n_1 × ... × n_N parameters.
+    It is possible to couple two parameters by giving both the same index.
+    To use logarithmic spacing, the spacing for all dimensions must be
+    provided explicitly in the form (l_i = linear or log):
+    --scale l_0 l_1 ... l_N
+
+Examples:
+
+Swipe over vac=1,2,...,10:
+    python gen_data.py --method=J --nmax=10 --omega=10 --vac 1 10 --steps=10
+    or equivalently:
+    python gen_data.py --method=J --nmax=10 --omega=10 --vac 1 10 0 --steps=10
+
+Keep omega=vac and swipe over (omega,vac)=1,2,...,10 (generates 10 data points):
+    python gen_data.py --method=J --nmax=10 --omega 1 10 --vac 1 10 --steps 10
+    or equivalently:
+    python gen_data.py --method=J --nmax=10 --omega 1 10 0 --vac 1 10 0 --steps 10
+
+Swipe over omega=10,12,...,20 and vac=1,2,...,10 independently (generates 60 data points):
+    python gen_data.py --method=J --nmax=10 --omega 10 20 0 --vac 1 10 1 --steps 6 10
+
+
+Full usage example running from installed package:
+OMP_NUM_THREADS=1 \\
+DB_CONNECTION_STRING="sqlite:////$HOME/data/frtrg.sqlite" \\
+python -m frtrg_kondo.gen_data \\
+--method mu \\
+--omega 10 \\
+--nmax 10 20 1 \\
+--voltage_branches 3 \\
+--vdc 0 50 0 \\
+--vac 2 16 1 \\
+--steps 51 8 \\
+--save reduced \\
+--rtol=1e-8 \\
+--atol=1e-10 \\
+--d=1e9 \\
+--threads=4 \\
+--log_time=-1 \\
+--filename $HOME/data/frtrg-01.h5
+""",
+            formatter_class = argparse.RawDescriptionHelpFormatter,
+            add_help = False)
+
+    # Options for parallelization and swiping over parameters
+    parallel_group = parser.add_argument_group(title="Parallelization, swiping over parameters")
+    parallel_group.add_argument("--steps", metavar="int", type=int, nargs='+',
+            help = "Number of steps, provided for each independent parameter swipe dimension")
+    parallel_group.add_argument("--scale", type=str, nargs='+', default="linear",
+            choices = ("linear", "log"),
+            help = "Scale used for swipes (must get same number of options as --steps)")
+    parallel_group.add_argument("--threads", type=int, metavar="int", default=4,
+            help = "Number parallel processes (set to 0 to use all CPUs)")
+
+    # Saving
+    save_group = parser.add_argument_group(title="Saving data")
+    save_group.add_argument("--save", type=str, default="all",
+            choices = ("all", "reduced", "observables", "minimal"),
+            help = "select which part of the Floquet matrices should be saved")
+    save_group.add_argument("--filename", metavar='file', type=str,
+            default = os.path.join(settings.BASEPATH, settings.FILENAME),
+            help = "HDF5 file to which data should be saved")
+
+    # Physical parameters
+    phys_group = parser.add_argument_group(title="Physical parameters")
+    phys_group.add_argument("--omega", metavar='float', type=float, nargs='+', default=0.,
+            help = "Frequency, units of Tkrg")
+    phys_group.add_argument("--vdc", metavar='float', type=float, nargs='+', default=0.,
+            help="Vdc, units of Tkrg")
+    fourier_coef_group = phys_group.add_mutually_exclusive_group()
+    fourier_coef_group.add_argument("--vac", metavar='float', type=float, nargs='+', default=0.,
+            help = "Vac, units of Tkrg")
+    fourier_coef_group.add_argument("--fourier_coef", metavar='tuple', type=float, nargs='*',
+            help = "Voltage Fourier arguments, units of omega(?)")
+    phys_group.add_argument("--xL", metavar='float', type=float, nargs='+', default=0.5,
+            help = "Asymmetry, 0 < xL < 1")
+
+    # Method parameters
+    method_group = parser.add_argument_group(title="Method")
+    method_group.add_argument("--method", type=str, required=True, choices=('J', 'mu'),
+            help = "J: include all time dependence in coupling by unitary transformation.\nmu: describe time dependence by Floquet matrix for chemical potentials.")
+    method_group.add_argument("--simplified_initial_conditions", metavar="bool", type=bool, default=False,
+            help = "Set initial condition for gammaL to 0")
+    method_group.add_argument("--d", metavar='float', type=float, nargs='+', default=1e9,
+            help = "D (UV cutoff), units of Tkrg")
+    method_group.add_argument("--resonant_dc_shift", metavar='int', type=int, default=0,
+            help = "Describe DC voltage (partially) by shift in Floquet matrices. --vdc is the full voltage.")
+
+    # Numerical parameters concerning Floquet matrices
+    numerics_group = parser.add_argument_group(title="Numerical parameters")
+    numerics_group.add_argument("--nmax", metavar='int', type=int, nargs='+', required=True,
+            help = "Floquet matrix size")
+    numerics_group.add_argument("--padding", metavar='int', type=int, nargs='+', default=0,
+            help = "Floquet matrix ppadding")
+    numerics_group.add_argument("--voltage_branches", metavar='int', type=int, required=True,
+            help = "Voltage branches")
+    numerics_group.add_argument("--compact", metavar='{0,1,2}', type=int, default=0,
+            help = "compact FRTRG implementation (0, 1, or 2)")
+    numerics_group.add_argument("--lazy_inverse_factor", metavar='float', type=float,
+            default = settings.LAZY_INVERSE_FACTOR,
+            help = "Factor between 0 and 1 for truncation of extended matrix before inversion.\n0 gives most precise results, 1 means discarding padding completely in inversion.\nOverwrites value set by environment variable LAZY_INVERSE_FACTOR.")
+    numerics_group.add_argument("--extrapolate_voltage", metavar='bool', type=bool,
+            default = settings.EXTRAPOLATE_VOLTAGE,
+            help = "Extrapolate along voltage branches (quadratic extrapolation).\nOverwrites value set by environment variable EXTRAPOLATE_VOLTAGE.")
+    numerics_group.add_argument("--check_symmetries", metavar='bool', type=bool,
+            default = settings.CHECK_SYMMETRIES,
+            help = "Check symmetries during RG flow.\nOverwrites value set by environment variable CHECK_SYMMETRIES.")
+    symmetry_group = numerics_group.add_mutually_exclusive_group()
+    symmetry_group.add_argument("--ignore_symmetries", metavar='bool', type=bool,
+            default = settings.IGNORE_SYMMETRIES,
+            help = "Do not use any symmetries.\nOverwrites value set by environment variable IGNORE_SYMMETRIES.")
+    symmetry_group.add_argument("--enforce_symmetric", metavar='bool', type=bool,
+            default = settings.IGNORE_SYMMETRIES,
+            help = "Enforce using symmetries, throw errors if no symmetries can be used.\nOverwrites value set by environment variable ENFORCE_SYMMETRIC.")
+    numerics_group.add_argument("--use_reference_implementation", metavar='bool', type=bool,
+            default = settings.USE_REFERENCE_IMPLEMENTATION,
+            help = "Use slower reference implementation of RG equations instead of optimized implementation.\nOverwrites value set by environment variable USE_REFERENCE_IMPLEMENTATION.")
+
+    # Convergence parameters concerning solver and D convergence
+    solver_group = parser.add_argument_group("Solver")
+    solver_group.add_argument("--rtol", metavar="float", type=float, default=1e-7,
+            help = "Solver relative tolerance")
+    solver_group.add_argument("--atol", metavar="float", type=float, default=1e-9,
+            help = "Solver relative tolerance")
+    solver_group.add_argument("--solver_method", metavar="str", type=str, default="RK45",
+            help = "ODE solver algorithm")
+
+    # Output
+    log_group = parser.add_argument_group(title="Console output")
+    log_group.add_argument("-h", "--help", action="help",
+            help = "show help message and exit")
+    log_group.add_argument("--log_level", metavar="str", type=str,
+            default = _levelToName.get(settings.logger.level, "INFO"),
+            choices = ("INFO", "DEBUG", "WARNING", "ERROR"),
+            help = "logging level")
+    log_group.add_argument("--log_time", metavar="int", type=int, default=settings.LOG_TIME,
+            help = "log time interval, in s")
+
+    args = parser.parse_args()
+    options = args.__dict__
+
+    # extract options that are handled by data management and not by Kondo module
+    threads = options.pop("threads")
+    filename = options.pop("filename")
+    include = options.pop("save")
+
+    # update settings
+    for name in settings.GlobalFlags.defaults.keys():
+        try:
+            value = options.pop(name.lower())
+            if value is not None:
+                settings.defaults[name] = value
+        except KeyError:
+            pass
+    settings.defaults.logger.setLevel(options.pop("log_level"))
+    settings.defaults.update_globals()
+
+    # Translate method argument for Kondo(...) arguments
+    options.update(unitary_transformation = options.pop('method') == 'J')
+    # extract options for solver that are passed to Kondo.run(...) instead of Kondo(...)
+    solver_options = dict(
+            rtol = options.pop("rtol"),
+            atol = options.pop("atol"),
+            method = options.pop("solver_method"),
+            )
+
+    # Detect number of CPUs (if necessary)
+    if threads == 0:
+        threads = mp.cpu_count()
+
+    # Generate data
+    if threads == 1:
+        # no parallelization
+        dm = DataManager()
+        for kondo_options in gen_option_iter(**options):
+            vdc = kondo_options['vdc'] + kondo_options['omega']*kondo_options['resonant_dc_shift']
+            settings.logger.info(f"Vdc={vdc}, Vac={kondo_options['vac']}, Ω={kondo_options['omega']}")
+            kondo = Kondo(**kondo_options)
+            kondo.run(**solver_options)
+            settings.logger.info(f"Saving Vdc={vdc}, Vac={kondo_options['vac']}, Ω={kondo_options['omega']} to {filename}")
+            dm.save_h5(kondo, filename, include)
+    else:
+        # generate data points in parallel
+        lock = mp.Lock()
+        queue = mp.Queue()
+        # create processes
+        processes = [mp.Process(target=save_data_mp, args=(queue, lock, solver_options, filename, include)) for i in range(threads)]
+        # start processes
+        for p in processes:
+            p.start()
+        # send data to processes
+        for kondo_options in gen_option_iter(**options):
+            queue.put(kondo_options)
+        # send end signal to processes
+        for p in processes:
+            queue.put(None)
+
+
+def save_data_mp(queue, lock, solver_options, filename, include='all', overwrite=False):
+    """
+    Generate data points in own process and save them to HDF5 file.
+    Each process owns one DataManager instance.
+    In each run a new Kondo instance is created.
+    """
+    dm = DataManager()
+    while True:
+        kondo_options = queue.get()
+        if kondo_options is None:
+            break
+        vdc = kondo_options['vdc'] + kondo_options['omega']*kondo_options['resonant_dc_shift']
+        settings.logger.info(f"Vdc={vdc}, Vac={kondo_options['vac']}, Ω={kondo_options['omega']}")
+        kondo = Kondo(**kondo_options)
+        kondo.run(**solver_options)
+        lock.acquire()
+        try:
+            settings.logger.info(f"Saving Vdc={vdc}, Vac={kondo_options['vac']}, Ω={kondo_options['omega']} to {filename}")
+            dm.save_h5(kondo, filename, include, overwrite)
+        finally:
+            lock.release()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/package/src/frtrg_kondo/kondo.py b/package/src/frtrg_kondo/kondo.py
new file mode 100644
index 0000000000000000000000000000000000000000..e13c7ee2a9af5db15bbd3c66d395ba59d677173d
--- /dev/null
+++ b/package/src/frtrg_kondo/kondo.py
@@ -0,0 +1,1355 @@
+# Copyright 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, main module for RG calculations
+
+Floquet real-time renormalization group implementation for the
+spin 1/2 isotropic Kondo model.
+
+Example usage:
+>>> from frtrg_kondo.kondo import Kondo, np
+>>> nmax = 10
+>>> vb = 3
+>>> # Compute the RG flow in 2 different ways
+>>> kondo1 = Kondo(unitary_transformation=1, omega=10, nmax=nmax, padding=8, vdc=4, vac=5, voltage_branches=vb)
+>>> kondo2 = Kondo(unitary_transformation=0, omega=10, nmax=nmax, padding=0, vdc=4, vac=5, voltage_branches=vb)
+>>> solver1 = kondo1.run()
+>>> solver2 = kondo2.run()
+>>> # Check if the results agree
+>>> np.abs(kondo1.gammaL[:,nmax] - kondo2.gammaL[:,nmax]).max()
+2.4691660784226606e-05
+>>> np.abs(kondo1.deltaGammaL[:,nmax] - kondo2.deltaGammaL[:,nmax]).max()
+1.2758585339583961e-05
+>>> np.abs(kondo1.gamma[vb,:,nmax] - kondo2.gamma[vb,:,nmax]).max()
+0.00018744930313729924
+>>> np.abs(kondo1.z[vb,:,nmax] - kondo2.z[vb,:,nmax]).max()
+1.421144031910071e-05
+
+Further information: https://vbruch.eu/frtrg.html
+"""
+
+import hashlib
+import numpy as np
+from time import time, process_time
+from scipy.special import jn as bessel_jn
+from scipy.fftpack import fft
+from scipy.integrate import solve_ivp
+from frtrg_kondo import settings
+from frtrg_kondo.rtrg import GlobalRGproperties, RGfunction
+from frtrg_kondo.reservoirmatrix import ReservoirMatrix, einsum_34_12_43, einsum_34_12_43_double, product_combinations
+from frtrg_kondo.compact_rtrg import SymRGfunction
+
+# Log times (global variables)
+REF_TIME = time()
+LAST_LOG_TIME = REF_TIME
+
+def driving_voltage(tau, *fourier_coef):
+    """
+    Generate function of time given Fourier coefficients.
+
+    tau = t/T so that 0 <= tau <= 1.
+    fourier_coef[n] = V_{n+1}/Ω
+
+    The result is given in the same units as fourier_coef.
+    """
+    res = np.zeros_like(tau)
+    for n, c in enumerate(fourier_coef, 1):
+        res += (c * np.exp(2j*np.pi*n*tau)).real
+    return 2*res
+
+def driving_voltage_integral(tau, *fourier_coef):
+    """
+    Compute time-integral given Fourier coefficients.
+
+    tau = t/T so that 0 <= tau <= 1.
+    fourier_coef[n] = V_{n+1}/Ω
+
+              t
+    return  Ω ∫dt' V(t') ,  t = tau*T
+              0
+
+    fourier_coef should be in units of Ω, then the result has unit hbar/e = 1
+    """
+    res = np.zeros_like(tau)
+    for n, c in enumerate(fourier_coef, 1):
+        res += (c/n * (np.exp(2j*np.pi*n*tau) - 1)).imag
+    return 2*res
+
+
+def gen_init_matrix(nmax, *fourier_coef, resonant_dc_shift=0, resolution=5000):
+    """
+    Generate Floquet matrix of the bare coupling without scalar coupling prefactor.
+
+    fourier_coef must be in units of Ω:
+    fourier_coef[n] = V_{n+1}/Ω
+    TODO: signs have been chosen such that the result looks correct
+    """
+    if len(fourier_coef) == 1 or np.allclose(fourier_coef[1:], 0):
+        # Simple driving, only one frequency:
+        coef = bessel_jn(np.arange(-2*nmax+resonant_dc_shift, 2*nmax+resonant_dc_shift+1), -2*fourier_coef[0])
+    elif len(fourier_coef) == 0:
+        coef = np.zeros(4*nmax+1)
+        coef[2*nmax-resonant_dc_shift] = 1
+    else:
+        # Anharmonic driving: use FFT
+        assert resolution > 2*nmax + abs(resonant_dc_shift)
+        assert resolution % 2 == 0
+        fft_coef = fft(np.exp(
+                    -1j*driving_voltage_integral(
+                        np.linspace(0, 1, resolution, endpoint=False),
+                        *fourier_coef
+                    )
+                ))
+        coef = np.ndarray(4*nmax+1, dtype=np.complex128)
+        coef[2*nmax-resonant_dc_shift:] = fft_coef[:2*nmax+resonant_dc_shift+1].conjugate() / resolution
+        coef[:2*nmax-resonant_dc_shift] = fft_coef[-2*nmax+resonant_dc_shift:].conjugate() / resolution
+    init_matrix = np.ndarray((2*nmax+1, 2*nmax+1), dtype=np.complex128)
+    for j in range(2*nmax+1):
+        init_matrix[:,j] = coef[2*nmax-j:4*nmax+1-j]
+    return init_matrix
+
+
+def solveTV0_scalar(d, tk=1, rtol=1e-8, atol=1e-10, full_output=False, **solveopts):
+    """
+    Solve the ODE in equilibrium at T=V=0 for scalars from 0 to d.
+    returns: (gamma, z, j, solver)
+    """
+    # Initial conditions
+    jbar = 2/(np.pi*np.sqrt(3))
+    j0 = jbar/(1-jbar)**2
+    # Θ := d/dΛ Γ = 1/Z - 1
+    theta0 = (1-jbar)**-2 - 1
+    gamma0 = np.sqrt((1-jbar)/jbar)*np.exp(1/(2*jbar)) * tk
+
+    # Solve on imaginary axis
+
+    def ode_function_imaxis(lmbd, values):
+        'RG eq. for Kondo model on the imaginary axis, ODE of functions of variable lmbd = Λ'
+        gamma, theta, j = values
+        dgamma = theta
+        dtheta = -4*j**2/(lmbd + gamma)
+        dj = dtheta*(1 + j/(1 + theta))/2
+        return np.array([dgamma, dtheta, dj])
+
+    result = solve_ivp(
+            ode_function_imaxis,
+            (0, d),
+            np.array([gamma0, theta0, j0]),
+            t_eval = None if full_output else (d,),
+            rtol = rtol,
+            atol = atol,
+            **solveopts)
+    assert result.success
+
+    gamma, theta, j = result.y[:, -1]
+    z = 1/(1+theta)
+    return (gamma, z, j, result)
+
+def solveTV0_Utransformed(d, properties, tk=1, rtol=1e-8, atol=1e-10, **solveopts):
+    '''
+    Solve the ODE in equilibrium at T=V=0 to obtain initial conditions for Γ, Z and J.
+    Here all time-dependence is assumed to be contained in J.
+
+    returns: (gamma, z, j)
+
+    Return values are arrays representing the quantities at energies shifted
+    by Ω and μ as required for the initial conditions.
+    If properties.resonant_dc_shift ≠ 0, then a larger array of energies
+    is considered, equivalent to mapping nmax → nmax+resonant_dc_shift in
+    the shape of properties.energies.
+    '''
+    # Check input
+    assert isinstance(properties, GlobalRGproperties)
+    # Solve equilibrium RG equations from 0 to d.
+    solveopts.update(rtol=rtol, atol=atol)
+    gamma, z, j, scalar_solver = solveTV0_scalar(d, **solveopts)
+
+    # Solve for constant imaginary part, go to required points in complex plane.
+    nmax = properties.nmax
+    vb = properties.voltage_branches
+
+    def ode_function_imconst(rE, values):
+        'RG eq. for Kondo model for constant Im(E), ODE of functions of rE = Re(E)'
+        gamma, theta, j = values
+        dgamma = theta
+        dtheta = -4*j**2/(d - 1j*rE + gamma)
+        dj = dtheta*(1 + j/(1 + theta))/2
+        return -1j*np.array([dgamma, dtheta, dj])
+
+    # Define flattened array of real parts of energy values, for which we want
+    # to know, Γ, Z, J
+    nmax_plus = nmax + abs(properties.resonant_dc_shift)
+    if vb:
+        energies_orig = \
+                properties.energy.real + properties.vdc*np.arange(-vb, vb+1).reshape((2*vb+1, 1)) \
+                + properties.omega*np.arange(-nmax_plus, nmax_plus+1).reshape((1, 2*nmax_plus+1))
+    else:
+        energies_orig = properties.energy.real + properties.omega * np.arange(-nmax_plus, nmax_plus+1)
+
+    energies = energies_orig.flatten()
+    energies_unique, inverse_indices = np.unique(energies, return_inverse=True)
+    if energies_unique.size == 1:
+        y = scalar_solver.y[:,-1].reshape((3,1))
+    else:
+        split_idx = np.searchsorted(energies_unique, 0)
+        energies_left = energies_unique[:split_idx]
+        energies_right = energies_unique[split_idx:]
+
+        result_left = solve_ivp(
+                ode_function_imconst,
+                t_span = (0, energies_left[0]),
+                y0 = np.array(scalar_solver.y[:,-1], dtype=np.complex128),
+                t_eval = energies_left[::-1],
+                **solveopts
+            )
+        assert result_left.success
+        result_right = solve_ivp(
+                ode_function_imconst,
+                t_span = (0, energies_right[-1]),
+                y0 = np.array(scalar_solver.y[:,-1], dtype=np.complex128),
+                t_eval = energies_right,
+                **solveopts
+            )
+        assert result_right.success
+        y = np.concatenate((result_left.y[:,::-1], result_right.y), axis=1)
+    gamma = y[0][inverse_indices].reshape(energies_orig.shape)
+    z = ( 1/(1 + y[1]) )[inverse_indices].reshape(energies_orig.shape)
+    j = y[2][inverse_indices].reshape(energies_orig.shape)
+
+    return gamma, z, j
+
+def solveTV0_untransformed(d, properties, tk=1, rtol=1e-8, atol=1e-10, **solveopts):
+    '''
+    Solve the ODE in equilibrium at T=V=0 to obtain initial conditions for Γ, Z and J.
+    Here all time-dependence is assumed to be included in the Floquet
+    matrix μ used for the voltage shift.
+
+    returns: (gamma, z, j, zj_square)
+
+    Return values represent Floquet matrices of the quantities at energies
+    shifted by μ as required for the initial conditions.
+    '''
+    # Check input
+    assert isinstance(properties, GlobalRGproperties)
+    # Solve equilibrium RG equations from 0 to d.
+    solveopts.update(rtol=rtol, atol=atol)
+    gamma, z, j, scalar_solver = solveTV0_scalar(d, **solveopts)
+
+    # Solve for constant imaginary part, go to required points in complex plane.
+    nmax = properties.nmax
+    vb = properties.voltage_branches
+
+    def ode_function_imconst(rE, values):
+        'RG eq. for Kondo model for constant Im(E), ODE of functions of rE = Re(E)'
+        gamma, theta, j = values
+        dgamma = theta
+        dtheta = -4*j**2/(d - 1j*rE + gamma)
+        dj = dtheta*(1 + j/(1 + theta))/2
+        return -1j*np.array([dgamma, dtheta, dj])
+
+    shifts = properties.mu.values + properties.omega*np.diag(np.arange(-nmax, nmax+1)).reshape((1,2*nmax+1,2*nmax+1))
+    assert np.allclose(shifts.imag, 0)
+    eigvals, eigvecs = np.linalg.eigh(shifts)
+    assert all(np.allclose(eigvecs[i] @ np.diag(eigvals[i]) @ eigvecs[i].T.conjugate(), shifts[i]) for i in range(2*vb+1))
+
+    energies = eigvals.flatten()
+    energies_unique, inverse_indices = np.unique(energies, return_inverse=True)
+    if energies_unique.size == 1:
+        y = scalar_solver.y[:,-1].reshape((3,1))
+    else:
+        split_idx = np.searchsorted(energies_unique, 0)
+        energies_left = energies_unique[:split_idx]
+        energies_right = energies_unique[split_idx:]
+
+        result_left = solve_ivp(
+                ode_function_imconst,
+                t_span = (0, energies_left[0]),
+                y0 = np.array(scalar_solver.y[:,-1], dtype=np.complex128),
+                t_eval = energies_left[::-1],
+                **solveopts
+            )
+        assert result_left.success
+        result_right = solve_ivp(
+                ode_function_imconst,
+                t_span = (0, energies_right[-1]),
+                y0 = np.array(scalar_solver.y[:,-1], dtype=np.complex128),
+                t_eval = energies_right,
+                **solveopts
+            )
+        assert result_right.success
+        y = np.concatenate((result_left.y[:,::-1], result_right.y), axis=1)
+    gamma_raw = y[0][inverse_indices].reshape(eigvals.shape)
+    z_raw = ( 1/(1 + y[1]) )[inverse_indices].reshape(eigvals.shape)
+    j_raw = y[2][inverse_indices].reshape(eigvals.shape)
+
+    gamma = np.einsum('kij,kj,klj->kil', eigvecs, gamma_raw, eigvecs.conjugate())
+    z = np.einsum('kij,kj,klj->kil', eigvecs, z_raw, eigvecs.conjugate())
+    j = np.einsum('kij,kj,klj->kil', eigvecs, j_raw, eigvecs.conjugate())
+    zj_square = np.einsum('kij,kj,klj->kil', eigvecs, (j_raw*z_raw)**2, eigvecs.conjugate())
+
+    return gamma, z, j, zj_square
+
+
+
+class Kondo:
+    '''
+    Kondo model with RG flow equations and routines for initial conditions.
+
+    Always accessible properties:
+        total_iterations: total number of calls to self.updateRGequations()
+        global_properties: Properties for the RG functions, energy, ...
+            Properties stored in global_properties can be accessed directly from
+            self, e.g. using self.omega or self.energy.
+
+    When setting initial values, the following properties are added:
+        xL : asymmetry factor of the coupling, defaults to 0.5
+        d : UV cutoff
+        vac : AC voltage amplitude relative to Kondo temperature Tk
+        ir_cutoff : IR cutoff, should be 0, but is adapted when RG flow is
+            interrupted earlier
+        z     : RGfunction, Z = 1/( 1 + i dΓ/dE )
+        gamma : RGfunction, Γ  as in  Π = 1/(E+iΓ)
+        deltaGammaL : RGfunction, δΓL (conductivity)
+        deltaGamma : RGfunction, δΓ
+        g2 : matrix in reservoir space of RG functions, coupling vertex G2
+        g3 : matrix in reservoir space of RG functions, coupling vertex G3
+        current : matrix in reservoir space fo RG functions, representing the
+            current vertex I^L
+
+    When running self.updateRGequations() the following properties are added:
+        pi : RGfunction, Π = 1/(E+iΓ)
+        zE : derivative of self.z with respect to E
+        gammaE : derivative of self.gamma with respect to E
+        deltaGammaE : derivative of self.deltaGamma with respect to E
+        deltaGammaLE : derivative of self.deltaGammaL with respect to E
+        g2E : derivative of self.g2 with respect to E
+        g3E : derivative of self.g3 with respect to E
+        currentE : derivative of self.current with respect to E
+    '''
+
+    def __init__(self,
+            unitary_transformation = True,
+            nmax = 0,
+            padding = 0,
+            vdc = 0,
+            vac = 0,
+            omega = 0,
+            d = 1e9,
+            fourier_coef = None,
+            voltage_branches = 0,
+            resonant_dc_shift : 'DC bias voltages, multiples of Ω, positive int' = 0,
+            xL : 'asymmetry factor' = 0.5,
+            compact = 0,
+            simplified_initial_conditions = False,
+            **rg_properties):
+        '''
+        Create Kondo object, initialize global properties shared by all Floquet
+        matrices.
+
+        Expected arguments:
+
+        omega : frequency Ω, in units of Tk
+        nmax : size of Floquet matrix = (2*nmax+1, 2*nmax+1)
+        padding : extrapolation to avoid Floquet matrix truncation effects,
+                valid values: 0 ≤ padding ≤ 2*nmax-2
+        vdc : DC voltage, in units of Tk, including voltage due to
+                resonant_dc_shift.
+        vac : AC voltage, in units of Tk
+        voltage_branches : keep copies of Floquet matrices with energies
+                shifted by n Vdc, n = ±1,...,±voltage_branches.
+                Must be 0 or >=2
+        resonant_dc_shift : Describe DC voltage vdc partially by shifts in the
+                Floquet in the initial conditions.
+                valid values: non-negative integers
+        d : UV cutoff (convergence parameter)
+        xL = 1 - xR : asymmetry factor of coupling. Must fulfill 0 <= xL <= 1.
+        clear_corners : improve convergence for large Floquet matrices by
+                setting parts of the matrices to 0 after each multiplication.
+                Handle with care!
+                valid values: padding + clear_corners <= 2*nmax + 1
+        compact : Use extra symmetry to improve efficiency for large matrices.
+                compact != 0 requires the symmetry V(t+(π/Ω)) = - V(t).
+                valid values are:
+                    0: don't use compact form.
+                    1: use compact form only in ODE solver, not in RG equations.
+                    2: use compact form.
+        '''
+        self.global_properties = GlobalRGproperties(
+                nmax = nmax,
+                omega = omega,
+                vdc = 0,
+                mu = None,
+                voltage_branches = voltage_branches,
+                resonant_dc_shift = resonant_dc_shift,
+                padding = padding,
+                fourier_coef = fourier_coef,
+                energy = 0j,
+                **rg_properties)
+        self.global_settings = settings.export()
+        self.unitary_transformation = unitary_transformation
+        self.simplified_initial_conditions = simplified_initial_conditions
+        self.compact = compact
+        self.vdc = vdc
+        self.vac = vac
+        self.xL = xL
+        if xL != 0.5:
+            self.global_properties.symmetric = False
+
+        # Some checks of the input
+        if (vac or fourier_coef is not None) and self.nmax == 0:
+            raise ValueError("Bad parameters: driving != 0 requires nmax > 0")
+        if xL < 0 or xL > 1:
+            raise ValueError("Bad parameter: need  0 <= xL <= 1")
+        if resonant_dc_shift and 2*abs(resonant_dc_shift) > self.nmax:
+            raise ValueError("Bad parameters: resonant_dc_shift must be <= 2*nmax")
+        if compact:
+            assert unitary_transformation
+            assert self.vdc == omega*resonant_dc_shift
+            assert fourier_coef is None or np.allclose(fourier_coef[1::2], 0)
+            assert self.resonant_dc_shift == 0 # extending the implementation to allow for even resonant_dc_shift requires some checks, odd resonant_dc_shift require some rewriting.
+            if self.compact == 2:
+                assert self.padding % 2 == 0
+        assert d.imag == 0.
+        self.d = d
+
+        if self.unitary_transformation:
+            self.compact = compact
+            self.global_properties.vdc = vdc - resonant_dc_shift*omega
+        else:
+            assert resonant_dc_shift == 0
+            mu = np.zeros((2*nmax+1, 2*nmax+1), dtype=np.complex128)
+            mu[np.diag_indices(2*nmax+1)] = vdc
+            if fourier_coef is None:
+                mu[np.arange(2*nmax), np.arange(1, 2*nmax+1)] = vac/2
+                mu[np.arange(1, 2*nmax+1), np.arange(2*nmax)] = vac/2
+            else:
+                for i, f in enumerate(fourier_coef, 1):
+                    mu[np.arange(2*nmax+1-i), np.arange(i, 2*nmax+1)] = f
+                    mu[np.arange(i, 2*nmax+1), np.arange(2*nmax+1-i)] = f.conjugate()
+            self.global_properties.mu = RGfunction(self.global_properties, np.arange(-voltage_branches, voltage_branches+1).reshape((2*voltage_branches+1,1,1)) * mu.reshape((1,2*nmax+1,2*nmax+1)), symmetry=-1)
+        self.total_iterations = 0
+
+    def __getattr__(self, name):
+        'self.<name> is defined as shortcut for self.global_properties.<name>'
+        return getattr(self.global_properties, name)
+
+    def getParameters(self):
+        '''
+        Get most relevant parameters. The returned dict can be used to label this object.
+        '''
+        return {
+                'Ω' : self.omega,
+                'nmax' : self.nmax,
+                'padding' : self.padding,
+                'Vdc' : self.vdc,
+                'Vac' : self.vac,
+                'V_DC/Ω' : self.resonant_dc_shift,
+                'V_branches' : self.voltage_branches,
+                'Vac' : getattr(self, 'vac', None),
+                'D' : getattr(self, 'd', None),
+                'solveopts' : getattr(self, 'solveopts', None),
+                'xL' : getattr(self, 'xL', None),
+                'IR_cutoff' : getattr(self, 'ir_cutoff', None),
+                }
+
+    def initialize_untransformed(self,
+            **solveopts : 'keyword arguments passed to solver',
+            ):
+        '''
+        Arguments:
+        **solveopts: keyword arguments passed to the solver. Most relevant
+                are rtol and atol.
+
+        Get initial conditions for Γ, Z and G2 by numerically solving the
+        equilibrium RG equations from E=0 to E=iD and for all required Re(E).
+        Initialize G3, Iγ, δΓ, δΓγ.
+        '''
+        sqrtxx = np.sqrt(self.xL*(1-self.xL))
+        symmetry = 0 if settings.IGNORE_SYMMETRIES else 1
+
+
+        #### Initial conditions from exact results at T=V=0
+        # Get Γ, Z and J (G2) for T=V=0.
+        gamma0, z0, j0, zj0_square = solveTV0_untransformed(d=self.d, properties=self.global_properties, **solveopts)
+
+        # Create Γ and Z with the just calculated initial values.
+        self.gamma = RGfunction(self.global_properties, gamma0, symmetry=symmetry)
+        self.z = RGfunction(self.global_properties, z0, symmetry=symmetry)
+
+
+        # Create G2 from J:    G2_{ij} = - 2 sqrt(x_i x_j) J
+        self.g2 = ReservoirMatrix(self.global_properties, symmetry=symmetry)
+        j_rgfunction = RGfunction(self.global_properties, j0, symmetry=symmetry)
+        self.g2[0,0] = -2*self.xL * j_rgfunction
+        self.g2[1,1] = -2*(1-self.xL) * j_rgfunction
+        j_rgfunction.symmetry = 0
+        self.g2[0,1] = -2*sqrtxx * j_rgfunction
+        self.g2[1,0] = -2*sqrtxx * j_rgfunction
+
+
+        ## Initial conditions for G3
+        # G3 ~ Jtilde^2  with  Jtilde = Z J
+        # Every entry of G3 will be of the following form (up to prefactors):
+        self.g3 = ReservoirMatrix(self.global_properties, symmetry=-symmetry)
+        g3_entry = RGfunction(self.global_properties, 1j*np.pi * zj0_square, symmetry=-symmetry)
+        self.g3[0,0] = 2*self.xL * g3_entry
+        self.g3[1,1] = 2*(1-self.xL) * g3_entry
+        g3_entry.symmetry = 0
+        self.g3[0,1] = 2*sqrtxx * g3_entry
+        self.g3[1,0] = 2*sqrtxx * g3_entry
+
+
+        ## Initial conditions for current I^{γ=L} = J0 (1 - Jtilde)
+        # Note that j0[self.voltage_branches] and z0[self.voltage_branches] are diagonal Floquet matrices.
+        current_entry = 2*sqrtxx * j0[self.voltage_branches] * ( \
+                np.identity(2*self.nmax+1, dtype=np.complex128) \
+                - j0[self.voltage_branches] * z0[self.voltage_branches] )
+        self.current = ReservoirMatrix(self.global_properties, symmetry=-symmetry)
+        self.current[0,0] = RGfunction(self.global_properties, np.zeros_like(current_entry), symmetry=-symmetry)
+        self.current[1,1] = RGfunction(self.global_properties, np.zeros_like(current_entry), symmetry=-symmetry)
+        self.current[0,1] = RGfunction(self.global_properties,  current_entry, symmetry=0)
+        self.current[1,0] = RGfunction(self.global_properties, -current_entry, symmetry=0)
+
+        ## Initial conditions for voltage-variation of Γ: δΓ
+        self.deltaGamma = RGfunction(
+                self.global_properties,
+                np.zeros((3,2*self.nmax+1,2*self.nmax+1), dtype=np.complex128),
+                symmetry = symmetry
+                )
+
+        ## Initial conditions for voltage-variation of current-Γ: δΓ_L
+        # Note that j0[self.voltage_branches] and z0[self.voltage_branches] are diagonal Floquet matrices.
+        self.deltaGammaL = RGfunction(
+                self.global_properties,
+                3*np.pi*sqrtxx**2 * zj0_square[self.voltage_branches],
+                symmetry = symmetry
+                )
+
+
+        ### Derivative of full current
+        self.yL = RGfunction(
+                self.global_properties,
+                np.zeros((2*self.nmax+1,2*self.nmax+1), dtype=np.complex128),
+                symmetry=-symmetry
+                )
+
+        ### Full current, also includes AC current
+        self.gammaL = self.mu.reduced(shift=1) @ self.deltaGammaL
+        if self.simplified_initial_conditions:
+            self.gammaL *= 0
+
+        self.global_properties.energy = 1j*self.d
+
+    def initialize_Utransformed(self,
+            **solveopts : 'keyword arguments passed to solver',
+            ):
+        '''
+        Arguments:
+        **solveopts: keyword arguments passed to the solver. Most relevant
+                are rtol and atol.
+
+        Get initial conditions for Γ, Z and G2 by numerically solving the
+        equilibrium RG equations from E=0 to E=iD and for all required Re(E).
+        Initialize G3, Iγ, δΓ, δΓγ.
+        '''
+
+        sqrtxx = np.sqrt(self.xL*(1-self.xL))
+        symmetry = 0 if settings.IGNORE_SYMMETRIES else 1
+
+
+        #### Initial conditions from exact results at T=V=0
+        # Get Γ, Z and J (G2) for T=V=0.
+        gamma0, z0, j0 = solveTV0_Utransformed(d=self.d, properties=self.global_properties, **solveopts)
+
+        # Write T=V=0 results to Floquet index n=0.
+        gammavalues = np.zeros(self.shape(), dtype=np.complex128)
+        zvalues = np.zeros(self.shape(), dtype=np.complex128)
+        jvalues = np.zeros(self.shape(), dtype=np.complex128)
+
+        # construct diagonal matrices
+        diag_idx = (..., *np.diag_indices(2*self.nmax+1))
+        if self.resonant_dc_shift:
+            gammavalues[diag_idx] = gamma0[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+            zvalues[diag_idx] = z0[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+            jvalues[diag_idx] = j0[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+        else:
+            gammavalues[diag_idx] = gamma0
+            zvalues[diag_idx] = z0
+            jvalues[diag_idx] = j0
+
+        RGclass = SymRGfunction if self.compact == 2 else RGfunction
+
+        # Create Γ and Z with the just calculated initial values.
+        self.gamma = RGclass(self.global_properties, gammavalues, symmetry=symmetry)
+        self.z = RGclass(self.global_properties, zvalues, symmetry=symmetry)
+
+        # Create G2 from J:    G2_{ij} = - 2 sqrt(x_i x_j) J
+        self.g2 = ReservoirMatrix(self.global_properties, symmetry=symmetry)
+        j_rgfunction = RGclass(self.global_properties, jvalues, symmetry=symmetry)
+        self.g2[0,0] = -2*self.xL * j_rgfunction
+        self.g2[1,1] = -2*(1-self.xL) * j_rgfunction
+        if self.vac or self.resonant_dc_shift or self.fourier_coef is not None:
+            # Coefficients are given by the Bessel function of the first kind.
+            if self.fourier_coef is not None:
+                init_matrix = gen_init_matrix(self.nmax, *(f/self.omega for f in self.fourier_coef), resonant_dc_shift=self.resonant_dc_shift)
+            else:
+                init_matrix = gen_init_matrix(self.nmax, self.vac/(2*self.omega), resonant_dc_shift=self.resonant_dc_shift)
+            j_LR = np.einsum('ij,...j->...ij', init_matrix, j0[...,2*self.resonant_dc_shift:])
+            j_RL = np.einsum('ji,...j->...ij', init_matrix.conjugate(), j0[...,:j0.shape[-1]-2*self.resonant_dc_shift])
+            j_LR = RGfunction(self.global_properties, j_LR)
+            j_RL = RGfunction(self.global_properties, j_RL)
+            self.g2[0,1] = -2*sqrtxx * j_LR
+            self.g2[1,0] = -2*sqrtxx * j_RL
+        else:
+            assert self.compact != 2
+            j_rgfunction.symmetry = 0
+            self.g2[0,1] = -2*sqrtxx * j_rgfunction
+            self.g2[1,0] = -2*sqrtxx * j_rgfunction
+
+
+        ## Initial conditions for G3
+        # G3 ~ Jtilde^2  with  Jtilde = Z J
+        # Every entry of G3 will be of the following form (up to prefactors):
+        self.g3 = ReservoirMatrix(self.global_properties, symmetry=-symmetry)
+        g3_entry = np.zeros(self.shape(), dtype=np.complex128)
+        g3_entry[diag_idx] = 1j*np.pi * (jvalues[diag_idx]*zvalues[diag_idx])**2
+        g3_entry = RGclass(self.global_properties, g3_entry, symmetry=-symmetry)
+        self.g3[0,0] = 2*self.xL * g3_entry
+        self.g3[1,1] = 2*(1-self.xL) * g3_entry
+        if self.vac or self.resonant_dc_shift or self.fourier_coef is not None:
+            g30 = 1j*np.pi*(z0*j0)**2
+            g3_LR = np.einsum('ij,...j->...ij', init_matrix, g30[...,2*self.resonant_dc_shift:])
+            g3_RL = np.einsum('ji,...j->...ij', init_matrix.conjugate(), g30[...,:g30.shape[-1]-2*self.resonant_dc_shift])
+            g3_LR = RGfunction(self.global_properties, g3_LR)
+            g3_RL = RGfunction(self.global_properties, g3_RL)
+            self.g3[0,1] = 2*sqrtxx * g3_LR
+            self.g3[1,0] = 2*sqrtxx * g3_RL
+        else:
+            assert self.compact != 2
+            g3_entry.symmetry = 0
+            self.g3[0,1] = 2*sqrtxx * g3_entry
+            self.g3[1,0] = 2*sqrtxx * g3_entry
+
+
+        ## Initial conditions for current I^{γ=L} = J0 (1 - Jtilde)
+        if self.voltage_branches:
+            current_entry = np.diag( 2*sqrtxx * jvalues[self.voltage_branches][diag_idx] * (1 - jvalues[self.voltage_branches][diag_idx] * zvalues[self.voltage_branches][diag_idx] ) )
+        else:
+            current_entry = np.diag( 2*sqrtxx * jvalues[diag_idx] * (1 - jvalues[diag_idx] * zvalues[diag_idx] ) )
+        current_entry = RGclass(self.global_properties, current_entry, symmetry=-symmetry)
+        self.current = ReservoirMatrix(self.global_properties, symmetry=-symmetry)
+        if self.compact == 2:
+            self.current[0,0] = RGclass(self.global_properties, None, symmetry=symmetry)
+            self.current[0,0].submatrix01 = np.zeros((self.nmax+1, self.nmax), np.complex128)
+            self.current[0,0].submatrix10 = np.zeros((self.nmax, self.nmax+1), np.complex128)
+        else:
+            self.current[0,0] = 0*current_entry
+        self.current[0,0].symmetry = -symmetry
+        self.current[1,1] = self.current[0,0].copy()
+        if self.vac or self.resonant_dc_shift or self.fourier_coef is not None:
+            if self.voltage_branches:
+                i0 = j0[self.voltage_branches] * (1 - j0[self.voltage_branches]*z0[self.voltage_branches])
+            else:
+                i0 = j0 * (1 - j0*z0)
+            i_LR = np.einsum('ij,...j->...ij', init_matrix, i0[2*self.resonant_dc_shift:])
+            i_RL = np.einsum('ji,...j->...ij', init_matrix.conjugate(), i0[:i0.size-2*self.resonant_dc_shift])
+            i_LR = RGfunction(self.global_properties, i_LR)
+            i_RL = RGfunction(self.global_properties, i_RL)
+            self.current[0,1] =  2*sqrtxx * i_LR
+            self.current[1,0] = -2*sqrtxx * i_RL
+        else:
+            assert self.compact != 2
+            current_entry.symmetry = 0
+            self.current[0,1] =  2*sqrtxx * current_entry
+            self.current[1,0] = -2*sqrtxx * current_entry
+
+        ## Initial conditions for voltage-variation of Γ: δΓ
+        self.deltaGamma = RGclass(
+                self.global_properties,
+                None if self.compact == 2 else np.zeros((3,2*self.nmax+1,2*self.nmax+1) if self.voltage_branches else self.shape(), dtype=np.complex128),
+                symmetry = symmetry
+                )
+
+        ## Initial conditions for voltage-variation of current-Γ: δΓ_L
+        self.deltaGammaL = RGclass(
+                self.global_properties,
+                None if self.compact == 2 else np.zeros((2*self.nmax+1, 2*self.nmax+1), dtype=np.complex128),
+                symmetry = symmetry
+                )
+        if self.resonant_dc_shift:
+            assert self.compact != 2
+            if self.voltage_branches:
+                self.deltaGammaL.values[diag_idx] = 3*np.pi*sqrtxx**2 * (j0[self.voltage_branches,self.resonant_dc_shift:-self.resonant_dc_shift]*z0[self.voltage_branches,self.resonant_dc_shift:-self.resonant_dc_shift])**2
+            else:
+                self.deltaGammaL.values[diag_idx] = 3*np.pi*sqrtxx**2 * (j0[self.resonant_dc_shift:-self.resonant_dc_shift]*z0[self.resonant_dc_shift:-self.resonant_dc_shift])**2
+        else:
+            if self.voltage_branches:
+                diag_values = 3*np.pi*sqrtxx**2 * (j0[self.voltage_branches]*z0[self.voltage_branches])**2
+            else:
+                diag_values = 3*np.pi*sqrtxx**2 * (j0*z0)**2
+            if self.compact == 2:
+                assert diag_values.dtype == np.complex128
+                self.deltaGammaL.submatrix00 = np.diag(diag_values[0::2])
+                self.deltaGammaL.submatrix11 = np.diag(diag_values[1::2])
+            else:
+                self.deltaGammaL.values[diag_idx] = diag_values
+            del diag_values
+
+
+        ### Derivative of full current
+        self.yL = RGclass(
+                self.global_properties,
+                None if self.compact == 2 else np.zeros((2*self.nmax+1,2*self.nmax+1), dtype=np.complex128),
+                symmetry=-symmetry
+                )
+
+        ### Full current, also includes AC current
+        self.gammaL = self.vdc * self.deltaGammaL.reduced()
+        if self.vac and self.fourier_coef is None:
+            if self.voltage_branches:
+                gammaL_AC = 3*np.pi*sqrtxx**2 * self.vac/2 * (j0[self.voltage_branches]*z0[self.voltage_branches])**2
+            else:
+                gammaL_AC = 3*np.pi*sqrtxx**2 * self.vac/2 * (j0*z0)**2
+            if self.resonant_dc_shift:
+                gammaL_AC = gammaL_AC[...,self.resonant_dc_shift:-self.resonant_dc_shift]
+            idx = (np.arange(1, 2*self.nmax+1), np.arange(2*self.nmax))
+            self.gammaL.values[idx] = gammaL_AC[...,1:]
+            idx = (np.arange(0, 2*self.nmax), np.arange(1, 2*self.nmax+1))
+            self.gammaL.values[idx] = gammaL_AC[...,:-1]
+        if self.simplified_initial_conditions:
+            self.gammaL *= 0
+
+        if self.compact == 2:
+            self.deltaGamma.submatrix01 = np.zeros((self.nmax+1, self.nmax), dtype=np.complex128)
+            self.deltaGamma.submatrix10 = np.zeros((self.nmax, self.nmax+1), dtype=np.complex128)
+            self.yL.submatrix01 = np.zeros((self.nmax+1, self.nmax), dtype=np.complex128)
+            self.yL.submatrix10 = np.zeros((self.nmax, self.nmax+1), dtype=np.complex128)
+            self.gammaL.submatrix00 = None
+            self.gammaL.submatrix11 = None
+            self.gammaL.submatrix01 = np.zeros((self.nmax+1, self.nmax), dtype=np.complex128)
+            self.gammaL.submatrix10 = np.zeros((self.nmax, self.nmax+1), dtype=np.complex128)
+
+        self.global_properties.energy = 1j*self.d
+
+
+    def initialize(self, **solveopts):
+        if self.unitary_transformation:
+            self.initialize_Utransformed(**solveopts)
+        else:
+            self.initialize_untransformed(**solveopts)
+
+    def run(self,
+            ir_cutoff : 'IR cutoff of RG flow' = 0,
+            forget_flow : 'do not store RG flow' = True,
+            save_filename : 'save intermediate results: string containing %d for number of iterations' = '',
+            save_iterations : 'number of iterations after which intermediate result should be saved' = 0,
+            **solveopts : 'keyword arguments passed to solver',
+            ):
+        '''
+        Initialize and solve the RG equations.
+
+        Arguments:
+        d = D = UV cutoff (on imaginary axis)
+        vac = amplitude of sin(Ωt) driving of bias voltage.
+        resonant_dc_shift >= 0, int: add DC voltage of (Ω resonant_dc_shift)
+        xL = 1 - xR: asymmetry factor of coupling. Must fulfill 0 <= xL <= 1.
+        ir_cutoff: Stop the RG flow at Λ = -iE = ir_cutoff (instead of Λ=0).
+                If the RG flow is interrupted earlier, ir_cutoff will be adapted.
+        **solveopts: keyword arguments passed to the solver. Most interesting
+                are rtol and atol.
+
+        1.  Get initial conditions for Γ, Z and G2 by numerically solving the
+            equilibrium RG equations from E=0 to E=iD and for all required Re(E).
+            Initialize G3, Iγ, δΓ, δΓγ.
+        2.  Solve RG equations from E=iD to E=0
+            for Γ, Z, G2, G3, Iγ, δΓ, δΓγ.
+            Write parameters and solution for E=0 to self.<variables>
+            Return the ODE solver.
+        '''
+        self.initialize(**solveopts)
+
+        self.save_filename = save_filename
+        self.save_iterations = save_iterations
+
+        if ir_cutoff:
+            self.ir_cutoff = ir_cutoff
+        self.solveopts = solveopts
+
+        if ir_cutoff >= self.d:
+            return
+
+        ### Solve RG ODE
+        output = self.solveOdeIm(self.d, ir_cutoff, only_final=forget_flow, **solveopts)
+
+        # Write final values to Floquet matrices in self.
+        try:
+            # Shift energy
+            self.global_properties.energy = self.energy.real + 1j*output.t[-1]
+            # Unpack values
+            self.unpackFlattenedValues(output.y[:,-1])
+        except:
+            settings.logger.exception("Failed to read solver results:")
+
+        return output
+
+
+    def updateRGequations(self):
+        '''
+        Calculates the energy derivatives using the RG equations.
+        The derivative of self.<name> is written to self.<name>E.
+
+        A human readable reference implementation for this function
+        is provided in updateRGequations_reference.
+        '''
+        if settings.ENFORCE_SYMMETRIC:
+            if hasattr(self, 'pi'):
+                assert self.pi.symmetry == -1
+            assert self.z.symmetry == 1
+            assert self.yL.symmetry == -1
+            assert self.gamma.symmetry == 1
+            assert self.gammaL.symmetry == 1
+            assert self.deltaGamma.symmetry == 1
+            assert self.deltaGammaL.symmetry == 1
+            assert self.g2[0,0].symmetry == 1
+            assert self.g2[1,1].symmetry == 1
+            assert self.g3[0,0].symmetry == -1
+            assert self.g3[1,1].symmetry == -1
+            assert self.current[0,0].symmetry == -1
+            assert self.current[1,1].symmetry == -1
+
+        # Print some log message to indicate progress
+        global LAST_LOG_TIME, REF_TIME
+        if settings.LOG_TIME > 0 and time() - LAST_LOG_TIME >= settings.LOG_TIME:
+            LAST_LOG_TIME = time()
+            settings.logger.info("%9.2fs:  Λ = %.4e,  iterations = %d"%(process_time(), self.energy.imag, self.total_iterations))
+            if settings.logger.level == settings.logging.DEBUG:
+                settings.logger.debug("   ", "  ".join("%2s:%4d"%(hex(i)[2:], c) for i,c in enumerate(RGfunction.MM_COUNTER) if c))
+                settings.logger.debug("   ", "  ".join("%2s:%4d"%(hex(i)[2:], c) for i,c in enumerate(SymRGfunction.MMC_COUNTER) if c))
+
+        if settings.USE_REFERENCE_IMPLEMENTATION:
+            return self.updateRGequations_reference()
+
+
+        # Denote costs in terms of Floquet matrix products as x/y/z where
+        # x is the number of multiplications for resonant_dc_shift != 0 without symmetry.
+        # y is the number of multiplications for xL != xR but resonant_dc_shift == 0,
+        # z is the number of multiplications for xL == xR,
+
+        # Total costs: (+ 2 inversions, optionally + 2 multiplications for pi.derivative)
+        # 178 / 116 / 67
+
+        ## RG eq for Γ
+        zinv = self.z.inverse() # costs: 1 inversion
+        self.gammaE = -1j*( zinv - (SymRGfunction if self.compact == 2 else RGfunction)(self.global_properties, 'identity') )
+        del zinv
+
+        # Derivative of G2
+        # First calculate Π (at E and shifted by multiples of vdc or mu):
+        self.pi = (-1j*self.gamma).k2lambda(self.mu) # costs: 1 inversion
+
+        # first terms of g2E:
+        # 1/2  G13  Π  G32 ,
+        # 1/2  G32  Π  G13
+        g2_pi = self.g2 @ self.pi # costs: 4/3/2
+        g2E1, g2E2 = product_combinations( g2_pi, self.g2 ) # costs: 12/7/4
+        # g2E1.tr() = -i (d/dE)² Γ
+        g2E1tr = g2E1.tr()
+        if self.compact == 2 and not isinstance(g2E1tr, SymRGfunction):
+            g2E1tr_values = g2E1tr.values
+            g2E1tr = SymRGfunction(g2E1tr.global_properties, values=None, symmetry=-1)
+            g2E1tr.submatrix00 = g2E1tr_values[0::2,0::2]
+            g2E1tr.submatrix11 = g2E1tr_values[1::2,1::2]
+            del g2E1tr_values
+        else:
+            g2E1tr.symmetry = -1
+        g2E1 *= 0.5
+        g2E2 *= 0.5
+        self.zE = self.z @ g2E1tr @ self.z # costs: 2/2/2
+        del g2E1tr
+
+        ## RG eq for Z
+        # third term for g2E
+        # -1/4  G34 ( Π  G12  Z  +  Z  G12  Π ) G43
+
+        # First the part inside the brackets:
+        pi_g2_z = self.pi @ self.g2 @ self.z + self.z @ g2_pi # costs: 12/9/6
+
+        g2_bracket_g2, g2_bracket_g3 = einsum_34_12_43_double( self.g2, pi_g2_z, self.g2, self.g3 ) # costs: 48/30/15
+
+        ## RG eq for G2
+        self.g2E = g2E1 + g2E2 - 0.25*g2_bracket_g2
+        self.deltaGammaE = 1j * ( g2E1[0,0] - g2E1[1,1] + g2E2[1,1] - g2E2[0,0] ).reduced_to_voltage_branches(1)
+        del g2E1, g2E2, g2_bracket_g2
+
+
+        # first terms of G3E:
+        # G2_13  Π  G3_32 ,
+        # G2_32  Π  G3_13
+        g3E1, g3E2 = product_combinations( g2_pi, self.g3 ) # costs: 12/7/4
+        del g2_pi
+
+
+        # third term for g3E
+        # 1/2  G2_34 ( Π  G2_12  Z  +  Z  G2_12  Π ) G3_43
+        # -> already calculated
+
+        self.g3E = g3E1 + g3E2 + 0.5 * g2_bracket_g3
+        del g3E1, g3E2, g2_bracket_g3
+
+
+        # first terms of iE:
+        # I13  Π  G32 ,
+        # I32  Π  G13
+        i_pi = self.current @ self.pi # costs: 4/3/2
+        i_pi.symmetry = 0
+        if settings.ENFORCE_SYMMETRIC:
+            assert i_pi[0,0].symmetry == 1
+            assert i_pi[1,1].symmetry == 1
+        iE1, iE2 = product_combinations( i_pi, self.g2 ) # costs: 12/7/4
+
+        # third term for iE
+        # 1/2  I34 ( Π  G2_12  Z  +  Z  G2_12  Π ) G43
+        # The part in the brackets has been calculated before.
+        iE3 = 0.5 * einsum_34_12_43( self.current, pi_g2_z, self.g2 ) # costs: 32/20/10
+        del pi_g2_z
+
+        self.currentE = iE1 + iE2 + iE3
+        del iE1, iE2, iE3
+
+
+        # RG equation for δΓ_L
+        # First part:  (δ1L - δ2L)  I_12  Π  G^3_21
+        i_pi_g3_01 = i_pi[0,1] @ self.g3[1,0] # costs: 1
+        i_pi_g3_10 = i_pi[1,0] @ self.g3[0,1] # costs: 1
+        deltaGammaLE1 = i_pi_g3_01 - i_pi_g3_10
+        # Second part:  I_12  Z  Π  δΓ  G^3_21
+        z_reduced = self.z.reduced_to_voltage_branches(1)
+        dGamma_reduced = self.deltaGamma.reduced_to_voltage_branches(1)
+        pi_reduced = self.pi.reduced_to_voltage_branches(1)
+        z_dgamma_pi = z_reduced @ dGamma_reduced @ pi_reduced + pi_reduced @ dGamma_reduced @ z_reduced # costs: 4/4/4
+        if self.compact == 2:
+            assert isinstance(z_dgamma_pi, SymRGfunction)
+        del z_reduced, pi_reduced, dGamma_reduced
+        if settings.ENFORCE_SYMMETRIC:
+            assert z_dgamma_pi.symmetry == -1
+        deltaGammaLE2 = einsum_34_12_43( self.current, z_dgamma_pi, self.g3 ) # costs: 32/20/10
+        self.deltaGammaLE = 1.5j*(deltaGammaLE1 + 0.5j*deltaGammaLE2)
+        del deltaGammaLE1, deltaGammaLE2, z_dgamma_pi
+
+
+        # RG equation for ΓL
+        self.gammaLE = self.yL
+        self.yLE = 1.5j*(i_pi[0,0] @ self.g3[0,0] + i_pi[1,1] @ self.g3[1,1] + i_pi_g3_01 + i_pi_g3_10) # costs: 2/2/2
+        del i_pi, i_pi_g3_10, i_pi_g3_01
+
+        # Count calls to RG equations.
+        self.total_iterations += 1
+
+
+    def updateRGequations_reference(self):
+        '''
+        Reference implementation of updateRGequations without optimization.
+        This reference implementation serves as a check for the more efficient
+        function updateRGequations.
+
+        This function takes approximately twice as long as updateRGequations.
+        '''
+        # Notation (mainly allow using objects without "self")
+        z = self.z
+        gamma = self.gamma
+        deltaGamma = self.deltaGamma
+        deltaGammaL = self.deltaGammaL
+        g2 = self.g2
+        g3 = self.g3
+        il = self.current
+        yL = self.yL
+        # Identity matrix
+        identity = RGfunction(self.global_properties, 'identity')
+        # Resolvent
+        pi = self.pi = (-1j*gamma).k2lambda(self.mu)
+
+        # Compute the sum
+        #  Σ  A   B  C
+        # 1,2  12     21
+        einsum_34_x_43 = lambda a, b, c: \
+                  a[0,0] @ b @ c[0,0] \
+                + a[0,1] @ b @ c[1,0] \
+                + a[1,0] @ b @ c[0,1] \
+                + a[1,1] @ b @ c[1,1]
+
+        ### RG equations in human readable form
+
+        # Note that the shifts in the energy arguments of all RGfunction
+        # objects is implicitly handled by the multiplication operators.
+        # The muliplication operators for ReservoirMatrix objects are:
+        # @ for normal matrix multiplication (matrix indices ij, jk → ik with sum over j)
+        # % for transpose matrix multiplication (matrix indices jk, ij → ik with sum over j)
+        #                 ⎛ ⊤   ⊤⎞⊤
+        #   i.e.  A % B = ⎜A @ B ⎟  when there are no energy argument shifts.
+        #                 ⎝      ⎠
+
+        # dΓ      ⎛ 1     ⎞
+        # —— = -i ⎜ — - 1 ⎟
+        # dE      ⎝ Z     ⎠
+        self.gammaE = -1j*(z.inverse() - identity)
+
+        # dZ
+        # —— = Z tr( G² Π G² ) Z
+        # dE
+        self.zE = z @ (g2 @ pi @ g2).tr() @ z
+
+        # bracket = Π G² Z + Z G² Π
+        bracket = pi @ g2 @ z + z @ g2 @ pi
+
+        # dG²   1           1 ⎛  ⊤     ⊤⎞⊤   1     ⎛                 ⎞
+        # ——— = — G² Π G² + — ⎜G²  Π G² ⎟  - — G²  ⎜ Π G² Z + Z G² Π ⎟ G²
+        # dE    2           2 ⎝         ⎠    4  34 ⎝                 ⎠  43
+        self.g2E = .5 * g2 @ pi @ g2 + .5 * ((g2 @ pi) % g2) - .25 * einsum_34_x_43(g2, bracket, g2)
+
+        # dG³             ⎛  ⊤     ⊤⎞⊤   1     ⎛                 ⎞
+        # ——— = G² Π G³ + ⎜G²  Π G³ ⎟  + — G²  ⎜ Π G² Z + Z G² Π ⎟ G³
+        # dE              ⎝         ⎠    2  34 ⎝                 ⎠  43
+        self.g3E = g2 @ pi @ g3 + ((g2 @ pi) % g3) + .5 * einsum_34_x_43(g2, bracket, g3)
+
+        #   γ
+        # dI     γ        ⎛ γ⊤     ⊤⎞⊤   1  γ  ⎛                 ⎞
+        # ——— = I  Π G² + ⎜I   Π G² ⎟  + — I   ⎜ Π G² Z + Z G² Π ⎟ G³
+        # dE              ⎝         ⎠    2  34 ⎝                 ⎠  43
+        self.currentE = il @ pi @ g2 + ((il @ pi) % g2) + .5 * einsum_34_x_43(il, bracket, g2)
+
+        # dδΓ     ⎛           ⎞
+        # ——— = i ⎜ δ  - δ    ⎟ G²  Π G²
+        # dE      ⎝  1L    2L ⎠  12    21
+        deltaGammaE = 1j * (g2[0,1] @ pi @ g2[1,0] - g2[1,0] @ pi @ g2[0,1])
+
+        # Reduction of voltage branches as required for the solver.
+        # In this step some information is thrown away that cannot affect the
+        # result of the physical observables.
+        self.deltaGammaE = deltaGammaE.reduced_to_voltage_branches(1)
+        pi_reduced = pi.reduced_to_voltage_branches(1)
+        z_reduced = z.reduced_to_voltage_branches(1)
+
+        #    γ
+        # dδΓ    3   ⎛           ⎞  γ          3    ⎛ γ⎛                 ⎞   ⎞
+        # ———— = — i ⎜ δ  - δ    ⎟ I   Π G³  - — tr ⎜I ⎜ Π δΓ Z + Z δΓ Π ⎟ G³⎟
+        #  dE    2   ⎝  1L    2L ⎠  12    21   4    ⎝  ⎝                 ⎠   ⎠
+        self.deltaGammaLE = 1.5j * (il[0,1] @ pi @ g3[1,0] - il[1,0] @ pi @ g3[0,1]) \
+                - 0.75 * (il @ (pi_reduced @ deltaGamma @ z_reduced + z_reduced @ deltaGamma @ pi_reduced) @ g3).tr()
+
+        #   γ
+        # dΓ     γ
+        # ——— = Y
+        # dE
+        self.gammaLE = yL
+
+        #   γ
+        # dY    3      ⎛  γ      ⎞
+        # ——— = — i tr ⎜ I  Π G³ ⎟
+        # dE    2      ⎝         ⎠
+        self.yLE = 1.5j * (il @ pi @ g3).tr()
+
+        # Count calls to RG equations.
+        self.total_iterations += 1
+
+
+    def check_symmetry(self):
+        '''
+        Check if all symmetries are fulfilled
+        '''
+        self.gamma.check_symmetry()
+        self.gammaL.check_symmetry()
+        self.deltaGamma.check_symmetry()
+        self.deltaGammaL.check_symmetry()
+        self.z.check_symmetry()
+        self.yL.check_symmetry()
+        self.g2.check_symmetry()
+        self.g3.check_symmetry()
+        self.current.check_symmetry()
+
+
+    def unpackFlattenedValues(self, flattened_values):
+        '''
+        Translate between 1d array used by the solver and Floquet matrices
+        used in RG equations. Given a 1d array, write the values of this array
+        to the Floquet matrices self.<values>.
+
+        Order of flattened_values:
+        Γ, Z, δΓ, *G2, *G3, *IL, δΓL, ΓL, YL
+        '''
+        if self.compact == 0:
+            s = self.yL.values.size
+            m = self.deltaGamma.values.size
+            l = self.z.values.size
+            assert flattened_values.size == 10*l+m+7*s
+            self.gamma.values = flattened_values[:l].reshape(self.gamma.values.shape)
+            self.z.values = flattened_values[l:2*l].reshape(self.z.values.shape)
+            self.deltaGamma.values = flattened_values[2*l:2*l+m].reshape(self.deltaGamma.values.shape)
+            for (g2i, flat) in zip(self.g2.data.flat, np.split(flattened_values[2*l+m:6*l+m], 4)):
+                g2i.values = flat.reshape(g2i.values.shape)
+            for (g3i, flat) in zip(self.g3.data.flat, np.split(flattened_values[6*l+m:10*l+m], 4)):
+                g3i.values = flat.reshape(g3i.values.shape)
+            for (ii, flat) in zip(self.current.data.flat, np.split(flattened_values[10*l+m:10*l+m+4*s], 4)):
+                ii.values = flat.reshape(ii.values.shape)
+            self.deltaGammaL.values = flattened_values[10*l+m+4*s:10*l+m+5*s].reshape(self.deltaGammaL.values.shape)
+            self.gammaL.values = flattened_values[10*l+m+5*s:10*l+m+6*s].reshape(self.gammaL.values.shape)
+            self.yL.values = flattened_values[10*l+m+6*s:10*l+m+7*s].reshape(self.yL.values.shape)
+        elif self.compact == 1:
+            raise NotImplementedError()
+        elif self.compact == 2:
+            raise NotImplementedError()
+
+        if settings.CHECK_SYMMETRIES:
+            self.check_symmetry()
+
+
+    def packFlattenedDerivatives(self):
+        '''
+        Pack Floquet matrices representing derivatives in one flattened
+        (1d) array for the solver.
+
+        Order of flattened_values:
+        Γ, Z, δΓ, *G2, *G3, *IL, δΓL, ΓL, YL
+        '''
+        if self.compact == 0:
+            return np.concatenate((
+                        self.gammaE.values,
+                        self.zE.values,
+                        self.deltaGammaE.values,
+                        *(g.values for g in self.g2E.data.flat),
+                        *(g.values for g in self.g3E.data.flat),
+                        *(i.values for i in self.currentE.data.flat),
+                        self.deltaGammaLE.values,
+                        self.gammaLE.values,
+                        self.yLE.values,
+                    ), axis = None
+                )
+        elif self.compact == 1:
+            raise NotImplementedError()
+        elif self.compact == 2:
+            raise NotImplementedError()
+
+
+    def packFlattenedValues(self):
+        '''
+        Translate between 1d array used by the solver and Floquet matrices
+        used in RG equations. Collect all Floquet matrices in one flattened
+        (1d) array.
+
+        Order of flattened_values:
+        Γ, Z, δΓ, *G2, *G3, *IL, δΓL, ΓL, YL
+        '''
+        if self.compact == 0:
+            return np.concatenate((
+                        self.gamma.values,
+                        self.z.values,
+                        self.deltaGamma.values,
+                        *(g.values for g in self.g2.data.flat),
+                        *(g.values for g in self.g3.data.flat),
+                        *(i.values for i in self.current.data.flat),
+                        self.deltaGammaL.values,
+                        self.gammaL.values,
+                        self.yL.values,
+                    ), axis = None
+                )
+        elif self.compact == 1:
+            raise NotImplementedError()
+        elif self.compact == 2:
+            raise NotImplementedError()
+
+
+    def hash(self):
+        data = self.packFlattenedValues()
+        data.flags["WRITEABLE"] = False
+        return hashlib.sha1(data.data).hexdigest()
+
+
+    def odeFunctionIm(self, imenergy, flattened_values):
+        '''
+        ODE as given to the solver for solving the RG equations along the
+        imaginary axis. Given a flattened array containing all Floquet
+        matrices, evaluate the RG equations and return a flattened array of
+        all derivatives.
+        imenergy = Im(E) is used as function argument since the solver cannot
+        handle complex flow parameters.
+        '''
+        try:
+            if self.save_filename and self.save_iterations > 0 and self.iterations % self.save_iterations == 0:
+                try:
+                    self.save_compact()
+                except:
+                    settings.logger.exception("Failed to save intermediate result:")
+        except AttributeError:
+            pass
+        except:
+            settings.logger.exception("Failed trying to save intermediate result:")
+
+        self.global_properties.energy = self.energy.real + 1j*imenergy
+
+        self.unpackFlattenedValues(flattened_values)
+
+        # Evaluate RG equations
+        try:
+            self.updateRGequations()
+        except KeyboardInterrupt as error:
+            settings.logger.critical('Interrupted at Im(E) = %e'%imenergy)
+            try:
+                self.ir_cutoff = max(self.ir_cutoff, imenergy)
+            except AttributeError:
+                self.ir_cutoff = imenergy
+            raise error
+        except Exception as error:
+            settings.logger.exception('Unhandled error at Im(E) = %e'%imenergy)
+            try:
+                self.ir_cutoff = max(self.ir_cutoff, imenergy)
+            except AttributeError:
+                self.ir_cutoff = imenergy
+            raise error
+
+        # Pack values
+        # use  d/d(Im E) = i d/dE
+        return 1j*self.packFlattenedDerivatives()
+
+
+    def odeFunctionRe(self, reenergy, flattened_values):
+        '''
+        ODE as given to the solver for solving the RG equations along the
+        real axis. Given a flattened array containing all Floquet matrices,
+        evaluate the RG equations and return a flattened array of all
+        derivatives.
+        '''
+        if self.save_filename and self.save_iterations > 0 and self.iterations % self.save_iterations == 0:
+            try:
+                self.save_compact()
+            except:
+                settings.logger.exception('Failed to save intermediate result:')
+
+        self.global_properties.energy = reenergy + 1j*self.energy.imag
+
+        self.unpackFlattenedValues(flattened_values)
+
+        # Evaluate RG equations
+        try:
+            self.updateRGequations()
+        except KeyboardInterrupt as error:
+            settings.logger.critical('Interrupted at Re(E) = %g, Im(E) = %g'%(reenergy, energy0.imag))
+            raise error
+        except:
+            settings.logger.exception('Unhandled error at Re(E) = %g, Im(E) = %g'%(reenergy, energy0.imag))
+            raise error
+
+        # Pack values
+        return self.packFlattenedDerivatives()
+
+
+    def solveOdeIm(self, eiminit, eimfinal, init_values=None, g2max=1e6, only_final=False, **solveopts):
+        '''
+        Solve the RG equations along the imaginary axis, starting from
+        E = Ereal + eiminit*1j and ending at E = Ereal + eimfinal*1j
+        where Ereal is the real part of the current energy.
+
+        Other arguments:
+        init_values : flattened array of initial values, by default taken from
+                self.packFlattenedValues()
+        g2max : Threshold for a trigger element in one of the Floquet matrices.
+                This element will detect whether a pole is reached by the RG
+                flow and will stop the flow at the pole.
+                This is only implemented for self.compact == 0
+        only_final : only save final result, do not save the RG flow.
+                This saves memory.
+        **solveopts : arguments that are directly passed on to the solver. Most
+                relevant are rtol and atol.
+        '''
+        assert np.allclose(self.energy.imag, eiminit)
+        if self.compact == 0:
+            try:
+                # Index points to G2[0,0][nmax, nmax, voltage_branches]
+                idx = 2*self.gamma.values.size + self.nmax * (2*self.nmax+1) * (2*self.voltage_branches+1) + self.nmax * (2*self.voltage_branches+1) + self.voltage_branches
+            except TypeError:
+                # Index points to G2[0,0][nmax, nmax]
+                idx = 2*self.gamma.values.size + self.nmax * (2*self.nmax+1)  + self.nmax
+            event = lambda t, y: abs(y[idx]) - g2max
+            event.terminal = True
+        if init_values is None:
+            init_values = self.packFlattenedValues()
+        output = solve_ivp(
+                self.odeFunctionIm,
+                (eiminit, eimfinal),
+                init_values,
+                events = event if self.compact == 0 else None,
+                t_eval = ((eimfinal,) if only_final else None),
+                **solveopts
+            )
+        return output
+
+
+    def solveOdeRe(self, reEinit, reEfinal, init_values=None, g2max=1e6, only_final=False, **solveopts):
+        '''
+        Solve the RG equations along the real axis, starting from
+        E = reEinit + 1j*Eimag and ending at E = reEfinal + 1j*Eimag.
+        where Eimag is the imaginary part of the current energy.
+
+        Other arguments:
+        init_values : flattened array of initial values, by default taken from
+                self.packFlattenedValues()
+        g2max : Threshold for a trigger element in one of the Floquet matrices.
+                This element will detect whether a pole is reached by the RG
+                flow and will stop the flow at the pole.
+                This is only implemented for self.compact == 0
+        only_final : only save final result, do not save the RG flow.
+                This saves memory.
+        **solveopts : arguments that are directly passed on to the solver. Most
+                relevant are rtol and atol.
+        '''
+        assert abs(self.energy.real - reEinit) < 1e-8
+        try:
+            # Index points to G2[0,0][nmax, nmax, voltage_branches]
+            idx = 2*self.gamma.values.size + self.nmax * (2*self.nmax+1) * (2*self.voltage_branches+1) + self.nmax * (2*self.voltage_branches+1) + self.voltage_branches
+        except TypeError:
+            # Index points to G2[0,0][nmax, nmax]
+            idx = 2*self.gamma.values.size + self.nmax * (2*self.nmax+1)  + self.nmax
+        event = lambda t, y: abs(y[idx]) - g2max
+        event.terminal = True
+        if init_values is None:
+            init_values = self.packFlattenedValues()
+        output = solve_ivp(
+                self.odeFunctionRe,
+                (reEinit, reEfinal),
+                init_values,
+                events = event,
+                t_eval = ((eimfinal,) if only_final else None),
+                **solveopts
+            )
+        return output
+
+
+    def save_compact(self, values, compressed=False):
+        '''
+        Automatically save current state of the RG flow in the most compact
+        form. The file name will be
+            self.save_filename % self.iterations
+        or
+            self.save_filename.format(self.iterations).
+        In a computationally expensive RG flow this allows saving intermediate
+        steps of the RG flow.
+        '''
+        try:
+            filename = self.save_filename%self.iterations
+        except TypeError:
+            filename = self.save_filename.format(self.iterations)
+        (np.savez_compressed if compressed else np.savez)(
+                filename,
+                values = values,
+                energy = self.energy,
+                compact = self.compact,
+                )
+
+
+    def load_compact(self, filename):
+        '''
+        Load a file that was created with Kondo.save_compact. This overwrites
+        the current state of self with the values given in the file.
+        '''
+        data = np.load(filename)
+        assert data['compact'] == self.compact
+        self.unpackFlattenedValues(data['values'])
+        self.global_properties.energy = data['energy']
diff --git a/package/src/frtrg_kondo/plot.py b/package/src/frtrg_kondo/plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1a34cfddb0ffea2088efe11627adbed727550d9
--- /dev/null
+++ b/package/src/frtrg_kondo/plot.py
@@ -0,0 +1,594 @@
+#!/usr/bin/env python3
+
+# Copyright 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, generate plots based on save data
+"""
+
+import matplotlib.pyplot as plt
+import matplotlib.colors as mplcolors
+import argparse
+import pandas as pd
+import numpy as np
+from frtrg_kondo import settings
+import os
+from scipy.optimize import curve_fit
+from frtrg_kondo.data_management import DataManager, KondoImport
+
+# reference_values maps (omega, vdc, vac) to reference values
+REFERENCE_VALUES = {
+        (10, 24, 22) : dict(
+                idc=2.2563205,
+                idc_err=2e-4,
+                iac=0.7540501,
+                iac_err=1e-4,
+                gdc=0.07053006,
+                gdc_err=2e-7,
+                acphase=0.06650,
+                acphase_err=2e-5,
+            ),
+        }
+
+TK_VOLTAGE = 3.30743526735
+
+
+def main():
+    """
+    Parse command line arguments and call other functions
+    """
+    parser = argparse.ArgumentParser(description=main.__doc__)
+    valid_functions = {f.__name__:f for f in globals().values() if type(f) == type(main)  and getattr(f, '__module__', '') == '__main__' and f.__name__[0] != '_'}
+    parser.add_argument("functions", type=str, nargs='+', choices=valid_functions.keys(), help="functions to be called")
+    parser.add_argument("--omega", type=float, help="Frequency, units of Tk")
+    parser.add_argument("--method", type=str, choices=('J', 'mu'), help="method: J or mu")
+    parser.add_argument("--nmax", metavar='int', type=int, help="Floquet matrix size")
+    parser.add_argument("--padding", metavar='int', type=int, help="Floquet matrix ppadding")
+    parser.add_argument("--voltage_branches", metavar='int', type=int, help="Voltage branches")
+    parser.add_argument("--resonant_dc_shift", metavar='int', type=int, help="resonant DC shift")
+    parser.add_argument("--vdc", metavar='float', type=float, help="Vdc, units of Tk")
+    fourier_coef_group = parser.add_mutually_exclusive_group()
+    fourier_coef_group.add_argument("--vac", metavar='float', type=float, help="Vac, units of Tk")
+    fourier_coef_group.add_argument("--fourier_coef", metavar='tuple', type=float, nargs='*', help="Voltage Fourier arguments, units of omega")
+    parser.add_argument("--d", metavar='float', type=float, help="D (UV cutoff), units of Tk")
+    parser.add_argument("--xL", metavar='float', type=float, nargs='+', default=0.5, help="Asymmetry, 0 < xL < 1")
+    parser.add_argument("--compact", metavar='int', type=int, help="compact FRTRG implementation (0,1, or 2)")
+    parser.add_argument("--solver_tol_rel", metavar="float", type=float, help="Solver relative tolerance")
+    parser.add_argument("--solver_tol_abs", metavar="float", type=float, help="Solver relative tolerance")
+    args = parser.parse_args()
+
+    dm = DataManager()
+    options = args.__dict__
+    for name in options.pop("functions"):
+        valid_functions[name](dm=dm, **options)
+    plt.show()
+
+def plot(dm, **parameters):
+    """
+    Plot as function of that physical parameters (omega, vdc, or vac) that is
+    not specified. This function required that two out of these three physical
+    parameters are given.
+    """
+    if not 'omega' in parameters or parameters['omega'] is None:
+        parameter = "omega"
+    if not 'vdc' in parameters or parameters['vdc'] is None:
+        parameter = "vdc"
+    if not 'vac' in parameters or parameters['vac'] is None:
+        parameter = "vac"
+    table = dm.list(**parameters)
+    table.sort_values(parameter, inplace=True)
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=False)
+    ax1.set_ylabel("Idc")
+    ax2.set_ylabel("Gdc")
+    ax3.set_ylabel("Iac")
+    ax4.set_ylabel("AC phase")
+    ax3.set_xlabel("Vdc")
+    ax4.set_xlabel("Vdc")
+    ax1.plot(table[parameter], table.dc_current, '.-')
+    ax2.plot(table[parameter], np.pi*table.dc_conductance, '.-')
+    ax3.plot(table[parameter], table.ac_current_abs, '.-')
+    ax4.plot(table[parameter], table.ac_current_phase, '.-')
+
+def plot_overview(dm, omega, **trashoptions):
+    """
+    Plot overview of dc and ac current and dc conductance for harmonic driving
+    at fixed frequency as function of Vdc and Vac.
+    """
+    results_J = dm.list(omega=omega, method='J')
+    results_mu = dm.list(omega=omega, method='mu')
+
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=True)
+    ax1.set_ylabel("Vac")
+    ax3.set_ylabel("Vac")
+    ax3.set_xlabel("Vdc")
+    ax4.set_xlabel("Vdc")
+
+    # DC current
+    idc_min = min(results_J.size and results_J.dc_current.min(), results_mu.size and results_mu.dc_current.min())
+    idc_max = max(results_J.size and results_J.dc_current.max(), results_mu.size and results_mu.dc_current.max())
+    idc_norm = plt.Normalize(idc_min, idc_max)
+    ax1.set_title('DC current')
+    ax1.scatter(results_J.vdc, results_J.vac, c=results_J.dc_current, marker='x', norm=idc_norm, cmap=plt.cm.viridis)
+    plot = ax1.scatter(results_mu.vdc, results_mu.vac, c=results_mu.dc_current, marker='+', norm=idc_norm, cmap=plt.cm.viridis)
+    fig.colorbar(plot, ax=ax1)
+    # DC conductance
+    gmin = np.pi*min(results_J.dc_conductance.min() if results_J.size else 1, results_mu.dc_conductance.min() if results_mu.size else 1)
+    gmax = np.pi*max(results_J.dc_conductance.max() if results_J.size else 0, results_mu.dc_conductance.max() if results_mu.size else 0)
+    gnorm = plt.Normalize(gmin, gmax)
+    ax2.set_title('DC conductance')
+    ax2.scatter(results_J.vdc, results_J.vac, c=np.pi*results_J.dc_conductance, marker='x', norm=gnorm, cmap=plt.cm.viridis)
+    plot = ax2.scatter(results_mu.vdc, results_mu.vac, c=np.pi*results_mu.dc_conductance, marker='+', norm=gnorm, cmap=plt.cm.viridis)
+    fig.colorbar(plot, ax=ax2)
+    # AC current (abs)
+    ax3.set_title('AC current')
+    iac_min = min(results_J.size and results_J.ac_current_abs.min(), results_mu.size and results_mu.ac_current_abs.min())
+    iac_max = max(results_J.size and results_J.ac_current_abs.max(), results_mu.size and results_mu.ac_current_abs.max())
+    iac_norm = plt.Normalize(iac_min, iac_max)
+    ax3.scatter(results_J.vdc, results_J.vac, c=results_J.ac_current_abs, marker='x', norm=iac_norm, cmap=plt.cm.viridis)
+    plot = ax3.scatter(results_mu.vdc, results_mu.vac, c=results_mu.ac_current_abs, marker='+', norm=iac_norm, cmap=plt.cm.viridis)
+    fig.colorbar(plot, ax=ax3)
+    # AC current (phase)
+    ax4.set_title('AC phase')
+    phase_norm = plt.Normalize(-np.pi, np.pi)
+    ax4.scatter(results_J.vdc, results_J.vac, c=results_J.ac_current_phase, marker='x', norm=phase_norm, cmap=plt.cm.hsv)
+    plot = ax4.scatter(results_mu.vdc, results_mu.vac, c=results_mu.ac_current_phase, marker='+', norm=phase_norm, cmap=plt.cm.hsv)
+    fig.colorbar(plot, ax=ax4)
+
+def plot_comparison(dm, omega, **trashoptions):
+    """
+    Compare current and dc conductance computed from both methods (J and mu) at
+    fixed frequency as function of Vdc and Vac.
+    Only data points which exist for both methods with equal Vdc, Vac, D and
+    frequency are considered.
+    """
+    results_J = dm.list(omega=omega, method='J')
+    results_mu = dm.list(omega=omega, method='mu')
+    merged = pd.merge(results_J, results_mu, how="inner", on=["vdc","vac","d","omega"], suffixes=("_J", "_mu"))
+
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=True)
+    ax1.set_ylabel("Vac")
+    ax3.set_ylabel("Vac")
+    ax3.set_xlabel("Vdc")
+    ax4.set_xlabel("Vdc")
+
+    # DC current
+    idc_diff = merged.dc_current_J - merged.dc_current_mu
+    idc_max = abs(idc_diff).max()
+    idc_norm = plt.Normalize(-idc_max, idc_max)
+    ax1.set_title('DC current')
+    plot = ax1.scatter(merged.vdc, merged.vac, c=idc_diff, marker='o', norm=idc_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax1)
+    # DC conductance
+    gdc_diff = np.pi*(merged.dc_conductance_J - merged.dc_conductance_mu)
+    gdc_max = abs(gdc_diff).max()
+    gdc_norm = plt.Normalize(-gdc_max, gdc_max)
+    ax2.set_title('DC conductance')
+    plot = ax2.scatter(merged.vdc, merged.vac, c=gdc_diff, marker='o', norm=gdc_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax2)
+    # AC current (abs)
+    iac_diff = merged.ac_current_abs_J - merged.ac_current_abs_mu
+    iac_max = abs(iac_diff).max()
+    iac_norm = plt.Normalize(-iac_max, iac_max)
+    ax3.set_title('AC current')
+    plot = ax3.scatter(merged.vdc, merged.vac, c=iac_diff, marker='o', norm=iac_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax3)
+    # AC current (phase)
+    phase_diff = (merged.ac_current_phase_J - merged.ac_current_phase_mu + np.pi) % (2*np.pi) - np.pi
+    phase_max = abs(phase_diff).max()
+    phase_norm = plt.Normalize(-phase_max, phase_max)
+    ax4.set_title('AC phase')
+    plot = ax4.scatter(merged.vdc, merged.vac, c=phase_diff, marker='o', norm=phase_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax4)
+
+def plot_comparison_relative(dm, omega, **trashoptions):
+    """
+    Compare current and dc conductance computed from both methods (J and mu) at
+    fixed frequency as function of Vdc and Vac.
+    Only data points which exist for both methods with equal Vdc, Vac, D and
+    frequency are considered.
+    """
+    results_J = dm.list(omega=omega, method='J')
+    results_mu = dm.list(omega=omega, method='mu')
+    merged = pd.merge(results_J, results_mu, how="inner", on=["vdc","vac","d","omega"], suffixes=("_J", "_mu"))
+
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=True)
+    ax1.set_ylabel("Vac")
+    ax3.set_ylabel("Vac")
+    ax3.set_xlabel("Vdc")
+    ax4.set_xlabel("Vdc")
+
+    # DC current
+    idc_diff = 2*(merged.dc_current_J - merged.dc_current_mu)/(merged.dc_current_J + merged.dc_current_mu)
+    idc_diff[merged.vdc == 0] = 0
+    idc_max = abs(idc_diff).max()
+    idc_norm = plt.Normalize(-idc_max, idc_max)
+    ax1.set_title('DC current')
+    plot = ax1.scatter(merged.vdc, merged.vac, s=5+5e3*np.abs(idc_diff), c=idc_diff, marker='o', norm=idc_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax1)
+    # DC conductance
+    gdc_diff = 2*(merged.dc_conductance_J - merged.dc_conductance_mu)/(merged.dc_conductance_J + merged.dc_conductance_mu)
+    gdc_max = abs(gdc_diff).max()
+    gdc_norm = plt.Normalize(-gdc_max, gdc_max)
+    ax2.set_title('DC conductance')
+    plot = ax2.scatter(merged.vdc, merged.vac, s=5+5e3*np.abs(gdc_diff), c=gdc_diff, marker='o', norm=gdc_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax2)
+    # AC current (abs)
+    iac_diff = 2*(merged.ac_current_abs_J - merged.ac_current_abs_mu)/(merged.ac_current_abs_J + merged.ac_current_abs_mu)
+    iac_max = abs(iac_diff).max()
+    iac_norm = plt.Normalize(-iac_max, iac_max)
+    ax3.set_title('AC current')
+    plot = ax3.scatter(merged.vdc, merged.vac, s=5+1e4*np.abs(iac_diff), c=iac_diff, marker='o', norm=iac_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax3)
+    # AC current (phase)
+    phase_diff = (merged.ac_current_phase_J - merged.ac_current_phase_mu + np.pi) % (2*np.pi) - np.pi
+    phase_max = abs(phase_diff).max()
+    phase_norm = plt.Normalize(-phase_max, phase_max)
+    ax4.set_title('AC phase')
+    plot = ax4.scatter(merged.vdc, merged.vac, s=5+2e4*np.abs(phase_diff), c=phase_diff, marker='o', norm=phase_norm, cmap=plt.cm.seismic)
+    fig.colorbar(plot, ax=ax4)
+
+def plot_floquet_matrices(kondo : KondoImport, norm_min=1e-6):
+    fig, axes = plt.subplots(3, 3)
+    axes = axes.flatten()
+    gamma = kondo.gamma
+    idx = kondo.voltage_branches if gamma.ndim == 3 else ...
+    try:
+        axes[0].set_title("Γ")
+        img = axes[0].imshow(np.abs(gamma[idx]), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[0])
+    except AttributeError:
+        pass
+    try:
+        axes[1].set_title("Z")
+        img = axes[1].imshow(np.abs(kondo.z[idx]), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[1])
+    except AttributeError:
+        pass
+    try:
+        axes[2].set_title("ΓL")
+        img = axes[2].imshow(np.abs(kondo.gammaL), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[2])
+    except AttributeError:
+        pass
+    try:
+        axes[3].set_title("δΓL")
+        img = axes[3].imshow(np.abs(kondo.deltaGammaL), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[3])
+    except AttributeError:
+        pass
+    try:
+        axes[4].set_title("δΓ")
+        img = axes[4].imshow(np.abs(kondo.deltaGamma[1 if gamma.ndim == 3 else ...]), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[4])
+    except AttributeError:
+        pass
+    try:
+        axes[5].set_title("yL")
+        img = axes[5].imshow(np.abs(kondo.yL), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[5])
+    except AttributeError:
+        pass
+    try:
+        axes[6].set_title("G2")
+        img = axes[6].imshow(np.abs(kondo.g2[:,:,idx].transpose(0,2,1,3).reshape((4*kondo.nmax+2, 4*kondo.nmax+2))), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[6])
+    except AttributeError:
+        pass
+    try:
+        axes[7].set_title("G3")
+        img = axes[7].imshow(np.abs(kondo.g3[:,:,idx].transpose(0,2,1,3).reshape((4*kondo.nmax+2, 4*kondo.nmax+2))), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[7])
+    except AttributeError:
+        pass
+    try:
+        axes[8].set_title("I")
+        img = axes[8].imshow(np.abs(kondo.current.transpose(0,2,1,3).reshape((4*kondo.nmax+2, 4*kondo.nmax+2))), norm=mplcolors.LogNorm(norm_min))
+        fig.colorbar(img, ax=axes[8])
+    except AttributeError:
+        pass
+
+def check_results(dm, max_num=5, **parameters):
+    table = dm.list(**parameters)
+    counter = 0
+    for index, row in table.iterrows():
+        for kondo in KondoImport.read_from_h5(os.path.join(row.dirname, row.basename), row.hash):
+            try:
+                plot_floquet_matrices(kondo)
+            except KeyboardInterrupt:
+                kondo._h5file.close()
+                return
+            counter += 1
+            if counter >= max_num:
+                settings.logger.warning("Maximum number of files read, stopping here")
+                return
+        if not kondo._owns_h5file:
+            kondo._h5file.close()
+
+def check_convergence(dm, **parameters):
+    if not ("omega" in parameters and "vdc" in parameters and "vac" in parameters):
+        settings.logger.warning("check_convergence expects specification of all physical parameters")
+    table = dm.list(**parameters)
+    mod = (table.solver_flags & DataManager.SOLVER_FLAGS["simplified_initial_conditions"]) != 0
+    j = (~mod) & (table.method == "J")
+    mu = (~mod) & (table.method == "mu")
+    d_j = table.d[j]
+    d_mu = table.d[mu]
+    d_mod = table.d[mod]
+    x = 1/np.log(table.d)**3
+    x_j = x[j]
+    x_mu = x[mu]
+    x_mod = x[mod]
+    drtol = table.d*table.solver_tol_rel
+    norm = mplcolors.LogNorm(max(drtol.min(), 0.05), min(drtol.max(), 500))
+    c_j = drtol[j]
+    c_mu = drtol[mu]
+    c_mod = drtol[mod]
+    s_j = 0.05 * table.nmax[j]**2
+    s_mu = 0.08 * table.nmax[mu]**2
+    s_mod = 0.08 * table.nmax[mod]**2
+    lw_j = 0.3 * table.voltage_branches[j]
+    lw_mu = 0.3 * table.voltage_branches[mu]
+    lw_mod = 0.3 * table.voltage_branches[mod]
+
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=False)
+    ax3.set_xlabel("D")
+    ax4.set_xlabel("D")
+    ax1.set_xscale("log")
+
+    # Show reference values
+    try:
+        reference_values = REFERENCE_VALUES[(parameters['omega'], parameters['vdc'], parameters['vac'])]
+    except KeyError:
+        reference_values = None
+
+    if reference_values is not None:
+        x = (table.d.min(), table.d.max())
+        ax1.plot(x, (reference_values['idc'],reference_values['idc']), 'k:')
+        ax1.fill_between(x, (reference_values['idc']-reference_values['idc_err'],reference_values['idc']-reference_values['idc_err']), (reference_values['idc']+reference_values['idc_err'],reference_values['idc']+reference_values['idc_err']), color='#80808080')
+        ax2.plot(x, (reference_values['gdc'],reference_values['gdc']), 'k:')
+        ax2.fill_between(x, (reference_values['gdc']-reference_values['gdc_err'],reference_values['gdc']-reference_values['gdc_err']), (reference_values['gdc']+reference_values['gdc_err'],reference_values['gdc']+reference_values['gdc_err']), color='#80808080')
+        ax3.plot(x, (reference_values['iac'],reference_values['iac']), 'k:')
+        ax3.fill_between(x, (reference_values['iac']-reference_values['iac_err'],reference_values['iac']-reference_values['iac_err']), (reference_values['iac']+reference_values['iac_err'],reference_values['iac']+reference_values['iac_err']), color='#80808080')
+        ax4.plot(x, (reference_values['acphase'],reference_values['acphase']), 'k:')
+        ax4.fill_between(x, (reference_values['acphase']-reference_values['acphase_err'],reference_values['acphase']-reference_values['acphase_err']), (reference_values['acphase']+reference_values['acphase_err'],reference_values['acphase']+reference_values['acphase_err']), color='#80808080')
+
+    ax1.set_title("DC current")
+    ax1.scatter(d_j, table.dc_current[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax1.scatter(d_mu, table.dc_current[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax1.scatter(d_mod, table.dc_current[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax2.set_title("DC conductance")
+    ax2.scatter(d_j, table.dc_conductance[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax2.scatter(d_mu, table.dc_conductance[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax2.scatter(d_mod, table.dc_conductance[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax3.set_title("AC current")
+    ax3.scatter(d_j, table.ac_current_abs[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax3.scatter(d_mu, table.ac_current_abs[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax3.scatter(d_mod, table.ac_current_abs[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax4.set_title("AC phase")
+    ax4.scatter(d_j, table.ac_current_phase[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax4.scatter(d_mu, table.ac_current_phase[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax4.scatter(d_mod, table.ac_current_phase[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+def check_convergence_nmax(dm, **parameters):
+    if not ("omega" in parameters and "vdc" in parameters and "vac" in parameters and "d"):
+        settings.logger.warning("check_convergence_nmax expects specification of all physical parameters and D")
+    table = dm.list(**parameters)
+    mod = (table.solver_flags & DataManager.SOLVER_FLAGS["simplified_initial_conditions"]) != 0
+    j = (~mod) & (table.method == "J")
+    mu = (~mod) & (table.method == "mu")
+    norm = mplcolors.LogNorm(table.voltage_branches.min(), table.voltage_branches.max())
+    nmax_j = table.nmax[j]
+    nmax_mu = table.nmax[mu]
+    nmax_mod = table.nmax[mod]
+    s_j = -3*np.log(table.solver_tol_rel[j])
+    s_mu = -3*np.log(table.solver_tol_rel[mu])
+    s_mod = -3*np.log(table.solver_tol_rel[mod])
+    lw_j = -0.1*np.log(table.solver_tol_abs[j])
+    lw_mu = -0.1*np.log(table.solver_tol_abs[mu])
+    lw_mod = -0.1*np.log(table.solver_tol_abs[mod])
+    c_j = table.voltage_branches[j]
+    c_mu = table.voltage_branches[mu]
+    c_mod = table.voltage_branches[mod]
+
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=False)
+    ax3.set_xlabel("nmax")
+    ax4.set_xlabel("nmax")
+
+    # Show reference values
+    try:
+        reference_values = REFERENCE_VALUES[(parameters['omega'], parameters['vdc'], parameters['vac'])]
+    except KeyError:
+        reference_values = None
+
+    if reference_values is not None:
+        x = (table.nmax.min(), table.nmax.max())
+        ax1.plot(x, (reference_values['idc'],reference_values['idc']), 'k:')
+        ax1.fill_between(x, (reference_values['idc']-reference_values['idc_err'],reference_values['idc']-reference_values['idc_err']), (reference_values['idc']+reference_values['idc_err'],reference_values['idc']+reference_values['idc_err']), color='#80808080')
+        ax2.plot(x, (reference_values['gdc'],reference_values['gdc']), 'k:')
+        ax2.fill_between(x, (reference_values['gdc']-reference_values['gdc_err'],reference_values['gdc']-reference_values['gdc_err']), (reference_values['gdc']+reference_values['gdc_err'],reference_values['gdc']+reference_values['gdc_err']), color='#80808080')
+        ax3.plot(x, (reference_values['iac'],reference_values['iac']), 'k:')
+        ax3.fill_between(x, (reference_values['iac']-reference_values['iac_err'],reference_values['iac']-reference_values['iac_err']), (reference_values['iac']+reference_values['iac_err'],reference_values['iac']+reference_values['iac_err']), color='#80808080')
+        ax4.plot(x, (reference_values['acphase'],reference_values['acphase']), 'k:')
+        ax4.fill_between(x, (reference_values['acphase']-reference_values['acphase_err'],reference_values['acphase']-reference_values['acphase_err']), (reference_values['acphase']+reference_values['acphase_err'],reference_values['acphase']+reference_values['acphase_err']), color='#80808080')
+
+    ax1.set_title("DC current")
+    ax1.scatter(nmax_j, table.dc_current[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax1.scatter(nmax_mu, table.dc_current[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax1.scatter(nmax_mod, table.dc_current[mod], marker='_', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax2.set_title("DC conductance")
+    ax2.scatter(nmax_j, table.dc_conductance[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax2.scatter(nmax_mu, table.dc_conductance[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax2.scatter(nmax_mod, table.dc_conductance[mod], marker='_', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax3.set_title("AC current")
+    ax3.scatter(nmax_j, table.ac_current_abs[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax3.scatter(nmax_mu, table.ac_current_abs[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax3.scatter(nmax_mod, table.ac_current_abs[mod], marker='_', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax4.set_title("AC phase")
+    ax4.scatter(nmax_j, table.ac_current_phase[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax4.scatter(nmax_mu, table.ac_current_phase[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax4.scatter(nmax_mod, table.ac_current_phase[mod], marker='_', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+def check_convergence_fit(dm, **parameters):
+    if not ("omega" in parameters and "vdc" in parameters and "vac" in parameters):
+        settings.logger.warning("check_convergence expects specification of all physical parameters")
+    table = dm.list(**parameters)
+    mod = (table.solver_flags & DataManager.SOLVER_FLAGS["simplified_initial_conditions"]) != 0
+    d_fit_max = 9e11
+    d_fit_max_j_nopadding = 9e11
+    d_fit_max_j_shifted_nopadding = 2e9
+    j = (~mod) & (table.method == "J")
+    mu = (~mod) & (table.method == "mu")
+    logd_inv_arr = np.linspace(0, 1/np.log10(table.d.min()), 200)
+    logd_inv = 1/np.log10(table.d)
+    x = 1/np.log10(table.d)
+    x3 = 1/np.log10(table.d)**3
+    x_j = x[j]
+    x_mu = x[mu]
+    x_mod = x[mod]
+    drtol = table.d*table.solver_tol_rel
+    norm = mplcolors.LogNorm(max(drtol.min(), 0.05), min(drtol.max(), 500))
+    c_j = drtol[j]
+    c_mu = drtol[mu]
+    c_mod = drtol[mod]
+    s_j = 0.05 * table.nmax[j]**2
+    s_mu = 0.08 * table.nmax[mu]**2
+    s_mod = 0.08 * table.nmax[mod]**2
+    lw_j = 0.3 * table.voltage_branches[j]
+    lw_mu = 0.3 * table.voltage_branches[mu]
+    lw_mod = 0.3 * table.voltage_branches[mod]
+
+    selections = {}
+    for r in range(10):
+        suffix = '_%d'%r if r else ''
+        if r in table.resonant_dc_shift.values:
+            mu_shift = (~mod) & (table.method == "mu") & (table.d <= d_fit_max) & (table.resonant_dc_shift == r)
+            mu_mod_shift = mod & (table.method == "mu") & (table.d <= d_fit_max) & (table.resonant_dc_shift == r)
+            j_shift = (~mod) & (table.method == "J") & (((table.padding > 0) & (table.d <= d_fit_max)) | (table.d <= (d_fit_max_j_nopadding if r == 0 else d_fit_max_j_shifted_nopadding))) & (table.resonant_dc_shift == r)
+            j_mod_shift = mod & (table.method == "J") & (((table.padding > 0) & (table.d <= d_fit_max)) | (table.d <= (d_fit_max_j_nopadding if r == 0 else d_fit_max_j_shifted_nopadding))) & (table.resonant_dc_shift == r)
+            if mu_shift.any():
+                selections["mu" + suffix] = mu_shift
+            if mu_mod_shift.any():
+                selections["mu_mod" + suffix] = mu_mod_shift
+            if j_shift.any():
+                selections["J" + suffix] = j_shift
+            if j_mod_shift.any():
+                selections["J_mod" + suffix] = j_mod_shift
+
+    fit_func = lambda logd_inv, a, b, c: a + b * logd_inv**c
+
+    x_mean = x3[~mod].mean()
+    x_max = x3[~mod].max()
+    x_shifted = x3[~mod] - x_mean
+    s = (~mod).sum()
+    s_xx = (x_shifted**2).sum()
+
+    xmod_mean = x3[mod].mean()
+    xmod_max = x3[mod].max()
+    xmod_shifted = x3[mod] - xmod_mean
+    smod = mod.sum()
+    smod_xx = (xmod_shifted**2).sum()
+
+    # Create figure
+    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True, sharey=False)
+    ax3.set_xlabel("1/log10(D)")
+    ax4.set_xlabel("1/log10(D)")
+
+    # Show reference values
+    try:
+        reference_values = REFERENCE_VALUES[(parameters['omega'], parameters['vdc'], parameters['vac'])]
+    except KeyError:
+        reference_values = None
+
+    if reference_values is not None:
+        ax1.plot((0,logd_inv_arr[-1]), (reference_values['idc'],reference_values['idc']), 'k:')
+        ax1.fill_between((0,logd_inv_arr[-1]), (reference_values['idc']-reference_values['idc_err'],reference_values['idc']-reference_values['idc_err']), (reference_values['idc']+reference_values['idc_err'],reference_values['idc']+reference_values['idc_err']), color='#80808080')
+        ax2.plot((0,logd_inv_arr[-1]), (reference_values['gdc'],reference_values['gdc']), 'k:')
+        ax2.fill_between((0,logd_inv_arr[-1]), (reference_values['gdc']-reference_values['gdc_err'],reference_values['gdc']-reference_values['gdc_err']), (reference_values['gdc']+reference_values['gdc_err'],reference_values['gdc']+reference_values['gdc_err']), color='#80808080')
+        ax3.plot((0,logd_inv_arr[-1]), (reference_values['iac'],reference_values['iac']), 'k:')
+        ax3.fill_between((0,logd_inv_arr[-1]), (reference_values['iac']-reference_values['iac_err'],reference_values['iac']-reference_values['iac_err']), (reference_values['iac']+reference_values['iac_err'],reference_values['iac']+reference_values['iac_err']), color='#80808080')
+        ax4.plot((0,logd_inv_arr[-1]), (reference_values['acphase'],reference_values['acphase']), 'k:')
+        ax4.fill_between((0,logd_inv_arr[-1]), (reference_values['acphase']-reference_values['acphase_err'],reference_values['acphase']-reference_values['acphase_err']), (reference_values['acphase']+reference_values['acphase_err'],reference_values['acphase']+reference_values['acphase_err']), color='#80808080')
+
+    ax1.set_title("DC current")
+    ax1.scatter(x_j, table.dc_current[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax1.scatter(x_mu, table.dc_current[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax1.scatter(x_mod, table.dc_current[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    bb = table.dc_current[~mod].mean()
+    aa = (x_shifted*table.dc_current[~mod]).sum()/s_xx
+    #ax1.plot(logd_inv_arr, aa*(logd_inv_arr**3 - x_mean) + bb, linewidth=0.5)
+
+    bbmod = table.dc_current[mod].mean()
+    aamod = (xmod_shifted*table.dc_current[mod]).sum()/smod_xx
+    #ax1.plot(logd_inv_arr, aamod*(logd_inv_arr**3 - xmod_mean) + bbmod, linewidth=0.5)
+
+    for name, selection in selections.items():
+        try:
+            (a, b, c), covar = curve_fit(fit_func, logd_inv[selection], table.dc_current[selection], (bb-aa*x_mean, aa, 3))
+            aerr, berr, cerr = covar.diagonal()**0.5
+            print(f"DC current ({name:9}): a={a:.9}±{aerr:.9}, b={b:.9}±{berr:.9}, c={c:.9}±{cerr:.9}")
+            ax1.plot(logd_inv_arr, fit_func(logd_inv_arr, a, b, c), label=name)
+            ax1.plot((0,logd_inv_arr[-1]), (a,a), ':', label=name)
+        except:
+            pass
+
+    ax2.set_title("DC conductance")
+    ax2.scatter(x_j, table.dc_conductance[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax2.scatter(x_mu, table.dc_conductance[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax2.scatter(x_mod, table.dc_conductance[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    ax3.set_title("AC current")
+    ax3.scatter(x_j, table.ac_current_abs[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax3.scatter(x_mu, table.ac_current_abs[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax3.scatter(x_mod, table.ac_current_abs[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    b = table.ac_current_abs[~mod].mean()
+    a = (x_shifted*table.ac_current_abs[~mod]).sum()/s_xx
+    #ax3.plot(logd_inv_arr, a*(logd_inv_arr**3 - x_mean) + b, linewidth=0.5)
+
+    bmod = table.ac_current_abs[mod].mean()
+    amod = (xmod_shifted*table.ac_current_abs[mod]).sum()/smod_xx
+    #ax3.plot(logd_inv_arr, amod*(logd_inv_arr**3 - xmod_mean) + bmod, linewidth=0.5)
+
+    for name, selection in selections.items():
+        try:
+            (a, b, c), covar = curve_fit(fit_func, logd_inv[selection], table.ac_current_abs[selection], (bb-aa*x_mean, aa, 3))
+            aerr, berr, cerr = covar.diagonal()**0.5
+            print(f"AC current ({name:9}): a={a:.9}±{aerr:.9}, b={b:.9}±{berr:.9}, c={c:.9}±{cerr:.9}")
+            ax3.plot(logd_inv_arr, fit_func(logd_inv_arr, a, b, c), label=name)
+            ax3.plot((0,logd_inv_arr[-1]), (a,a), ':', label=name)
+        except:
+            pass
+
+    ax4.set_title("AC phase")
+    ax4.scatter(x_j, table.ac_current_phase[j], marker='x', s=s_j, c=c_j, linewidths=lw_j, norm=norm)
+    ax4.scatter(x_mu, table.ac_current_phase[mu], marker='+', s=s_mu, c=c_mu, linewidths=lw_mu, norm=norm)
+    ax4.scatter(x_mod, table.ac_current_phase[mod], marker='*', s=s_mod, c=c_mod, linewidths=lw_mod, norm=norm)
+
+    b = table.ac_current_phase[~mod].mean()
+    a = (x_shifted*table.ac_current_phase[~mod]).sum()/s_xx
+    #ax4.plot(logd_inv_arr, a*(logd_inv_arr**3 - x_mean) + b, linewidth=0.5)
+
+    bmod = table.ac_current_phase[mod].mean()
+    amod = (xmod_shifted*table.ac_current_phase[mod]).sum()/smod_xx
+    #ax4.plot(logd_inv_arr, amod*(logd_inv_arr**3 - xmod_mean) + bmod, linewidth=0.5)
+
+    for name, selection in selections.items():
+        try:
+            (a, b, c), covar = curve_fit(fit_func, logd_inv[selection], table.ac_current_phase[selection], (bb-aa*x_mean, aa, 3))
+            aerr, berr, cerr = covar.diagonal()**0.5
+            print(f"AC phase ({name:9}): a={a:.9}±{aerr:.9}, b={b:.9}±{berr:.9}, c={c:.9}±{cerr:.9}")
+            ax4.plot(logd_inv_arr, fit_func(logd_inv_arr, a, b, c), label=name)
+            ax4.plot((0,logd_inv_arr[-1]), (a,a), ':', label=name)
+        except:
+            pass
+
+
+if __name__ == '__main__':
+    main()
diff --git a/package/src/frtrg_kondo/reservoirmatrix.py b/package/src/frtrg_kondo/reservoirmatrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..c24deb3214bf68a2c69e5c486bf72d604e1dc3ca
--- /dev/null
+++ b/package/src/frtrg_kondo/reservoirmatrix.py
@@ -0,0 +1,522 @@
+# Copyright 2021 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, module defining vertice in RG equations
+
+Module defining class ReservoirMatrix and some functions for efficient
+handling of matrices of Floquet matrices as used for the Kondo model.
+
+See also: rtrg.py
+"""
+
+import numpy as np
+from numbers import Number
+from frtrg_kondo import settings
+from frtrg_kondo.rtrg import RGobj, RGfunction
+
+
+class ReservoirMatrix(RGobj):
+    '''
+    2x2 matrix of RGfunctions.
+    This includes a system of book keeping for energy shifts by multiples of
+    the voltage in products of ReservoirMatrices.
+    if symmetry != 0:
+        self.data[1,0] == symmetry * self.data[0,1].floquetConjugate()
+    if symmety != 0 and global_properties.symmetric:
+        self.data[0,0] == self.data[1,1]
+
+    Multiplication operators for ReservoirMatrix objects are:
+    *   for multiplication with scalars
+    @   for multiplication with RGfunctions and normal matrix multiplication
+        with ReservoirMatrices (matrix indices ij, jk → ik with sum over j)
+    %   for transpose matrix multiplication with ReservoirMatrices
+        (matrix indices jk, ij → ik with sum over j)
+                      ⎛ ⊤   ⊤⎞⊤
+        i.e.  A % B = ⎜A @ B ⎟  when there are no energy argument shifts.
+                      ⎝      ⎠
+    '''
+
+    def __init__(self, global_properties, symmetry=0):
+        super().__init__(global_properties, symmetry)
+        self.data = np.ndarray((2, 2), dtype=RGfunction)
+        self.voltage_shifts = 0
+
+    def __getitem__(self, arg):
+        assert self.data[arg].voltage_shifts == self.voltage_shifts + arg[1] - arg[0]
+        return self.data[arg]
+
+    def __setitem__(self, indices, value):
+        self.data.__setitem__(indices, value)
+        if isinstance(value, RGfunction):
+            assert value.global_properties is self.global_properties
+            self.data.__getitem__(indices).voltage_shifts = self.voltage_shifts + indices[1] - indices[0]
+
+    def __add__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            res = ReservoirMatrix(self.global_properties, symmetry)
+            res.voltage_shifts = self.voltage_shifts
+            res.data = self.data + other.data
+            return res
+        else:
+            raise NotImplementedError('Addition is not defined for types %s and %s'%(ReservoirMatrix, type(other)))
+
+    def __iadd__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            self.data += other.data
+            if self.symmetry != other.symmetry:
+                self.symmetry = 0
+        else:
+            raise NotImplementedError('Addition is not defined for types %s and %s'%(ReservoirMatrix, type(other)))
+        return self
+
+    def __sub__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            res = ReservoirMatrix(self.global_properties, symmetry)
+            res.voltage_shifts = self.voltage_shifts
+            res.data = self.data - other.data
+            return res
+        else:
+            raise NotImplementedError('Subtraction is not defined for types %s and %s'%(ReservoirMatrix, type(other)))
+
+    def __isub__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            self.data -= other.data
+            if self.symmetry != other.symmetry:
+                self.symmetry = 0
+        else:
+            raise NotImplementedError('Subtraction is not defined for types %s and %s'%(ReservoirMatrix, type(other)))
+        return self
+
+    def __neg__(self):
+        res = ReservoirMatrix(self.global_properties, self.symmetry)
+        res.voltage_shifts = voltage_shifts
+        res.data = -self.data
+        return res
+
+    def __mul__(self, other):
+        if isinstance(other, ReservoirMatrix) or isinstance(other, RGfunction):
+            raise TypeError("Multiplication of reservoir matrices uses the @ symbol.")
+        else:
+            res = ReservoirMatrix(self.global_properties)
+            if other.imag == 0:
+                res.symmetry = self.symmetry
+            elif other.real == 0:
+                res.symmetry = -self.symmetry
+            else:
+                res.symmetry = 0
+            res.data = self.data * other
+        return res
+
+    def __imul__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            raise TypeError("Multiplication of reservoir matrices uses the @ symbol.")
+        else:
+            if other.imag != 0:
+                if other.real == 0:
+                    self.symmetry = -self.symmetry
+                else:
+                    self.symmetry = 0
+            self.data *= other
+        return self
+
+    def __rmul__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            raise TypeError("Multiplication of reservoir matrices uses the @ symbol.")
+        elif isinstance(other, RGfunction):
+            res = ReservoirMatrix(self.global_properties, other.symmetry * self.symmetry)
+            res.data = other * self.data
+        elif isinstance(other, Number):
+            res = ReservoirMatrix(self.global_properties)
+            res.data = other * self.data
+            if other.imag == 0:
+                res.symmetry = self.symmetry
+            elif other.real == 0:
+                res.symmetry = -self.symmetry
+            else:
+                res.symmetry = 0
+        return res
+
+    def __matmul__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            # 8 multiplications without symmetry
+            # 7 multiplications with symmetry but xL != xR
+            # 4 multiplications with symmetry and xL == xR
+            assert other.voltage_shifts == 0
+            assert self.global_properties is other.global_properties
+            res = ReservoirMatrix(self.global_properties, symmetry=0)
+            res.voltage_shifts = self.voltage_shifts
+
+            res_00_00 = self[0,0] @ other[0,0]
+            res_01_10 = self[0,1] @ other[1,0]
+            res[0,0] = res_00_00 + res_01_10
+            res[0,1] = self[0,0] @ other[0,1] + self[0,1] @ other[1,1]
+            symmetry = self.symmetry * other.symmetry * (self.voltage_shifts == 0)
+            if symmetry == 0 or settings.IGNORE_SYMMETRIES:
+                res[1,1] = self[1,0] @ other[0,1] + self[1,1] @ other[1,1]
+                res[1,0] = self[1,0] @ other[0,0] + self[1,1] @ other[1,0]
+            elif self.global_properties.symmetric:
+                res[1,1] = res_00_00 + symmetry * res_01_10.floquetConjugate()
+                res[1,0] = symmetry * res[0,1].floquetConjugate()
+            else:
+                res[1,1] = self[1,1] @ other[1,1] + symmetry * res_01_10.floquetConjugate()
+                res[1,0] = self[1,0] @ other[0,0] + self[1,1] @ other[1,0]
+            return res
+        elif isinstance(other, RGfunction):
+            # 4 multiplications without symmetry
+            # 3 multiplications with symmetry but xL != xR
+            # 2 multiplications with symmetry and xL == xR
+            assert self.global_properties is other.global_properties
+            res = ReservoirMatrix(self.global_properties, self.symmetry * other.symmetry * (self.voltage_shifts == 0))
+            res.voltage_shifts = self.voltage_shifts + other.voltage_shifts
+            res[0,0] = self[0,0] @ other
+            if res.symmetry == 0 or settings.IGNORE_SYMMETRIES:
+                res[0,1] = self[0,1] @ other
+                res[1,0] = self[1,0] @ other
+                res[1,1] = self[1,1] @ other
+            else:
+                if res.global_properties.symmetric:
+                    res[1,1] = res[0,0].copy()
+                else:
+                    res[1,1] = self[1,1] @ other
+                res[0,1] = self[0,1] @ other
+                res[1,0] = res.symmetry * res[0,1].floquetConjugate()
+            return res
+        else:
+            raise TypeError('Math multiplication is not defined for types %s and %s'%(ReservoirMatrix, type(other)))
+
+    def __rmatmul__(self, other):
+        if isinstance(other, RGfunction):
+            assert self.voltage_shifts == 0
+            assert self.global_properties is other.global_properties
+            res = ReservoirMatrix(self.global_properties, self.symmetry * other.symmetry * (other.voltage_shifts == 0))
+            res.voltage_shifts = other.voltage_shifts
+            res[0,0] = other @ self[0,0]
+            if res.symmetry == 0 or settings.IGNORE_SYMMETRIES:
+                res[0,1] = other @ self[0,1]
+                res[1,0] = other @ self[1,0]
+                res[1,1] = other @ self[1,1]
+            else:
+                if res.global_properties.symmetric:
+                    res[1,1] = res[0,0].copy()
+                else:
+                    res[1,1] = other @ self[1,1]
+                res[0,1] = other @ self[0,1]
+                res[1,0] = res.symmetry * res[0,1].floquetConjugate()
+            return res
+        else:
+            raise TypeError('Math multiplication is not defined for types %s and %s'%(type(other), ReservoirMatrix))
+
+    def __imatmul__(self, other):
+        if isinstance(other, ReservoirMatrix):
+            # 8 multiplications without symmetry
+            # 7 multiplications with symmetry but xL != xR
+            # 4 multiplications with symmetry and xL == xR
+            assert other.voltage_shifts == 0
+            assert self.global_properties is other.global_properties
+            res_00_00 = self[0,0] @ other[0,0]
+            res_01_10 = self[0,1] @ other[1,0]
+            res_00 = res_00_00 + res_01_10
+            res_01 = self[0,0] @ other[0,1] + self[0,1] @ other[1,1]
+            symmetry = self.symmetry * other.symmetry
+            if symmetry == 0 or settings.IGNORE_SYMMETRIES:
+                res_11 = self[1,0] @ other[0,1] + self[1,1] @ other[1,1]
+                res_10 = self[1,0] @ other[0,0] + self[1,1] @ other[1,0]
+            elif self.global_properties.symmetric:
+                res_11 = res_00_00 + symmetry * res_01_10.floquetConjugate()
+                res_10 = symmetry * res_01.floquetConjugate()
+            else:
+                res_11 = self[1,1] @ other[1,1] + symmetry * res_01_10.floquetConjugate()
+                res_10 = self[1,0] @ other[0,0] + self[1,1] @ other[1,0]
+            self[0,0] = res_00
+            self[0,1] = res_01
+            self[1,0] = res_10
+            self[1,1] = res_11
+            self.symmetry = 0
+            return self
+        else:
+            raise TypeError('Math multiplication is not defined for types %s and %s'%(ReservoirMatrix, type(other)))
+
+    def __mod__(self, other):
+        '''
+        Transpose multiplication: Given A, B return C such that
+
+            C   = A   B
+             12    32  13
+        '''
+        assert isinstance(other, ReservoirMatrix)
+        assert other.voltage_shifts == 0
+        res = ReservoirMatrix(self.global_properties, symmetry=0)
+        res.voltage_shifts = self.voltage_shifts
+
+        res_00_00 = self[0,0] @ other[0,0]
+        res_10_01 = self[1,0] @ other[0,1]
+        res[0,0] = res_00_00 + res_10_01
+        res[0,1] = self[0,1] @ other[0,0] + self[1,1] @ other[0,1]
+        # TODO: check symmetry
+        symmetry = self.symmetry * other.symmetry * (self.voltage_shifts == 0)
+        if symmetry == 0 or settings.IGNORE_SYMMETRIES:
+            res[1,1] = self[0,1] @ other[1,0] + self[1,1] @ other[1,1]
+            res[1,0] = self[0,0] @ other[1,0] + self[1,0] @ other[1,1]
+        elif self.global_properties.symmetric:
+            # TODO: check symmetric case
+            res[1,1] = res_00_00 + symmetry * res_10_01.floquetConjugate()
+            res[1,0] = symmetry * res[0,1].floquetConjugate()
+        else:
+            res[1,1] = self[1,1] @ other[1,1] + symmetry * res_01_10.floquetConjugate()
+            res[1,0] = self[0,0] @ other[1,0] + self[1,0] @ other[1,1]
+        return res
+
+    def tr(self):
+        return self[0,0] + self[1,1]
+
+    def copy(self):
+        res = ReservoirMatrix(self.global_properties, self.symmetry)
+        res.voltage_shifts = self.voltage_shifts
+        for i in range(2):
+            for j in range(2):
+                res[i,j] = self[i,j].copy()
+        return res
+
+    def __eq__(self, other):
+        return self.global_properties is other.global_properties and np.all(self.data == other.data)
+
+    def shift_energies(self, n=1, derivative=None, **kwargs):
+        '''
+        Apply RGfunction.shift_energies to every entry.
+        '''
+        raise DeprecationWarning("function ReservoirMatrix.shift_energies should not be used")
+        shifted = ReservoirMatrix(self.global_properties, 0)
+        if derivative is None:
+            for i in range(2):
+                for j in range(2):
+                    shifted[i,j] = self[i,j].shift_energies(n=n, derivative=None, **kwargs)
+        else:
+            assert isinstance(derivative, ReservoirMatrix)
+            for i in range(2):
+                for j in range(2):
+                    shifted[i,j] = self[i,j].shift_energies(n=n, derivative=derivative[i,j], **kwargs)
+        return shifted
+
+    def to_numpy_array(self):
+        array = np.ndarray((2,2,*self[0,0].values.shape), dtype=np.complex128)
+        for i in range(2):
+            for j in range(2):
+                array[i,j] = self[i,j].values
+        return array
+
+    def check_symmetry(self):
+        assert self.symmetry in (-1,0,1)
+        if self.global_properties.symmetric:
+            assert np.allclose(self[0,0].values, self[1,1].values)
+        if self.symmetry:
+            assert np.allclose(self[0,1].values, self.symmetry*self[1,0].floquetConjugate().values)
+            self[0,0].check_symmetry()
+            self[1,1].check_symmetry()
+
+
+
+def einsum_34_12_43(a:ReservoirMatrix, b:RGobj, c:ReservoirMatrix) -> RGobj:
+    '''
+    A_34 B_12 C_43
+
+    8 multiplications if b is a scalar,
+    32 multiplications if b is a reservoir matrix without symmetry,
+    20 multiplications if b is a reservoir matrix with symmetry and xL != xR,
+    10 multiplications if b is a reservoir matrix with symmetry and xL == xR.
+    '''
+    assert isinstance(a, ReservoirMatrix)
+    assert isinstance(c, ReservoirMatrix)
+    assert a.global_properties is b.global_properties is c.global_properties
+    assert c.voltage_shifts == 0
+    symmetry = a.symmetry * b.symmetry * c.symmetry
+    if symmetry == 0 or settings.IGNORE_SYMMETRIES:
+        if settings.ENFORCE_SYMMETRIC:
+            raise RuntimeError("Unsymmetric einsum_34_12_43: %d %d %d"%(a.symmetry, b.symmetry, c.symmetry))
+        return a[0,0] @ b @ c[0,0] \
+                + a[0,1] @ b @ c[1,0] \
+                + a[1,0] @ b @ c[0,1] \
+                + a[1,1] @ b @ c[1,1]
+    if not isinstance(b, ReservoirMatrix):
+        res_01_10 = a[0,1] @ b @ c[1,0]
+        if a.global_properties.symmetric:
+            res = 2*a[0,0] @ b @ c[0,0] + res_01_10 + symmetry * res_01_10.floquetConjugate()
+        else:
+            res = a[0,0] @ b @ c[0,0] + a[1,1] @ b @ c[1,1] + res_01_10 + symmetry * res_01_10.floquetConjugate()
+        res.symmetry = symmetry
+        return res
+    if a.global_properties.symmetric:
+        # xL = xR = 0.5
+        res_00_01 = a[0,1] @ b[0,0] @ c[1,0]
+        res_00 = 2 * a[0,0] @ b[0,0] @ c[0,0] + res_00_01 + symmetry * res_00_01.floquetConjugate()
+        res_01 = 2 * a[0,0] @ b[0,1] @ c[0,0] + a[0,1] @ b[0,1] @ c[1,0] + a[1,0] @ b[0,1] @ c[0,1]
+        res = ReservoirMatrix(a.global_properties, symmetry)
+        res.voltage_shifts = a.voltage_shifts + b.voltage_shifts + c.voltage_shifts
+        res[0,0] = res_00
+        res[1,1] = res_00.copy()
+        res[0,1] = res_01
+        res[1,0] = symmetry * res_01.floquetConjugate()
+        return res
+    else:
+        # TODO: check!
+        # xL != xR
+        res_00_01 = a[0,1] @ b[0,0] @ c[1,0]
+        res_11_01 = a[0,1] @ b[1,1] @ c[1,0]
+        res_00 = a[0,0] @ b[0,0] @ c[0,0] + a[1,1] @ b[0,0] @ c[1,1] + res_00_01 + symmetry * res_00_01.floquetConjugate()
+        res_11 = a[0,0] @ b[1,1] @ c[0,0] + a[1,1] @ b[1,1] @ c[1,1] + res_11_01 + symmetry * res_11_01.floquetConjugate()
+        res_01 = a[1,1] @ b[0,1] @ c[1,1] + a[0,0] @ b[0,1] @ c[0,0] + a[0,1] @ b[0,1] @ c[1,0] + a[1,0] @ b[0,1] @ c[0,1]
+        res = ReservoirMatrix(a.global_properties, symmetry)
+        res.voltage_shifts = a.voltage_shifts + b.voltage_shifts + c.voltage_shifts
+        res[0,0] = res_00
+        res[1,1] = res_11
+        res[0,1] = res_01
+        res[1,0] = symmetry * res_01.floquetConjugate()
+        return res
+
+def einsum_34_12_43_double(a:ReservoirMatrix, b:ReservoirMatrix, c:ReservoirMatrix, d:ReservoirMatrix) -> (ReservoirMatrix,ReservoirMatrix):
+    '''
+    A_34 B_12 C_43 , A_34 B_12 D_43
+
+    48 multiplications if b is a reservoir matrix,
+    30 multiplications with symmetries if xL != xR,
+    15 multiplications with symmetries if xL == xR.
+    '''
+    assert isinstance(a, ReservoirMatrix)
+    assert isinstance(b, ReservoirMatrix)
+    assert isinstance(c, ReservoirMatrix)
+    assert isinstance(d, ReservoirMatrix)
+    assert a.global_properties is b.global_properties is c.global_properties is d.global_properties
+    assert c.voltage_shifts == d.voltage_shifts == 0
+    symmetry_c = a.symmetry * b.symmetry * c.symmetry
+    symmetry_d = a.symmetry * b.symmetry * d.symmetry
+    if symmetry_c == 0 or symmetry_d == 0 or settings.IGNORE_SYMMETRIES:
+        if settings.ENFORCE_SYMMETRIC:
+            raise RuntimeError("Unsymmetric einsum_34_12_43_double: %d %d %d %d"%(a.symmetry, b.symmetry, c.symmetry, d.symmetry))
+        ab_00 = a[0,0] @ b
+        ab_01 = a[0,1] @ b
+        ab_10 = a[1,0] @ b
+        ab_11 = a[1,1] @ b
+        return (
+                ab_00 @ c[0,0] + ab_01 @ c[1,0] + ab_10 @ c[0,1] + ab_11 @ c[1,1],
+                ab_00 @ d[0,0] + ab_01 @ d[1,0] + ab_10 @ d[0,1] + ab_11 @ d[1,1]
+                )
+    if not isinstance(b, ReservoirMatrix):
+        raise NotImplementedError
+    if a.global_properties.symmetric:
+        # xL == xR == 0.5
+        ab_00_00 = a[0,0] @ b[0,0]
+        ab_00_01 = a[0,0] @ b[0,1]
+        ab_01_00 = a[0,1] @ b[0,0]
+        ab_01_01 = a[0,1] @ b[0,1]
+        ab_10_01 = a[1,0] @ b[0,1]
+        res_c_00_01 = ab_01_00 @ c[1,0]
+        res_c_00 = 2 * ab_00_00 @ c[0,0] + res_c_00_01 + symmetry_c * res_c_00_01.floquetConjugate()
+        res_c_01 = 2 * ab_00_01 @ c[0,0] + ab_01_01 @ c[1,0] + ab_10_01 @ c[0,1]
+        res_c = ReservoirMatrix(a.global_properties, symmetry_c)
+        res_c.voltage_shifts = a.voltage_shifts + b.voltage_shifts + c.voltage_shifts
+        res_c[0,0] = res_c_00
+        res_c[1,1] = res_c_00.copy()
+        res_c[0,1] = res_c_01
+        res_c[1,0] = symmetry_c * res_c_01.floquetConjugate()
+        res_d_00_01 = ab_01_00 @ d[1,0]
+        res_d_00 = 2 * ab_00_00 @ d[0,0] + res_d_00_01 + symmetry_d * res_d_00_01.floquetConjugate()
+        res_d_01 = 2 * ab_00_01 @ d[0,0] + ab_01_01 @ d[1,0] + ab_10_01 @ d[0,1]
+        res_d = ReservoirMatrix(a.global_properties, symmetry_d)
+        res_d.voltage_shifts = a.voltage_shifts + b.voltage_shifts + d.voltage_shifts
+        res_d[0,0] = res_d_00
+        res_d[1,1] = res_d_00.copy()
+        res_d[0,1] = res_d_01
+        res_d[1,0] = symmetry_d * res_d_01.floquetConjugate()
+        return res_c, res_d
+    else:
+        # TODO: check!
+        # xL != xR
+        ab_00_00 = a[0,0] @ b[0,0]
+        ab_11_00 = a[1,1] @ b[0,0]
+        ab_00_11 = a[0,0] @ b[1,1]
+        ab_11_11 = a[1,1] @ b[1,1]
+        ab_00_01 = a[0,0] @ b[0,1]
+        ab_11_01 = a[1,1] @ b[0,1]
+        ab_01_00 = a[0,1] @ b[0,0]
+        ab_01_11 = a[0,1] @ b[1,1]
+        ab_01_01 = a[0,1] @ b[0,1]
+        ab_10_01 = a[1,0] @ b[0,1]
+        res_c_00_01 = ab_01_00 @ c[1,0]
+        res_c_11_01 = ab_01_11 @ c[1,0]
+        res_c_00 = ab_00_00 @ c[0,0] + ab_11_00 @ c[1,1] + res_c_00_01 + symmetry_c * res_c_00_01.floquetConjugate()
+        res_c_11 = ab_00_11 @ c[0,0] + ab_11_11 @ c[1,1] + res_c_11_01 + symmetry_c * res_c_11_01.floquetConjugate()
+        res_c_01 = ab_11_01 @ c[1,1] + ab_00_01 @ c[0,0] + ab_01_01 @ c[1,0] + ab_10_01 @ c[0,1]
+        res_c = ReservoirMatrix(a.global_properties, symmetry_c)
+        res_c.voltage_shifts = a.voltage_shifts + b.voltage_shifts + c.voltage_shifts
+        res_c[0,0] = res_c_00
+        res_c[1,1] = res_c_11
+        res_c[0,1] = res_c_01
+        res_c[1,0] = symmetry_c * res_c_01.floquetConjugate()
+        res_d_00_01 = ab_01_00 @ d[1,0]
+        res_d_11_01 = ab_01_11 @ d[1,0]
+        res_d_00 = ab_00_00 @ d[0,0] + ab_11_00 @ d[1,1] + res_d_00_01 + symmetry_d * res_d_00_01.floquetConjugate()
+        res_d_11 = ab_00_11 @ d[0,0] + ab_11_11 @ d[1,1] + res_d_11_01 + symmetry_d * res_d_11_01.floquetConjugate()
+        res_d_01 = ab_11_01 @ d[1,1] + ab_00_01 @ d[0,0] + ab_01_01 @ d[1,0] + ab_10_01 @ d[0,1]
+        res_d = ReservoirMatrix(a.global_properties, symmetry_d)
+        res_d.voltage_shifts = a.voltage_shifts + b.voltage_shifts + d.voltage_shifts
+        res_d[0,0] = res_d_00
+        res_d[1,1] = res_d_11
+        res_d[0,1] = res_d_01
+        res_d[1,0] = symmetry_d * res_d_01.floquetConjugate()
+        return res_c, res_d
+        raise NotImplementedError
+
+def product_combinations(a:ReservoirMatrix, b:ReservoirMatrix) -> (ReservoirMatrix,ReservoirMatrix):
+    '''
+    Equivalent to
+
+        lambda a, b: a @ b, a % b
+
+    but more efficient.
+    Arguments must be two ReservoirMatrices.
+
+    12 multiplications (instead of 16) without symmetry,
+    1 ReservoirMatrix multiplication with symmetry
+    -> 4 multiplications with xL == xR,
+    -> 7 multiplications with xL != xR,
+    -> 8 multiplications without symmetry
+    '''
+    assert isinstance(a, ReservoirMatrix)
+    assert isinstance(b, ReservoirMatrix)
+    assert a.global_properties is b.global_properties
+    symmetry = a.symmetry * b.symmetry
+    if symmetry == 0 or settings.IGNORE_SYMMETRIES:
+        if settings.ENFORCE_SYMMETRIC:
+            raise RuntimeError("Unsymmetric product_combinations: %d %d"%(a.symmetry, b.symmetry))
+        ab_0000 = a[0,0] @ b[0,0]
+        ab_0110 = a[0,1] @ b[1,0]
+        ab_1001 = a[1,0] @ b[0,1]
+        ab_1111 = a[1,1] @ b[1,1]
+        ab = ReservoirMatrix(a.global_properties)
+        ab_cross = ReservoirMatrix(a.global_properties)
+        ab[0,0] = ab_0000 + ab_0110
+        ab[1,1] = ab_1001 + ab_1111
+        ab[0,1] = a[0,0] @ b[0,1] + a[0,1] @ b[1,1]
+        ab[1,0] = a[1,0] @ b[0,0] + a[1,1] @ b[1,0]
+        ab_cross[0,0] = ab_0000 + ab_1001
+        ab_cross[1,1] = ab_0110 + ab_1111
+        ab_cross[0,1] = a[0,1] @ b[0,0] + a[1,1] @ b[0,1]
+        ab_cross[1,0] = a[0,0] @ b[1,0] + a[1,0] @ b[1,1]
+        return ab, ab_cross
+
+    ab = a @ b
+    ab_cross = ReservoirMatrix(a.global_properties)
+    ab_cross[0,0] = symmetry * ab[0,0].floquetConjugate()
+    ab_cross[0,1] = symmetry * ab[1,0].floquetConjugate()
+    ab_cross[1,0] = symmetry * ab[0,1].floquetConjugate()
+    ab_cross[1,1] = symmetry * ab[1,1].floquetConjugate()
+    return ab, ab_cross
diff --git a/package/src/frtrg_kondo/rtrg.py b/package/src/frtrg_kondo/rtrg.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c4572bd6c058562baf9b47406f61b194116eeb1
--- /dev/null
+++ b/package/src/frtrg_kondo/rtrg.py
@@ -0,0 +1,576 @@
+# Copyright 2021 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, module defining RG objects
+
+Module defining classes GlobalProperties, RGobj, and RGfunction for objects
+appearing in RG equations. A GlobalProperties object is shared by instances
+of RGobj to ensure they are compatible. RGfunction defines a Floquet matrix.
+
+See also: reservoirmatrix.py, compact_rtrg.py
+"""
+
+import numpy as np
+from numbers import Number
+from scipy.interpolate import interp1d
+from frtrg_kondo import settings
+
+# rtrg_c contains functions written in C to speed up the calculation.
+# In principle these funtions are also available using CUBLAS to boost
+# the matrix multiplication. However, it is usually not efficient to use
+# the CUBLAS version.
+if settings.USE_CUBLAS:
+    try:
+        import frtrg_kondo.rtrg_cublas as rtrg_c
+    except:
+        settings.logger.warning("Failed to load rtrg_cublas, falling back to rtrg_c", exc_info=True)
+        from frtrg_kondo import rtrg_c
+else:
+    from frtrg_kondo import rtrg_c
+
+
+class GlobalRGproperties:
+    '''
+    Shared properties container object for RGfunction.
+    The properties stored in this object must be shared by all RGfunctions
+    which are used together in the same calculation. Usually all RGfunctions
+    own a reference to the same GlobalRGproperties object, which makes it
+    simple to adopt the energies of all these RGfunctions and to check whether
+    different RGfunctions are compatible.
+    '''
+    DEFAULTS = dict(
+            nmax = 0,
+            padding = 0,
+            voltage_branches = 0,
+            resonant_dc_shift = 0,
+            energy = 0j,
+            symmetric = True,
+            mu = None,
+            vdc = 0,
+            clear_corners = 0,
+            )
+
+    def __init__(self, **kwargs):
+        '''
+        expected arguments:
+        energy
+        omega
+        nmax
+        voltage_branches = 0
+        vdc = 0 or mu = 0
+        '''
+        settings.logger.debug('Created new GlobalRGproperties object')
+        self.__dict__.update(kwargs)
+
+    def __getattr__(self, name):
+        '''
+        Return property of take value from DEFAULTS if property is not set.
+        '''
+        try:
+            return self.DEFAULTS[name]
+        except KeyError:
+            raise AttributeError()
+
+    def shape(self):
+        '''
+        Shape of RGfunction.values for all RGfunctions with these global
+        properties.
+        '''
+        floquet_dim = 2*self.__dict__['nmax'] + 1
+        if self.__dict__['voltage_branches']:
+            return (2*self.__dict__['voltage_branches'] + 1, floquet_dim, floquet_dim)
+        else:
+            return (floquet_dim, floquet_dim)
+
+    def copy(self):
+        '''
+        Create a copy of self. It is assumed that all attributes of self are
+        implicitly copied on assignment.
+        '''
+        return GlobalRGproperties(**self.__dict__)
+
+
+class RGobj:
+    def __init__(self, global_properties:GlobalRGproperties, symmetry:int=0):
+        self.global_properties = global_properties
+        self.symmetry = symmetry
+
+    def __getattr__(self, name):
+        return getattr(self.global_properties, name)
+
+
+class RGfunction(RGobj):
+    # Flags:
+    # 1 << 0 : matrix C contiguous
+    # 1 << 1 : matrix F contiguous
+    INV_COUNTER = [0 for i in range(1 << 2)]
+    # Flags:
+    # 1 << 0 : symmetric
+    # 1 << 1 : matrix 1 C contiguous
+    # 1 << 2 : matrix 1 F contiguous
+    # 1 << 3 : matrix 2 C contiguous
+    # 1 << 4 : matrix 2 F contiguous
+    MM_COUNTER = [0 for i in range(1 << 5)]
+    '''
+    Matrix X_{nm}(E) = X_{n-m}(E+mΩ) with n,m = -nmax...nmax
+
+    self.values:
+    This contains an array of the shapes
+    (2*voltage_branches+1, 2*nmax+1, 2*nmax+1) or (2*nmax+1, 2*nmax+1) as values.
+
+    self.voltage_shifts:
+    energy shifts (in units of DC voltage) which should be included in all
+    RGfunctions standing on the right of self.
+    In a product the voltage_shifts of both RGfunctions are added.
+    This is just for book keeping in products of RGfunctions.
+
+    self.global_properties:
+    Additionally, some properties (energy, omega, voltage, nmax, ...)
+    are stored in the shared object global_properties.
+
+    self.symmetry:
+    Valid values are 0, -1, +1. If non-zero, it states the symmetry
+    X[::-1,::-1,::-1] == symmetry * X.conjugate() for this Floquet matrix.
+    '''
+    def __init__(self, global_properties, values=None, voltage_shifts=0, symmetry=0, **kwargs):
+        super().__init__(global_properties, symmetry)
+        self.energy_shifted_copies = {}
+        self.voltage_shifts = voltage_shifts
+        for (key, value) in kwargs.items():
+            setattr(self, key, value)
+        if (values is None):
+            # should be implemented by a class inheriting from RGfunction:
+            self._values = self.initial()
+        elif (type(values) == str and values == 'identity'):
+            self.symmetry = 1
+            if self.voltage_branches:
+                self._values = np.broadcast_to(
+                        np.identity(2*self.nmax+1, dtype=np.complex128).reshape(1, 2*self.nmax+1, 2*self.nmax+1),
+                        self.shape())
+            else:
+                self._values = np.identity(2*self.nmax+1, dtype=np.complex128).T
+        else:
+            self._values = np.asarray(values, dtype=np.complex128)
+            assert self._values.shape[-1] == self._values.shape[-2] == 2*self.nmax+1
+
+    @property
+    def values(self):
+        return self._values
+
+    @values.setter
+    def values(self, values):
+        assert self._values.shape[-1] == self._values.shape[-2] == 2*self.nmax+1
+        self._values = values
+        self.energy_shifted_copies.clear()
+
+    def copy(self):
+        '''
+        Copy only values, take a reference to global_properties.
+        '''
+        return RGfunction(self.global_properties, self._values.copy(), self.voltage_shifts, self.symmetry)
+
+    def floquetConjugate(self):
+        '''
+        For a Floquet matrix A(E)_{nm} this returns the C-transform
+            C A(E)_{nm} C = A(-E*)_{-n,-m}
+        with the superoperator C defined by
+            C x := x^\dag.
+        This uses the symmetry of self if self has a symemtry. If this
+        C-transform leaves self invariant, this function will return a
+        copy of self, but never a reference to self.
+
+        This can only be evaluated if the energy of self lies on the
+        imaginary axis.
+        '''
+        if self.symmetry == 1:
+            return self.copy()
+        elif self.symmetry == -1:
+            return -self
+        if self._values.ndim == 2:
+            assert abs(self.energy.real) < 1e-12
+            return RGfunction(self.global_properties, np.conjugate(self._values[::-1,::-1]), self.voltage_shifts)
+        elif self._values.ndim == 3:
+            assert abs(self.energy.real) < 1e-12
+            return RGfunction(self.global_properties, np.conjugate(self._values[::-1,::-1,::-1]), self.voltage_shifts)
+        else:
+            raise NotImplementedError()
+
+    def __matmul__(self, other):
+        '''
+        Convolution (or product in Floquet space) of two RG functions.
+        Other must be of type RGfunction.
+
+        Note: This is only approximately associative, as long the function
+        converges to 0 for |n| of order of nmax.
+        '''
+        if isinstance(other, SymRGfunction):
+            return NotImplemented
+        if not isinstance(other, RGfunction):
+            return NotImplemented
+        assert self.global_properties is other.global_properties
+        if self._values.ndim == 2 and other._values.ndim == 3:
+            symmetry = self.symmetry * other.symmetry * (self.voltage_shifts == 0)
+            vb = other._values.shape[0]//2
+            matrix = rtrg_c.multiply_extended(other._values[vb+self.voltage_shifts].T, self._values.T, self.padding, symmetry, self.clear_corners).T
+        else:
+            assert self._values.shape == other._values.shape
+            right = other.shift_energies(self.voltage_shifts)
+            symmetry = self.symmetry * other.symmetry * (self.voltage_shifts == 0)
+            matrix = rtrg_c.multiply_extended(right._values.T, self._values.T, self.padding, symmetry*(self._values.ndim==2), self.clear_corners).T
+        if settings.logger.level == settings.logging.DEBUG:
+            RGfunction.MM_COUNTER[(symmetry != 0) | (self._values.T.flags.c_contiguous << 3) | (self._values.T.flags.f_contiguous << 4) | (right._values.T.flags.c_contiguous << 1) | (right._values.T.flags.f_contiguous << 2)]  += 1
+        res = RGfunction( self.global_properties, matrix, self.voltage_shifts + other.voltage_shifts, symmetry )
+        return res
+
+    def __imatmul__(self, other):
+        if isinstance(other, SymRGfunction):
+            return NotImplemented
+        if not isinstance(other, RGfunction):
+            return NotImplemented
+        assert self.global_properties is other.global_properties
+        self.energy_shifted_copies.clear()
+        if self._values.ndim == 2 and other._values.ndim == 3:
+            symmetry = self.symmetry * other.symmetry * (self.voltage_shifts == 0)
+            vb = other._values.shape[0]//2
+            matrix = rtrg_c.multiply_extended(other._values[vb+self.voltage_shifts].T, self._values.T, self.padding, symmetry, self.clear_corners).T
+        else:
+            assert self._values.shape == other._values.shape
+            right = other.shift_energies(self.voltage_shifts)
+            self.symmetry *= other.symmetry
+            self._values = rtrg_c.multiply_extended(right._values.T, self._values.T, self.padding, self.symmetry*(self._values.ndim==2), self.clear_corners, OVERWRITE_LEFT).T
+        if settings.logger.level == settings.logging.DEBUG:
+            RGfunction.MM_COUNTER[(symmetry != 0) | (self._values.T.flags.c_contiguous << 3) | (self._values.T.flags.f_contiguous << 4) | (right._values.T.flags.c_contiguous << 1) | (right._values.T.flags.f_contiguous << 2)]  += 1
+        self.voltage_shifts += other.voltage_shifts
+        return self
+
+    def __add__(self, other):
+        '''
+        Add other to self. If other is a scalar or a scalar function of energy
+        represented by an array of values at self.energies, this treats other
+        as an identity (or diagonal) Floquet matrix.
+        Other must be a scalar or array of same shape as self.energies or an
+        RGfunction of the same shape and energies as self.
+        '''
+        if isinstance(other, RGfunction):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            return RGfunction(self.global_properties, self._values + other._values, self.voltage_shifts, symmetry)
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            newvalues = self._values.copy()
+            newvalues[(...,*np.diag_indices(self._values.shape[-1]))] += other
+            return RGfunction(self.global_properties, newvalues, self.voltage_shifts)
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+
+    def __sub__(self, other):
+        if isinstance(other, RGfunction):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            symmetry = (self.symmetry == other.symmetry) * self.symmetry
+            return RGfunction(self.global_properties, self._values - other._values, self.voltage_shifts, symmetry)
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            newvalues = self._values.copy()
+            newvalues[(...,*np.diag_indices(self._values.shape[-1]))] -= other
+            return RGfunction(self.global_properties, newvalues, self.voltage_shifts)
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+
+    def __neg__(self):
+        '''
+        Return a copy of self with inverted sign of self._values.
+        '''
+        return RGfunction(self.global_properties, -self._values, self.voltage_shifts, self.symmetry)
+
+    def __iadd__(self, other):
+        if isinstance(other, RGfunction):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            self._values += other._values
+            self.symmetry = (self.symmetry == other.symmetry) * self.symmetry
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            self._values[(...,*np.diag_indices(self._values.shape[-1]))] += other
+            self.symmetry = 0
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+        self.energy_shifted_copies.clear()
+        return self
+
+    def __isub__(self, other):
+        if isinstance(other, RGfunction):
+            assert self.global_properties is other.global_properties
+            assert self.voltage_shifts == other.voltage_shifts
+            self._values -= other._values
+            self.symmetry = (self.symmetry == other.symmetry) * self.symmetry
+        elif np.shape(other) == () or np.shape(other) == (2*self.nmax+1,):
+            # TODO: symmetries
+            # Assume that other represents a (possibly energy-dependent) scalar.
+            self._values[(...,*np.diag_indices(self._values.shape[-1]))] -= other
+        else:
+            raise TypeError("unsupported operand types for +: RGfunction and", type(other))
+        self.energy_shifted_copies.clear()
+        return self
+
+    def __mul__(self, other):
+        '''
+        Multiplication by scalar or RGfunction.
+        If other is an RGfunction, this calculates the matrix product
+        (equivalent to __matmul__). If other is a scalar, this multiplies
+        self._values by other.
+        '''
+        if isinstance(other, RGfunction):
+            return self @ other
+        if isinstance(other, Number):
+            if other.imag == 0:
+                symmetry = self.symmetry
+            elif other.real == 0:
+                symmetry = -self.symmetry
+            else:
+                symmetry = 0
+            return RGfunction(self.global_properties, other*self._values, self.voltage_shifts, symmetry)
+        return NotImplemented
+
+    def __imul__(self, other):
+        '''
+        In-place multiplication by scalar or RGfunction.
+        If other is an RGfunction, this calculates the matrix product
+        (equivalent to __matmul__). If other is a scalar, this multiplies
+        self._values by other.
+        '''
+        if isinstance(other, RGfunction):
+            self @= other
+            self.energy_shifted_copies.clear()
+        elif isinstance(other, Number):
+            if other.imag != 0:
+                if other.real == 0:
+                    self.symmetry = -self.symmetry
+                else:
+                    self.symmetry = 0
+            self._values *= other
+            for copy in self.energy_shifted_copies.values():
+                copy *= other
+        else:
+            return NotImplemented
+        return self
+
+    def __rmul__(self, other):
+        '''
+        Reverse multiplication by scalar or RGfunction.
+        If other is an RGfunction, this calculates the matrix product
+        (equivalent to __matmul__). If other is a scalar, this multiplies
+        self._values by other.
+        '''
+        if isinstance(other, RGfunction):
+            return other @ self
+        if isinstance(other, Number):
+            if other.imag == 0:
+                symmetry = self.symmetry
+            elif other.real == 0:
+                symmetry = -self.symmetry
+            else:
+                symmetry = 0
+            return RGfunction(self.global_properties, other*self._values, self.voltage_shifts, symmetry)
+        return NotImplemented
+
+    def __truediv__(self, other):
+        '''
+        Divide self by other, which must be a scalar.
+        '''
+        if isinstance(other, Number):
+            if other.imag == 0:
+                symmetry = self.symmetry
+            elif other.real == 0:
+                symmetry = -self.symmetry
+            else:
+                symmetry = 0
+            return RGfunction(self.global_properties, self._values/other, self.voltage_shifts, symmetry)
+        return NotImplemented
+
+    def __itruediv__(self, other):
+        '''
+        Divide self in-place by other, which must be a scalar.
+        '''
+        if isinstance(other, Number):
+            if other.imag != 0:
+                if other.real == 0:
+                    self.symmetry = -self.symmetry
+                else:
+                    self.symmetry = 0
+            self._values /= other
+            for copy in self.energy_shifted_copies.values():
+                copy /= other
+            return self
+        return NotImplemented
+
+    def __radd__(self, other):
+        return self + other
+
+    def __rsub__(self, other):
+        return -self + other
+
+    def __str__(self):
+        return str(self._values)
+
+    def __repr__(self):
+        return 'RGfunction{ %s,\n%s }'%(self.energy, self._values.__repr__())
+
+    def __getitem__(self, arg):
+        return self._values.__getitem__(arg)
+
+    def __eq__(self, other):
+        return ( self.global_properties is other.global_properties ) and self.voltage_shifts == other.voltage_shifts and np.allclose(self._values, other._values) and self.symmetry == other.symmetry
+
+    def k2lambda(self, shift_matrix=None, calculate_energy_shifts=False):
+        '''
+        shift is usually n*zinv*mu.
+        Assume that self is K_n^m(E) = K_n(E-mΩ).
+        Then calculate Λ_n^m(E) such that (approximately)
+
+                     m    [                   m-k    ]
+            δ   = Σ Λ (E) [ (E-(m-n)Ω) δ   - K   (E) ] .
+             n0   k  k    [             kn    n-k    ]
+
+        This calculates the propagator from an effective Liouvillian.
+        Some of the linear systems of equation which we need to solve here are
+        overdetermined. This means that we can in general only get an
+        approximate solution because an exact solution does not exist.
+        '''
+        if shift_matrix is not None:
+            shift_matrix_vb = shift_matrix._values.shape[0]//2
+        if self._values.ndim == 3 and calculate_energy_shifts:
+            invert_array = np.ndarray((2*self.voltage_branches+5,2*self.nmax+1,2*self.nmax+1), dtype=np.complex128)
+            invert_array[2:-2] = -self._values
+            for v in range(-self.voltage_branches, self.voltage_branches+1):
+                invert_array[(v+2+self.voltage_branches, *np.diag_indices(2*self.nmax+1))] += self.energy + v*self.vdc + self.omega*np.arange(-self.nmax, self.nmax+1)
+                if shift_matrix is not None:
+                    invert_array[v+2+self.voltage_branches] += shift_matrix[v + shift_matrix_vb]
+            dim0 = 2*self.voltage_branches+1
+            if settings.EXTRAPOLATE_VOLTAGE:
+                try:
+                    interp = interp1d(np.arange(dim0), invert_array[2:-2], 'quadratic', axis=0, fill_value='extrapolate')
+                except ValueError:
+                    interp = interp1d(np.arange(dim0), invert_array[2:-2], 'linear', axis=0, fill_value='extrapolate')
+                invert_array[-2:] = interp(dim0 + np.arange(2))
+                invert_array[:2] = interp(np.arange(-2, 0))
+            else:
+                invert_array[-2:] = invert_array[-3]
+                if shift_matrix is not None:
+                    shift_matrix_vb = shift_matrix._values.shape[0]//2
+                    invert_array[-2:] += shift_matrix[shift_matrix_vb+1:shift_matrix_vb+3]
+                    invert_array[:2] = invert_array[2]
+                    invert_array[:2] += shift_matrix[shift_matrix_vb-2:shift_matrix_vb]
+            inverted = rtrg_c.invert_extended(invert_array.T, self.padding, round(settings.LAZY_INVERSE_FACTOR*self.padding)).T
+            if settings.logger.level == settings.logging.DEBUG:
+                RGfunction.INV_COUNTER[invert_array.T.flags.c_contiguous | (invert_array.T.flags.f_contiguous << 1)] += 1
+            symmetry = -1 if (self.symmetry == -1 and shift_matrix.symmetry == -1) else 0
+            res = RGfunction(self.global_properties, inverted[2:-2], voltage_shifts=self.voltage_shifts, symmetry=symmetry)
+            res.energy_shifted_copies[2] = RGfunction(self.global_properties, inverted[4:], self.voltage_shifts, symmetry=0)
+            res.energy_shifted_copies[1] = RGfunction(self.global_properties, inverted[3:-1], self.voltage_shifts, symmetry=0)
+            res.energy_shifted_copies[-1] = RGfunction(self.global_properties, inverted[1:-3], self.voltage_shifts, symmetry=0)
+            res.energy_shifted_copies[-2] = RGfunction(self.global_properties, inverted[:-4], self.voltage_shifts, symmetry=0)
+            return res
+        else:
+            invert = -self
+            if self._values.ndim == 3:
+                for v in range(-self.voltage_branches, self.voltage_branches+1):
+                    invert._values[(v+self.voltage_branches, *np.diag_indices(2*self.nmax+1))] += self.energy + v*self.vdc + self.omega*np.arange(-self.nmax, self.nmax+1)
+                    if shift_matrix is not None:
+                        invert._values[v+self.voltage_branches] += shift_matrix[v+shift_matrix_vb]
+            elif self._values.ndim == 2:
+                invert._values[np.diag_indices(2*self.nmax+1)] += self.energy + self.omega*np.arange(-self.nmax, self.nmax+1)
+                if shift_matrix is not None:
+                    invert._values += shift_matrix[shift_matrix_vb]
+            else:
+                raise ValueError('Invalid RG object (shape %s)'%invert._values.shape)
+            invert.symmetry = -1 if (self.symmetry == -1 and (shift_matrix is None or shift_matrix.symmetry == -1)) else 0
+            return invert.inverse()
+
+    def reduced(self, shift=0):
+        '''
+        Remove voltage-shifted copies
+        '''
+        if self._values.ndim == 2 and shift == 0:
+            return self
+        assert self._values.ndim == 3
+        assert abs(shift) <= self.voltage_branches
+        return RGfunction(self.global_properties, self._values[self.voltage_branches+shift], symmetry=self.symmetry*(shift==0))
+
+    def reduced_to_voltage_branches(self, voltage_branches):
+        if self._values.ndim == 2 or 2*voltage_branches+1 == self._values.shape[0]:
+            return self
+        assert 0 < voltage_branches < self._values.shape[0]//2
+        assert self._values.ndim == 3
+        diff = self._values.shape[0]//2 - voltage_branches
+        return RGfunction(self.global_properties, self._values[diff:-diff], symmetry=self.symmetry, voltage_shifts=self.voltage_shifts)
+
+    def inverse(self):
+        '''
+        multiplicative inverse
+        '''
+        assert self.voltage_shifts == 0
+        if self.padding == 0 or settings.LAZY_INVERSE_FACTOR*self.padding < 0.5:
+            res = np.linalg.inv(self._values)
+        else:
+            try:
+                res = rtrg_c.invert_extended(self._values.T, self.padding, round(settings.LAZY_INVERSE_FACTOR*self.padding)).T
+                if settings.logger.level == settings.logging.DEBUG:
+                    RGfunction.INV_COUNTER[self._values.T.flags.c_contiguous | (self._values.T.flags.f_contiguous << 1)] += 1
+            except:
+                settings.logger.exception("padded inversion failed")
+                res = np.linalg.inv(self._values)
+        return RGfunction(self.global_properties, res, self.voltage_shifts, self.symmetry)
+
+    def shift_energies(self, n=0):
+        # TODO: use symmetries
+        '''
+        Shift energies by n*self.vdc. The calculated RGfunction is kept in cache.
+        Assumptions:
+        * On the last axis of self.energies is linear with values separated by self.vdc
+        * n is an integer
+        * derivative is a RGfunction or None
+        * if the length of the last axis of self.energies is < 2, then derivative must not be None
+        '''
+        if n==0 or (self.vdc==0 and self.mu is None):
+            return self
+        try:
+            return self.energy_shifted_copies[n]
+        except KeyError:
+            assert self._values.ndim == 3
+            newvalues = np.ndarray(self._values.shape, dtype=np.complex128)
+            if settings.EXTRAPOLATE_VOLTAGE:
+                try:
+                    interp = interp1d(np.arange(self._values.shape[0]), self._values, 'quadratic', axis=0, fill_value='extrapolate')
+                except ValueError:
+                    interp = interp1d(np.arange(self._values.shape[0]), self._values, 'linear', axis=0, fill_value='extrapolate')
+                if n > 0:
+                    newvalues[:-n] = self._values[n:]
+                    newvalues[-n:] = interp(self._values.shape[0] + np.arange(n))
+                else:
+                    newvalues[-n:] = self._values[:n]
+                    newvalues[:-n] = interp(np.arange(n, 0))
+            else:
+                if n > 0:
+                    newvalues[:-n] = self._values[n:]
+                    newvalues[-n:] = self._values[-1:]
+                else:
+                    newvalues[-n:] = self._values[:n]
+                    newvalues[:-n] = self._values[:1]
+            self.energy_shifted_copies[n] = RGfunction(self.global_properties, newvalues, self.voltage_shifts)
+            return self.energy_shifted_copies[n]
+
+    def check_symmetry(self):
+        assert self.symmetry in (-1,0,1)
+        if self.symmetry:
+            if self._values.ndim == 2:
+                conjugate = self._values[::-1,::-1].conjugate()
+            elif self._values.ndim == 3:
+                conjugate = self._values[::-1,::-1,::-1].conjugate()
+            assert np.allclose(self._values, self.symmetry*conjugate)
+
+from frtrg_kondo.compact_rtrg import SymRGfunction, OVERWRITE_LEFT, OVERWRITE_BOTH, OVERWRITE_RIGHT
diff --git a/package/src/frtrg_kondo/rtrg_c.c b/package/src/frtrg_kondo/rtrg_c.c
new file mode 100644
index 0000000000000000000000000000000000000000..cb975c9273cbf22a46c42d928db06e01f1ef3b3f
--- /dev/null
+++ b/package/src/frtrg_kondo/rtrg_c.c
@@ -0,0 +1,2888 @@
+/*
+MIT License
+
+Copyright (c) 2021 Valentin Bruch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * @file
+ * @section sec_config Configuration
+ *
+ * The following variables can be defined:
+ * * CBLAS: if defined, use CBLAS instead of directly calling BLAS functions
+ * * LAPACK_C: include LAPACK C header instead of just linking to LAPACK
+ * * PARALLEL_EXTRA_DIMS: use OpenMP to parallelize repeated operations over
+ *          extra dimensions of arrays. Note that internal parallelization of
+ *          BLAS functions might be faster.
+ * * PARALLEL_EXTRAPOLATION: use OpenMP to parallelize the extrapolation loops.
+ *          This is usually helpful except for small matrices.
+ * * PARALLEL: Do some matrix products simultaneously (in parallel) using
+ *          OpenMP. This can be useful, but might also decrease performance
+ *          because internal parallelization of the BLAS functions is much
+ *          more efficient.
+ * * PARALLELIZE_CORRECTION_THREADS_THRESHOLD: number of threads at which
+ *          maximal parallelization is used. In practice this maximal
+ *          parallelization does not seem very useful and a high value
+ *          is recommended.
+ * * DEBUG: print debugging information to stderr. This is neither complete
+ *          nor really useful.
+ * The following macros can be redefined to optimize performance, adapt to your
+ * BLAS and LAPACK installation, and adapt to the concrete mathematical problem.
+ * * TRIANGULAR_OPTIMIZE_THRESHOLD: Threshold for subdividing multiplication
+ *          of two triangular matrices (see below).
+ * * extrapolate: function for extrapolation of unknown matrix elements
+ * * complex_type, NPY_COMPLEX_TYPE: data type or basically everything
+ * * gemm, trmm: (C)BLAS function names, need to be adapted to complex_type.
+ * * getrf, getri: LAPACK function names, need to be adapted to complex_type.
+ */
+
+
+/**
+ * Threshold for subdividing multiplication of two triangular matrices
+ * (multiply_LU_inplace and multiply_UL_inplace).
+ * The optimal value for this probably depends on the parallelization
+ * used in BLAS functions. When using a GPU for matrix multiplication,
+ * you should probably choose a large value here. Be aware that the
+ * functions implemented here may be slow on a GPU.
+ * If a matrix is smaller (less rows/columns) than this threshold, then
+ * trmm from BLAS is used directly, which discards the fact that the
+ * left matrix is also triangular. Otherwise the problem is recursively
+ * subdivided. */
+#define TRIANGULAR_OPTIMIZE_THRESHOLD 128
+
+#define PARALLELIZE_CORRECTION_THREADS_THRESHOLD 16
+
+/**
+ * Simple linear extrapolation based on the last 3 elements.
+ * Given the mapping {0:a, -1:b, -2:c} estimate the value at i. */
+#define extrapolate(i, a, b, c) ((1 + 0.75*i)*(a) - 0.5*i*((b) + 0.5*(c)))
+
+/* Define data type and select CBLAS and LAPACK functions accordingly */
+#include <complex.h>
+#define complex_type complex double
+#define NPY_COMPLEX_TYPE NPY_COMPLEX128
+#define lapack_complex_double complex_type
+
+#ifdef LAPACK_C
+#include <lapack.h>
+//#include <mkl_lapack.h>
+#define getrf LAPACK_zgetrf
+#define getri LAPACK_zgetri
+#else /* LAPACK_C */
+#define getrf zgetrf_
+#define getri zgetri_
+extern void zgetrf_(const int*, const int*, double complex*, const int*, int*, int*);
+extern void zgetri_(const int*, double complex*, const int*, const int*, complex double*, const int*, int*);
+#endif /* LAPACK_C */
+
+#ifdef CBLAS
+#include <cblas.h>
+//#include <mkl_cblas.h>
+#define gemm cblas_zgemm
+#define trmm cblas_ztrmm
+#else /* CBLAS */
+extern void zgemm_(const char*, const char*, const int*, const int*, const int*, const complex double*, const complex double*, const int*, const complex double*, const int*, const double complex*, double complex*, const int*);
+extern void ztrmm_(const char*, const char*, const char*, const char*, const int*, const int*, const complex double*, const complex double*, const int*, complex double*, const int*);
+#define gemm zgemm_
+#define trmm ztrmm_
+static const char N='N', L='L', R='R', U='U';
+#endif /* CBLAS */
+
+static const complex_type zero = 0.;
+static const complex_type one = 1.;
+
+
+#define PY_SSIZE_T_CLEAN
+
+#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
+
+#include <Python.h>
+#include <numpy/arrayobject.h>
+
+#ifdef DEBUG
+#include <stdio.h>
+#endif
+
+/* It is easy to parallelize the iteration over extra dimensions.
+ * But this seems to be quite inefficient and does not speed up the
+ * calculations. If you want to use PARALLEL_EXTRA_DIMS, you should
+ * first test whether it gives you an advantage.*/
+#if defined(PARALLEL_EXTRA_DIMS) || defined(PARALLEL) || defined(PARALLEL_EXTRAPOLATION)
+#include <omp.h>
+#endif
+
+/**
+ * Flags for overwriting input in symmetric matrix multiplication.
+ * Symmetric matrix multiplication can be faster if it is allowed to overwrite
+ * the left matrix. This enumerator defines flags for this option. */
+enum {
+    OVERWRITE_LEFT = 1 << 0,
+    OVERWRITE_RIGHT = 1 << 1,
+};
+
+
+/**
+ * @file
+ * @section sec_extrapolate_multiplication Extrapolate matrix for multiplication
+ *
+ * For example, for cutoff = 2 and a matrix
+ *
+ *          m00 m01 m02 m03 m04 m05
+ *          m10 m11 m12 m13 m14 m15
+ *          m20 m21 m22 m23 m24 m25
+ *          m30 m31 m32 m33 m34 m35
+ *          m40 m41 m42 m43 m44 m45
+ *
+ * the following functions fill the arrays t, l, r, b in the extrapolated
+ * matrix
+ *
+ *          t00
+ *          t10 t11
+ *  l00 l01 m00 m01 m02 m03 m04 m05
+ *      l11 m10 m11 m12 m13 m14 m15
+ *          m20 m21 m22 m23 m24 m25
+ *          m30 m31 m32 m33 m34 m35 r00
+ *          m40 m41 m42 m43 m44 m45 r10 r11
+ *                          b00 b01
+ *                              b11
+ *
+ * In this representation, the pointers handed to the following functions have
+ * the meaning:
+ * extrapolate_top:    output=t00, input=m00
+ * extrapolate_left:   output=l00, input=m00
+ * extrapolate_bottom: output=b00, input=m45
+ * extrapolate_right:  output=r00, input=m45
+ *
+ * @todo This could be parallelized.
+ */
+
+/**
+ * Extrapolate matrix to the top.
+ * input and output must be in columns major order (Fortran style).
+ * output is treated as a square matrix, it must have at least cutoff
+ * columns.
+ *
+ * The following requirements are not explicitly checked:
+ * * out_rows >= cutoff
+ * * rows_in >= 3
+ * * cols_in >= cutoff + 3
+ *
+ * @see extrapolate_top_full()
+ * @see extrapolate_bottom()
+ * @see extrapolate_left()
+ * @see extrapolate_right()
+ */
+static void extrapolate_top(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_top\n");
+#endif
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp parallel
+    {
+        complex_type row0, row1, row2;
+        int chunk, i;
+#pragma omp for
+        for(chunk=1; chunk <= cutoff; chunk++)
+        {
+            row0 = input[chunk*rows_in];
+            row1 = input[(chunk+1)*rows_in+1];
+            row2 = input[(chunk+2)*rows_in+2];
+            i = chunk + 1;
+            while(--i > 0)
+                output[cutoff-chunk+(chunk-i)*(out_rows+1)] = extrapolate(i, row0, row1, row2);
+        }
+    }
+#else /* PARALLEL_EXTRAPOLATION */
+    input += rows_in;
+    const complex_type *row1 = input + rows_in + 1;
+    const complex_type *row2 = row1 + rows_in + 1;
+    complex_type *write;
+    int chunk = 1, i;
+    while (chunk <= cutoff)
+    {
+        write = output + cutoff - chunk;
+        i = ++chunk;
+        while (--i > 0)
+        {
+            *write = extrapolate(i, *input, *row1, *row2);
+            write += out_rows + 1;
+        }
+        input += rows_in;
+        row1 += rows_in;
+        row2 += rows_in;
+    }
+#endif /* PARALLEL_EXTRAPOLATION */
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_top\n");
+#endif
+}
+
+/**
+ * Extrapolate matrix to the bottom.
+ * input and output must be in columns major order (Fortran style).
+ * output is treated as a square matrix, it must have at least cutoff
+ * columns.
+ *
+ * input points to the last element of the matrix!
+ *
+ * The following requirements are not explicitly checked:
+ * * rows_in >= 3
+ * * cols_in >= cutoff + 3
+ * * out_rows >= cutoff
+ *
+ * @see extrapolate_top()
+ * @see extrapolate_bottom_full()
+ * @see extrapolate_left()
+ * @see extrapolate_right()
+ */
+static void extrapolate_bottom(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_bottom\n");
+#endif
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp parallel
+    {
+        complex_type row0, row1, row2;
+        int i, chunk;
+#pragma omp for
+        for(chunk=1; chunk <= cutoff; chunk++)
+        {
+            row0 = input[-chunk*rows_in];
+            row1 = input[-(chunk+1)*rows_in-1];
+            row2 = input[-(chunk+2)*rows_in-2];
+            i = chunk + 1;
+            while(--i > 0)
+                output[chunk - cutoff + (cutoff - 1 - chunk + i)*(out_rows+1)] = extrapolate(i, row0, row1, row2);
+        }
+    }
+#else /* PARALLEL_EXTRAPOLATION */
+    input -= rows_in;
+    const complex_type *row1 = input - rows_in - 1;
+    const complex_type *row2 = row1 - rows_in - 1;
+    complex_type *write;
+    int chunk = 1, i;
+    while (chunk <= cutoff)
+    {
+        write = output + (cutoff - 1) * out_rows + chunk - 1;
+        i = ++chunk;
+        while (--i > 0)
+        {
+            *write = extrapolate(i, *input, *row1, *row2);
+            write -= out_rows + 1;
+        }
+        input -= rows_in;
+        row1 -= rows_in;
+        row2 -= rows_in;
+    }
+#endif /* PARALLEL_EXTRAPOLATION */
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_bottom\n");
+#endif
+}
+
+/**
+ * Extrapolate matrix to the left.
+ * input and output must be in columns major order (Fortran style).
+ * output is treated as a square matrix, it must have at least cutoff
+ * columns.
+ * The input matrix must have at least 3 columns.
+ *
+ * The following requirements are not explicitly checked:
+ * * rows_in >= cutoff + 3
+ * * out_rows >= cutoff
+ *
+ * @see extrapolate_top()
+ * @see extrapolate_bottom()
+ * @see extrapolate_left_full()
+ * @see extrapolate_right()
+ */
+static void extrapolate_left(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_left\n");
+#endif
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp parallel
+    {
+    int j, jmax;
+#pragma omp for
+        for (int i=1; i <= cutoff; i++)
+        {
+            jmax = cutoff - i;
+            j = -1;
+            while (++j <= jmax)
+                output[out_rows * jmax + j] = extrapolate(i, input[i+j], input[i+j+rows_in+1], input[i+j+2*(rows_in+1)]);
+        }
+    }
+#else /* PARALLEL_EXTRAPOLATION */
+    ++input;
+    const complex_type *col1 = input + rows_in + 1;
+    const complex_type *col2 = col1 + rows_in + 1;
+    output += out_rows * (cutoff - 1);
+    int i=0, j, jmax;
+    while (++i <= cutoff)
+    {
+        jmax = cutoff - i;
+        j = -1;
+        while (++j <= jmax)
+            output[j] = extrapolate(i, input[j], col1[j], col2[j]);
+        output -= out_rows;
+        ++input;
+        ++col1;
+        ++col2;
+    }
+#endif /* PARALLEL_EXTRAPOLATION */
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_left\n");
+#endif
+}
+
+/**
+ * Extrapolate matrix to the right.
+ * input and output must be in columns major order (Fortran style).
+ * output is treated as a square matrix, it must have at least cutoff
+ * columns.
+ * The input matrix must have at least 3 columns.
+ *
+ * input points to the last element of the matrix!
+ *
+ * The following requirements are not explicitly checked:
+ * rows_in >= cutoff + 3
+ * out_rows >= cutoff
+ *
+ * @see extrapolate_top()
+ * @see extrapolate_bottom()
+ * @see extrapolate_left()
+ * @see extrapolate_right_full()
+ */
+static void extrapolate_right(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_right\n");
+#endif
+    input -= cutoff;
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp parallel
+    {
+    int j, jmax;
+#pragma omp for
+        for (int i=1; i <= cutoff; i++)
+        {
+            jmax = cutoff - i;
+            j = -1;
+            while (++j <= jmax)
+                output[(out_rows+1) * (i-1) + j] = extrapolate(i, input[j], input[j-rows_in-1], input[j-2*rows_in-2]);
+        }
+    }
+#else /* PARALLEL_EXTRAPOLATION */
+    const complex_type *col1 = input - rows_in - 1;
+    const complex_type *col2 = col1 - rows_in - 1;
+    int i=0, j, jmax;
+    while (i < cutoff)
+    {
+        jmax = cutoff - ++i;
+        j = -1;
+        while (++j <= jmax)
+            output[j] = extrapolate(i, input[j], col1[j], col2[j]);
+        output += out_rows + 1;
+    }
+#endif /* PARALLEL_EXTRAPOLATION */
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_right\n");
+#endif
+}
+
+
+/**
+ * @file
+ * @section sec_extrapolate_inversion Extrapolate matrix for inversion
+ *
+ * For example, for cutoff = 2 and a matrix
+ *
+ *          m00 m01 m02 m03 m04 m05
+ *          m10 m11 m12 m13 m14 m15
+ *          m20 m21 m22 m23 m24 m25
+ *          m30 m31 m32 m33 m34 m35
+ *          m40 m41 m42 m43 m44 m45
+ *
+ * the following functions fill the arrays t, l, r, b in the extrapolated
+ * matrix
+ *
+ *  l00 t01 t02
+ *  l10 l11 t12 t13
+ *  l20 l21 m00 m01 m02 m03 m04 m05
+ *      l31 m10 m11 m12 m13 m14 m15
+ *          m20 m21 m22 m23 m24 m25
+ *          m30 m31 m32 m33 m34 m35 r00
+ *          m40 m41 m42 m43 m44 m45 r10 r11
+ *                          b00 b01 r20 r21
+ *                              b11 b12 r31
+ *
+ * In this representation, the pointers handed to the following functions have
+ * the meaning:
+ * extrapolate_top:    output=t00, input=m00
+ * extrapolate_left:   output=l00, input=m00
+ * extrapolate_bottom: output=b00, input=m45
+ * extrapolate_right:  output=r00, input=m45
+ */
+
+/**
+ * Extrapolate matrix to the top.
+ * input and output must be in columns major order (Fortran style).
+ * output must have at least 2*cutoff columns.
+ *
+ * The following requirements are not explicitly checked:
+ * * out_rows >= cutoff
+ * * rows_in >= 3
+ * * cols_in >= cutoff + 3
+ *
+ * @see extrapolate_top()
+ * @see extrapolate_bottom_full()
+ * @see extrapolate_left_full()
+ * @see extrapolate_right_full()
+ */
+static void extrapolate_top_full(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_top_full\n");
+#endif
+    input += rows_in;
+    const complex_type
+        *row1 = input + rows_in + 1,
+        *row2 = row1 + rows_in + 1,
+        *endin = input + cutoff*rows_in;
+    int i;
+    output += out_rows;
+    while (input < endin)
+    {
+        i = cutoff + 1;
+        while (--i > 0)
+        {
+            *output = extrapolate(i, *input, *row1, *row2);
+            output += out_rows + 1;
+        }
+        output -= out_rows * (cutoff - 1) + cutoff;
+        input += rows_in;
+        row1 += rows_in;
+        row2 += rows_in;
+    }
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_top_full\n");
+#endif
+}
+
+/**
+ * Extrapolate matrix to the bottom.
+ * input and output must be in columns major order (Fortran style).
+ * output must have at least 2*cutoff columns.
+ *
+ * input points to the last element of the matrix!
+ *
+ * The following requirements are not explicitly checked:
+ * * rows_in >= 3
+ * * cols_in >= cutoff + 3
+ * * out_rows >= cutoff
+ *
+ * @see extrapolate_top_full()
+ * @see extrapolate_bottom()
+ * @see extrapolate_left_full()
+ * @see extrapolate_right_full()
+ */
+static void extrapolate_bottom_full(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_bottom_full\n");
+#endif
+    input -= rows_in;
+    const complex_type
+        *row1 = input - rows_in - 1,
+        *row2 = row1 - rows_in - 1,
+        *endin = input - cutoff*rows_in;
+    int i;
+    output += (2*cutoff-2)*out_rows + cutoff - 1;
+    while (input > endin)
+    {
+        i = cutoff + 1;
+        while (--i > 0)
+        {
+            *output = extrapolate(i, *input, *row1, *row2);
+            output -= out_rows + 1;
+        }
+        output += (cutoff - 1) * out_rows + cutoff;
+        input -= rows_in;
+        row1 -= rows_in;
+        row2 -= rows_in;
+    }
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_bottom_full\n");
+#endif
+}
+
+/**
+ * Extrapolate matrix to the left.
+ * input and output must be in columns major order (Fortran style).
+ * output must have at least 2*cutoff columns.
+ * The input matrix must have at least 3 columns.
+ *
+ * The following requirements are not explicitly checked:
+ * * rows_in >= cutoff + 3
+ * * out_rows >= cutoff
+ *
+ * @see extrapolate_top_full()
+ * @see extrapolate_bottom_full()
+ * @see extrapolate_left()
+ * @see extrapolate_right_full()
+ */
+static void extrapolate_left_full(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_left_full\n");
+#endif
+    const complex_type *col1 = input + rows_in + 1;
+    const complex_type *col2 = col1 + rows_in + 1;
+    output += out_rows * (cutoff - 1) + cutoff - 1;
+    int i=0, j;
+    while (++i <= cutoff)
+    {
+        j = -1;
+        while (++j <= cutoff)
+            output[j] = extrapolate(i, input[j], col1[j], col2[j]);
+        output -= out_rows + 1;
+    }
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_left_full\n");
+#endif
+}
+
+/**
+ * Extrapolate matrix to the right.
+ * input and output must be in columns major order (Fortran style).
+ * output must have at least 2*cutoff columns.
+ * The input matrix must have at least 3 columns.
+ *
+ * input points to the last element of the matrix!
+ *
+ * The following requirements are not explicitly checked:
+ * * rows_in >= cutoff + 3
+ * * out_rows >= cutoff
+ *
+ * @see extrapolate_top_full()
+ * @see extrapolate_bottom_full()
+ * @see extrapolate_left_full()
+ * @see extrapolate_right()
+ */
+static void extrapolate_right_full(
+        const complex_type *input,
+        const int rows_in,
+        complex_type *output,
+        const int cutoff,
+        const int out_rows
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting extrapolate_right_full\n");
+#endif
+    input -= cutoff;
+    const complex_type *col1 = input - rows_in - 1;
+    const complex_type *col2 = col1 - rows_in - 1;
+    int i=0, j;
+    while (++i <= cutoff)
+    {
+        j = -1;
+        while (++j <= cutoff)
+            output[j] = extrapolate(i, input[j], col1[j], col2[j]);
+        output += out_rows + 1;
+    }
+#ifdef DEBUG
+    fprintf(stderr, "Done extrapolate_right_full\n");
+#endif
+}
+
+
+
+/**
+ * @file
+ * @section sec_helper_functions Helper functions for multiplication
+ */
+
+/**
+ * Multiply upper and lower triangular matrix.
+ * @param A upper triangular matrix, fortran ordered. A will be overwritten by the prodyct AB.
+ * @param B lower triangular matrix, fortran ordered.
+ *
+ * Both matrices are in Fortran order (columns major) not unit triangular.
+ * Both matrices must have shape (size, size).
+ *
+ * @see multiply_LU_inplace()
+ */
+static void multiply_UL_inplace(
+        const int size,
+        complex_type *a,
+        const int a_dim0,
+        const complex_type *b,
+        const int b_dim0
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting multiply_UL_inplace %d\n", size);
+#endif
+    if (size < TRIANGULAR_OPTIMIZE_THRESHOLD)
+    {
+#ifdef CBLAS
+        trmm(
+                CblasColMajor, // layout
+                CblasRight, // order: this means B := B A
+                CblasLower, // A is a lower triangular matrix
+                CblasNoTrans, // A is not modified (no adjoint or transpose)
+                CblasNonUnit, // A is not unit triangular
+                size, // rows of B (int)
+                size, // columns of B (int)
+                &one, // global prefactor
+                b, // matrix A
+                b_dim0, // first dimension of A (int)
+                a, // matrix B
+                a_dim0  // first dimension of B (int)
+                );
+#else /* CBLAS */
+        trmm(
+                &R, // order: this means B := B A
+                &L, // A is a lower triangular matrix
+                &N, // A is not modified (no adjoint or transpose)
+                &N, // A is not unit triangular
+                &size, // rows of B (int)
+                &size, // columns of B (int)
+                &one, // global prefactor
+                b, // matrix A
+                &b_dim0, // first dimension of A (int)
+                a, // matrix B
+                &a_dim0  // first dimension of B (int)
+                );
+#endif /* CBLAS */
+        return;
+    }
+    /* TODO: The following commands can be parallelized (partially) */
+
+    /*
+     * Matrices are labeled as follows:
+     *
+     * A = [ Aa  Ab ]  B = [ Ba  Bb ]
+     *     [ Ac  Ad ]      [ Bc  Bd ]
+     * Initially Ac = Bb = 0.
+     */
+    const int
+        part1 = size / 2,
+        part2 = size - part1;
+
+    /* Step 1: overwrite Ac with Ad Bc.
+     * This requires that first Bc is copied to Ac. */
+    a += part1;
+    b += part1;
+    int i=-1;
+    while (++i<part1)
+        memcpy( a + i*a_dim0, b + i*b_dim0, part2 * sizeof(complex_type) );
+    a -= part1;
+    b -= part1;
+#ifdef CBLAS
+    trmm(
+            CblasColMajor, // layout
+            CblasLeft, // order: this means B := A B
+            CblasUpper, // A is an upper triangular matrix
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNonUnit, // A is not unit triangular
+            part2, // rows of B (int)
+            part1, // columns of B (int)
+            &one, // global prefactor
+            a + part1*(1 + a_dim0), // matrix A
+            a_dim0, // first dimension of A (int)
+            a + part1, // matrix B
+            a_dim0  // first dimension of B (int)
+            );
+#else /* CBLAS */
+    trmm(
+            &L, // order: this means B := A B
+            &U, // A is an upper triangular matrix
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // A is not unit triangular
+            &part2, // rows of B (int)
+            &part1, // columns of B (int)
+            &one, // global prefactor
+            a + part1*(1 + a_dim0), // matrix A
+            &a_dim0, // first dimension of A (int)
+            a + part1, // matrix B
+            &a_dim0  // first dimension of B (int)
+            );
+#endif /* CBLAS */
+
+    /* Step 2: overwrite Ad with Ad Bd */
+    multiply_UL_inplace(part2, a + (a_dim0+1)*part1, a_dim0, b + (b_dim0+1)*part1, b_dim0);
+    /* Step 3: overwrite Aa with Aa Ba */
+    multiply_UL_inplace(part1, a, a_dim0, b, b_dim0);
+    /* Step 4: add Ab Bc to Aa */
+#ifdef CBLAS
+    gemm(
+            CblasColMajor, // layout
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNoTrans, // B is not modified (no adjoint or transpose)
+            part1, // rows of A (int)
+            part1, // columns of B (int)
+            part2, // columns of A = rows of B (int)
+            &one, // global prefactor
+            a + part1*a_dim0, // matrix A
+            a_dim0, // first dimension of A (int)
+            b + part1, // matrix B
+            b_dim0,  // first dimension of B (int)
+            &one, // weight of C
+            a, // matrix C
+            a_dim0 // first dimension of C
+            );
+#else /* CBLAS */
+    gemm(
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // B is not modified (no adjoint or transpose)
+            &part1, // rows of A (int)
+            &part1, // columns of B (int)
+            &part2, // columns of A = rows of B (int)
+            &one, // global prefactor
+            a + part1*a_dim0, // matrix A
+            &a_dim0, // first dimension of A (int)
+            b + part1, // matrix B
+            &b_dim0,  // first dimension of B (int)
+            &one, // weight of C
+            a, // matrix C
+            &a_dim0 // first dimension of C
+            );
+#endif /* CBLAS */
+
+    /* Step 5: overwrite Ab with Ab Bd */
+#ifdef CBLAS
+    trmm(
+            CblasColMajor, // layout
+            CblasRight, // order: this means B := B A
+            CblasLower, // A is a lower triangular matrix
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNonUnit, // A is not unit triangular
+            part1, // rows of B (int)
+            part2, // columns of B (int)
+            &one, // global prefactor
+            b + (1 + b_dim0)*part1, // matrix A
+            b_dim0, // first dimension of A (int)
+            a + part1*a_dim0, // matrix B
+            a_dim0  // first dimension of B (int)
+            );
+#else /* CBLAS */
+    trmm(
+            &R, // order: this means B := B A
+            &L, // A is a lower triangular matrix
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // A is not unit triangular
+            &part1, // rows of B (int)
+            &part2, // columns of B (int)
+            &one, // global prefactor
+            b + (1 + b_dim0)*part1, // matrix A
+            &b_dim0, // first dimension of A (int)
+            a + part1*a_dim0, // matrix B
+            &a_dim0  // first dimension of B (int)
+            );
+#endif /* CBLAS */
+}
+
+/**
+ * Multiply lower and upper triangular matrix.
+ * @param A lower triangular matrix, fortran ordered. A will be overwritten by the prodyct AB.
+ * @param B upper triangular matrix, fortran ordered.
+ *
+ * Both matrices are in Fortran order (columns major) not unit triangular.
+ * Both matrices must have shape (size, size).
+ *
+ * @see multiply_UL_inplace()
+ */
+static void multiply_LU_inplace(
+        const int size,
+        complex_type *a,
+        const int a_dim0,
+        const complex_type *b,
+        const int b_dim0
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Starting multiply_LU_inplace %d\n", size);
+#endif
+    if (size < TRIANGULAR_OPTIMIZE_THRESHOLD)
+    {
+#ifdef CBLAS
+        trmm(
+                CblasColMajor, // layout
+                CblasRight, // order: this means B := B A
+                CblasUpper, // A is an upper triangular matrix
+                CblasNoTrans, // A is not modified (no adjoint or transpose)
+                CblasNonUnit, // A is not unit triangular
+                size, // rows of B (int)
+                size, // columns of B (int)
+                &one, // global prefactor
+                b, // matrix A
+                b_dim0, // first dimension of A (int)
+                a, // matrix B
+                a_dim0  // first dimension of B (int)
+                );
+#else /* CBLAS */
+        trmm(
+                &R, // order: this means B := B A
+                &U, // A is a lower triangular matrix
+                &N, // A is not modified (no adjoint or transpose)
+                &N, // A is not unit triangular
+                &size, // rows of B (int)
+                &size, // columns of B (int)
+                &one, // global prefactor
+                b, // matrix A
+                &b_dim0, // first dimension of A (int)
+                a, // matrix B
+                &a_dim0  // first dimension of B (int)
+                );
+#endif /* CBLAS */
+        return;
+    }
+    /* TODO: The following commands can be parallelized (partially) */
+    /*
+     * Matrices are labeled as follows:
+     *
+     * A = [ Aa  Ab ]  B = [ Ba  Bb ]
+     *     [ Ac  Ad ]      [ Bc  Bd ]
+     * Initially Ab = Bc = 0.
+     */
+    const int
+        part1 = size / 2,
+        part2 = size - part1;
+
+    /* Step 1: overwrite Ab with Aa Bb.
+     * This requires that first Bb is copied to Ab. */
+    a += a_dim0*part1;
+    b += b_dim0*part1;
+    int i=-1;
+    while (++i<part2)
+        memcpy( a + i*a_dim0, b + i*b_dim0, part1 * sizeof(complex_type) );
+    a -= a_dim0*part1;
+    b -= b_dim0*part1;
+
+#ifdef CBLAS
+    trmm(
+            CblasColMajor, // layout
+            CblasLeft, // order: this means B := A B
+            CblasLower, // A is a lower triangular matrix
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNonUnit, // A is not unit triangular
+            part1, // rows of B (int)
+            part2, // columns of B (int)
+            &one, // global prefactor
+            a, // matrix A
+            a_dim0, // first dimension of A (int)
+            a + a_dim0*part1, // matrix B
+            a_dim0  // first dimension of B (int)
+            );
+#else /* CBLAS */
+    trmm(
+            &L, // order: this means B := A B
+            &L, // A is a lower triangular matrix
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // A is not unit triangular
+            &part1, // rows of B (int)
+            &part2, // columns of B (int)
+            &one, // global prefactor
+            a, // matrix A
+            &a_dim0, // first dimension of A (int)
+            a + a_dim0*part1, // matrix B
+            &a_dim0  // first dimension of B (int)
+            );
+#endif /* CBLAS */
+
+    /* Step 2: overwrite Aa with Aa Ba */
+    multiply_LU_inplace(part1, a, a_dim0, b, b_dim0);
+    /* Step 3: overwrite Ad with Ad Bd */
+    multiply_LU_inplace(part2, a + (a_dim0+1)*part1, a_dim0, b + (b_dim0+1)*part1, b_dim0);
+    /* Step 4: add Ac Bb to Ad */
+#ifdef CBLAS
+    gemm(
+            CblasColMajor, // layout
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNoTrans, // B is not modified (no adjoint or transpose)
+            part2, // rows of A (int)
+            part2, // columns of B (int)
+            part1, // columns of A = rows of B (int)
+            &one, // global prefactor
+            a + part1, // matrix A
+            a_dim0, // first dimension of A (int)
+            b + part1*b_dim0, // matrix B
+            b_dim0,  // first dimension of B (int)
+            &one, // weight of C
+            a + (a_dim0+1)*part1, // matrix C
+            a_dim0 // first dimension of C
+            );
+#else /* CBLAS */
+    gemm(
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // B is not modified (no adjoint or transpose)
+            &part2, // rows of A (int)
+            &part2, // columns of B (int)
+            &part1, // columns of A = rows of B (int)
+            &one, // global prefactor
+            a + part1, // matrix A
+            &a_dim0, // first dimension of A (int)
+            b + part1*b_dim0, // matrix B
+            &b_dim0,  // first dimension of B (int)
+            &one, // weight of C
+            a + (a_dim0+1)*part1, // matrix C
+            &a_dim0 // first dimension of C
+            );
+#endif /* CBLAS */
+
+    /* Step 5: overwrite Ac with Ac Ba */
+#ifdef CBLAS
+    trmm(
+            CblasColMajor, // layout
+            CblasRight, // order: this means B := B A
+            CblasUpper, // A is an upper triangular matrix
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNonUnit, // A is not unit triangular
+            part2, // rows of B (int)
+            part1, // columns of B (int)
+            &one, // global prefactor
+            b, // matrix A
+            b_dim0, // first dimension of A (int)
+            a + part1, // matrix B
+            a_dim0  // first dimension of B (int)
+            );
+#else /* CBLAS */
+    trmm(
+            &R, // order: this means B := B A
+            &U, // A is an upper triangular matrix
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // A is not unit triangular
+            &part2, // rows of B (int)
+            &part1, // columns of B (int)
+            &one, // global prefactor
+            b, // matrix A
+            &b_dim0, // first dimension of A (int)
+            a + part1, // matrix B
+            &a_dim0  // first dimension of B (int)
+            );
+#endif /* CBLAS */
+}
+
+
+/**
+ * @file
+ * @section sec_extend Extend matrix
+ */
+
+/**
+ * @param input in_rows × in_cols
+ * @param output (in_rows + 2*cutoff) × (in_cols + 2*cutoff)
+ */
+static void extend_matrix_worker(
+        const int nrow_in,
+        const int ncol_in,
+        const int cutoff,
+        const complex_type *input,
+        const int in_dim0,
+        complex_type *output,
+        const int out_dim0
+        )
+{
+    /* Copy the matrix. */
+    complex_type *auxptr = output + cutoff * (out_dim0 + 1);
+    int i=-1;
+    while (++i<ncol_in)
+        memcpy( auxptr + i*out_dim0, input + i*in_dim0, nrow_in * sizeof(complex_type) );
+
+    if (cutoff <= 0)
+        return;
+
+    /* outptr points to the first element of the original matrix in the extended matrix. */
+#ifdef PARALLEL
+#pragma omp sections
+    {
+#pragma omp section
+#endif /* PARALLEL */
+        extrapolate_top_full(
+                auxptr,
+                nrow_in+2*cutoff,
+                output,
+                cutoff,
+                nrow_in+2*cutoff
+                );
+#ifdef PARALLEL
+#pragma omp section
+#endif
+        extrapolate_left_full(
+                auxptr,
+                nrow_in+2*cutoff,
+                output,
+                cutoff,
+                nrow_in+2*cutoff
+                );
+#ifdef PARALLEL
+#pragma omp section
+#endif
+        extrapolate_bottom_full(
+                auxptr + (ncol_in - 1) * out_dim0 + nrow_in - 1,
+                nrow_in+2*cutoff,
+                output + ncol_in * out_dim0 + nrow_in + cutoff,
+                cutoff,
+                nrow_in+2*cutoff
+                );
+#ifdef PARALLEL
+#pragma omp section
+#endif
+        extrapolate_right_full(
+                auxptr + (ncol_in - 1) * out_dim0 + nrow_in - 1,
+                nrow_in+2*cutoff,
+                output + (ncol_in + cutoff) * out_dim0 + nrow_in,
+                cutoff,
+                nrow_in+2*cutoff
+                );
+#ifdef PARALLEL
+    }
+#endif
+}
+
+/**
+ * Given a Fortran-contiguous square matrix, extend it by linear extrapolation
+ * in each direction by <cutoff> rows/columns.
+ */
+static PyArrayObject* extend_matrix_nd(PyArrayObject *input, const int cutoff)
+{
+    const int ndim = PyArray_NDIM(input);
+    npy_intp *shape = malloc( ndim*sizeof(npy_intp) );
+    memcpy( shape, PyArray_DIMS(input), ndim*sizeof(npy_intp) );
+    shape[0] += 2*cutoff;
+    shape[1] += 2*cutoff;
+    PyArrayObject *output = (PyArrayObject*) PyArray_ZEROS(ndim, shape, NPY_COMPLEX_TYPE, 1);
+    if (!output)
+        return NULL;
+
+    const int
+        in_dim0 = PyArray_STRIDE(input, 1) / sizeof(complex_type),
+        out_dim0 = PyArray_STRIDE(output, 1) / sizeof(complex_type),
+        in_matrixstride = PyArray_STRIDE(input, 2),
+        out_matrixstride = PyArray_STRIDE(output, 2);
+    int i=1, nmatrices=1;
+    while (++i<ndim)
+        nmatrices *= shape[i];
+
+    for (i=0; i<nmatrices; ++i)
+        extend_matrix_worker(
+                PyArray_DIM(input, 0),
+                PyArray_DIM(input, 1),
+                cutoff,
+                PyArray_DATA(input) + i*in_matrixstride,
+                in_dim0,
+                PyArray_DATA(output) + i*out_matrixstride,
+                out_dim0
+                );
+
+    return output;
+}
+
+#ifdef PARALLEL_EXTRA_DIMS
+/**
+ * Given a Fortran-contiguous square matrix, extend it by linear extrapolation
+ * in each direction by <cutoff> rows/columns.
+ */
+static PyArrayObject* extend_matrix_nd_parallel(PyArrayObject *input, const int cutoff)
+{
+    const int ndim = PyArray_NDIM(input);
+    npy_intp *shape = malloc( ndim*sizeof(npy_intp) );
+    memcpy( shape, PyArray_DIMS(input), ndim*sizeof(npy_intp) );
+    shape[0] += 2*cutoff;
+    shape[1] += 2*cutoff;
+    PyArrayObject *output = (PyArrayObject*) PyArray_ZEROS(ndim, shape, NPY_COMPLEX_TYPE, 1);
+    if (!output)
+        return NULL;
+
+    const int
+        in_dim0 = PyArray_STRIDE(input, 1) / sizeof(complex_type),
+        out_dim0 = PyArray_STRIDE(output, 1) / sizeof(complex_type),
+        in_matrixstride = PyArray_STRIDE(input, 2),
+        out_matrixstride = PyArray_STRIDE(output, 2);
+    int nmatrices=1;
+    for (int j=1; ++j<ndim;)
+        nmatrices *= shape[j];
+
+#pragma omp for
+    for (int i=0; i<nmatrices; ++i)
+        extend_matrix_worker(
+                PyArray_DIM(input, 0),
+                PyArray_DIM(input, 1),
+                cutoff,
+                PyArray_DATA(input) + i*in_matrixstride,
+                in_dim0,
+                PyArray_DATA(output) + i*out_matrixstride,
+                out_dim0
+                );
+
+    return output;
+}
+#endif /* PARALLEL_EXTRA_DIMS */
+
+/**
+ * Take an n×n Floquet matrix M and positive integer c as arguments; Extrapolate
+ * M to shape (n+2c)×(n+2c).
+ */
+static PyObject* extend_matrix(PyObject *self, PyObject *args)
+{
+    PyArrayObject *input, *output;
+    int cutoff;
+    /* Parse the arguments: input should be an array and an integer. */
+    if (!PyArg_ParseTuple(args, "O!i", &PyArray_Type, &input, &cutoff))
+        return NULL;
+
+    if (PyArray_TYPE(input) != NPY_COMPLEX_TYPE)
+        return PyErr_Format(PyExc_ValueError, "array of type complex128 required");
+    if (PyArray_NDIM(input) < 2)
+        return PyErr_Format(PyExc_ValueError, "1st argument must have at least 2 dimensions.");
+
+    if (cutoff <= 0)
+    {
+        Py_INCREF(input);
+        return (PyObject*) input;
+    }
+
+    if ((PyArray_DIM(input, 0) < cutoff + 3) || (PyArray_DIM(input, 1) < cutoff + 3))
+        return PyErr_Format(PyExc_ValueError, "Matrix is too small or cutoff too large.");
+
+    PyArrayObject *finput = (PyArrayObject*) PyArray_FromArray(
+                input,
+                PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                NPY_ARRAY_WRITEABLE
+                    | NPY_ARRAY_F_CONTIGUOUS
+                    | NPY_ARRAY_ALIGNED
+            );
+    if (!finput)
+        return PyErr_Format(PyExc_RuntimeError, "Failed to create array");
+
+    if (PyArray_NDIM(finput) == 2)
+    {
+        npy_intp shape[] = {PyArray_DIM(finput, 0) + 2*cutoff, PyArray_DIM(finput, 1) + 2*cutoff};
+        output = (PyArrayObject*) PyArray_ZEROS(2, shape, NPY_COMPLEX_TYPE, 1);
+        if (output)
+            extend_matrix_worker(
+                    PyArray_DIM(finput, 0),
+                    PyArray_DIM(finput, 1),
+                    cutoff,
+                    PyArray_DATA(finput),
+                    PyArray_STRIDE(finput, 1)/sizeof(complex_type),
+                    PyArray_DATA(output),
+                    PyArray_STRIDE(output, 1)/sizeof(complex_type)
+                    );
+    }
+#ifdef PARALLEL_EXTRA_DIMS
+    else if (omp_get_max_threads() > 1)
+        output = extend_matrix_nd_parallel(finput, cutoff);
+#endif /* PARALLEL_EXTRA_DIMS */
+    else
+        output = extend_matrix_nd(finput, cutoff);
+    Py_DECREF(finput);
+
+    return (PyObject*) output;
+}
+
+
+/**
+ * @file
+ * @section sec_invert Invert matrix
+ */
+
+/**
+ * matrix must be Fortran contiguous
+ */
+void invert_matrix(
+        complex_type *matrix,
+        const int size,
+        const int dim0,
+        int *status
+        )
+{
+    int *const ipiv = malloc( size * sizeof(int) );
+    if (!ipiv)
+    {
+        *status = 1;
+        return;
+    }
+
+    getrf(&size, &size, matrix, &dim0, ipiv, status);
+    if (*status != 0)
+    {
+        free( ipiv );
+        return;
+    }
+
+    int lwork = -1;
+    complex_type dummy_work;
+    getri(&size, matrix, &dim0, ipiv, &dummy_work, &lwork, status);
+    lwork = (int) dummy_work;
+#ifdef DEBUG
+    fprintf(stderr, "LWORK = %d\n", lwork);
+#endif
+    if (lwork < size)
+        lwork = size;
+    complex_type *work = malloc( lwork * sizeof(complex_type) );
+    if (work)
+    {
+        getri(&size, matrix, &dim0, ipiv, work, &lwork, status);
+        free( work );
+    }
+    else
+        *status = 1;
+
+    free( ipiv );
+}
+
+
+/**
+ * @see invert_nd()
+ * @see invert_matrix()
+ */
+static PyArrayObject *invert_2d(
+        PyArrayObject *finput,
+        const int cutoff,
+        const int reduce_cutoff
+        )
+{
+    const int
+        size = PyArray_DIM(finput, 0),
+        /* TODO: better aligned arrays for better performance */
+        extended_stride = size + 2*cutoff;
+
+    complex_type *extended = calloc( (size + 2*cutoff) * extended_stride, sizeof(complex_type) );
+    if (!extended)
+        return NULL;
+
+    extend_matrix_worker(
+            size,
+            size,
+            cutoff,
+            PyArray_DATA(finput),
+            PyArray_STRIDE(finput, 1)/sizeof(complex_type),
+            extended,
+            extended_stride
+            );
+
+    int status;
+    invert_matrix(
+            extended + reduce_cutoff * (extended_stride + 1),
+            size + 2*(cutoff-reduce_cutoff),
+            extended_stride,
+            &status
+            );
+    PyArrayObject *output;
+    if (status)
+    {
+        output = NULL;
+        PyErr_SetString(PyExc_ValueError, "encountered singular matrix.");
+    }
+    else
+    {
+        output = (PyArrayObject*) PyArray_EMPTY(2, PyArray_DIMS(finput), NPY_COMPLEX_TYPE, 1);
+        if (output)
+            for (int i=0; i<size; ++i)
+                memcpy( PyArray_GETPTR2(output, 0, i), extended + cutoff + (i+cutoff)*extended_stride, size*sizeof(complex_type) );
+    }
+
+    free( extended );
+    return output;
+}
+
+/**
+ * input must have shape (n, n, ...) with n > cutoff+2 and cutoff >= reduce_cutoff.
+ * This is not cheked!
+ *
+ * @see invert_2d()
+ * @see invert_nd_parallel()
+ * @see invert_matrix()
+ */
+static PyArrayObject *invert_nd(
+        PyArrayObject *input,
+        const int cutoff,
+        const int reduce_cutoff
+        )
+{
+    const int
+        size = PyArray_DIM(input, 0),
+        /* TODO: better aligned arrays for better performance */
+        extended_stride = size + 2*cutoff;
+
+    const int ndim = PyArray_NDIM(input);
+    PyArrayObject *output = (PyArrayObject*) PyArray_EMPTY(ndim, PyArray_DIMS(input), NPY_COMPLEX_TYPE, 1);
+    if (!output)
+        return NULL;
+
+    const int
+        in_matrixstride = PyArray_STRIDE(input, 2),
+        out_matrixstride = PyArray_STRIDE(output, 2),
+        out_colstride = PyArray_STRIDE(output, 1);
+    int i=1, nmatrices=1, status, j;
+    while (++i<ndim)
+        nmatrices *= PyArray_DIM(input, i);
+
+    complex_type *extended = malloc( (size + 2*cutoff) * extended_stride * sizeof(complex_type) );
+    if (!extended)
+    {
+        Py_DECREF(output);
+        return NULL;
+    }
+
+    void *outptr;
+    for (i=0; i<nmatrices; ++i)
+    {
+        memset( extended, 0, (size + 2*cutoff) * extended_stride * sizeof(complex_type) );
+
+        extend_matrix_worker(
+                size,
+                size,
+                cutoff,
+                PyArray_DATA(input) + i*in_matrixstride,
+                PyArray_STRIDE(input, 1)/sizeof(complex_type),
+                extended,
+                extended_stride
+                );
+
+        invert_matrix(
+                extended + reduce_cutoff * (extended_stride + 1),
+                size + 2*(cutoff-reduce_cutoff),
+                extended_stride,
+                &status
+                );
+        if (status)
+            PyErr_WarnEx(PyExc_RuntimeWarning, "encountered singular matrix.", 1);
+        outptr = PyArray_DATA(output) + i*out_matrixstride;
+        for (j=0; j<size; ++j)
+            memcpy( outptr + j*out_colstride, extended + cutoff + (j+cutoff)*extended_stride, size*sizeof(complex_type) );
+    }
+
+    free( extended );
+    return output;
+}
+
+#ifdef PARALLEL_EXTRA_DIMS
+/**
+ * input must have shape (n, n, ...) with n > cutoff+2 and cutoff >= reduce_cutoff.
+ * This is not cheked!
+ *
+ * @see invert_nd()
+ * @see invert_2d()
+ * @see invert_matrix()
+ */
+static PyArrayObject *invert_nd_parallel(
+        PyArrayObject *input,
+        const int cutoff,
+        const int reduce_cutoff
+        )
+{
+    const int
+        size = PyArray_DIM(input, 0),
+        /* TODO: better aligned arrays for better performance */
+        extended_stride = size + 2*cutoff;
+
+    const int ndim = PyArray_NDIM(input);
+    PyArrayObject *output = (PyArrayObject*) PyArray_EMPTY(ndim, PyArray_DIMS(input), NPY_COMPLEX_TYPE, 1);
+    if (!output)
+        return NULL;
+
+    const int
+        in_matrixstride = PyArray_STRIDE(input, 2),
+        out_matrixstride = PyArray_STRIDE(output, 2),
+        out_colstride = PyArray_STRIDE(output, 1);
+    int nmatrices=1;
+    for (int j=1; ++j<ndim;)
+        nmatrices *= PyArray_DIM(input, j);
+
+    void *outptr;
+
+    char fatal_error = 0;
+#pragma omp for
+    for (int i=0; i<nmatrices; ++i)
+    {
+        if (fatal_error)
+            continue;
+        int status;
+        complex_type *extended = calloc( (size + 2*cutoff) * extended_stride, sizeof(complex_type) );
+        if (!extended)
+        {
+            fatal_error = 1;
+            continue;
+        }
+
+        extend_matrix_worker(
+                size,
+                size,
+                cutoff,
+                PyArray_DATA(input) + i*in_matrixstride,
+                PyArray_STRIDE(input, 1)/sizeof(complex_type),
+                extended,
+                extended_stride
+                );
+
+        invert_matrix(
+                extended + reduce_cutoff * (extended_stride + 1),
+                size + 2*(cutoff-reduce_cutoff),
+                extended_stride,
+                &status
+                );
+        if (status)
+            PyErr_WarnEx(PyExc_RuntimeWarning, "encountered singular matrix.", 1);
+        outptr = PyArray_DATA(output) + i*out_matrixstride;
+        for (int j=0; j<size; ++j)
+            memcpy( outptr + j*out_colstride, extended + cutoff + (j+cutoff)*extended_stride, size*sizeof(complex_type) );
+        free( extended );
+    }
+    if (fatal_error)
+    {
+        Py_DECREF(output);
+        return PyErr_Format(PyExc_RuntimeError, "Error in matrix inversion: memory allocation");
+    }
+
+    return output;
+}
+#endif /* PARALLEL_EXTRA_DIMS */
+
+/**
+ * @see invert_nd()
+ * @see invert_2d()
+ */
+static PyObject* invert_extended(PyObject *self, PyObject *args)
+{
+    PyArrayObject *input, *output;
+    int cutoff, reduce_cutoff = 0;
+    /* Parse the arguments: input should be an array and an integer. */
+    if (!PyArg_ParseTuple(args, "O!i|i", &PyArray_Type, &input, &cutoff, &reduce_cutoff))
+        return NULL;
+
+    if (PyArray_TYPE(input) != NPY_COMPLEX_TYPE)
+        return PyErr_Format(PyExc_ValueError, "array of type complex128 required");
+    if (PyArray_NDIM(input) < 2)
+        return PyErr_Format(PyExc_ValueError, "1st argument must have at least 2 dimensions.");
+    if (PyArray_DIM(input, 1) != PyArray_DIM(input, 0))
+        return PyErr_Format(PyExc_ValueError, "1st argument must be a square matrix.");
+    if ((cutoff < 0) || (reduce_cutoff >= cutoff))
+    {
+        cutoff = 0;
+        reduce_cutoff = 0;
+    }
+
+    PyArrayObject *finput = (PyArrayObject*) PyArray_FromArray(
+                input,
+                PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                NPY_ARRAY_WRITEABLE
+                    | NPY_ARRAY_F_CONTIGUOUS
+                    | NPY_ARRAY_ALIGNED
+            );
+    if (!finput)
+        return PyErr_Format(PyExc_RuntimeError, "Failed to create array");
+
+    if (PyArray_NDIM(finput) == 2)
+        output = invert_2d(finput, cutoff, reduce_cutoff);
+#ifdef PARALLEL_EXTRA_DIMS
+    else if (omp_get_max_threads() > 1)
+        output = invert_nd_parallel(finput, cutoff, reduce_cutoff);
+#endif /* PARALLEL_EXTRA_DIMS */
+    else
+        output = invert_nd(finput, cutoff, reduce_cutoff);
+    Py_DECREF(finput);
+
+
+    return (PyObject*) output;
+}
+
+
+/**
+ * @file
+ * @section sec_multiply Multiply matrices
+ */
+
+static void correct_top_left(
+        const int cutoff,
+        const complex_type *left,
+        const int left_dim0,
+        const complex_type *right,
+        const int right_dim0,
+        complex_type *auxarr_left,
+        complex_type *auxarr_right,
+        const int aux_dim0
+        )
+{
+#ifdef PARALLEL
+#pragma omp sections
+    {
+#pragma omp section
+#endif /* PARALLEL */
+        extrapolate_left(left, left_dim0, auxarr_left, cutoff, aux_dim0);
+#ifdef PARALLEL
+#pragma omp section
+#endif
+        extrapolate_top(right, right_dim0, auxarr_right, cutoff, aux_dim0);
+#ifdef PARALLEL
+    }
+#endif
+
+    /* Calculate matrix product. This overwrites auxarr_left. */
+#ifdef DEBUG
+    fprintf(stderr, "Calling multiply_UL_inplace for extrapolated matrices\n");
+#endif
+    multiply_UL_inplace(
+            cutoff, // size
+            auxarr_left, // matrix A
+            aux_dim0,  // first dimension of A (int)
+            auxarr_right, // matrix B
+            aux_dim0 // first dimension of B (int)
+            );
+}
+
+
+static void correct_bottom_right(
+        const int cutoff,
+        const complex_type *left_last,
+        const int left_dim0,
+        const complex_type *right_last,
+        const int right_dim0,
+        complex_type *auxarr_left,
+        complex_type *auxarr_right,
+        const int aux_dim0
+        )
+{
+#ifdef PARALLEL
+#pragma omp sections
+    {
+#pragma omp section
+#endif /* PARALLEL */
+        extrapolate_right(left_last, left_dim0, auxarr_left, cutoff, aux_dim0);
+#ifdef PARALLEL
+#pragma omp section
+#endif
+        extrapolate_bottom(right_last, right_dim0, auxarr_right, cutoff, aux_dim0);
+#ifdef PARALLEL
+    }
+#endif
+
+    /* Calculate matrix product. This overwrites auxarr_left. */
+#ifdef DEBUG
+    fprintf(stderr, "Calling multiply_LU_inplace for extrapolated matrices\n");
+#endif
+    multiply_LU_inplace(
+            cutoff,
+            auxarr_left,
+            aux_dim0,
+            auxarr_right,
+            aux_dim0
+            );
+}
+
+/**
+ * Helper function for multiply_extended().
+ * auxarr1 and auxarr3 must be initialized to 0.
+ * auxarr1 == auxarr3 and auxarr2 == auxarr4 is allowed and will avoid parallelization.
+ * auxarr arrays must have size cutoff*aux_dim0 with aux_dim0 >= cutoff.
+ */
+static void multiply_extended_worker(
+        const int cutoff,
+        const int nrowl,
+        const int ncoll,
+        const int ncolr,
+        const complex_type *left,
+        const int left_dim0,
+        const complex_type *right,
+        const int right_dim0,
+        complex_type *output,
+        const int out_dim0,
+        complex_type *auxarr1,
+        complex_type *auxarr2,
+#ifdef PARALLEL
+        complex_type *auxarr3,
+        complex_type *auxarr4,
+#endif
+        const int aux_dim0,
+        const int clear_corners
+        )
+{
+#ifdef CBLAS
+    gemm(
+            CblasColMajor, // layout
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNoTrans, // B is not modified (no adjoint or transpose)
+            nrowl, // rows of A (int)
+            ncolr, // columns of B (int)
+            ncoll, // columns of A = rows of B (int)
+            &one, // global prefactor
+            left, // matrix A
+            left_dim0, // first dimension of A (int)
+            right, // matrix B
+            right_dim0,  // first dimension of B (int)
+            &zero, // weight of C
+            output, // matrix C
+            out_dim0 // first dimension of C
+            );
+#else /* CBLAS */
+    gemm(
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // B is not modified (no adjoint or transpose)
+            &nrowl, // rows of A (int)
+            &ncolr, // columns of B (int)
+            &ncoll, // columns of A = rows of B (int)
+            &one, // global prefactor
+            left, // matrix A
+            &left_dim0, // first dimension of A (int)
+            right, // matrix B
+            &right_dim0,  // first dimension of B (int)
+            &zero, // weight of C
+            output, // matrix C
+            &out_dim0 // first dimension of C
+            );
+#endif /* CBLAS */
+
+    if (clear_corners > 0)
+    {
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp for
+#endif
+        for (int i=0; i<clear_corners; i++)
+            memset( output + (ncolr-clear_corners+i)*out_dim0, 0, (i+1)*sizeof(complex_type) );
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp for
+#endif
+        for (int i=0; i<clear_corners; i++)
+            memset( output + i*(out_dim0+1) + nrowl - clear_corners, 0, (clear_corners-i)*sizeof(complex_type) );
+    }
+
+    if (cutoff <= 0)
+        return;
+
+#ifdef PARALLEL
+    if (auxarr1 == auxarr3 || auxarr2 == auxarr4)
+    {
+#endif
+        correct_top_left(cutoff, left, left_dim0, right, right_dim0, auxarr1, auxarr2, aux_dim0);
+
+        /* Add result to top left of output array. */
+#ifdef DEBUG
+        fprintf(stderr, "Writing result to top left\n");
+#endif
+        const complex_type *read = auxarr1;
+#ifdef PARALLEL
+        const complex_type *end = auxarr1 + cutoff * aux_dim0;
+#else
+        const complex_type *const end = auxarr1 + cutoff * aux_dim0;
+#endif
+        complex_type *write = output;
+        int i;
+        while (read < end)
+        {
+            i = -1;
+            while (++i < cutoff)
+                write[i] += read[i];
+            write += out_dim0;
+            read += aux_dim0;
+        }
+
+#ifdef PARALLEL
+        if (auxarr1 == auxarr3)
+            memset( auxarr3, 0, cutoff*aux_dim0*sizeof(complex_type) );
+#else
+        memset( auxarr1, 0, cutoff*aux_dim0*sizeof(complex_type) );
+#endif
+        correct_bottom_right(
+                cutoff,
+                left + left_dim0*(ncoll-1) + nrowl-1,
+                left_dim0,
+                right + right_dim0*(ncolr-1) + ncoll-1,
+                right_dim0,
+#ifdef PARALLEL
+                auxarr3,
+                auxarr4,
+#else
+                auxarr1,
+                auxarr2,
+#endif
+                aux_dim0);
+
+        /* Add result to bottom right of output array. */
+#ifdef DEBUG
+        fprintf(stderr, "Writing result to bottom right\n");
+#endif
+#ifdef PARALLEL
+        read = auxarr3;
+        end = auxarr3 + cutoff * aux_dim0;
+#else
+        read = auxarr1;
+#endif
+        write = output + (ncolr - cutoff)*out_dim0 + nrowl - cutoff;
+        while (read < end)
+        {
+            i = -1;
+            while (++i < cutoff)
+                write[i] += read[i];
+            write += out_dim0;
+            read += aux_dim0;
+        }
+        return;
+#ifdef PARALLEL
+    }
+    correct_top_left(cutoff, left, left_dim0, right, right_dim0, auxarr1, auxarr2, aux_dim0);
+    correct_bottom_right(
+            cutoff,
+            left + left_dim0*(ncoll-1) + nrowl-1,
+            left_dim0,
+            right + right_dim0*(ncolr-1) + ncoll-1,
+            right_dim0,
+            auxarr3,
+            auxarr4,
+            aux_dim0);
+
+    /* Add result to top left of output array. */
+#ifdef DEBUG
+    fprintf(stderr, "Writing result to top left\n");
+#endif
+    const complex_type *read = auxarr1;
+    const complex_type * end = auxarr1 + cutoff * aux_dim0;
+    complex_type *write = output;
+    int i;
+    while (read < end)
+    {
+        i = -1;
+        while (++i < cutoff)
+            write[i] += read[i];
+        write += out_dim0;
+        read += aux_dim0;
+    }
+
+    /* Add result to bottom right of output array. */
+#ifdef DEBUG
+    fprintf(stderr, "Writing result to bottom right\n");
+#endif
+    read = auxarr3;
+    end = auxarr3 + cutoff * aux_dim0;
+    write = output + (ncolr - cutoff)*out_dim0 + nrowl - cutoff;
+    while (read < end)
+    {
+        i = -1;
+        while (++i < cutoff)
+            write[i] += read[i];
+        write += out_dim0;
+        read += aux_dim0;
+    }
+#endif /* PARALLEL */
+}
+
+
+/**
+ * left and right must be Fortran contiguous.
+ */
+static PyArrayObject* multiply_extended_2d(
+        PyArrayObject *left,
+        PyArrayObject *right,
+        const int cutoff,
+        const int clear_corners
+        )
+{
+    /* First just ordinary matrix multiplication. */
+    npy_intp dims[2] = {PyArray_DIM(left, 0), PyArray_DIM(right, 1)};
+    PyArrayObject *out = (PyArrayObject*) PyArray_EMPTY(2, dims, NPY_COMPLEX_TYPE, 1);
+    if (!out)
+        return NULL;
+
+    complex_type
+        *auxarr1 = calloc( cutoff*cutoff, sizeof(complex_type) ),
+        *auxarr2 = malloc( cutoff*cutoff* sizeof(complex_type) );
+#ifdef PARALLEL
+    complex_type *auxarr3, *auxarr4;
+    if (omp_get_max_threads() >= PARALLELIZE_CORRECTION_THREADS_THRESHOLD)
+    {
+        auxarr3 = calloc( cutoff*cutoff, sizeof(complex_type) ),
+        auxarr4 = malloc( cutoff*cutoff* sizeof(complex_type) );
+    }
+    else
+    {
+        auxarr3 = auxarr1;
+        auxarr4 = auxarr2;
+    }
+#endif /* PARALLEL */
+
+#ifdef PARALLEL
+    if (auxarr1 && auxarr2 && auxarr3 && auxarr4)
+#else /* PARALLEL */
+    if (auxarr1 && auxarr2)
+#endif /* PARALLEL */
+    {
+        multiply_extended_worker(
+                cutoff,
+                PyArray_DIM(left, 0),
+                PyArray_DIM(left, 1),
+                PyArray_DIM(right, 1),
+                PyArray_DATA(left),
+                PyArray_STRIDE(left, 1)/sizeof(complex_type),
+                PyArray_DATA(right),
+                PyArray_STRIDE(right, 1)/sizeof(complex_type),
+                PyArray_DATA(out),
+                PyArray_STRIDE(out, 1)/sizeof(complex_type),
+                auxarr1,
+                auxarr2,
+#ifdef PARALLEL
+                auxarr3,
+                auxarr4,
+#endif /* PARALLEL */
+                cutoff,
+                clear_corners
+                );
+    }
+    else
+    {
+        Py_DECREF(out);
+        out = NULL;
+    }
+
+#ifdef PARALLEL
+    if (auxarr1 != auxarr3)
+        free( auxarr3 );
+    if (auxarr2 != auxarr4)
+        free( auxarr4 );
+#endif /* PARALLEL */
+    free( auxarr1 );
+    free( auxarr2 );
+    return out;
+}
+
+/**
+ * left and right must be Fortran contiguous.
+ */
+static PyArrayObject* multiply_extended_nd(
+        PyArrayObject *left,
+        PyArrayObject *right,
+        const int cutoff,
+        const int clear_corners
+        )
+{
+    const int
+        nrowl = PyArray_DIM(left, 0),
+        ncoll = PyArray_DIM(left, 1),
+        ncolr = PyArray_DIM(right, 1);
+
+
+    const int ndim = PyArray_NDIM(left);
+    npy_intp *shape = malloc( ndim*sizeof(npy_intp) );
+    memcpy( shape, PyArray_DIMS(left), ndim*sizeof(npy_intp) );
+    shape[1] = ncolr;
+    PyArrayObject *out = (PyArrayObject*) PyArray_EMPTY(ndim, shape, NPY_COMPLEX_TYPE, 1);
+    if (!out)
+        return NULL;
+
+    const int
+        left_dim0 = PyArray_STRIDE(left, 1) / sizeof(complex_type),
+        right_dim0 = PyArray_STRIDE(right, 1) / sizeof(complex_type),
+        out_dim0 = PyArray_STRIDE(out, 1) / sizeof(complex_type),
+        left_matrixstride = PyArray_STRIDE(left, 2),
+        right_matrixstride = PyArray_STRIDE(right, 2),
+        out_matrixstride = PyArray_STRIDE(out, 2);
+
+    int i=1, nmatrices=1;
+    while (++i<ndim)
+        nmatrices *= shape[i];
+
+    complex_type
+        *auxarr1 = malloc( cutoff*cutoff * sizeof(complex_type) ),
+        *auxarr2 = malloc( cutoff*cutoff * sizeof(complex_type) );
+#ifdef PARALLEL
+    complex_type *auxarr3, *auxarr4;
+    if (omp_get_max_threads() >= PARALLELIZE_CORRECTION_THREADS_THRESHOLD)
+    {
+        auxarr3 = malloc( cutoff*cutoff* sizeof(complex_type) ),
+        auxarr4 = malloc( cutoff*cutoff* sizeof(complex_type) );
+    }
+    else
+    {
+        auxarr3 = auxarr1;
+        auxarr4 = auxarr2;
+    }
+#endif /* PARALLEL */
+
+#ifdef PARALLEL
+    if (auxarr1 && auxarr2 && auxarr3 && auxarr4)
+#else /* PARALLEL */
+    if (auxarr1 && auxarr2)
+#endif /* PARALLEL */
+    {
+        for (i=0; i<nmatrices; ++i)
+        {
+            memset( auxarr1, 0, cutoff*cutoff*sizeof(complex_type) );
+#ifdef PARALLEL
+            if (auxarr1 != auxarr3)
+                memset( auxarr3, 0, cutoff*cutoff*sizeof(complex_type) );
+#endif /* PARALLEL */
+            multiply_extended_worker(
+                    cutoff,
+                    nrowl,
+                    ncoll,
+                    ncolr,
+                    PyArray_DATA(left) + i*left_matrixstride,
+                    left_dim0,
+                    PyArray_DATA(right) + i*right_matrixstride,
+                    right_dim0,
+                    PyArray_DATA(out) + i*out_matrixstride,
+                    out_dim0,
+                    auxarr1,
+                    auxarr2,
+#ifdef PARALLEL
+                    auxarr3,
+                    auxarr4,
+#endif /* PARALLEL */
+                    cutoff,
+                    clear_corners
+                    );
+        }
+    }
+    else
+    {
+        Py_DECREF(out);
+        out = NULL;
+    }
+
+#ifdef PARALLEL
+    if (auxarr1 != auxarr3)
+        free( auxarr3 );
+    if (auxarr2 != auxarr4)
+        free( auxarr4 );
+#endif /* PARALLEL */
+    free( auxarr1 );
+    free( auxarr2 );
+    return out;
+}
+
+#ifdef PARALLEL_EXTRA_DIMS
+/**
+ * left and right must be Fortran contiguous.
+ */
+static PyArrayObject* multiply_extended_nd_parallel(
+        PyArrayObject *left,
+        PyArrayObject *right,
+        const int cutoff,
+        const int clear_corners
+        )
+{
+    const int
+        nrowl = PyArray_DIM(left, 0),
+        ncoll = PyArray_DIM(left, 1),
+        ncolr = PyArray_DIM(right, 1);
+
+
+    const int ndim = PyArray_NDIM(left);
+    npy_intp *shape = malloc( ndim*sizeof(npy_intp) );
+    memcpy( shape, PyArray_DIMS(left), ndim*sizeof(npy_intp) );
+    shape[1] = ncolr;
+    PyArrayObject *out = (PyArrayObject*) PyArray_EMPTY(ndim, shape, NPY_COMPLEX_TYPE, 1);
+    if (!out)
+        return NULL;
+
+    const int
+        left_dim0 = PyArray_STRIDE(left, 1) / sizeof(complex_type),
+        right_dim0 = PyArray_STRIDE(right, 1) / sizeof(complex_type),
+        out_dim0 = PyArray_STRIDE(out, 1) / sizeof(complex_type),
+        left_matrixstride = PyArray_STRIDE(left, 2),
+        right_matrixstride = PyArray_STRIDE(right, 2),
+        out_matrixstride = PyArray_STRIDE(out, 2);
+
+    int i=1, nmatrices=1;
+    while (++i<ndim)
+        nmatrices *= shape[i];
+
+    char fatal_error = 0;
+#pragma omp for
+    for (i=0; i<nmatrices; ++i)
+    {
+        if (fatal_error)
+            continue;
+        complex_type
+            *auxarr_left  = calloc( cutoff*cutoff,  sizeof(complex_type) ),
+            *auxarr_right = malloc( cutoff*cutoff * sizeof(complex_type) );
+
+        if (auxarr_left && auxarr_right)
+            multiply_extended_worker(
+                    cutoff,
+                    nrowl,
+                    ncoll,
+                    ncolr,
+                    PyArray_DATA(left) + i*left_matrixstride,
+                    left_dim0,
+                    PyArray_DATA(right) + i*right_matrixstride,
+                    right_dim0,
+                    PyArray_DATA(out) + i*out_matrixstride,
+                    out_dim0,
+                    auxarr_left,
+                    auxarr_right,
+#ifdef PARALLEL
+                    auxarr_left,
+                    auxarr_right,
+#endif /* PARALLEL */
+                    cutoff,
+                    clear_corners
+                    );
+        else
+            fatal_error = 1;
+
+        free( auxarr_left );
+        free( auxarr_right );
+    }
+    if (fatal_error)
+    {
+        Py_DECREF(out);
+        out = NULL;
+    }
+    return out;
+}
+#endif /* PARALLEL_EXTRA_DIMS */
+
+
+/**
+ * left and right must be Fortran contiguous. right must not be a square matrix.
+ * left is overwritten with the product of left and right.
+ * left must be large enough to contain the matrix product left*right.
+ */
+static void multiply_symmetric_nonsquare(
+        const int nrowl,
+        const int ncoll,
+        const int ncolr,
+        complex_type *left,
+        const int left_dim0,
+        const complex_type *right,
+        const int right_dim0,
+        const int symmetry,
+        const int clear_corners
+        )
+{
+    /*
+     * Calculate left := left @ right while assuming that right is a lower
+     * triangular matrix (upper triangular matrix would also work).
+     * Using this result and the symmetry of left and right, the correct matrix
+     * product can be calculated from left.
+     */
+#ifdef DEBUG
+    fprintf(stderr, "Calculating matrix product using trmm\n");
+#endif
+#ifdef CBLAS
+    trmm(
+            CblasColMajor, // layout
+            CblasRight, // order: this means B := B A
+            ncoll > ncolr ? CblasUpper : CblasLower, // A is treated as upper or lower triangular matrix, depending on its shape.
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNonUnit, // A is not unit triangular
+            nrowl, // rows of B (int)
+            ncoll > ncolr ? ncolr : ncoll, // columns of B (int) = rows of A
+            &one, // global prefactor
+            right, // matrix A
+            right_dim0, // first dimension of A (int)
+            left, // matrix B
+            left_dim0  // first dimension of B (int)
+            );
+#else /* CBLAS */
+    trmm(
+            &R, // order: this means B := B A
+            ncoll > ncolr ? &U : &L, // A is treated as upper or lower triangular matrix, depending on its shape.
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // A is not unit triangular
+            &nrowl, // rows of B (int)
+            ncoll > ncolr ? &ncolr : &ncoll, // columns of B (int) = rows of A
+            &one, // global prefactor
+            right, // matrix A
+            &right_dim0, // first dimension of A (int)
+            left, // matrix B
+            &left_dim0  // first dimension of B (int)
+            );
+#endif /* CBLAS */
+
+    /*
+     * Use symmetry of the matrix to correct the result.
+     */
+#ifdef DEBUG
+    fprintf(stderr, "Correct rest of the matrix\n");
+#endif
+    complex_type *fwd, *bck;
+    int i=0, j;
+    for (; i < ncolr - ncoll; ++i)
+    {
+        fwd = left + i*left_dim0;
+        bck = left + (ncolr - i - 1)*left_dim0;
+        j = nrowl;
+        while (--j >= 0)
+            bck[nrowl - j - 1] = symmetry * conj( fwd[j] );
+    }
+    for (; i < ncolr/2; ++i)
+    {
+        fwd = left + i*left_dim0;
+        bck = left + (ncolr - i - 1)*left_dim0;
+        j = nrowl;
+        while (--j >= 0)
+        {
+            fwd[j] += symmetry * conj( bck[nrowl - j - 1] );
+            bck[nrowl - j - 1] = symmetry * conj( fwd[j] );
+        }
+    }
+    if (2*i < ncolr)
+    {
+        fwd = left + i*left_dim0;
+        j = nrowl;
+        while (--j >= nrowl/2)
+        {
+            fwd[j] += symmetry * conj( fwd[nrowl - j - 1] );
+            fwd[nrowl - j - 1] = symmetry * conj( fwd[j] );
+        }
+    }
+
+    if (clear_corners > 0)
+    {
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp for
+#endif
+        for (int i=0; i<clear_corners; i++)
+            memset( left + (ncolr-clear_corners+i)*left_dim0, 0, (i+1)*sizeof(complex_type) );
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp for
+#endif
+        for (int i=0; i<clear_corners; i++)
+            memset( left + i*(left_dim0+1) + nrowl - clear_corners, 0, (clear_corners-i)*sizeof(complex_type) );
+    }
+}
+
+/**
+ * left and right must be Fortran contiguous. right must be a square matrix.
+ * left is overwritten with the product of left and right.
+ * Diagonal elements of right are temporarily overwritten but restored afterwards.
+ */
+static void multiply_symmetric_square(
+        const int nrowl,
+        const int ncolr,
+        complex_type *left,
+        const int left_dim0,
+        complex_type *right,
+        const int right_dim0,
+        const int symmetry,
+        const int clear_corners
+        )
+{
+    /* multiply diagonal of right by 1/2 */
+    int i = 0, j;
+    while (i < ncolr)
+        right[i++ * (right_dim0+1)] /= 2;
+
+    /*
+     * Calculate left := left @ right while assuming that right is a lower
+     * triangular matrix (upper triangular matrix would also work).
+     * Using this result and the symmetry of left and right, the correct matrix
+     * product can be calculated from left.
+     */
+#ifdef DEBUG
+    fprintf(stderr, "Calculating matrix product using trmm\n");
+#endif
+#ifdef CBLAS
+    trmm(
+            CblasColMajor, // layout
+            CblasRight, // order: this means B := B A
+            CblasLower, // A is interpreted as a lower triangular matrix
+            CblasNoTrans, // A is not modified (no adjoint or transpose)
+            CblasNonUnit, // A is not unit triangular
+            nrowl, // rows of B (int)
+            ncolr, // columns of B (int) = rows of A = columns of A
+            &one, // global prefactor
+            right, // matrix A
+            right_dim0, // first dimension of A (int)
+            left, // matrix B
+            left_dim0 // first dimension of B (int)
+            );
+#else /* CBLAS */
+    trmm(
+            &R, // order: this means B := B A
+            &L, // A is interpreted as a lower triangular matrix
+            &N, // A is not modified (no adjoint or transpose)
+            &N, // A is not unit triangular
+            &nrowl, // rows of B (int)
+            &ncolr, // columns of B (int) = rows of A = columns of A
+            &one, // global prefactor
+            right, // matrix A
+            &right_dim0, // first dimension of A (int)
+            left, // matrix B
+            &left_dim0 // first dimension of B (int)
+            );
+#endif /* CBLAS */
+
+    /* multiply diagonal of right by 2 */
+#ifdef DEBUG
+    fprintf(stderr, "Multiplying diagonal of right array by 2\n");
+#endif
+    i = 0;
+    while (i < ncolr)
+        right[i++ * (right_dim0+1)] *= 2;
+
+    /*
+     * Use symmetry of the matrix to correct the result.
+     */
+#ifdef DEBUG
+    fprintf(stderr, "Correct rest of the matrix\n");
+#endif
+    i = 0;
+    j = left_dim0 * (ncolr-1) + nrowl - 1;
+    while (i <= j)
+    {
+        left[i] += symmetry * conj(left[j]);
+        left[j--] = symmetry * conj(left[i++]);
+    }
+
+    if (clear_corners > 0)
+    {
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp for
+#endif
+        for (int i=0; i<clear_corners; i++)
+            memset( left + (ncolr-clear_corners+i)*left_dim0, 0, (i+1)*sizeof(complex_type) );
+#ifdef PARALLEL_EXTRAPOLATION
+#pragma omp for
+#endif
+        for (int i=0; i<clear_corners; i++)
+            memset( left + i*(left_dim0+1) + nrowl - clear_corners, 0, (clear_corners-i)*sizeof(complex_type) );
+    }
+
+#ifdef DEBUG
+    fprintf(stderr, "Finished multiply_2d_symmetric\n");
+#endif
+}
+
+
+/**
+ * left and right must be Fortran contiguous. right must not be a square matrix.
+ * left is overwritten with the product of left and right.
+ * left must be large enough to contain the matrix product left*right.
+ */
+static void multiply_symmetric_worker(
+        const int nrowl,
+        const int ncoll,
+        const int ncolr,
+        const int symmetry,
+        const int cutoff,
+        complex_type *const left,
+        const int left_dim0,
+        complex_type *const right,
+        const int right_dim0,
+        complex_type *const auxmatrixl,
+        complex_type *const auxmatrixr,
+        const int aux_dim0,
+        const int clear_corners
+        )
+{
+    /* If cutoff == 0 (padding is disabled), just return the matrix product. */
+    if (cutoff <= 0)
+    {
+        if (ncoll == ncolr)
+            multiply_symmetric_square(
+                    nrowl,
+                    ncolr,
+                    left,
+                    left_dim0,
+                    right,
+                    right_dim0,
+                    symmetry,
+                    clear_corners
+                    );
+        else
+            multiply_symmetric_nonsquare(
+                    nrowl,
+                    ncoll,
+                    ncolr,
+                    left,
+                    left_dim0,
+                    right,
+                    right_dim0,
+                    symmetry,
+                    clear_corners
+                    );
+        return;
+    }
+
+#ifdef PARALLEL
+#pragma omp sections
+    {
+#pragma omp section
+#endif /* PARALLEL */
+        {
+        memset( auxmatrixl, 0, cutoff*aux_dim0*sizeof(complex_type) );
+        /* Extrapolate left (upper part) of left matrix */
+        extrapolate_left(left, nrowl, auxmatrixl, cutoff, cutoff);
+        }
+#ifdef PARALLEL
+#pragma omp section
+#endif
+        /* Extrapolate top (left part) of right matrix */
+        extrapolate_top(right, ncoll, auxmatrixr, cutoff, cutoff);
+#ifdef PARALLEL
+    }
+#endif
+
+    /* Calculate matrix product. This overwrites auxmatrixl. */
+    if (ncoll == ncolr)
+        multiply_symmetric_square(
+                nrowl,
+                ncolr,
+                left,
+                left_dim0,
+                right,
+                right_dim0,
+                symmetry,
+                clear_corners
+                );
+    else
+        multiply_symmetric_nonsquare(
+                nrowl,
+                ncoll,
+                ncolr,
+                left,
+                left_dim0,
+                right,
+                right_dim0,
+                symmetry,
+                clear_corners
+                );
+
+    multiply_UL_inplace(
+            cutoff,
+            auxmatrixl,
+            aux_dim0,
+            auxmatrixr,
+            aux_dim0
+            );
+
+    /* Add result to top left of output array. */
+    const complex_type *read = auxmatrixl;
+    const complex_type * const end = auxmatrixl + cutoff * cutoff;
+    complex_type *write = left;
+    int i;
+    while (read < end)
+    {
+        i = -1;
+        while (++i < cutoff)
+            write[i] += read[i];
+        write += left_dim0;
+        read += aux_dim0;
+    }
+
+    /* Add conjugate of result to bottom right of output array. */
+    read = auxmatrixl;
+    write = left + left_dim0*(ncolr - 1) + nrowl - cutoff - 1;
+    while (read < end)
+    {
+        i = -1;
+        while (++i < cutoff)
+            write[cutoff-i] += symmetry * conj(read[i]);
+        write -= left_dim0;
+        read += aux_dim0;
+    }
+}
+
+/**
+ * multiply 2 matrices with the following requirements, which are not checked:
+ * 1. left[::-1, ::-1] == s1 * left.conjugate() and right[::-1, ::-1] == s2 * right.conjugate()
+ *    with symmetry = s1*s2, s1 and s2 must be +1 or -1.
+ * 2. left.shape == (n, k), right.shape == (k, m) where
+ *    n,k,m \in {N,N+1} for some integer N >= cutoff + 2
+ *
+ * left will be overwritten. right also needs to be writable.
+ * Matrices are extended as defined by padding.
+ */
+static PyArrayObject* multiply_extended_2d_symmetric(
+        PyArrayObject *left,
+        PyArrayObject *right,
+        const int cutoff,
+        const int symmetry,
+        const int clear_corners,
+        const char flags
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Entering multiply_extended_2d_symmetric\n");
+#endif
+    const int
+        nrowl = PyArray_DIM(left, 0),
+        ncoll = PyArray_DIM(left, 1),
+        ncolr = PyArray_DIM(right, 1);
+
+    PyArrayObject *fright = (PyArrayObject*) PyArray_FromArray(
+                right,
+                PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                NPY_ARRAY_WRITEABLE
+                    | NPY_ARRAY_F_CONTIGUOUS
+                    | NPY_ARRAY_ALIGNED
+            );
+    if (!fright)
+        return NULL;
+    PyArrayObject *fleft = (PyArrayObject*) PyArray_FromArray(
+                left,
+                PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                NPY_ARRAY_WRITEABLE
+                    | NPY_ARRAY_F_CONTIGUOUS
+                    | NPY_ARRAY_ALIGNED
+                    | ((flags & OVERWRITE_LEFT) ? 0 : NPY_ARRAY_ENSURECOPY)
+            );
+    if (!fleft)
+    {
+        Py_DECREF(fright);
+        return NULL;
+    }
+
+    if (ncolr > ncoll)
+    {
+        npy_intp shape[] = {nrowl, ncolr};
+        PyArray_Dims dims = {shape, 2};
+        if (!PyArray_Resize(fleft, &dims, 1, NPY_FORTRANORDER))
+        {
+            Py_DECREF(fright);
+            Py_DECREF(fleft);
+            PyErr_SetString(PyExc_RuntimeError, "Failed to resize (enlargen) matrix.");
+            return NULL;
+        }
+    }
+
+    complex_type *const auxmatrixl = malloc( cutoff*cutoff * sizeof(complex_type) );
+    complex_type *const auxmatrixr = malloc( cutoff*cutoff * sizeof(complex_type) );
+
+    if (!auxmatrixl || !auxmatrixr)
+    {
+        free(auxmatrixl);
+        free(auxmatrixr);
+        Py_DECREF(fleft);
+        Py_DECREF(fright);
+        return NULL;
+    }
+
+    multiply_symmetric_worker(
+            nrowl,
+            ncoll,
+            ncolr,
+            symmetry,
+            cutoff,
+            PyArray_DATA(fleft),
+            PyArray_STRIDE(fleft, 1) / sizeof(complex_type),
+            PyArray_DATA(fright),
+            PyArray_STRIDE(fright, 1) / sizeof(complex_type),
+            auxmatrixl,
+            auxmatrixr,
+            cutoff,
+            clear_corners
+            );
+
+    /* Free auxilliary arrays. */
+    free(auxmatrixl);
+    free(auxmatrixr);
+    Py_DECREF(fright);
+
+    if (ncolr < ncoll)
+    {
+        npy_intp shape[] = {nrowl, ncolr};
+        PyArray_Dims dims = {shape, 2};
+        if (!PyArray_Resize(fleft, &dims, 1, NPY_FORTRANORDER))
+        {
+            Py_DECREF(fleft);
+            PyErr_SetString(PyExc_RuntimeError, "Failed to resize (truncate) matrix.");
+            return NULL;
+        }
+    }
+
+    return fleft;
+}
+
+/**
+ * CAUTION: THIS DOES NOT DO WHAT IS REQUIRED FOR FRTRG!
+ */
+static PyArrayObject* multiply_extended_nd_symmetric(
+        PyArrayObject *left,
+        PyArrayObject *fright,
+        const int cutoff,
+        const int symmetry,
+        const int clear_corners,
+        const char flags
+        )
+{
+#ifdef DEBUG
+    fprintf(stderr, "Entering multiply_extended_nd_symmetric\n");
+#endif
+    const int
+        nrowl = PyArray_DIM(left, 0),
+        ncoll = PyArray_DIM(left, 1),
+        ncolr = PyArray_DIM(fright, 1),
+        ndim = PyArray_NDIM(left);
+
+    int i=1, nmatrices=1;
+    while (++i<ndim)
+        nmatrices *= PyArray_DIM(left, i);
+
+    void *left_data;
+    PyArrayObject *fleft;
+    int left_dim0, left_matrixstride;
+    if (ncolr == ncoll)
+    {
+        fleft = (PyArrayObject*) PyArray_FromArray(
+                    left,
+                    PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                    NPY_ARRAY_WRITEABLE
+                        | NPY_ARRAY_F_CONTIGUOUS
+                        | NPY_ARRAY_ALIGNED
+                        | ((flags & OVERWRITE_LEFT) ? 0 : NPY_ARRAY_ENSURECOPY)
+                );
+        if (!fleft)
+            return NULL;
+        left_data = PyArray_DATA(fleft);
+        left_dim0 = PyArray_STRIDE(fleft, 1) / sizeof(complex_type);
+        left_matrixstride = PyArray_STRIDE(fleft, 2);
+    }
+    else
+    {
+        fleft = (PyArrayObject*) PyArray_FromArray(
+                    left,
+                    PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                    NPY_ARRAY_WRITEABLE
+                        | NPY_ARRAY_F_CONTIGUOUS
+                        | NPY_ARRAY_ALIGNED
+                );
+        if (!fleft)
+            return NULL;
+        left_dim0 = nrowl;
+        left_data = malloc(
+                (ncoll > ncolr ? (nmatrices - 1) * ncolr + ncoll : nmatrices * ncolr)
+                * left_dim0 * sizeof(complex_type) );
+        if (!left_data)
+        {
+            Py_DECREF(fleft);
+            return NULL;
+        }
+        left_matrixstride = nrowl * ncolr * sizeof(complex_type);
+    }
+
+    const int
+        right_dim0 = PyArray_STRIDE(fright, 1) / sizeof(complex_type),
+        right_matrixstride = PyArray_STRIDE(fright, 2);
+
+    complex_type *const auxmatrixl = malloc( cutoff*cutoff* sizeof(complex_type) );
+    complex_type *const auxmatrixr = malloc( cutoff*cutoff* sizeof(complex_type) );
+
+    if (!auxmatrixl || !auxmatrixr)
+    {
+        if (ncolr != ncoll)
+            free(left_data);
+        free(auxmatrixl);
+        free(auxmatrixr);
+        Py_DECREF(fleft);
+        return NULL;
+    }
+
+    for (i=0; i<nmatrices; ++i)
+    {
+        if (ncolr != ncoll)
+            /* copy data */
+            memcpy( left_data + i*left_matrixstride, PyArray_DATA(fleft) + i*PyArray_STRIDE(fleft, 2), PyArray_STRIDE(fleft, 2) );
+        multiply_symmetric_worker(
+                nrowl,
+                ncoll,
+                ncolr,
+                symmetry,
+                cutoff,
+                left_data + i*left_matrixstride,
+                left_dim0,
+                PyArray_DATA(fright) + i*right_matrixstride,
+                right_dim0,
+                auxmatrixl,
+                auxmatrixr,
+                cutoff,
+                clear_corners
+                );
+    }
+
+    /* Free auxilliary arrays. */
+    free(auxmatrixl);
+    free(auxmatrixr);
+
+    if (ncoll != ncolr)
+    {
+        Py_DECREF(fleft);
+        npy_intp *shape = malloc( ndim*sizeof(npy_intp) );
+        memcpy( shape, PyArray_DIMS(left), ndim*sizeof(npy_intp) );
+        shape[1] = ncolr;
+        fleft = (PyArrayObject*) PyArray_New(
+                &PyArray_Type,
+                ndim,
+                shape,
+                NPY_COMPLEX_TYPE, // data type
+                NULL, // strides
+                left_data, // data
+                sizeof(complex_type), // item size
+                NPY_ARRAY_F_CONTIGUOUS, // flags
+                NULL // obj
+                );
+    }
+
+    return fleft;
+}
+
+
+
+/**
+ * Matrix multiplication function callable from python.
+ */
+static PyObject* multiply_extended(PyObject *self, PyObject *args)
+{
+    /* Define the arrays. */
+    PyArrayObject *left, *right;
+    int cutoff, symmetry = 0, clear_corners = 0;
+    char flags = 0;
+
+    /* Parse the arguments. symmetry and flags are optional. */
+    if (!PyArg_ParseTuple(args, "O!O!i|iic", &PyArray_Type, &left, &PyArray_Type, &right, &cutoff, &symmetry, &clear_corners, &flags))
+        return NULL;
+
+    if (PyArray_TYPE(left) != NPY_COMPLEX_TYPE || PyArray_TYPE(right) != NPY_COMPLEX_TYPE)
+        return PyErr_Format(PyExc_ValueError, "arrays of type complex128 required");
+    if (    PyArray_NDIM(left) != PyArray_NDIM(right) ||
+            PyArray_NDIM(left) < 2 ||
+            PyArray_DIM(left, 1) != PyArray_DIM(right, 0))
+        return PyErr_Format(PyExc_ValueError, "Shape of the 2 matrices does not allow multiplication.");
+
+    const int min_size = cutoff + (clear_corners > 4 ? clear_corners-1 : 3);
+    if (cutoff > 0 && (
+                PyArray_DIM(left, 0) < min_size ||
+                PyArray_DIM(left, 1) < min_size ||
+                PyArray_DIM(right, 1) < min_size))
+        return PyErr_Format(PyExc_ValueError, "Matrices is too small (%d, %d, %d) or cutoff too large (%d).", PyArray_DIM(left, 0), PyArray_DIM(left, 1), PyArray_DIM(right, 1), cutoff);
+
+
+    if (PyArray_NDIM(left) > 2 &&
+            memcmp(PyArray_DIMS(left) + 2, PyArray_DIMS(right) + 2, (PyArray_NDIM(left)-2)*sizeof(npy_intp)))
+        return PyErr_Format(PyExc_ValueError, "dimensions of matrices do not match");
+
+    /* Get an F-contiguous version of right. */
+    PyArrayObject *fright = (PyArrayObject*) PyArray_FromArray(
+                right,
+                PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                NPY_ARRAY_WRITEABLE
+                    | NPY_ARRAY_F_CONTIGUOUS
+                    | NPY_ARRAY_ALIGNED
+            );
+    if (!fright)
+        return PyErr_Format(PyExc_RuntimeError, "Failed to create array");
+
+    if (symmetry && (
+                abs(((int) PyArray_DIM(left,  0)) - ((int) PyArray_DIM(left,  1))) > 1 ||
+                abs(((int) PyArray_DIM(left,  0)) - ((int) PyArray_DIM(right, 1))) > 1 ||
+                abs(((int) PyArray_DIM(right, 0)) - ((int) PyArray_DIM(right, 1))) > 1 ))
+            symmetry = 0;
+
+    PyArrayObject *out;
+    if (symmetry)
+    {
+        /* In this case, functions decide dyamically whether fleft needs to be copied.
+         * Something like fleft will be created in these functions. */
+        if (PyArray_NDIM(left) == 2)
+            out = multiply_extended_2d_symmetric(left, fright, cutoff, symmetry, clear_corners, flags);
+        else
+            out = multiply_extended_nd_symmetric(left, fright, cutoff, symmetry, clear_corners, flags);
+    }
+    else
+    {
+        /* general matrix matrix multiplication does not overwrite the left array, no copy required.
+         * Create an F-contiguous version of left. */
+        PyArrayObject *fleft = (PyArrayObject*) PyArray_FromArray(
+                    left,
+                    PyArray_DescrFromType(NPY_COMPLEX_TYPE),
+                    NPY_ARRAY_WRITEABLE
+                        | NPY_ARRAY_F_CONTIGUOUS
+                        | NPY_ARRAY_ALIGNED
+                );
+        if (!fleft)
+        {
+            Py_DECREF(fright);
+            return PyErr_Format(PyExc_RuntimeError, "Failed to create array");
+        }
+
+        if (PyArray_NDIM(fleft) == 2)
+            out = multiply_extended_2d(fleft, fright, cutoff, clear_corners);
+#ifdef PARALLEL_EXTRA_DIMS
+        else if (omp_get_max_threads() > 1)
+            out = multiply_extended_nd_parallel(fleft, fright, cutoff, clear_corners);
+#endif /* PARALLEL_EXTRA_DIMS */
+        else
+            out = multiply_extended_nd(fleft, fright, cutoff, clear_corners);
+
+        Py_DECREF(fleft);
+    }
+
+    Py_DECREF(fright);
+    return (PyObject*) out;
+}
+
+
+/**
+ * @file
+ * @section sec_module_setup Python module setup
+ */
+
+
+/** define functions in module */
+static PyMethodDef FloquetMethods[] =
+{
+     {
+         "extend_matrix",
+         extend_matrix,
+         METH_VARARGS,
+         PyDoc_STR(
+                 "Extrapolate matrix along diagonals.\n"
+                 "Arguments:\n"
+                 "  1. numpy array of type complex128 and shape (n,m,...)\n"
+                 "  2. cutoff, positive integer fulfilling min(n,m)+2 > cutoff\n"
+                 "Returns:\n"
+                 "  numpy array of shape (n+2*cutoff, m+2*cutoff, ...)"
+             )
+     },
+     {
+         "multiply_extended",
+         multiply_extended,
+         METH_VARARGS,
+         PyDoc_STR(
+                 "Multiply virtually extended matrices.\n"
+                 "Arguments:\n"
+                 "  1. a, numpy array of type complex128 and shape (n,k,...)\n"
+                 "  2. b, numpy array of type complex128 and shape (k,m,...)\n"
+                 "  3. cutoff, positive integer fulfilling min(n,k,m)+2 > cutoff\n"
+                 "  4. (optional) symmetry, integer, allowed values: {-1,0,+1}\n"
+                 "     Speed up calculation by symmetry = s1*s2 if\n"
+                 "         a[::-1,::-1].conjugate() == s1*a  and\n"
+                 "         b[::-1,::-1].conjugate() == s2*b\n"
+                 "  5. (optional) clear_corners: integer < 2*nmax+1 - cutoff\n"
+                 "  6. (optional) flags, char, set to 1 to allow overwriting left matrix\n"
+                 "Returns:\n"
+                 "  numpy array of shape (n,m,...), product of a and b.\n"
+                 "Note: the extra dimensions denoted ... must be the same for all matrices."
+             )
+     },
+     {
+         "invert_extended",
+         invert_extended,
+         METH_VARARGS,
+         PyDoc_STR(
+                 "Extrapolate matrix, invert it and reduce it to original size.\n"
+                 "Arguments:\n"
+                 "  1. numpy array of type complex128 and shape (n,n,...)\n"
+                 "  2. cutoff, positive integer fulfilling n+2 > cutoff\n"
+                 "  3. (optional) lazy_factor, int, 0 <= lazy_factor < cutoff:\n"
+                 "     reduce matrix size by this amount in each direction after\n"
+                 "     extrapolation but before inversion.\n"
+                 "Returns:\n"
+                 "  inverse, numpy array of shape (n,n,...)"
+             )
+     },
+     {NULL, NULL, 0, NULL}
+};
+
+
+/** module initialization (python 3) */
+static struct PyModuleDef cModPyDem = {
+    PyModuleDef_HEAD_INIT,
+    "rtrg_c",
+    PyDoc_STR(
+            "Auxilliary functions for calculations with Floquet matrices.\n"
+            "Floquet matrices are represented by square matrices, which should\n"
+            "be extrapolated along the diagonal to avoid truncation effects.\n"
+            "Non-square matrices may also be used to account for reduced matrices\n"
+            "in special symmetric cases.\n\n"
+            "NOTE:\n"
+            "    Use\n"
+            "        rtrg_c.multiply_extended(b.T, a.T, cutoff, ...).T\n"
+            "        rtrg_c.invert_extended(a.T, cutoff, ...).T\n"
+            "        rtrg_c.extend_matrix(a.T, cutoff).T\n"
+            "    where the last 2 dimensions of a, b define Floquet matrices\n"
+            "    instead of\n"
+            "        rtrg_c.multiply_extended(a, b, cutoff, ...)\n"
+            "        rtrg_c.invert_extended(a, cutoff, ...)\n"
+            "        rtrg_c.extend_matrix(a, cutoff)\n"
+            "    for better performance. This will pass F-contiguous arrays to\n"
+            "    rtrg_c if original arrays were C-contiguous (standard in numpy).\n"
+        ),
+    -1,
+    FloquetMethods,
+    NULL,
+    NULL,
+    NULL,
+    NULL
+};
+
+PyMODINIT_FUNC PyInit_rtrg_c(void)
+{
+    PyObject *module;
+    module = PyModule_Create(&cModPyDem);
+    if(module == NULL)
+        return NULL;
+    /* IMPORTANT: this must be called */
+    import_array();
+    if (PyErr_Occurred())
+        return NULL;
+    return module;
+}
diff --git a/package/src/frtrg_kondo/settings.py b/package/src/frtrg_kondo/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..7224c62640634ebad0b0991b1b1f2008c7764f9b
--- /dev/null
+++ b/package/src/frtrg_kondo/settings.py
@@ -0,0 +1,175 @@
+# Copyright 2022 Valentin Bruch <valentin.bruch@rwth-aachen.de>
+# License: MIT
+"""
+Kondo FRTRG, module for handling global settings
+
+Settings for Kondo model FRTRG calculations.
+This module defines default values for some settings, which can be overwritten
+by environment variables. The complicated structure of these settings is not
+really necessary, but I learned something from it.
+
+You can switch between different environments:
+>>> import settings
+>>> settings.env1 = settings.GlobalFlags()
+>>> settings.env1.USE_REFERENCE_IMPLEMENTATION = 1
+>>> settings.env2 = settings.GlobalFlags()
+>>> settings.env2.IGNORE_SYMMETRIES = 1
+>>> settings.env1.update_globals()
+>>> # Now we are in environment 1
+>>> settings.env2.update_globals()
+>>> # Now we are in environment 2
+>>> settings.defaults.update_globals()
+>>> # Now we are in the default environment
+"""
+
+import os
+try:
+    import colorlog as logging
+    logging.basicConfig(level=logging.INFO, format='%(purple)s%(asctime)s%(reset)s %(log_color)s%(levelname)s%(reset)s %(message)s', datefmt="%H:%M:%S")
+except:
+    import logging
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', datefmt="%H:%M:%S")
+
+
+class GlobalFlags:
+    '''
+    Define global settings that should be available in all modules.
+
+    BASEPATH
+        Path to save and load files. This should be a directory.
+        The database will be stored in this directory.
+
+    FILENAME
+        File name to which data should be saved, must be relative to BASEPATH
+
+    DB_CONNECTION_STRING
+        String to connect to database, e.g.:
+        "sqlite:///path/to/file.sqlite"
+        "mariadb+pymysql://user:password@host/dbname"
+
+    MIN_VERSION = 12
+        Minimum baseversion for loading files. Files with older version will
+        be ignored.
+
+    LOG_TIME = 10
+        Log progress to stdout every LOG_TIME seconds
+
+    ENFORCE_SYMMETRIC = 0
+        Raise exception if no symmetries can be used in calculation steps.
+
+    CHECK_SYMMETRIES = 0
+        Check symmetries before each iteration of the RG equations.
+
+    IGNORE_SYMMETRIES = 0
+        Do not use any symmetries.
+
+    EXTRAPOLATE_VOLTAGE = 0
+        How to extrapolate voltage copies:
+        0 means don't extrapolate but just use nearest available voltage.
+        1 means do quadratic extrapolation.
+
+    LAZY_INVERSE_FACTOR = 0.25
+        Factor between 0 and 1 for truncation of extended matrix before inversion.
+        0 gives most precise results, 1 means discarding padding completely in inversion.
+
+    USE_CUBLAS = 0
+        (try to) use rtrg_cublas instead of rtrg_c
+
+    USE_REFERENCE_IMPLEMENTATION = 0
+        Use the (slower) reference implementation of RG equation.
+        Enabling this option also sets IGNORE_SYMMETRIES=1.
+    '''
+
+    # Default values of settings. These can be overwritten directly by setting
+    # environment variables.
+    defaults = dict(
+        BASEPATH = os.path.expanduser("~/frtrg_data"),
+        DB_CONNECTION_STRING = "sqlite:///" + os.path.expanduser("~/frtrg_data/frtrg.sqlite"),
+        FILENAME = "frtrg-01.h5",
+        VERSION = (14, 3, 68, 98059684),
+        MIN_VERSION = (14,0),
+        LOG_TIME = 10, # in s
+        ENFORCE_SYMMETRIC = 0,
+        CHECK_SYMMETRIES = 0,
+        IGNORE_SYMMETRIES = 0,
+        EXTRAPOLATE_VOLTAGE = 0,
+        LAZY_INVERSE_FACTOR = 0.25,
+        USE_CUBLAS = 0,
+        USE_REFERENCE_IMPLEMENTATION = 0,
+        logger = logging.getLogger("log"),
+    )
+
+    def __init__(self):
+        self.settings = {}
+        self.update_globals()
+
+    def __setattr__(self, key, value):
+        if key in GlobalFlags.defaults and key != 'settings':
+            self.settings[key] = value
+        else:
+            super().__setattr__(key, value)
+
+    def __setitem__(self, key, value):
+        if key in GlobalFlags.defaults and key != 'settings':
+            self.settings[key] = value
+        else:
+            raise KeyError("invalid key: %s"%key)
+
+    def __getattr__(self, key):
+        try:
+            return self.settings[key]
+        except KeyError:
+            try:
+                return self.__class__.defaults[key]
+            except KeyError:
+                raise AttributeError()
+
+    def __getitem__(self, key):
+        try:
+            return self.settings[key]
+        except KeyError:
+            return self.__class__.defaults[key]
+
+    @classmethod
+    def read_environment(cls, verbose=True):
+        for key, value in cls.defaults.items():
+            if key in os.environ:
+                cls.defaults[key] = type(value)(os.environ[key])
+                if verbose:
+                    cls.defaults['logger'].info('Updated from environment: %s = %s'%(key, cls.defaults[key]))
+        if cls.defaults['USE_REFERENCE_IMPLEMENTATION']:
+            cls.defaults['IGNORE_SYMMETRIES'] = 1
+        if "LOG_LEVEL" in os.environ:
+            cls.defaults["logger"].setLevel(os.environ["LOG_LEVEL"])
+
+    def reset(self):
+        self.settings.clear()
+
+    def assert_compatibility(self):
+        if self.USE_REFERENCE_IMPLEMENTATION:
+            self.IGNORE_SYMMETRIES = 1
+        assert not (self.IGNORE_SYMMETRIES and self.ENFORCE_SYMMETRIC)
+
+    def update_globals(self):
+        self.assert_compatibility()
+        settings = self.__class__.defaults.copy()
+        settings.update(self.settings)
+        globals().update(settings)
+
+GlobalFlags.defaults["logger"].setLevel(logging.INFO)
+
+
+def export():
+     return dict(
+            VERSION = VERSION,
+            ENFORCE_SYMMETRIC = ENFORCE_SYMMETRIC,
+            CHECK_SYMMETRIES = CHECK_SYMMETRIES,
+            IGNORE_SYMMETRIES = IGNORE_SYMMETRIES,
+            EXTRAPOLATE_VOLTAGE = EXTRAPOLATE_VOLTAGE,
+            LAZY_INVERSE_FACTOR = LAZY_INVERSE_FACTOR,
+            USE_CUBLAS = USE_CUBLAS,
+            USE_REFERENCE_IMPLEMENTATION = USE_REFERENCE_IMPLEMENTATION,
+        )
+
+GlobalFlags.read_environment()
+defaults = GlobalFlags()
diff --git a/rtrg_c.c b/rtrg_c.c
index cb975c9273cbf22a46c42d928db06e01f1ef3b3f..c2e1cdbe684e8bfb115f8866b6fa4ee0380b9064 100644
--- a/rtrg_c.c
+++ b/rtrg_c.c
@@ -1,7 +1,7 @@
 /*
 MIT License
 
-Copyright (c) 2021 Valentin Bruch
+Copyright (c) 2021 Valentin Bruch <valentin.bruch@rwth-aachen.de>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/rtrg_c.makefile b/rtrg_c.makefile
index a555c85217504f58ed98be2465d983a78fee56ed..ce9f6ebfc41f3fbc572856dc3db7806571fd1076 100644
--- a/rtrg_c.makefile
+++ b/rtrg_c.makefile
@@ -1,5 +1,5 @@
 #!/bin/make -f
-PYTHON_VERSION_STR="39-x86_64-linux-gnu"
+PYTHON_VERSION_STR="310-x86_64-linux-gnu"
 
 rtrg_c.cpython-$(PYTHON_VERSION_STR).so: rtrg_c.c
 	python setup.py build_ext --inplace
diff --git a/rtrg_cublas.makefile b/rtrg_cublas.makefile
index e5387e1a33097a75f12bb2e1aa270df90a7312b9..39cd7377416258736abadd1ca4030c0b87c858ad 100644
--- a/rtrg_cublas.makefile
+++ b/rtrg_cublas.makefile
@@ -1,5 +1,5 @@
 #!/bin/make -f
-PYTHON_VERSION_STR="39-x86_64-linux-gnu"
+PYTHON_VERSION_STR="310-x86_64-linux-gnu"
 
 all: libcuda_helpers.a rtrg_cublas.cpython-$(PYTHON_VERSION_STR).so
 
diff --git a/setup.py b/setup.py
index ed98ff77f7a6fdd5a762b340c102cda33a7493e1..83398546e513250ffddb0d78e45ef8163960235c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,7 @@
 # build with:
 # python3 setup.py build_ext --inplace
 
-from distutils.core import setup, Extension
+from setuptools import setup, Extension
 import numpy as np
 from os import environ