# DMFT for Hubbard and Hubbard-like models

We now present a Python script for performing DMFT calculations with the NRG
as the impurity solver. The script is fairly complete and robust, and as such it is suitable for production runs.
It supports a variety of models with different symmetry types (including cases
with block structure and with matrix-valued Green’s functions), controls
the chemical potential to achieve the selected band occupancy, performs linear or Broyden mixing
to accelerate the convergence, stores intermediate results (checkpoints) to enable
restarts, and supports arbitrary density of states in the band (e.g. tabulated in a file).
Here is the `listing`

:

```
# DMFT(NRG) for Hubbard and Hubbard-like models (including Hubbard-Holstein model)
# RZ, Feb 2020
from triqs.gf import *
from triqs.operators import *
from h5 import *
from triqs.utility import mpi
from nrgljubljana_interface import Solver, MeshReFreqPts, hilbert_transform_refreq
import math, os, warnings
import numpy as np
from scipy import interpolate, integrate, special, optimize
from collections import OrderedDict
# Parameters
model = "SIAM" # impurity model (from the template library)
dos = "Bethe" # "Bethe" for a Bethe lattice (semicircular) DOS or a file name for reading tabulated DOS data from a file;
# format of the DOS file: lines with "eps rho(eps)" pairs; arbitrary bandwidth, but should be within the NRG mesh
Bethe_unit = 1.0 # half-bandwidth in the Bethe lattice DOS for dos="Bethe"; 1.0 for D=1 units, 2.0 for t=1 units
U = 2.0 # Hubbard electron-electron repulsion
occupancy_goal = 0.8 # band occupancy (summed over spin)
T = 1e-4 # temperature
B = None # magnetic field for spin-polarized calculation with U(1) symmetry;
# None = non-spin-polarized calculation with SU(2) symmetry
omega = None # Holstein phonon frequency
g1 = None # electron-phonon coupling strength
n1 = None # shift n1 in the Holstein model definition
verbose = True # show info messages during the iteration
verbose_nrg = False # show detailed output from the NRG solver
store_steps = True # store intermediate results to files (one subdirectory per iteration)
min_iter = 5 # minimum number of iterations (prevents premature exit from the DMFT loop)
max_iter = 50 # maximum number of iterations (signals convergence failure)
eps_prev = 1e-5 # convergence criterium: integrated diff between two consecutive local spectral functions
eps_loc_imp = 1e-5 # convergence criterium: integrated diff between local and impurity spectral function
eps_occupancy = 1e-4 # convergence criterium: difference from the target occupancy (occupancy_goal)
mixing_method = "broyden" # "linear" or "broyden" mixing; the quantity being mixed is the hybridisation function
occup_method = "adjust" # "adjust" or None; adjust=shift mu so that the current GF meets the occupancy goal
alpha = 0.5 # mixing parameter from both linear and Broyden mixing
max_mu_adjust = 10 # number of cycles for adjusting the value of the chemical potential
mesh_max = 10.0 # logarithmic mesh: maximum frequency (should be large enough to contain the full spectrum)
mesh_min = 1e-5 # logarithmic mesh: minimum frequency (should be smaller than the temperature T)
mesh_ratio = 1.01 # logarithmic mesh: common ratio of the mesh (1.01 is usually a good choice)
Delta_min = 1e-5 # minimal value for the hybridisation function; if too large, it produces incorrect spectral distribution,
# if too small, it leads to discretization problems (1e-5 is usually a good and rather safe choice)
normalize_to_one = True # scaling of the spectral function (relevant in the case of block structure and/or matrix structure of GF)
solution_filename = "solution.h5"
checkpoint_filename = "checkpoint.h5"
# Additional quantities of interest
observables = ["n_d", "n_d^2"]
if B is not None:
observables.extend(["SZd"])
if "Holstein" in model:
observables.extend(["nph", "displ", "displ^2"])
# Run-time global variables
itern = 0 # iteration counter
verbose = verbose and mpi.is_master_node() # output is only produced by the master node
store_steps = store_steps and mpi.is_master_node() # output is only produced by the master node
symtype = ("QS" if B is None else "QSZ")
# Set up the Solver
S = Solver(model=model, symtype=symtype, mesh_max=mesh_max, mesh_min=mesh_min, mesh_ratio=mesh_ratio)
S.set_verbosity(verbose_nrg)
newG = lambda : S.G_w.copy() # Creates a BlockGf object of appropriate structure
nr_blocks = lambda bgf : len([bl for bl in bgf.indices]) # Returns the number of blocks in a BlockGf object
block_size = lambda bl : len(S.G_w[bl].indices[0]) # Matrix size of Green's functions in block 'bl'
identity = lambda bl : np.identity(block_size(bl)) # Returns the identity matrix in block 'bl'
# Solve Parameters
sp = { "T": T, "Lambda": 2.0, "Nz": 4, "Tmin": 1e-5, "keep": 10000, "keepenergy": 10.0 }
# Model Parameters
mp = { "U1": U, "B1": B, "omega": omega, "g1": g1, "n1": n1 }
for k,v in mp.items():
if v is None: del mp[k] # Remove undefined parameters from the dictionary
sp["model_parameters"] = mp
# Update the chemical potential
def set_mu(new_mu):
global mu
mu = new_mu
sp["model_parameters"]["eps1"] = -mu
# Low-level NRG Parameters (optional tweaks)
nrgp = {}
#nrgp["bandrescale"] = 5.0
S.set_nrg_params(**nrgp)
# Initialize a function ht0 for calculating the Hilbert transform of the DOS
if (dos == "Bethe"):
ht1 = lambda z: 2*(z-1j*np.sign(z.imag)*np.sqrt(1-z**2)) # Analytical expression for Hilbert transform of Bethe lattice DOS
global ht0
ht0 = lambda z: ht1(z/Bethe_unit)
else:
table = np.loadtxt(dos)
global dosA
dosA = Gf(mesh=MeshReFreqPts(table[:,0]), target_shape=[])
for i, w in enumerate(dosA.mesh):
dosA[w] = np.array([[ table[i,1] ]])
ht0 = lambda z: hilbert_transform_refreq(dosA, z)
# Wrapper for ht0 that ensures that the imaginary part of Sigma is negative
EPS = 1e-20 # Cut-off parameter for fixing causality violation
ht = lambda z: ht0(z.real+1j*(z.imag if z.imag>0.0 else EPS)) # Fix problems due to causality violation
# Calculate a GF from hybridisation and self-energy
def calc_G(Delta, Sigma, mu):
G = newG()
for bl in G.indices:
for w in G.mesh:
G[bl][w] = np.linalg.inv( (w + mu)*identity(bl) - Delta[bl][w] - Sigma[bl][w] ) # !!!
return G
# Index range of a GF
index_range = lambda G : range(len(G.indices[0]))
# Return an interpolation-object representation of a spectral function for GF G
def interp_A(G):
lx = np.array(list(G.mesh.values()))
ly = sum( sum( -(1.0/math.pi)*np.array(G[bl].data[:,i,i].imag) for i in index_range(G[bl]) ) # matrix trace
for bl in G.indices ) # sum over blocks
if normalize_to_one:
nr = sum( sum( 1 for i in index_range(G[bl]) ) for bl in G.indices ) # number of contributions
ly = ly/nr # rescale
return interpolate.interp1d(lx, ly, kind='cubic', bounds_error=False, fill_value=0)
# Calculate occupancy for given hybridisation, self-energy and chemical potential
def calc_occupancy(Delta, Sigma, mu):
Gtrial = calc_G(Delta, Sigma, mu)
f = interp_A(Gtrial)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
n = integrate.quad(lambda x : 2*f(x)*special.expit(-x/T), -mesh_max, mesh_max)
return n[0]
# Update mu towards reaching the occupancy goal
def update_mu(Delta, Sigma):
sol = optimize.root_scalar(lambda x : calc_occupancy(Delta, Sigma, x)-occupancy_goal, x0=mu, x1=mu-0.1)
set_mu(sol.root)
# Calculate the local lattice GF and the hybridisation function for the effective impurity model
# for a given self-energy function
def self_consistency(Sigma):
Gloc = newG()
for bl in Gloc.indices:
for w in Gloc.mesh:
for i in range(block_size(bl)):
for j in range(block_size(bl)): # assuming square matrix
if i == j:
Gloc[bl][w][i,i] = ht(w + mu - Sigma[bl][w][i,i]) # Hilbert-transform
else:
assert abs(Sigma[bl][w][i,j])<1e-10, "This implementation only supports diagonal self-energy"
Gloc[bl][w][i,j] = 0.0
Glocinv = Gloc.inverse()
Delta = newG()
for bl in Delta.indices:
for w in Delta.mesh:
Delta[bl][w] = (w+mu)*identity(bl) - Sigma[bl][w] - Glocinv[bl][w] # !!!
return Gloc, Delta
# Iteratively adjust mu, taking into account the self-consistency equation.
# Returns an improved estimate for the hybridisation function.
def adjust_mu(Delta_in, Sigma):
old_mu = mu
Delta = Delta_in.copy()
for _ in range(max_mu_adjust):
update_mu(Delta, Sigma)
Gloc, Delta = self_consistency(Sigma)
new_mu = mu
if verbose: print("Adjusted mu from %f to %f\n" % (old_mu,new_mu))
return Gloc, Delta
# Difference between two Green's functions evaluated as the integrated squared difference between the
# corresponding spectral functions.
def gf_diff(a, b):
f_a = interp_A(a)
f_b = interp_A(b)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
diff = integrate.quad(lambda x : (f_a(x)-f_b(x))**2, -mesh_max, mesh_max)
return diff[0]
# Save a Green's function to a tabulated ASCII file
def save_Gf(fn, gf):
f = open(fn, "w")
for w in gf.mesh:
z = gf[w]
f.write("%f %f %f\n" % (w, z.real, z.imag))
# Save all blocks for a block GF to tabulated ASCII files
def save_BlockGf(fn, bgf):
for bl in bgf.indices:
save_Gf(fn + "_" + bl + ".dat", bgf[bl])
# Save a spectral function (-1/Pi Im GF) to a tabulated ASCII file
def save_A(fn, gf):
f = open(fn, "w")
for w in gf.mesh:
z = gf[w]
f.write("%f %f\n" % (w, -1.0/math.pi*z.imag))
# Save spectral functions for all blocks of the block GF
def save_BlockA(fn, bgf):
for bl in bgf.indices:
save_A(fn + "_" + bl + ".dat", bgf[bl])
# Store the complete set of results
def store_result(fn, S):
with HDFArchive(fn, 'w') as arch:
arch["S"] = S
# Global variables
arch["Gself"] = Gself
arch["Gloc"] = Gloc
arch["mu"] = mu
# Load the minimal set of stored results for restarting the calculation
def load_Sigma_mu(fn):
with HDFArchive(fn, 'r') as arch:
Sigma = arch["S"].Sigma_w
mu = arch["mu"]
return Sigma, mu
# Initial Sigma and mu from a file
def restart_calculation(fn):
if verbose: print("Starting from stored results in file %s\n" % fn)
return load_Sigma_mu(fn) # Load data from an HDF5 archive
# Initial Sigma and mu when starting from scratch
def new_calculation():
if verbose: print("Starting from scratch\n")
Sigma = newG()
for bl in Sigma.indices:
for w in Sigma.mesh:
Sigma[bl][w] = U*occupancy_goal/2.0 # Initialize self-energy with the Hartree shift
mu = U/2.0 # initial approximaiton for the chemical potential
return Sigma, mu
# Provides the initial Sigma and mu
def initial_Sigma_mu():
if os.path.isfile(solution_filename): # continue DMFT iteration after failed convergence
return restart_calculation(solution_filename)
elif os.path.isfile(checkpoint_filename): # continue DMFT iteration after interruption
return restart_calculation(checkpoint_filename)
else: # start from scratch
return new_calculation()
# Exception to raise when convergence is reached
class Converged(Exception):
def __init__(self, message):
self.message = message
# Exception to raise when convergence is not reached (e.g. maximum nr of iterations exceeded)
class FailedToConverge(Exception):
def __init__(self, message):
self.message = message
# Formatting of the header in stats.dat
def fmt_str_header(nr_val):
str = "{:>5}" # itern
for _ in range(nr_val-1): str += " {:>15}"
return str + "\n"
# Formatting of the results in stats.dat
def fmt_str(nr_val):
str = "{:>5}" # itern
for _ in range(nr_val-1): str += " {:>15.8g}"
return str + "\n"
# Adjust Im(Delta) so that the hybridisation strength is not too small for the NRG discretization
# scheme to break down.
def fix_hyb_function(Delta, Delta_min):
Delta_fixed = Delta.copy()
for bl in Delta.indices:
for w in Delta.mesh:
for n in range(block_size(bl)): # only diagonal parts
r = Delta[bl][w][n,n].real
i = Delta[bl][w][n,n].imag
Delta_fixed[bl][w][n,n] = r + 1j*(i if i<-Delta_min else -Delta_min)
# Possible improvement: re-adjust the real part so that the Kramers-Kronig relation is maintained
return Delta_fixed
# Perform a DMFT step. Input is the hybridization function for solving the effective impurity model,
# output is a new hybridization function resulting from the application of the DMFT self-consistency equation.
def dmft_step(Delta_in):
global itern
itern += 1
if verbose:
print("Iteration %i min_iter=%i max_iter=%i\n" % (itern, min_iter, max_iter))
Delta_in_fixed = fix_hyb_function(Delta_in, Delta_min)
S.Delta_w << Delta_in_fixed
S.solve(**sp) # Solve the impurity model
global Gself, Gloc, Gloc_prev
Gself = calc_G(Delta_in_fixed, S.Sigma_w, mu) # impurity GF ("self-energy-trick" improved)
Gloc, Delta = self_consistency(S.Sigma_w) # apply the DMFT self-consistency equation
diff_loc_imp = gf_diff(Gself, Gloc) # difference between impurity and local lattice GF
diff_prev = gf_diff(Gloc, Gloc_prev) # difference between two consecutively computed local latice GFs
Gloc_prev = Gloc.copy()
occupancy = calc_occupancy(Delta, S.Sigma_w, mu)
diff_occupancy = abs(occupancy-occupancy_goal) # this difference is used as the measure of deviation
stats = OrderedDict([("itern", itern), ("mu", mu), ("diff_loc_imp", diff_loc_imp), ("diff_prev", diff_prev),
("diff_occupancy", diff_occupancy), ("occupancy", occupancy)])
for i in observables:
stats[i] = S.expv[i]
header_string = fmt_str_header(len(stats)).format(*[i for i in stats.keys()])
stats_string = fmt_str(len(stats)).format(*[i for i in stats.values()])
if mpi.is_master_node():
if itern == 1: stats_file.write(header_string)
stats_file.write(stats_string)
if verbose: print("stats: %sstats: %s" % (header_string, stats_string))
if store_steps:
os.mkdir(str(itern)) # one subdirectory per iteration
save_BlockGf(str(itern)+"/Delta", Delta_in_fixed)
save_BlockGf(str(itern)+"/Sigma", S.Sigma_w) # self-energy
save_BlockGf(str(itern)+"/G", Gloc) # local lattice Green's function
save_BlockA(str(itern)+"/A", Gloc) # spectral function of local lattice GF
store_result(str(itern)+"/"+solution_filename, S)
if mpi.is_master_node():
store_result(checkpoint_filename, S) # for checkpoint/restart functionality
# Check for convergence. The only way to exit the DMFT loop is by generating exceptions.
if (diff_loc_imp < eps_loc_imp and
diff_prev < eps_prev and
diff_occupancy < eps_occupancy and
itern >= min_iter):
raise Converged(stats_string)
if (itern == max_iter):
raise FailedToConverge(stats_string)
if occup_method == "adjust":
Gloc, Delta = adjust_mu(Delta, S.Sigma_w) # here we update mu to get closer to target occupancy
return Delta
# DMFT driver routine with linear mixing
def solve_with_linear_mixing(Delta, alpha):
Delta_in = Delta.copy()
while True:
Delta_out = dmft_step(Delta_in)
newDelta = alpha*Delta_out + (1-alpha)*Delta_in
Delta_in << newDelta
# Convert GF object to a linear nparray
def gf_to_nparray(gf):
return gf.data.flatten()
# Stack elements from all blocks in a block BF
def bgf_to_nparray(bgf):
return np.hstack((bgf[bl].data.flatten() for bl in bgf.indices))
# Convert a linear numpy array to a single GF object
def nparray_to_gf(a, gf):
b = a.reshape(gf.data.shape)
gf.data[:,:,:] = b[:,:,:]
# Extract blocks from linear numpy array to a block GF
def nparray_to_bgf(a):
G = newG()
split = np.split(a, nr_blocks(G)) # Here we assume all blocks are equally large
for i, bl in enumerate(G.indices):
nparray_to_gf(split[i], G[bl])
return G
# DMFT driver routine with Broyden mixing: the goal is to find a root of the function F(Delta)=dmft_step(Delta)-Delta.
F = lambda Delta : dmft_step(Delta)-Delta
npF = lambda x : bgf_to_nparray(F(nparray_to_bgf(x)))
def solve_with_broyden_mixing(Delta, alpha):
xin = bgf_to_nparray(Delta)
optimize.broyden1(npF, xin, alpha=alpha, reduction_method="svd", max_rank=10, verbose=verbose, f_tol=1e-99) # Loop forever (small f_tol!)
def solve(Delta):
if (mixing_method=="linear"):
solve_with_linear_mixing(Delta, alpha)
if (mixing_method=="broyden"):
solve_with_broyden_mixing(Delta, alpha)
# Initialization
Sigma0, mu0 = initial_Sigma_mu() # Get initial self-energy and chemical potential
set_mu(mu0) # Set global variable mu and update the impurity level (epsilon_d = -mu)
Gloc, Delta = self_consistency(Sigma0) # Initialize local GF Gloc and hybridization function Delta
Gloc, Delta = adjust_mu(Delta, Sigma0) # Adjust mu (and return updated Glocal and Delta as well)
Gloc_prev = Gloc.copy() # Store copy for checking convergence
if mpi.is_master_node():
global stats_file
stats_file = open("stats.dat", "w", buffering=1) # line buffered
try:
solve(Delta)
except Converged as c:
if verbose: print("Converged: %s" % c.message)
if mpi.is_master_node():
store_result(solution_filename, S) # full converged results as an HDF5 file
os.remove(checkpoint_filename) # checkpoint file is no longer needed
save_BlockA("A", Gloc) # converged spectral function for quick plotting
except FailedToConverge as c:
if verbose: print("Failed to converge: %s" % c.message) # ... but restart is possible from the checkpoint file
mpi.barrier() # Synchronized exit
```

Running this script takes between an hour or several hours, depending on the
degree of parallelization that is enabled and the available computing resources.
During the run, some basic information about the DMFT iteration is stored in the file `stats.dat`

:

```
itern mu diff_loc_imp diff_prev diff_occupancy occupancy n_d n_d^2
1 0.64216213 0.008410854 0.1639845 0.065470795 0.86547079 0.88853941 1.0170551
2 0.57416456 0.0030978054 0.0033493961 0.048255692 0.84825569 0.86718719 0.97416922
3 0.52350142 0.00032006683 0.0045037239 0.044544862 0.84454486 0.85230857 0.94144344
4 0.47628807 0.00017268295 0.00071735455 0.027302305 0.82730231 0.8329584 0.9156406
5 0.44771388 5.3787042e-05 0.0006119636 0.015770286 0.81577029 0.81874584 0.89894272
6 0.4314115 3.43912e-05 9.5803891e-05 0.0092612995 0.8092613 0.81131439 0.88949259
7 0.42188467 3.6268579e-05 7.0325948e-05 0.0066369482 0.80663695 0.80579098 0.88292743
8 0.41508851 4.5411105e-05 1.8109524e-05 0.0033722505 0.80337225 0.80338313 0.87950144
9 0.41163793 5.8842502e-05 3.9934759e-06 0.0018214352 0.80182144 0.80201381 0.87764421
10 0.4097753 6.3801883e-05 1.2231238e-06 0.0010891672 0.80108917 0.80114589 0.8765355
11 0.40866208 6.6673362e-05 4.6656617e-07 0.00065926119 0.80065926 0.80061656 0.87586398
12 0.40798848 6.8445133e-05 1.7684942e-07 0.00039989342 0.80039989 0.80029503 0.87545665
13 0.40757997 6.9588067e-05 6.2929404e-08 0.00024353334 0.80024353 0.8000997 0.87520934
14 0.40733122 6.6379215e-05 1.9177454e-05 0.0011155475 0.80111555 0.79886188 0.87412851
15 0.40619387 4.4607833e-05 3.3092449e-05 0.0015344904 0.79846551 0.79964663 0.87487525
16 0.40776042 9.3749908e-07 2.8603095e-05 0.00053041152 0.79946959 0.79906779 0.87497248
17 0.40830088 2.4812637e-07 1.0216056e-07 0.00028239426 0.79971761 0.79922373 0.87523217
18 0.40858862 1.3398751e-07 1.5633394e-07 0.00031811496 0.79968189 0.79939042 0.87547037
19 0.40891278 4.8884804e-09 6.2918295e-08 0.00011984463 0.79988016 0.79949007 0.87561332
20 0.4090349 1.3450417e-09 1.0571106e-08 6.0784735e-05 0.79993922 0.79955157 0.87568519
```

The first column is the iteration number, the second the current estimate of the chemical potential, the following three columns provide information about the convergence (difference between local lattice and impurity Green’s functions, difference between two consecutive local lattice Green’s function, and the difference between target and current band occupancy). This is followed by the occupancy calculed from the spectral function and by the expectation values of a range of local operators (the set depends on the model considered and on the symmetry type).

Note: NRG impurity solver produces big temporary files during the calculation. By default, these are stored in the working directory that is created in the current directory. To override this behavior, set the environment variable NRG_WORKDIR. A suitable choice is a local scratch hard drive or SSD, or a global scratch mount on a high-performance filesystem.

When convergence is reached, the final results are stored in an HDF5 archive file called `solution.h5`

.
This file contains all quantities of interest. Let us plot the spectral function:

```
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import math
from triqs.gf import *
from h5 import *
from nrgljubljana_interface import MeshReFreqPts
def GF_to_nparrays(A):
lx = np.array(list(A.mesh.values()))
ly = np.array(-1.0/math.pi*A.data[:,0,0].imag)
return lx, ly
with HDFArchive('solution.h5','r') as ar:
# Expectation values
print("<n>=",ar['expv']['n_d'])
print("<n^2>=",ar['expv']['n_d^2'])
# Spectral function (of local lattice GF)
A_w = ar['Gloc']['imp']
lx, ly = GF_to_nparrays(A_w)
plt.plot(lx, ly)
plt.show()
```

(Source code, png, hires.png, pdf)

Let us now go through the script in some more detail. We first import a number of useful modules:

```
from triqs.gf import *
from triqs.operators import *
from h5 import *
from triqs.utility import mpi
from nrgljubljana_interface import Solver, MeshReFreqPts, hilbert_transform_refreq
import math, os, warnings
import numpy as np
from scipy import interpolate, integrate, special, optimize
from collections import OrderedDict
```

We make use of scipy interpolation and integration routines to obtain highly accurate results based on
the real-frequency spectra, and we make use of the Broyden solver from `scipy.optimize`

.

The model name is a full path to the model definition in the template library, except the symmetry part (e.g. “SIAM”, “Holstein/Nph=10”, etc.).

```
model = "SIAM" # impurity model (from the template library)
```

The code comes with a built-in support for the Bethe lattice (with half-bandwidth set by the parameter `Bethe_unit`

),
or one can use tabulated DOS (example `script`

for generating the DOS file).

```
dos = "Bethe" # "Bethe" for a Bethe lattice (semicircular) DOS or a file name for reading tabulated DOS data from a file;
# format of the DOS file: lines with "eps rho(eps)" pairs; arbitrary bandwidth, but should be within the NRG mesh
Bethe_unit = 1.0 # half-bandwidth in the Bethe lattice DOS for dos="Bethe"; 1.0 for D=1 units, 2.0 for t=1 units
```

We then set key parameters, such as Hubbard \(U\), the occupancy, the temperature, and optionally the magnetic field.
The parameters that are not used should be set to `None`

.

```
U = 2.0 # Hubbard electron-electron repulsion
occupancy_goal = 0.8 # band occupancy (summed over spin)
T = 1e-4 # temperature
B = None # magnetic field for spin-polarized calculation with U(1) symmetry;
# None = non-spin-polarized calculation with SU(2) symmetry
```

One can control the level of verbosity and the amount of intermediate results that are stored to disk:

```
verbose = True # show info messages during the iteration
verbose_nrg = False # show detailed output from the NRG solver
store_steps = True # store intermediate results to files (one subdirectory per iteration)
```

The DMFT iteration parameters are set next. Note that there are several convergence criteria: they must all be satisfied.

```
min_iter = 5 # minimum number of iterations (prevents premature exit from the DMFT loop)
max_iter = 50 # maximum number of iterations (signals convergence failure)
eps_prev = 1e-5 # convergence criterium: integrated diff between two consecutive local spectral functions
eps_loc_imp = 1e-5 # convergence criterium: integrated diff between local and impurity spectral function
eps_occupancy = 1e-4 # convergence criterium: difference from the target occupancy (occupancy_goal)
```

To accelerate the convergence to the converged result, one can make use of two different mixing schemes: linear mixing (with parameter \(\alpha\)) or Broyden mixing (with initial Jacobian \(J=-1/\alpha\)). The direct DMFT iteration corresponds to linear mixing with \(\alpha=1\) which actually corresponds to no mixing at all.

```
mixing_method = "broyden" # "linear" or "broyden" mixing; the quantity being mixed is the hybridisation function
occup_method = "adjust" # "adjust" or None; adjust=shift mu so that the current GF meets the occupancy goal
alpha = 0.5 # mixing parameter from both linear and Broyden mixing
```

For adjusting the chemical potential, it is useful to go through several DMFT self-consistency cycles (using the same self-energy obtained in the last NRG run) to obtain an improved estimate. In practice order 10 cycles are quite sufficient in all cases.

```
max_mu_adjust = 10 # number of cycles for adjusting the value of the chemical potential
```

The logarithmic discretization mesh should span the full frequency range where the spectral function has non-negligible value. The common ratio should be sufficiently small to produce a fine discretization mesh.

```
mesh_max = 10.0 # logarithmic mesh: maximum frequency (should be large enough to contain the full spectrum)
mesh_min = 1e-5 # logarithmic mesh: minimum frequency (should be smaller than the temperature T)
mesh_ratio = 1.01 # logarithmic mesh: common ratio of the mesh (1.01 is usually a good choice)
```

The NRG has issues with regions of very low density of states in the band. To control those we define a minimal value of the hybridisation strenth \(\Gamma=-\mathrm{Im}(\Delta)\):

```
Delta_min = 1e-5 # minimal value for the hybridisation function; if too large, it produces incorrect spectral distribution,
# if too small, it leads to discretization problems (1e-5 is usually a good and rather safe choice)
```

The Python dictionary `observables`

contains the list of all operators whose expectation values we want to evaluate.
Some standard choices are the impurity occupancy and impurity occupancy squared, in the presence of magnetic field
also the impurity magnetization, and in the presence of phonon modes we might be interested in the phonon number
and oscillator displacement operators.

```
observables = ["n_d", "n_d^2"]
if B is not None:
observables.extend(["SZd"])
if "Holstein" in model:
observables.extend(["nph", "displ", "displ^2"])
```

We then set some further global variables that are used in the DMFT iteration:

```
itern = 0 # iteration counter
verbose = verbose and mpi.is_master_node() # output is only produced by the master node
store_steps = store_steps and mpi.is_master_node() # output is only produced by the master node
symtype = ("QS" if B is None else "QSZ")
```

One may notice that in the presence of the magnetic field, the code automatically switches to the appropriate
symmetry type (`QSZ`

, meaning conserved total charge and the z-component of total spin).

We are now ready to construct the impurity solver object:

```
S = Solver(model=model, symtype=symtype, mesh_max=mesh_max, mesh_min=mesh_min, mesh_ratio=mesh_ratio)
S.set_verbosity(verbose_nrg)
```

We usually want to suppress the detailed output from the NRG by setting `verbose_nrg`

to False. (In fact,
what this does is to redirect the output to a file named `log`

in the temporary directory, so that
the calculation can still be monitored.)

We define some useful functions for working with BlockGf objects:

```
newG = lambda : S.G_w.copy() # Creates a BlockGf object of appropriate structure
nr_blocks = lambda bgf : len([bl for bl in bgf.indices]) # Returns the number of blocks in a BlockGf object
block_size = lambda bl : len(S.G_w[bl].indices[0]) # Matrix size of Green's functions in block 'bl'
identity = lambda bl : np.identity(block_size(bl)) # Returns the identity matrix in block 'bl'
```

The key NRG solver parameters that control the discretization and the truncation are set here:

```
# Solve Parameters
sp = { "T": T, "Lambda": 2.0, "Nz": 4, "Tmin": 1e-5, "keep": 10000, "keepenergy": 10.0 }
```

The default value of the discretization parameter \(\Lambda=2\) is an excellent choice for most cases, as long as the calculation remains
feasible in reasonable time with the given computing resources; if not, one should increase \(\Lambda\).
We average over `Nz=4`

interleaves discretization grids; this is a good number for \(\Lambda=2\), but should be
increased for coarses discretization to reduce the artifacts. Furthermore, it needs to be increased for high-resolution calculations
where the broadening parameters are lower.
The parameter `Tmin`

controls the length of the Wilson chain and it has to be smaller than the physical temperature
\(T\). The truncation at rescaled energy 10 is sufficient to achieve good convergence of the spectral functions,
while beyond 12 there are hardly any improvements. `keep`

(the maximum number of multiplets kept) should
be some large number and it is basically constrained by the available memory.

Model parameters are contained as a separate Python dictionary. We set it here:

```
mp = { "U1": U, "B1": B, "omega": omega, "g1": g1, "n1": n1 }
for k,v in mp.items():
if v is None: del mp[k] # Remove undefined parameters from the dictionary
sp["model_parameters"] = mp
```

We remove parameters that are not relevant (those set to `None`

). Furthermore, the interface checks if
all required parameters for a given template are in fact defined, to prevent user errors that are
difficult to detect (e.g. typos).

We provide a function for updating the chemical potential. It sets the global variable `mu`

, as well
as updates the impurity level to \(\epsilon_d=-\mu\).

```
def set_mu(new_mu):
global mu
mu = new_mu
sp["model_parameters"]["eps1"] = -mu
```

If some details of the NRG calculation need to be tweaked, this can be achieve by setting the appropriate
parameters in `nrg_params`

.

```
nrgp = {}
#nrgp["bandrescale"] = 5.0
S.set_nrg_params(**nrgp)
```

Now we define the function `ht0`

which evaluates the Hilbert transform of the density of states for
a given complex value \(z\). The analytical expression for the Bethe lattice is hard-coded, while the
generic case is handled by reading the DOS from a file and doing the Hilbert transform numerically.

```
if (dos == "Bethe"):
ht1 = lambda z: 2*(z-1j*np.sign(z.imag)*np.sqrt(1-z**2)) # Analytical expression for Hilbert transform of Bethe lattice DOS
global ht0
ht0 = lambda z: ht1(z/Bethe_unit)
else:
table = np.loadtxt(dos)
global dosA
dosA = Gf(mesh=MeshReFreqPts(table[:,0]), target_shape=[])
for i, w in enumerate(dosA.mesh):
dosA[w] = np.array([[ table[i,1] ]])
ht0 = lambda z: hilbert_transform_refreq(dosA, z)
```

We wrap this in another function `ht`

which ensures that in cases of causality violation (imaginary
part of the self-energy being positive) things do no go terribly wrong. Tiny causality violations in NRG
commonly occur at very low temperatures close to the Fermi level, where \(\mathrm{Im}\Sigma\)
overshoots zero. This problem is well understood and, while annoying, it is usually not an issue, unless
one is explicitly interested in the low-temperature asymptotics.

```
EPS = 1e-20 # Cut-off parameter for fixing causality violation
ht = lambda z: ht0(z.real+1j*(z.imag if z.imag>0.0 else EPS)) # Fix problems due to causality violation
```

In addition, another potential problem in NRG calculation is the artifacts arising from regions of very low DOS.
We alleviate this problem by providing a minimal cutoff value `Delta_min`

for the hybridisation function:

```
def fix_hyb_function(Delta, Delta_min):
Delta_fixed = Delta.copy()
for bl in Delta.indices:
for w in Delta.mesh:
for n in range(block_size(bl)): # only diagonal parts
r = Delta[bl][w][n,n].real
i = Delta[bl][w][n,n].imag
Delta_fixed[bl][w][n,n] = r + 1j*(i if i<-Delta_min else -Delta_min)
# Possible improvement: re-adjust the real part so that the Kramers-Kronig relation is maintained
return Delta_fixed
```

In the following we will make use of a function which calculates a Green’s function given hybridisation, self-energy and chemical potential:

```
def calc_G(Delta, Sigma, mu):
G = newG()
for bl in G.indices:
for w in G.mesh:
G[bl][w] = np.linalg.inv( (w + mu)*identity(bl) - Delta[bl][w] - Sigma[bl][w] ) # !!!
return G
```

For accurate integration over spectral functions, we make use of cubic interpolation. Note that for frequencies outside the mesh range, the spectral function is assummed to be strictly zero.

```
def interp_A(G):
lx = np.array(list(G.mesh.values()))
ly = sum( sum( -(1.0/math.pi)*np.array(G[bl].data[:,i,i].imag) for i in index_range(G[bl]) ) # matrix trace
for bl in G.indices ) # sum over blocks
if normalize_to_one:
nr = sum( sum( 1 for i in index_range(G[bl]) ) for bl in G.indices ) # number of contributions
ly = ly/nr # rescale
return interpolate.interp1d(lx, ly, kind='cubic', bounds_error=False, fill_value=0)
```

This interpolation object is then used e.g. to compute the occupancy given hybridisation, self-energy and chemical potential:

```
def calc_occupancy(Delta, Sigma, mu):
Gtrial = calc_G(Delta, Sigma, mu)
f = interp_A(Gtrial)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
n = integrate.quad(lambda x : 2*f(x)*special.expit(-x/T), -mesh_max, mesh_max)
return n[0]
```

And this, in turn, is used for occupancy control. The following function determines the optimal value of the
chemical potential for given hybridisation and self-energy. It makes use of `root_scalar`

, which implements
the secant method with initial values shifted by 0.1. This seems to be robust and works quite well in practice.

```
def update_mu(Delta, Sigma):
sol = optimize.root_scalar(lambda x : calc_occupancy(Delta, Sigma, x)-occupancy_goal, x0=mu, x1=mu-0.1)
set_mu(sol.root)
```

Now comes the key function which calculates the lattice Green’s function, and from this a new
hybridisation function for the effective impurity model. The input is the self-energy (and implicitly the
chemical potential `mu`

):

```
def self_consistency(Sigma):
Gloc = newG()
for bl in Gloc.indices:
for w in Gloc.mesh:
for i in range(block_size(bl)):
for j in range(block_size(bl)): # assuming square matrix
if i == j:
Gloc[bl][w][i,i] = ht(w + mu - Sigma[bl][w][i,i]) # Hilbert-transform
else:
assert abs(Sigma[bl][w][i,j])<1e-10, "This implementation only supports diagonal self-energy"
Gloc[bl][w][i,j] = 0.0
Glocinv = Gloc.inverse()
Delta = newG()
for bl in Delta.indices:
for w in Delta.mesh:
Delta[bl][w] = (w+mu)*identity(bl) - Sigma[bl][w] - Glocinv[bl][w] # !!!
return Gloc, Delta
```

Note that this implementation is limited to problems with diagonal self-energy (and we test for this explicitly, as a precaution). This means that this code is only suitable for channel/orbital conserving models.

Now we are in position to provide an improved method for adjusting the chemical potential, which
repeatedly recalculates the hybridisation function and readjust the chemical potential. We do this
for `max_mu_adjust`

cycles, with 10 being a good choice.

```
def adjust_mu(Delta_in, Sigma):
old_mu = mu
Delta = Delta_in.copy()
for _ in range(max_mu_adjust):
update_mu(Delta, Sigma)
Gloc, Delta = self_consistency(Sigma)
new_mu = mu
if verbose: print("Adjusted mu from %f to %f\n" % (old_mu,new_mu))
return Gloc, Delta
```

We now define some miscelaneous functions. First we need a way of estimating the difference between two Green’s functions. This will be used in the following for convergence testing. Here we again make use of interpolation, and we define the difference as the integral over all frequencies (within the discretization mesh window) of the difference squared. We ignore possible warnings about loss of accuracy in integration.

```
def gf_diff(a, b):
f_a = interp_A(a)
f_b = interp_A(b)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
diff = integrate.quad(lambda x : (f_a(x)-f_b(x))**2, -mesh_max, mesh_max)
return diff[0]
```

We now define some functions for saving Green’s function object to tabulated ASCII files. The filename is just
the prefix. To this we append the block name and the suffix `dat`

.

```
# Save a Green's function to a tabulated ASCII file
def save_Gf(fn, gf):
f = open(fn, "w")
for w in gf.mesh:
z = gf[w]
f.write("%f %f %f\n" % (w, z.real, z.imag))
# Save all blocks for a block GF to tabulated ASCII files
def save_BlockGf(fn, bgf):
for bl in bgf.indices:
save_Gf(fn + "_" + bl + ".dat", bgf[bl])
# Save a spectral function (-1/Pi Im GF) to a tabulated ASCII file
def save_A(fn, gf):
f = open(fn, "w")
for w in gf.mesh:
z = gf[w]
f.write("%f %f\n" % (w, -1.0/math.pi*z.imag))
# Save spectral functions for all blocks of the block GF
def save_BlockA(fn, bgf):
for bl in bgf.indices:
save_A(fn + "_" + bl + ".dat", bgf[bl])
```

In addition, the full set of results is stores as an HDF5 file. We store the relevant items from the solver object, as well as some additional quantities (self-energy-improved impurity Green’s function, local lattice Green’s function, chemical potential).

```
def store_result(fn, S):
with HDFArchive(fn, 'w') as arch:
arch["S"] = S
# Global variables
arch["Gself"] = Gself
arch["Gloc"] = Gloc
arch["mu"] = mu
```

When restarting from stored results, we only need to read in the self-energy and the chemical potential, since this is sufficient to initialize the DMFT calculation.

```
def load_Sigma_mu(fn):
with HDFArchive(fn, 'r') as arch:
Sigma = arch["S"].Sigma_w
mu = arch["mu"]
return Sigma, mu
```

```
def restart_calculation(fn):
if verbose: print("Starting from stored results in file %s\n" % fn)
return load_Sigma_mu(fn) # Load data from an HDF5 archive
```

If not starting from a file, the initial self-energy includes the Hartree term, and the chemical potential is set to \(U/2\):

```
def new_calculation():
if verbose: print("Starting from scratch\n")
Sigma = newG()
for bl in Sigma.indices:
for w in Sigma.mesh:
Sigma[bl][w] = U*occupancy_goal/2.0 # Initialize self-energy with the Hartree shift
mu = U/2.0 # initial approximaiton for the chemical potential
return Sigma, mu
```

The DMFT loop is terminated by raising exceptions. We define them here:

```
# Exception to raise when convergence is reached
class Converged(Exception):
def __init__(self, message):
self.message = message
# Exception to raise when convergence is not reached (e.g. maximum nr of iterations exceeded)
class FailedToConverge(Exception):
def __init__(self, message):
self.message = message
```

Now comes the main part, the function which performs a DMFT step. Let us disect it. First we update the iteration counter and initialize the hybridisation function in the impurity solver object after fixing it to a minimal value.

```
def dmft_step(Delta_in):
global itern
itern += 1
if verbose:
print("Iteration %i min_iter=%i max_iter=%i\n" % (itern, min_iter, max_iter))
Delta_in_fixed = fix_hyb_function(Delta_in, Delta_min)
S.Delta_w << Delta_in_fixed
```

We now solve the impurity model, calculate an improved estimate of the impurity GF using the “self-energy trick”, and apply the self-consistency equation:

```
S.solve(**sp) # Solve the impurity model
global Gself, Gloc, Gloc_prev
Gself = calc_G(Delta_in_fixed, S.Sigma_w, mu) # impurity GF ("self-energy-trick" improved)
Gloc, Delta = self_consistency(S.Sigma_w) # apply the DMFT self-consistency equation
```

We calculate some quantities for estimating the degree of convergence:

```
diff_loc_imp = gf_diff(Gself, Gloc) # difference between impurity and local lattice GF
diff_prev = gf_diff(Gloc, Gloc_prev) # difference between two consecutively computed local latice GFs
Gloc_prev = Gloc.copy()
occupancy = calc_occupancy(Delta, S.Sigma_w, mu)
diff_occupancy = abs(occupancy-occupancy_goal) # this difference is used as the measure of deviation
```

```
stats = OrderedDict([("itern", itern), ("mu", mu), ("diff_loc_imp", diff_loc_imp), ("diff_prev", diff_prev),
("diff_occupancy", diff_occupancy), ("occupancy", occupancy)])
for i in observables:
stats[i] = S.expv[i]
header_string = fmt_str_header(len(stats)).format(*[i for i in stats.keys()])
stats_string = fmt_str(len(stats)).format(*[i for i in stats.values()])
if mpi.is_master_node():
if itern == 1: stats_file.write(header_string)
stats_file.write(stats_string)
if verbose: print("stats: %sstats: %s" % (header_string, stats_string))
```

Furthermore, if `store_steps=True`

, we save the information about each DMFT step in a separate subdirectory:

```
if store_steps:
os.mkdir(str(itern)) # one subdirectory per iteration
save_BlockGf(str(itern)+"/Delta", Delta_in_fixed)
save_BlockGf(str(itern)+"/Sigma", S.Sigma_w) # self-energy
save_BlockGf(str(itern)+"/G", Gloc) # local lattice Green's function
save_BlockA(str(itern)+"/A", Gloc) # spectral function of local lattice GF
store_result(str(itern)+"/"+solution_filename, S)
```

At each step, we also store the HDF5 to permit the checkpoint/restart functionality:

```
if mpi.is_master_node():
store_result(checkpoint_filename, S) # for checkpoint/restart functionality
```

We now test for convergence (and for failure to reach the convergence within the maximal number of iterations):

```
if (diff_loc_imp < eps_loc_imp and
diff_prev < eps_prev and
diff_occupancy < eps_occupancy and
itern >= min_iter):
raise Converged(stats_string)
if (itern == max_iter):
raise FailedToConverge(stats_string)
```

We are nearly done now. We adjust the chemical potential and return the optimized hybridisation function:

```
if occup_method == "adjust":
Gloc, Delta = adjust_mu(Delta, S.Sigma_w) # here we update mu to get closer to target occupancy
return Delta
```

There are two types of driving routines for the full DMFT iteration. The first is based on linear mixing:

```
def solve_with_linear_mixing(Delta, alpha):
Delta_in = Delta.copy()
while True:
Delta_out = dmft_step(Delta_in)
newDelta = alpha*Delta_out + (1-alpha)*Delta_in
Delta_in << newDelta
```

The second one is based on Broyden mixing. The idea is that the DMFT iteration can be thought of as a
procedure for finding the fixed point of the mutli-dimensional function \(F(\Delta_\mathrm{in}) = \Delta_\mathrm{out}[\Delta_\mathrm{in}] - \Delta_\mathrm{in}\),
where \(\Delta_\mathrm{in}\) is the input while \(\Delta_\mathrm{out}\) the output of one DMFT step.
For ths purpose we make use of `optimize.broyden1`

from scipy, but in order to do so, we
need to wrap/unwrap the Green’s function objects to/from numpy arrays, which adds a degree of complexity:

```
# Convert GF object to a linear nparray
def gf_to_nparray(gf):
return gf.data.flatten()
# Stack elements from all blocks in a block BF
def bgf_to_nparray(bgf):
return np.hstack((bgf[bl].data.flatten() for bl in bgf.indices))
# Convert a linear numpy array to a single GF object
def nparray_to_gf(a, gf):
b = a.reshape(gf.data.shape)
gf.data[:,:,:] = b[:,:,:]
# Extract blocks from linear numpy array to a block GF
def nparray_to_bgf(a):
G = newG()
split = np.split(a, nr_blocks(G)) # Here we assume all blocks are equally large
for i, bl in enumerate(G.indices):
nparray_to_gf(split[i], G[bl])
return G
# DMFT driver routine with Broyden mixing: the goal is to find a root of the function F(Delta)=dmft_step(Delta)-Delta.
F = lambda Delta : dmft_step(Delta)-Delta
npF = lambda x : bgf_to_nparray(F(nparray_to_bgf(x)))
def solve_with_broyden_mixing(Delta, alpha):
xin = bgf_to_nparray(Delta)
optimize.broyden1(npF, xin, alpha=alpha, reduction_method="svd", max_rank=10, verbose=verbose, f_tol=1e-99) # Loop forever (small f_tol!)
```

Here we pick the method:

```
def solve(Delta):
if (mixing_method=="linear"):
solve_with_linear_mixing(Delta, alpha)
if (mixing_method=="broyden"):
solve_with_broyden_mixing(Delta, alpha)
```

The problem is initialized here:

```
Sigma0, mu0 = initial_Sigma_mu() # Get initial self-energy and chemical potential
set_mu(mu0) # Set global variable mu and update the impurity level (epsilon_d = -mu)
Gloc, Delta = self_consistency(Sigma0) # Initialize local GF Gloc and hybridization function Delta
Gloc, Delta = adjust_mu(Delta, Sigma0) # Adjust mu (and return updated Glocal and Delta as well)
Gloc_prev = Gloc.copy() # Store copy for checking convergence
```

And the solver is started here:

```
try:
solve(Delta)
except Converged as c:
if verbose: print("Converged: %s" % c.message)
if mpi.is_master_node():
store_result(solution_filename, S) # full converged results as an HDF5 file
os.remove(checkpoint_filename) # checkpoint file is no longer needed
save_BlockA("A", Gloc) # converged spectral function for quick plotting
except FailedToConverge as c:
if verbose: print("Failed to converge: %s" % c.message) # ... but restart is possible from the checkpoint file
```

This completes the exposition. Have fun running DMFT(NRG) calculations!

Note: this script has not yet been tested for the case of matrix-valued Green’s functions.