Exact diagonalization : tutorial 1
Lanczos algorithm

Power method and Krylov basis

1/ Give an estimate of the number of non-zero matrix-element for a spin chain Hamiltonian of length $L$ and nearest neighbour interactions. Comment.

We do not want to store the Hamiltonian in a full matrix to take advantage of its sparse structure. There are two solutions : either use a sparse storage scheme in which only non-zero elements are stored or we can even recompute matrix elements on-the-fly on demand. Yet, we need an algorithm that allows us to find the ground-state based on the mere operation $$ H|{\psi}\rangle $$ and basics linear algebra for vectors (and not matrices).

A variational argument

Consider an Hamiltonian $H$ in a finite-size Hilbert space and the mean-energy in a given vector $|{x}\rangle$: $$E(x) = \frac{\langle{x}|{H}|{x}\rangle}{\langle{x}|{x}\rangle}$$

2/ Show that minimizing $E(x)$ suggests to look for the ground-state in a certain direction.

Therefore, given in inital wave-vector $|{x}\rangle$, we may look for the ground-state in the space spanned by vectors $\{|{x}\rangle, H|{x}\rangle, H^2|{x}\rangle, \ldots, H^p|{x}\rangle \}$ with $p$ a number of iterations. This set of vectors is called the Krylov space.

The power method

A first possible algorithm is called the power method and amounts to filter the ground-state starting from a random initial state $|{x}\rangle$ and iteratively applying $H$.

In [2]:
from quantumising import *

L, hx = 4, 0.5

HamH = GeneralOp([ [ -hx, Sx(i)] for i in range(L) ])
HamIsing = GeneralOp([ [ 1.0, SzSz(i,(i+1)%L)] for i in range(L) ])
Ham = HamIsing + HamH

hilbert = IsingHilbert(L=L)
x = randomWf(hilbert)
for i in range(201):
    x = Ham*x
    x.normalize()
    if not i%10: print(i,x.measure(Ham))

# to cross-check results
Ham.createMatrix(hilbert)
Ham.diagonalize(with_eigvec=False)
E0 = np.min(Ham.En)
print("expected E0=",E0)
0 0.492701854974
10 0.584569015532
20 0.599033981718
30 0.622752811625
40 0.636862925773
50 0.640967855511
60 0.641883682458
70 0.642075030628
80 0.642114451365
90 0.642122549019
100 0.642124211411
110 0.642124552647
120 0.64212462269
130 0.642124637067
140 0.642124640018
150 0.642124640624
160 0.642124640748
170 0.642124640774
180 0.642124640779
190 0.64212464078
200 0.64212464078
expected E0= -1.30656296488

3/ What is the condition on $|{x}\rangle$ for the algorithm to work. Why the algorithm above is not in general working ?

4/ Let is introduce a constant $C$ to circumvent this issue. How must $C$ be chosen in the algorithm below to ensure convergence ? Is there a better choice for $C$ ? Is that useful in practice?

In [3]:
E0, Emax = np.min(Ham.En), np.max(Ham.En)
C = Emax
x = randomWf(hilbert)
#x = basisvectorWf(hilbert,3)
for i in range(201):
    x = Ham*x - C*x
    x.normalize()
    if not i%10: print(i,x.measure(Ham),E0,Emax)
0 -0.46553840342 -1.30656296488 1.30656296488
10 -1.20678550308 -1.30656296488 1.30656296488
20 -1.21325633706 -1.30656296488 1.30656296488
30 -1.21957454441 -1.30656296488 1.30656296488
40 -1.23072538684 -1.30656296488 1.30656296488
50 -1.24724722548 -1.30656296488 1.30656296488
60 -1.26630488026 -1.30656296488 1.30656296488
70 -1.28285578701 -1.30656296488 1.30656296488
80 -1.29404153506 -1.30656296488 1.30656296488
90 -1.30038029035 -1.30656296488 1.30656296488
100 -1.30361883345 -1.30656296488 1.30656296488
110 -1.30518603955 -1.30656296488 1.30656296488
120 -1.30592451665 -1.30656296488 1.30656296488
130 -1.30626812192 -1.30656296488 1.30656296488
140 -1.30642705724 -1.30656296488 1.30656296488
150 -1.30650037243 -1.30656296488 1.30656296488
160 -1.30653414932 -1.30656296488 1.30656296488
170 -1.30654970155 -1.30656296488 1.30656296488
180 -1.3065568605 -1.30656296488 1.30656296488
190 -1.30656015548 -1.30656296488 1.30656296488
200 -1.30656167194 -1.30656296488 1.30656296488

The Lanczos algorithm

The Lanczos algorithm builds a tridiagonal representation of the Hamiltonian in an orthonormalized basis of the Krylov space. It possesses remarkable properties, such as exponential convergence to the extremal eigenvalues and numerical statibility.

Recursion relations

The Lanczos vectors are denoted by $\{|{i}\rangle\}_{i=0,p-1}$ with $p$ the number of iterations. They statisfy the following recursion relations : \begin{align} |{i+1'}\rangle &= H|{i}\rangle - a_i |{i}\rangle - b_{i-1} |{i-1}\rangle \\ |{i+1}\rangle &= |{i+1'}\rangle/b_{i} \\ \end{align} where $b_i = \||{i+1'}\rangle\|$ and $b_{-1}=0$ by convention.

1/ Give the expression of the $a_i$ coefficient as a function of known data at iteration $i$. Show that the matrix representation of the Hamiltonian in the Lanczos subbasis is tridiagonal and gives the expression of its elements.

2/ Implement the Lanczos algorithm to compute the spectrum of the approximate representation of the Hamiltonian.

In [ ]:
def lanczosSpectrum(a,b):
    tridiag = np.diag(a)
    tridiag+= np.diag(b,k=-1)
    return np.linalg.eigvalsh(tridiag)

numiteration = 20
numprint = 3
a, b = [], []
u2 = randomWf(hilbert)

for i in range(numiteration):
    ...
    ...
    ...
    ...

Computing the ground-state

3/ We want to compute the ground-state without keeping in memory all the intermediate Lanczos vectors. How to do that ? Implementation the corresponding algorithm.

In [ ]:
def lanczosSpectrum(a,b):
    tridiag = np.diag(a)
    tridiag+= np.diag(b,k=-1)
    return np.linalg.eigvalsh(tridiag)

def lanczosGS(a,b):
    tridiag = np.diag(a)
    tridiag+= np.diag(b,k=-1)
    ev, vec = np.linalg.eigh(tridiag)
    return vec[:,0]

numiteration = 20
numprint = 3
a, b = [], []
u2 = randomWf(hilbert)
gs = deepcopy(u2) # save the initial random state

    ...
    ...
    ...
    ...

Egs = gs.measure(Ham)
VarEgs = scalar(gs,Ham*(Ham*gs))-Egs**2
print("Checking energy and variance of GS : ", Egs, VarEgs)

Practical use

Play with the Lanczos algorithm to observe the exponential convergence and the presence of ghosts states due to lack of numerical precision.

We have written a new module quantumising_sparse.py that implements the lanczos calculations above, the sparse storage of the Hamiltonian matrix and also the Arnoldi algorithm which preserves orthogonality (using scipy).

Check that the Arnoldi algorithm cures the ghosts states issue.

In [ ]:
from quantumising_sparse import *

L, h = 10, 0.3

def IsingHam(L=4,h=0.0,J=1.0):
    HamH = GeneralOp([ [ -h, Sx(i)] for i in range(L) ])
    HamIsing = GeneralOp([ [ -J, SzSz(i,(i+1)%L)] for i in range(L) ])
    return HamIsing + HamH

hilbert = IsingHilbert(L=L)

Eigenvalues only

Lanczos
In [ ]:
# without storing the matrix
Ham = IsingHam(L,h)
En = Ham.Energies(hilbert,num=2,algorithm="Lanczos")
print(En)
# with storing the matrix
Ham.createMatrix(hilbert,sparse=True)
En = Ham.Energies(hilbert,num=2,algorithm="Lanczos")
print(En)
Fulldiag or Arnoli
In [ ]:
Ham = IsingHam(L,h)
En = Ham.Energies(hilbert,num=2,algorithm="Fulldiag")
print(En)
Ham = IsingHam(L,h)
En = Ham.Energies(hilbert,num=2,algorithm="Arnoldi") # recommended, default algorithm
print(En)

Get the ground-state

Lanczos

In [ ]:
# without storing the matrix
Ham = IsingHam(L,h)
psi0 = Ham.Groundstate(hilbert,algorithm="Lanczos")
print(psi0.measure(Ham))
# with storing the matrix
Ham.createMatrix(hilbert,sparse=True)
psi0 = Ham.Groundstate(hilbert,algorithm="Lanczos")
print(psi0.measure(Ham))

Fuldiag or Arnoldi

In [ ]:
Ham = IsingHam(L,h)
psi0 = Ham.Groundstate(hilbert,algorithm="Fulldiag")
print(psi0.measure(Ham))
In [ ]:
Ham = IsingHam(L,h)
psi0 = Ham.Groundstate(hilbert,algorithm="Arnoldi") # recommended
print(psi0.measure(Ham))