Examples

The following are several examples of how to use the algorithms and integrands provided by AutoBZCore.jl. For background on the essential interface see the Problem definitions page

Green's function integration

A common integral appearing in Dynamical mean-field theory is that of the local Green's function:

\[G(\omega) = \int d^d \vec{k}\ \operatorname{Tr} \left[ \left( \omega - H \left( \vec{k} \right) - \Sigma(\omega) \right)^{-1} \right].\]

For simplicity, we take $\Sigma(\omega) = -i\eta$. We can define the integrand as a function of $\vec{k}$ and $H$ and parameters $\eta, \omega$.

using LinearAlgebra
gloc_integrand(k, (; h, η, ω)) = tr(inv(complex(ω,η)*I-h(k)))
gloc_integrand (generic function with 1 method)

Here we use named tuple destructuring syntax to unpack a named tuple of parameters in the function definition.

Commonly, $H(\vec{k})$ is evaluated using Wannier interpolation, i.e. as a Fourier series. For a simple tight-binding model, the integer lattice, the Hamiltonian is given by

\[H(k) = \cos(2\pi k) = \frac{1}{2} \left( e^{2\pi ik} + e^{-2\pi ik} \right)\]

We can use the built-in function cos to evaluate this, however, for more complex Fourier series it becomes easier to use the representation in terms of Fourier coefficients. Using the package FourierSeriesEvaluators.jl, we can define $H(k) = \cos(2\pi k)$ by the following:

using FourierSeriesEvaluators
h = FourierSeries([0.5, 0.0, 0.5]; period=1, offset=-2)
3-element and (1,)-periodic FourierSeries with Float64 coefficients, (0,) derivative, (-2,) offset

The coefficient values of $1/2$ can be determined from Euler's formula, as used in the expansion of $\cos$ above, and the value of offset is chosen to offset the coefficient array indices, 1:3 since Julia has 1-based indexing, to correspond to values of $n$ in the phase factors $e^{2\pi i n k}$ used in the Fourier series above, i.e. -1:1. Now we proceed to the define the IntegralProblem and solve it with a generic adaptive integration scheme, QuadGKJL

using AutoBZCore
dom = (0, 1)
p = (; h, η=0.1, ω=0.0)
prob = IntegralProblem(gloc_integrand, dom, p)
alg = QuadGKJL()
solve(prob, alg; abstol=1e-3).value
4.327818095284375e-7 - 0.995037545189551im

BZ integration

To perform integration over a Brillouin zone, we can load one using the load_bz function and then construct an AutoBZProblem to solve. Since the Brillouin zone may be reduced using point group symmetries, a common optimization, it is also required to specify the symmetry representation of the integrand. Continuing the previous example, the trace of the Green's function has no band/orbital degrees of freedom and transforms trivially under the point group, so it is a TrivialRep. The previous calculation can be replicated as:

using AutoBZCore
bz = load_bz(FBZ(), 2pi*I(1))
p = (; h, η=0.1, ω=0.0)
prob = AutoBZProblem(TrivialRep(), IntegralFunction(gloc_integrand), bz, p)
alg = TAI()
solve(prob, alg; abstol=1e-3).value
5.551115123125783e-17 - 0.9950375451895511im

Now we proceed to multi-dimensional integrals. In this case, Wannier interpolation is much more efficient when Fourier series are evaluated one variable at a time. To understand, this suppose we have a series defined by $M \times M$ coefficients (i.e. a 2d series) that we want to evaluate on an $N \times N$ grid. Naively evaluating the series at each grid point will require $\mathcal{O}(M^{2} N^{2})$ operations, however, we can reduce the complexity by pre-evaluating certain coefficients as follows

\[f(x, y) = \sum_{m,n=1}^{M} f_{nm} e^{i(nx + my)} = \sum_{n=1}^{M} e^{inx} \left( \sum_{m=1}^{M} f_{nm} e^{imy} \right) = \sum_{n=1}^{M} e^{inx} \tilde{f}_{n}(y)\]

This means we can evaluate the series on the grid in $\mathcal{O}(M N^2 + M^2 N)$ operations. When $N \gg M$, this is $\mathcal{O}(M N^{2})$ operations, which is comparable to the computational complexity of a multi-dimensional FFT. Since the constants of a FFT may not be trivial, this scheme is competitive.

Let's use this with a Fourier series corresponding to $H(\vec{k}) = \cos(2\pi k_{x}) + \cos(2\pi k_{y})$ and define a new method of gloc_integrand that accepts the (efficiently) evaluated Fourier series in the second argument

h = FourierSeries([0.0; 0.5; 0.0;; 0.5; 0.0; 0.5;; 0.0; 0.5; 0.0]; period=1, offset=-2)
gloc_integrand(k, h_k, (; η, ω)) = tr(inv(complex(ω,η)*I-h_k))
gloc_integrand (generic function with 2 methods)

Similar to before, we construct an AutoBZCore.IntegralProblem and this time we take the integration domain to correspond to the full Brillouin zone of a square lattice with lattice vectors 2pi*I(2).

integrand = FourierIntegralFunction(gloc_integrand, h)
bz = load_bz(FBZ(2), 2pi*I(2))
p = (; η=0.1, ω=0.0)
prob = AutoBZProblem(TrivialRep(), integrand, bz, p)
alg = IAI()
solve(prob, alg).value
1.3530843112619095e-16 - 1.3941704645339514im

This package provides several AutoBZProblem algorithms that we can use to solve the multidimensional integral.

The repo's demo on density of states provides a complete example of how to compute and interpolate an integral as a function of its parameters using the init and solve! interface

Density of States

Computing the density of states (DOS) of a self-adjoint, or Hermitian, operator is a related, but distinct problem to the integrals also presented in this package. In fact, many DOS algorithms will compute integrals to approximate the DOS of an operator by introducing an artificial broadening. To handle the $T=0^{+}$ limit of the broadening, we implement the well-known Gilat-Raubenheimer method as an algorithm for the AutoBZCore.DOSProblem

Using the AutoBZCore.init and AutoBZCore.solve! functions, it is possible to construct a cache to solve a DOSProblem for several energies or several Hamiltonians. As an example of solving for several energies,

using AutoBZCore, FourierSeriesEvaluators, StaticArrays
h = FourierSeries(SMatrix{1,1,Float64,1}.([0.5, 0.0, 0.5]), period=1.0, offset=-2)
E = 0.3
bz = load_bz(FBZ(), [2pi;;])
prob = DOSProblem(h, E, bz)
alg = GGR(; npt=100)
cache = init(prob, alg)
Es = range(-1, 1, length=10) * 1.1
data = map(Es) do E
    cache.domain = E
    solve!(cache).value
end
10-element Vector{Float64}:
 0.0
 0.5940536926606439
 0.3934526572333864
 0.34235097468581055
 0.3208398038674902
 0.32083980386749017
 0.34235097468581066
 0.3934526572333864
 0.5940536926606439
 0.0

As an example of interchanging Hamiltonians, which must remain the same type, we can double the energies, which will halve the DOS

cache.domain = E
sol1 = AutoBZCore.solve!(cache)

h.c .*= 2
cache.isfresh = true
cache.domain = 2E

sol2 = AutoBZCore.solve!(cache)

sol1.value ≈ 2sol2.value
true