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