Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
afeiguin
GitHub Repository: afeiguin/comp-phys
Path: blob/master/12_01_quantum_spin_chain.ipynb
374 views
Kernel: Python 3

Quantum Spins

We are going consider a magnetic insulator, a Mott insulator. In this problem we have a valence electron per atom, with very localized wave-functions. The Coulomb repulsion between electrons on the same orbital is so strong, that electrons are bound to their host atom, and cannot move. For this reason, charge disappears from the equation, and the only remaining degree of freedom is the spin of the electrons. The corresponding local state can therefore be either |\uparrow\rangle or |\downarrow\rangle. The only interaction taking place is a process that “flips” two anti-aligned neighboring spins |\uparrow\rangle|\downarrow\rangle \rightarrow |\downarrow\rangle|\uparrow\rangle.

Let us now consider a collection of spins residing on site of a –one-dimensional for simplicity– lattice. An arbitrary state of NN-spins can be described by using the SzS^z projection (,\uparrow,\downarrow) of each spin as: s1,s2,...,sN|s_1,s_2,..., s_N\rangle. As we can easily see, there are 2N2^N of such configurations.

Let us now consider a collection of spins residing on site of a –one-dimensional for simplicity– lattice. An arbitrary state of NN-spins can be described by using the SzS^z projection (,\uparrow,\downarrow) of each spin as: s1,s2,...,sN|s_1,s_2,..., s_N\rangle. As we can easily see, there are 2N2^N of such configurations.

We shall describe the interactions between neighboring spins using the so-called Heisenberg Hamiltonian:

H^=i=1N1S^iS^i+1\hat{H}=\sum_{i=1}^{N-1} \hat{\mathbf{S}}_i \cdot \hat{\mathbf{S}}_{i+1}

where S^i=(S^x,S^y,S^z)\hat{\mathbf{S}}_i = (\hat{S}^x,\hat{S}^y,\hat{S}^z) is the spin operator acting on the spin on site ii.

Since we are concerned about spins one-half, S=1/2S=1/2, all these operators have a 2×22\times2 matrix representation, related to the well-known Pauli matrices:

Sz=(1/2001/2),Sx=(01/21/20),Sy=(0i/2i/20),S^z = \left( \begin{array}{cc} 1/2 & 0 \\ 0 & -1/2 \end{array} \right), S^x = \left( \begin{array}{cc} 0 & 1/2 \\ 1/2 & 0 \end{array} \right), S^y = \left( \begin{array}{cc} 0 & -i/2 \\ i/2 & 0 \end{array} \right),

These matrices act on two-dimensional vectors defined by the basis states |\uparrow\rangle and |\downarrow\rangle. It is useful to introduce the identities: S^±=(S^x±iS^y),\hat{S}^\pm = \left(\hat{S}^x \pm i \hat{S}^y\right), where S+S^+ and SS^- are the spin raising and lowering operators. It is intuitively easy to see why by looking at how they act on the basis states: S^+=\hat{S}^+|\downarrow\rangle = |\uparrow\rangle and S^=\hat{S}^-|\uparrow\rangle = |\downarrow\rangle. Their corresponding 2×22\times2 matrix representations are: S+=(0100),S=(0010),S^+ = \left( \begin{array}{cc} 0 & 1 \\ 0 & 0 \end{array} \right), S^- = \left( \begin{array}{cc} 0 & 0 \\ 1 & 0 \end{array} \right),

We can now re-write the Hamiltonian ([heisenberg]) as: H^=i=1N1S^izS^i+1z+12[S^i+S^i+1+S^iS^i+1+]\hat{H}=\sum_{i=1}^{N-1} \hat{S}^z_i \hat{S}^z_{i+1} + \frac{1}{2}\left[ \hat{S}^+_i \hat{S}^-_{i+1} + \hat{S}^-_i \hat{S}^+_{i+1} \right] The first term in this expression is diagonal and does not flip spins. This is the so-called Ising term. The second term is off-diagonal, and involves lowering and raising operators on neighboring spins, and is responsible for flipping anti-aligned spins. This is the “XYXY” part of the Hamiltonian.

The Heisenberg spin chain is a paradigmatic model in condensed matter. Not only it is attractive due to its relative simplicity, but can also describe real materials that can be studied experimentally. The Heisenberg chain is also a prototypical integrable system, that can be solved exactly by the Bethe Ansatz, and can be studied using bosonization techniques and conformal field theory.

The Heisenberg spin chain is a paradigmatic model in condensed matter. Not only it is attractive due to its relative simplicity, but can also describe real materials that can be studied experimentally. The Heisenberg chain is also a prototypical integrable system, that can be solved exactly by the Bethe Ansatz, and can be studied using bosonization techniques and conformal field theory.

In these lectures, we will be interested in obtaining its ground state properties of this model by numerically solving the time-independent Schrödinger equation: H^Ψ=EΨ,\hat{H}|\Psi\rangle = E|\Psi\rangle, where HH is the Hamiltonian of the problem, Ψ|\Psi\rangle its eigenstates, with the corresponding eigenvalues, or energies EE.

Exact diagonalization

blocks

Pictorial representation of the Hamiltonian building recursion explained in the text. At each step, the block size is increased by adding a spin at a time.

In this section we introduce a technique that will allow us to calculate the ground state, and even excited states of small Heisenberg chains. Exact Diagonalization (ED) is a conceptually simple technique which basically consists of diagonalizing the Hamiltonian matrix by brute force. Same as for the spin operators, the Hamiltonian also has a corresponding matrix representation. In principle, if we are able to compute all the matrix elements, we can use a linear algebra package to diagonalize it and obtain all the eigenvalues and eigenvectors @lapack.

In these lectures we are going to follow a quite unconventional procedure to describe how this technique works. It is important to point out that this is a quite inefficient and impractical way to diagonalize the Hamiltonian, and more sophisticated techniques are generally used in practice.

Two-spin problem

The Hilbert space for the two-spin problem consists of four possible configurations of two spins {,,,}\left\{ |\uparrow\uparrow\rangle,|\uparrow\downarrow\rangle,|\downarrow\uparrow\rangle,|\downarrow\downarrow\rangle \right\}

The problem is described by the Hamiltonian: H^=S^1zS^2z+12[S^1+S^2+S^1S^2+]\hat{H}= \hat{S}^z_1 \hat{S}^z_2 + \frac{1}{2}\left[ \hat{S}^+_1 \hat{S}^-_2 + \hat{S}^-_1 \hat{S}^+_2 \right]

The corresponding matrix will have dimensions 4×44 \times 4. In order to compute this matrix we shall use some simple matrix algebra to first obtain the single-site operators in the expanded Hilbert space. This is done by following the following simple scheme: And operator O1O_1 acting on the left spin, will have the following 4×44 \times 4 matrix form: O~1=O112\tilde{O}_1 = O_1 \otimes {1}_2 Similarly, for an operator O2O_2 acting on the right spin: O~2=12O2\tilde{O}_2 = {1}_2 \otimes O_2 where we introduced the n×nn \times n identity matrix 1n{1}_n. The product of two operators acting on different sites can be obtained as: O~12=O1O2\tilde{O}_{12} = O_1 \otimes O_2 It is easy to see that the Hamiltonian matrix will be given by: H12=SzSz+12[S+S+SS+]H_{12}= S^z \otimes S^z + \frac{1}{2}\left[ S^+ \otimes S^- + S^- \otimes S^+ \right] where we used the single spin (2×22 \times 2) matrices SzS^z and S±S^\pm. We leave as an exercise for the reader to show that the final form of the matrix is:

H12=(1/400001/41/2001/21/400001/4),H_{12} = \left( \begin{array}{cccc} 1/4 & 0 & 0 & 0 \\ 0 & -1/4 & 1/2 & 0 \\ 0 & 1/2 & -1/4 & 0 \\ 0 & 0 & 0 & 1/4 \\ \end{array} \right),

Obtaining the eigenvalues and eigenvectors is also a straightforward exercise: two of them are already given, and the entire problem now reduces to diagonalizing a two by two matrix. We therefore obtain the well known result: The ground state s=1/2[]|s\rangle = 1/\sqrt{2}\left[ |\uparrow\downarrow\rangle - |\downarrow\uparrow\rangle \right], has energy Es=3/4E_s=-3/4, and the other three eigenstates {,,1/2[+]}\left\{|\uparrow\uparrow\rangle,|\downarrow\downarrow\rangle,1/\sqrt{2}\left[ |\uparrow\downarrow\rangle + |\downarrow\uparrow\rangle \right] \right\} form a multiplet with energy Et=1/4E_t=1/4.

Many spins

Imagine now that we add a third spin to the right of our two spins. We can use the previous result to obtain the new 8×88 \times 8 Hamiltonian matrix as: H3=H212+S~2zSz+12[S~2+S+S~2S+]H_{3}= H_{2} \otimes {1}_2 + \tilde{S}^z_2 \otimes S^z + \frac{1}{2}\left[ \tilde{S}^+_2 \otimes S^- + \tilde{S}^-_2 \otimes S^+ \right] Here we used the single spin S1zS^z_1, S1±S^\pm_1, and the `tilde` matrices defined in Eqs.([tildeL]) and ([tildeR]): S~2z=12Sz,\tilde{S}^z_2 = {1}_2 \otimes S^z, and S~2±=12S±,\tilde{S}^\pm_2 = {1}_2 \otimes S^\pm,

It is easy to see that this leads to a recursion scheme to construct the 2i×2i2^i \times 2^i Hamiltonian matrix the ithi^\mathrm{th} step as:

Hi=Hi112+S~i1zSz+12[S~i1+S+S~i1S+]H_{i}= H_{i-1} \otimes {1}_2 + \tilde{S}^z_{i-1} \otimes S^z + \frac{1}{2}\left[ \tilde{S}^+_{i-1} \otimes S^- + \tilde{S}^-_{i-1} \otimes S^+ \right]

with S~i1z=12i2Sz,\tilde{S}^z_{i-1} = {1}_{2^{i-2}} \otimes S^z, and S~i1±=12i2S±,\tilde{S}^\pm_{i-1} = {1}_{2^{i-2}} \otimes S^\pm,

This recursion algorithm can be visualized as a left ‘block’, to which we add new ‘sites’ or spins to the right, one at a time, as shown in Fig.[fig:block].The block has a ‘block Hamiltonian’, HLH_L, that is iteratively built by connecting to the new spins through the corresponding interaction terms.

%matplotlib inline import numpy as np from matplotlib import pyplot # PARAMETERS nsites = 4 #Single site operators sz0 = np.zeros(shape=(2,2)) # single site Sz splus0 = np.zeros(shape=(2,2)) # single site S+ sz0[0,0] = -0.5 sz0[1,1] = 0.5 splus0[1,0] = 1.0 term_szsz = np.zeros(shape=(4,4)) #auxiliary matrix to store Sz.Sz term_szsz = np.kron(sz0,sz0) term_spsm = np.zeros(shape=(4,4)) #auxiliary matrix to store 1/2 S+.S- term_spsm = np.kron(splus0,np.transpose(splus0))*0.5 term_spsm += np.transpose(term_spsm) h12 = term_szsz+term_spsm H = np.zeros(shape=(2,2)) for i in range(1,nsites): diml = 2**(i-1) # 2^i dim = diml*2 print ("ADDING SITE ",i," DIML= ",diml) Ileft = np.eye(diml) Iright = np.eye(2) # We add the term for the interaction S_i.S_{i+1} aux = np.zeros(shape=(dim,dim)) aux = np.kron(H,Iright) H = aux H = H + np.kron(Ileft,h12) w, v = np.linalg.eigh(H) #Diagonalize the matrix print(w)
ADDING SITE 1 DIML= 1 ADDING SITE 2 DIML= 2 ADDING SITE 3 DIML= 4 [-1.6160254 -0.95710678 -0.95710678 -0.95710678 -0.25 -0.25 -0.25 0.1160254 0.45710678 0.45710678 0.45710678 0.75 0.75 0.75 0.75 0.75 ]
from matplotlib import pyplot pyplot.rcParams['axes.linewidth'] = 2 #set the value globally %matplotlib inline Beta = np.arange(0.1,10,0.1) et = np.copy(Beta) n = 0 for x in Beta: p = np.exp(-w*x) z = np.sum(p) et[n] = np.dot(w,p)/z print (x,et[n]) n+=1 pyplot.plot(1/Beta,et,lw=2);
0.1 -0.05760987249510802 0.2 -0.11772708133199768 0.30000000000000004 -0.1799749161303902 0.4 -0.24390632110272253 0.5 -0.30901701565428624 0.6 -0.37476304806595323 0.7000000000000001 -0.4405815376403906 0.8 -0.5059129169711236 0.9 -0.5702227720500874 1.0 -0.6330214400416623 1.1 -0.6938798489847898 1.2000000000000002 -0.7524406008645199 1.3000000000000003 -0.8084239045835981 1.4000000000000001 -0.8616285472956369 1.5000000000000002 -0.9119285611250054 1.6 -0.9592665450743981 1.7000000000000002 -1.0036447279137457 1.8000000000000003 -1.0451148298845272 1.9000000000000001 -1.0837676421128473 2.0 -1.1197230415459163 2.1 -1.1531209397429492 2.2 -1.1841134584514705 2.3000000000000003 -1.2128584531880562 2.4000000000000004 -1.2395143762135286 2.5000000000000004 -1.2642363821382747 2.6 -1.2871735275792655 2.7 -1.3084668931419337 2.8000000000000003 -1.328248453489718 2.9000000000000004 -1.3466405322009147 3.0000000000000004 -1.363755696693066 3.1 -1.3796969704179296 3.2 -1.3945582618585033 3.3000000000000003 -1.4084249307704304 3.4000000000000004 -1.4213744306172185 3.5000000000000004 -1.4334769818512214 3.6 -1.4447962435689152 3.7 -1.4553899613221313 3.8000000000000003 -1.4653105768129209 3.9000000000000004 -1.4746057911956585 4.0 -1.4833190781084635 4.1 -1.4914901456799556 4.2 -1.4991553488883003 4.3 -1.5063480550238921 4.3999999999999995 -1.5130989658181841 4.5 -1.5194364002033278 4.6 -1.525386541780906 4.7 -1.5309736549956194 4.8 -1.5362202738007493 4.9 -1.5411473663178232 5.0 -1.545774478670119 5.1 -1.5501198608343107 5.2 -1.5542005770242835 5.3 -1.5580326028072495 5.4 -1.5616309108616067 5.5 -1.5650095470219236 5.6 -1.5681816980203966 5.7 -1.5711597521256073 5.8 -1.5739553536973145 5.9 -1.5765794525182384 6.0 -1.5790423486282907 6.1 -1.5813537332709984 6.2 -1.5835227264637413 6.3 -1.585557911620593 6.4 -1.587467367587014 6.5 -1.5892586983874784 6.6 -1.5909390609386986 6.7 -1.59251519094089 6.8 -1.5939934271262712 6.9 -1.5953797340165645 7.0 -1.5966797233186383 7.1 -1.5978986740689103 7.2 -1.599041551621867 7.3 -1.6001130255655813 7.4 -1.6011174866368567 7.5 -1.6020590627002125 7.6 -1.6029416338480085 7.7 -1.6037688466733007 7.8 -1.604544127762277 7.9 -1.6052706964491754 8.0 -1.605951576873285 8.1 -1.6065896093747862 8.2 -1.607187461263813 8.3 -1.6077476369949997 8.4 -1.6082724877779597 8.5 -1.6087642206525095 8.6 -1.6092249070559606 8.7 -1.609656490908509 8.8 -1.6100607962414513 8.9 -1.6104395343918765 9.0 -1.6107943107863336 9.1 -1.6111266313349915 9.2 -1.6114379084568324 9.3 -1.6117294667554514 9.4 -1.612002548364183 9.5 -1.612258317978388 9.6 -1.612497867591895 9.700000000000001 -1.6127222209538343 9.8 -1.6129323377612643 9.9 -1.613129117602311
Image in a Jupyter notebook

Challenge 12.1:

Compute the energy and specific heat of the spin chain with L=12L=12 as a function of temperature, for T<4T < 4.

A practical exact diagonalization algorithm

  1. Initialization: Topology of the lattice, neighbors, and signs.

  2. Contruction of a basis suitable for the problem.

  3. Constuction of the matrix element of the Hamiltonian.

  4. Diagonalization of the matrix.

  5. Calculation of observables or expectation values.

As we shall see below, we are going to need the concept of “binary word”. A binary word is the binary representation of an integer (in powers of ‘two’, i.e. n=ibi.2in=\sum_{i}b_{i}.2^{i}), and consist of a sequence of ‘bits’. A bit bib_{i} in the binary basis correspond to our digits in the decimal system, and can assumme the values ‘zero’ or ‘one’. At this stage we consider appropriate to introduce the logical operators AND, OR, XOR. The binary operators act between bits and their multiplication tables are listed below

AND01
000
101
OR01
001
111
XOR01
001
110

The bit manipulation is a very useful tool, and most of the programming languages provide the needed commands.

Initilalization and definitions

In the program, it is convenient to store in arrays all those quantities that will be used more frequently. In particular, we must determine the topology of the cluster, labeling the sites, and storing the components of the lattice vectors. We must also generate arrays with the nearest neighbors, and the next-nearest neighbors, according to our needs.

Construction of the basis

Memory limitations impose severe restrictions on the size of the clusters that can be studied with this method. To understand this point, note that although the lowest energy state can be written in the {ϕn}\{|\phi _{n}\rangle \} basis as ψ0=mcmϕm|\psi _{0}\rangle =\sum_{m}c_{m}|\phi _{m}\rangle , this expression is of no practical use unless ϕm|\phi _{m}\rangle itself is expressed in a convenient basis to which the Hamiltonian can be easily applied. A natural orthonormal basis for fermion systems is the occupation number representation, describing all the possible distributions of NeN_{e} electrons over NN sites, while for spin systems it is covenient to work in a basis where the SzS_{z} is defined at every site, schematically represented as n=...|n\rangle =|\uparrow \downarrow \uparrow ...\rangle . The size of this type of basis set grows exponentially with the system size. In practice this problem can be considerably alleviated by the use of symmetries of the Hamiltonian that reduces the matrix to a block-form. The most obvious symmetry is the number of particles in the problem which is usually conserved at least for fermionic problems. The total projection of the spin StotalzS_{total}^{z}, is also a good quantum number. For translationally invariant problems, the total momentum k\mathbf{k% } of the system is also conserved introducing a reduction of 1/N1/N in the number of states (this does not hold for models with open boundary conditions or explicit disorder). In addition, several Hamiltonians have additional symmetries. On a square lattice, rotations in π/2\pi /2 about a given site, spin inversion, and reflections with respect to the lattice axis are good quantum numbers (although care must be taken in their implementation since some of these operations are combinations of others and thus not independent).

In the following we shall consider a spin-1/2 Heisenberg chain as a practical example. In this model it is useful to represent the spins pointing in the ‘up’ direction by a digit ‘1’, and the down-spins by a ‘0’. Following this rule, a state in the SzS^{z} basis can be represented by a sequence of ones and zeroes, i.e., a “binary word”. Thus, two Néel configurations in the 4-site chain can be seen as 1010,0101.\begin{aligned} \mid \uparrow \downarrow \uparrow \downarrow \rangle \equiv |1010\rangle , \\ \mid \downarrow \uparrow \downarrow \uparrow \rangle \equiv |0101\rangle .\end{aligned} Once the up-spins have been placed the whole configuration has been uniquely determined since the remaining sites can only be occupied by down-spins. The resulting binary number can be easily converted into integers ilbl.2li\equiv \sum_{l}b_{l}.2^{l}, where the summation is over all the sites of the lattice, and blb_{l} can be 11 or 00. For example: 23222120101021+23=10,010120+22=5.\begin{array}{lllll} 2^{3} & 2^{2} & 2^{1} & 2^{0} & \\ 1 & 0 & 1 & 0 & \rightarrow 2^{1}+2^{3}=10, \\ 0 & 1 & 0 & 1 & \rightarrow 2^{0}+2^{2}=5. \end{array}

Using the above convention, we can construct the whole basis for the given problem. However, we must consider the memory limitations of our computer, introducing some symetries to make the problem more tractable. The symetries are operations that commute with the Hamiltonian, allowing us to divide the basis in subspaces with well defined quantum numbers. By means of similarity transformations we can generate a sequence of smaller matrices along the diagonal (i.e. the Hamiltonian matrix is “block diagonal”), and each of them can be diagonalized independently. For fermionic systems, the simplest symmetries are associated to the conservation of the number of particles and the projection StotalzS_{total}^{z} of the total spin in the zz direction. In the spin-1/2 Heisenberg model, a matrix with 2N×2N2^{N}\times 2^{N} elements can be reduced to 2S+12S+1 smaller matrices, corresponding to the projections m=(NN)/2=S,S+1,...,S1,S% m=(N_{\uparrow }-N_{\downarrow })/2=-S,-S+1,...,S-1,S, with S=N/2S=N/2.

The dimension of each of these subspaces is obtained from the combinatory number (NN)=N!N!N!\left( \begin{array}{c} N \\ N_{\uparrow } \end{array} \right) = \frac{N!}{N_{\uparrow }!N_{\downarrow }!}.

Example: 4-site Heisenberg chain

The total basis has 24=162^{4}=16 states that can be grouped in 4+1=54+1=5 subspaces with well defined values of the quantum number m=Szm=S^z. The dimensions of these subspaces are:

mdimension
-21
-14
06
24
11

Since we know that the ground state of the Hilbert chain is a singlet, we are only interested in the subspace with Stotalz=m=0S_{total}^{z}=m=0. For our example, this subspace is given by (in increasing order): 00113,01015,01106,10019,101010,101012.\begin{darray}{rcl} \mid \downarrow \downarrow \uparrow \uparrow \rangle & \equiv &|0011\rangle \equiv |3\rangle , \\ \mid \downarrow \uparrow \downarrow \uparrow \rangle &\equiv &|0101\rangle \equiv |5\rangle , \\ \mid \downarrow \uparrow \uparrow \downarrow \rangle &\equiv &|0110\rangle \equiv |6\rangle , \\ \mid \uparrow \downarrow \downarrow \uparrow \rangle &\equiv &|1001\rangle \equiv |9\rangle , \\ \mid \uparrow \downarrow \uparrow \downarrow \rangle &\equiv &|1010\rangle \equiv |10\rangle , \\ \mid \uparrow \uparrow \downarrow \downarrow \rangle &\equiv &|1010\rangle \equiv |12\rangle . \end{darray}

Generating the possible configurations of NN up-spins (or spinless fermions) in LL sites is equivalent to the problem of generating the corresponding combinations of zeroes and ones in lexicographic order.

Construction of the Hamiltonian matrix elements

We now have to address the problem of generating all the Hamiltonian matrix elements . These are obtained by the application of the Hamiltonian on each state of the basis ϕ|\phi \rangle , generating all the values Hϕ,ϕ=ϕH^ϕH_{\phi ,\phi ^{\prime }}=\langle \phi ^{\prime }|{{\hat{H}}}% |\phi \rangle . We illustrate this procedure in the following pseudocode:

    for each term H^i{\hat{H}_i} of the Hamiltonian H^{\hat{H}}

        for all the states ϕ|\phi\rangle in the basis

            H^iϕ=ϕH^iϕϕ\hat{H}_i|\phi\rangle = \langle \phi^{\prime }|{\hat{H}_i}|\phi\rangle |\phi^{\prime }\rangle

             Hϕ,ϕ=Hϕ,ϕ+ϕH^iϕH_{\phi,\phi^{\prime }}=H_{\phi,\phi^{\prime }} + \langle \phi^{\prime }|{\hat{H}_i}|\phi\rangle

        end for

    end for

As the states are representated by binary words, the spin operators (as well as the creation and destructions operators) can be be easily implemented using the logical operators AND, OR, XOR.

As a practical example let us consider the spin-1/2 Heisenberg Hamiltonian on the 4-sites chain: H^=Ji=03Si.Si+1=Ji=03Siz.Si+1z+J2i=03(Si+Si+1+SiSi+1+),{{\hat{H}}}=J\sum_{i=0}^{3}\mathbf{S}_{i}.\mathbf{S}_{i+1}=J% \sum_{i=0}^{3}S_{i}^{z}.S_{i+1}^{z}+\frac{J}{2}% \sum_{i=0}^{3}(S_{i}^{+}S_{i+1}^{-}+S_{i}^{-}S_{i+1}^{+}),

with S4=S0\mathbf{S}_{4}=\mathbf{S}_{0}, due to the periodic boundary conditions. To evaluate the matrix elements in the basis ([basis0]) we must apply each term of H^{{\hat{H}}} on such states. The first term is called Ising term, and is diagonal in this basis (also called Ising basis). The last terms, or fluctuation terms, give strictly off-diagonal contributions to the matrix in this representation (when symmetries are considered, they can also have diagonal contributions). These fluctuations cause an exchange in the spin orientation between neighbors with opposite spins, e.g. \uparrow \downarrow \rightarrow \downarrow \uparrow . The way to implement spin-flip operations of this kind on the computer is defining new ‘masks’. A mask for this operation is a binary number with ‘zeroes’ everywhere, except in the positions of the spins to be flipped, e.g 00..0110...00. Then, the logical operator XOR is used between the initial state and the mask to invert the bits (spins) at the positions indicated by the mask. For example, (0101).\mathtt{.}XOR.(1100)=(1001), i.e., 5.XOR.12=9. It is useful to store all the masks for a given geometry in memory, generating them immediately after the tables for the sites and neighbors.

In the Heisenberg model the spin flips can be implemented using masks, with ‘zeroes’ everywhere, except in the positions of the spins to be flipped. To illustrate this let us show the effect of the off-diagonal terms on one of the Néel configurations: S0+S1+S0S1+0101(0011).XOR.(0101)=3.XOR.5=(0110)=60110,S1+S2+S1S2+0101(0110).XOR.(0101)=6.XOR.5=(0011)=30011,S2+S3+S2S3+0101(1100).XOR.(0101)=12.XOR.5=(1001)=91001,S3+S0+S3S0+0101(1001).XOR.(0101)=9.XOR.5=(1100)=121100.\begin{darray}{rcl} S_{0}^{+}S_{1}^{-}\,+S_{0}^{-}S_{1}^{+}\,|0101\rangle &\equiv &(0011)\mathtt{% .XOR.}(0101)=3\mathtt{.XOR.}5=(0110)=6\equiv \,|0110\rangle , \\ S_{1}^{+}S_{2}^{-}+S_{1}^{-}S_{2}^{+}\,\,|0101\rangle &\equiv &(0110)\mathtt{% .XOR.}(0101)=6\mathtt{.XOR.}5=(0011)=3\equiv \,|0011\rangle , \\ S_{2}^{+}S_{3}^{-}+S_{2}^{-}S_{3}^{+}\,\,|0101\rangle &\equiv &(1100)\mathtt{% .XOR.}(0101)=12\mathtt{.XOR.}5=(1001)=9\equiv \,|1001\rangle , \\ S_{3}^{+}S_{0}^{-}\,+S_{3}^{-}S_{0}^{+}|0101\rangle &\equiv &(1001)\mathtt{% .XOR.}(0101)=9\mathtt{.XOR.}5=(1100)=12\equiv \,|1100\rangle .% \end{darray}

After applying the Hamiltonian ([h4]) on our basis states, the reader can verify as an exercise that we obtain H^0101=J0101+J2[1100+1001+0011+0110],H^1010=J1010+J2[1100+1001+0011+0110],H^0011=J2[0101+1010],H^0110=J2[0101+1010],H^1001=J2[0101+1010],H^1100=J2[0101+1010].\begin{darray}{rcl} {{\hat{H}}}\,|0101\rangle &=&-J\,|0101\rangle +\frac{J}{2}\left[ \,|1100\rangle +\,|1001\rangle +\,|0011\rangle +\,|0110\rangle \right] , \\ {{\hat{H}}}\,|1010\rangle &=&-J\,|1010\rangle +\frac{J}{2}\left[ \,|1100\rangle +\,|1001\rangle +\,|0011\rangle +\,|0110\rangle \right] , \\ {{\hat{H}}}\,|0011\rangle &=&\frac{J}{2}\left[ \,\,|0101\rangle +\,|1010\rangle \right] , \\ {{\hat{H}}}\,|0110\rangle &=&\frac{J}{2}\left[ \,\,|0101\rangle +\,|1010\rangle \right] , \\ {{\hat{H}}}\,|1001\rangle &=&\frac{J}{2}\left[ \,|0101\rangle +\,|1010\rangle \right] , \\ {{\hat{H}}}\,|1100\rangle &=&\frac{J}{2}\left[ \,|0101\rangle +\,|1010\rangle \right] .% \end{darray}

The resulting matrix for the operator H^\hat{H}in the SzS^{z} representation is: H=J(01/2001/201/211/21/201/201/2001/2001/2001/201/201/21/211/201/2001/20).H=J\left( \begin{array}{llllll} 0 & 1/2 & 0 & 0 & 1/2 & 0 \\ 1/2 & -1 & 1/2 & 1/2 & 0 & 1/2 \\ 0 & 1/2 & 0 & 0 & 1/2 & 0 \\ 0 & 1/2 & 0 & 0 & 1/2 & 0 \\ 1/2 & 0 & 1/2 & 1/2 & -1 & 1/2 \\ 0 & 1/2 & 0 & 0 & 1/2 & 0 \end{array} \right) .

class BoundaryCondition: RBC, PBC = range(2) class Direction: RIGHT, TOP, LEFT, BOTTOM = range(4) L = 4 Nup = 2 maxdim = 2**L bc = BoundaryCondition.PBC # Tables for energy hdiag = np.zeros(4) # Diagonal energies hflip = np.zeros(4) # off-diagonal terms hdiag[0] = +0.25 hdiag[1] = -0.25 hdiag[2] = -0.25 hdiag[3] = +0.25 hflip[0] = 0. hflip[1] = 0.5 hflip[2] = 0.5 hflip[3] = 0. #hflip *= 2 #hdiag *= 0. # Lattice geometry (1D chain) nn = np.zeros(shape=(L,4), dtype=np.int16) for i in range(L): nn[i, Direction.RIGHT] = i-1 nn[i, Direction.LEFT] = i+1 if(bc == BoundaryCondition.RBC): # Open Boundary Conditions nn[0, Direction.RIGHT] = -1 # This means error nn[L-1, Direction.LEFT] = -1 else: # Periodic Boundary Conditions nn[0, Direction.RIGHT] = L-1 # We close the ring nn[L-1, Direction.LEFT] = 0 # We build basis basis = [] dim = 0 for state in range(maxdim): basis.append(state) dim += 1 print ("Basis:") print (basis) # We build Hamiltonian matrix H = np.zeros(shape=(dim,dim)) def IBITS(n,i): return ((n >> i) & 1) for i in range(dim): state = basis[i] # Diagonal term for site_i in range(L): site_j = nn[site_i, Direction.RIGHT] if(site_j != -1): # This would happen for open boundary conditions two_sites = IBITS(state,site_i) | (IBITS(state,site_j) << 1) value = hdiag[two_sites] H[i,i] += value for i in range(dim): state = basis[i] # Off-diagonal term for site_i in range(L): site_j = nn[site_i, Direction.RIGHT] if(site_j != -1): mask = (1 << site_i) | (1 << site_j) two_sites = IBITS(state,site_i) | (IBITS(state,site_j) << 1) value = hflip[two_sites] if(value != 0.): new_state = (state ^ mask) j = new_state H[i,j] += value print(H) d, v = np.linalg.eigh(H) #Diagonalize the matrix print("===================================================================================================================") print(d) print(v[:,0])
Basis: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] [[ 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. ] [ 0. 0. 0.5 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. 0. 0. ] [ 0. 0.5 0. 0. 0.5 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. ] [ 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. ] [ 0. 0. 0.5 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. 0. 0. ] [ 0. 0. 0. 0.5 0. -1. 0.5 0. 0. 0.5 0. 0. 0.5 0. 0. 0. ] [ 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. ] [ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.5 0. 0. 0.5 0. ] [ 0. 0.5 0. 0. 0.5 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. ] [ 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. ] [ 0. 0. 0. 0.5 0. 0. 0.5 0. 0. 0.5 -1. 0. 0.5 0. 0. 0. ] [ 0. 0. 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. 0.5 0. 0. ] [ 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. ] [ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.5 0. 0. 0.5 0. ] [ 0. 0. 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0. 0. 0.5 0. 0. ] [ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. ]] =================================================================================================================== [-2.00000000e+00 -1.00000000e+00 -1.00000000e+00 -1.00000000e+00 -2.25514052e-17 -1.78638187e-17 -7.68015095e-32 0.00000000e+00 2.42866341e-47 6.17036833e-34 1.31651255e-31 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.00000000e+00] [ 0. 0. 0. -0.28867513 0. 0.57735027 -0.28867513 0. 0. -0.28867513 0.57735027 0. -0.28867513 0. 0. 0. ]
# Using quantum number conservation : Sz L=8 Nup=4 maxdim = 2**L # Lattice geometry (1D chain) nn = np.zeros(shape=(L,4), dtype=np.int16) for i in range(L): nn[i, Direction.RIGHT] = i-1 nn[i, Direction.LEFT] = i+1 if(bc == BoundaryCondition.RBC): # Open Boundary Conditions nn[0, Direction.RIGHT] = -1 # This means error nn[L-1, Direction.LEFT] = -1 else: # Periodic Boundary Conditions nn[0, Direction.RIGHT] = L-1 # We close the ring nn[L-1, Direction.LEFT] = 0 basis = [] dim = 0 for state in range(maxdim): n_ones = 0 for bit in range(L): n_ones += IBITS(state,bit) if(n_ones == Nup): basis.append(state) dim += 1 print ("Dim=",dim) print ("Basis:") print (basis) #hflip[:] *= 2 # We build Hamiltonian matrix H = np.zeros(shape=(dim,dim)) def IBITS(n,i): return ((n >> i) & 1) for i in range(dim): state = basis[i] # Diagonal term for site_i in range(L): site_j = nn[site_i, Direction.RIGHT] if(site_j != -1): # This would happen for open boundary conditions two_sites = IBITS(state,site_i) | (IBITS(state,site_j) << 1) value = hdiag[two_sites] H[i,i] += value def bisect(state, basis): # Binary search, only works in a sorted list of integers # state : integer we seek # basis : list of sorted integers in increasing order # ret_val : return value, position on the list, -1 if not found ret_val = -1 dim = len(basis) # for i in range(dim): # if(state == basis[i]): # return i origin = 0 end = dim-1 middle = (origin+end)/2 while(1): index_old = middle middle = (origin+end)//2 if(state < basis[middle]): end = middle else: origin = middle if(basis[middle] == state): break if(middle == index_old): if(middle == end): end = end - 1 else: origin = origin + 1 ret_val = middle return ret_val for i in range(dim): state = basis[i] # Off-diagonal term for site_i in range(L): site_j = nn[site_i, Direction.RIGHT] if(site_j != -1): mask = (1 << site_i) | (1 << site_j) two_sites = IBITS(state,site_i) | (IBITS(state,site_j) << 1) value = hflip[two_sites] if(value != 0.): new_state = (state ^ mask) j = bisect(new_state, basis) H[i,j] += value print(H) d, v = np.linalg.eigh(H) #Diagonalize the matrix print(d,np.min(d)) print(v[:,0])
Dim= 70 Basis: [15, 23, 27, 29, 30, 39, 43, 45, 46, 51, 53, 54, 57, 58, 60, 71, 75, 77, 78, 83, 85, 86, 89, 90, 92, 99, 101, 102, 105, 106, 108, 113, 114, 116, 120, 135, 139, 141, 142, 147, 149, 150, 153, 154, 156, 163, 165, 166, 169, 170, 172, 177, 178, 180, 184, 195, 197, 198, 201, 202, 204, 209, 210, 212, 216, 225, 226, 228, 232, 240] [[1. 0.5 0. ... 0. 0. 0. ] [0.5 0. 0.5 ... 0. 0. 0. ] [0. 0.5 0. ... 0. 0. 0. ] ... [0. 0. 0. ... 0. 0.5 0. ] [0. 0. 0. ... 0.5 0. 0.5] [0. 0. 0. ... 0. 0.5 1. ]] [-3.65109341e+00 -3.12841906e+00 -2.69962815e+00 -2.45873851e+00 -2.45873851e+00 -2.14514837e+00 -2.14514837e+00 -1.85463768e+00 -1.85463768e+00 -1.80193774e+00 -1.70710678e+00 -1.70710678e+00 -1.61803399e+00 -1.61803399e+00 -1.26703510e+00 -1.26703510e+00 -1.20163968e+00 -1.14412281e+00 -1.14412281e+00 -1.00000000e+00 -8.58923550e-01 -8.58923550e-01 -7.60876722e-01 -7.26109445e-01 -6.28378427e-01 -6.28378427e-01 -5.96968283e-01 -5.96968283e-01 -5.12683203e-01 -5.12683203e-01 -4.45041868e-01 -4.37016024e-01 -4.37016024e-01 -2.92893219e-01 -2.92893219e-01 -2.58652023e-01 -2.58652023e-01 -2.60477563e-16 -6.54585555e-17 -4.45385084e-17 3.08216485e-16 3.47303424e-16 2.65452712e-01 2.65452712e-01 2.92893219e-01 2.92893219e-01 3.77202854e-01 4.37016024e-01 4.37016024e-01 4.51605963e-01 4.51605963e-01 6.18033989e-01 6.18033989e-01 8.92693358e-01 8.92693358e-01 1.00000000e+00 1.00000000e+00 1.00000000e+00 1.14412281e+00 1.14412281e+00 1.24697960e+00 1.33005874e+00 1.44572599e+00 1.44572599e+00 1.46050487e+00 1.52568712e+00 1.52568712e+00 1.70710678e+00 1.70710678e+00 2.00000000e+00] -3.651093408937174 [ 0.00746966 -0.03474209 0.05454486 -0.03474209 0.00746966 0.05454486 -0.16440628 0.13713385 -0.03474209 0.09005865 -0.16440628 0.05454486 0.05454486 -0.03474209 0.00746966 -0.03474209 0.13713385 -0.16440628 0.05454486 -0.16440628 0.39829674 -0.16440628 -0.16440628 0.13713385 -0.03474209 0.05454486 -0.16440628 0.09005865 0.13713385 -0.16440628 0.05454486 -0.03474209 0.05454486 -0.03474209 0.00746966 0.00746966 -0.03474209 0.05454486 -0.03474209 0.05454486 -0.16440628 0.13713385 0.09005865 -0.16440628 0.05454486 -0.03474209 0.13713385 -0.16440628 -0.16440628 0.39829674 -0.16440628 0.05454486 -0.16440628 0.13713385 -0.03474209 0.00746966 -0.03474209 0.05454486 0.05454486 -0.16440628 0.09005865 -0.03474209 0.13713385 -0.16440628 0.05454486 0.00746966 -0.03474209 0.05454486 -0.03474209 0.00746966]

Obtaining the ground-state: Lanczos diagonalization

Once we have a superblock matrix, we can apply a library routine to obtain the ground state of the superblock Ψ|\Psi\rangle. The two algorithms widely used for this purpose are the Lanczos and Davidson diagonalization. Both are explained to great extent in Ref.@noack, so we refer the reader to this material for further information. In these notes we will briefly explain the Lanczos procedure.

The basic idea of the Lanczos method is that a special basis can be constructed where the Hamiltonian has a tridiagonal representation. This is carried out iteratively as shown below. First, it is necessary to select an arbitrary seed vector ϕ0|\phi _{0}\rangle in the Hilbert space of the model being studied. If we are seeking the ground-state of the model, then it is necessary that the overlap between the actual ground-state ψ0|\psi _{0}\rangle , and the initial state ϕ0|\phi _{0}\rangle be nonzero. If no “a priori” information about the ground state is known, this requirement is usually easily satisfied by selecting an initial state with randomly chosen coefficients in the working basis that is being used. If some other information of the ground state is known, like its total momentum and spin, then it is convenient to initiate the iterations with a state already belonging to the subspace having those quantum numbers (and still with random coefficients within this subspace).

After ϕ0|\phi _{0}\rangle is selected, define a new vector by applying the Hamiltonian H^{{\hat{H}}}, over the initial state. Subtracting the projection over ϕ0|\phi _{0}\rangle , we obtain ϕ1=H^ϕ0ϕ0H^ϕ0ϕ0ϕ0ϕ0,|\phi _{1}\rangle ={{\hat{H}}}|\phi _{0}\rangle -\frac{{\langle }\phi _{0}|{{% \hat{H}}}|{\phi }_{0}{\rangle }}{\langle \phi _{0}|\phi _{0}\rangle }|\phi _{0}\rangle , that satisfies ϕ0ϕ1=0\langle \phi _{0}|\phi _{1}\rangle =0. Now, we can construct a new state that is orthogonal to the previous two as, ϕ2=H^ϕ1ϕ1H^ϕ1ϕ1ϕ1ϕ1ϕ1ϕ1ϕ0ϕ0ϕ0.|\phi _{2}\rangle ={{\hat{H}}}|\phi _{1}\rangle -{\frac{{\langle \phi _{1}|{% \hat{H}}|\phi _{1}\rangle }}{{\langle \phi _{1}|\phi _{1}\rangle }}}|\phi _{1}\rangle -{\frac{{\langle \phi _{1}|\phi _{1}\rangle }}{{\langle \phi _{0}|\phi _{0}\rangle }}}|\phi _{0}\rangle . It can be easily checked that ϕ0ϕ2=ϕ1ϕ2=0\langle \phi _{0}|\phi _{2}\rangle =\langle \phi _{1}|\phi _{2}\rangle =0. The procedure can be generalized by defining an orthogonal basis recursively as, ϕn+1=H^ϕnanϕnbn2ϕn1,|\phi _{n+1}\rangle ={{\hat{H}}}|\phi _{n}\rangle -a_{n}|\phi _{n}\rangle -b_{n}^{2}|\phi _{n-1}\rangle , where n=0,1,2,...n=0,1,2,..., and the coefficients are given by an=ϕnH^ϕnϕnϕn,bn2=ϕnϕnϕn1ϕn1,a_{n}={\frac{{\langle \phi _{n}|{\hat{H}}|\phi _{n}\rangle }}{{\langle \phi _{n}|\phi _{n}\rangle }}},\qquad b_{n}^{2}={\frac{{\langle \phi _{n}|\phi _{n}\rangle }}{{\langle \phi _{n-1}|\phi _{n-1}\rangle }}}, supplemented by b0=0b_{0}=0, ϕ1=0|\phi _{-1}\rangle =0. In this basis, it can be shown that the Hamiltonian matrix becomes, H=(a0b100...b1a1b20...0b2a2b3...00b3a3...)H=\left( \begin{array}{lllll} a_{0} & b_{1} & 0 & 0 & ... \\ b_{1} & a_{1} & b_{2} & 0 & ... \\ 0 & b_{2} & a_{2} & b_{3} & ... \\ 0 & 0 & b_{3} & a_{3} & ... \\ \vdots {} & \vdots & \vdots & \vdots & \end{array} \right) i.e. it is tridiagonal as expected. Once in this form the matrix can be diagonalized easily using standard library subroutines. However, note that to diagonalize completely a Hamiltonian on a finite cluster, a number of iterations equal to the size of the Hilbert space (or the subspace under consideration) are needed. In practice this would demand a considerable amount of CPU time. However, one of the advantages of this technique is that accurate enough information about the ground state of the problem can be obtained after a small number of iterations (typically of the order of 100\sim 100 or less).

Another way to formulate the problem is by obtaining the tridiagonal form of the Hamiltonian starting from a Krylov basis, which is spanned by the vectors {ϕ0,H^ϕ0,H^2ϕ0,...,H^nϕ0}\left\{|\phi_0\rangle,\hat{H}|\phi_0\rangle,\hat{H}^2|\phi_0\rangle,...,\hat{H}^n|\phi_0\rangle\right\} and asking that each vector be orthogonal to the previous two. Notice that each new iteration of the process requires one application of the Hamiltonian. Most of the time this simple procedure works for practical purposes, but care must be payed to the possibility of losing orthogonality between the basis vectors. This may happen due to the finite machine precision. In that case, a re-orthogonalization procedure may be required.

Notice that the new super-Hamiltonian matrix has dimensions DLDRd2×DLDRd2D_L D_R d^2 \times D_L D_R d^2. This could be a large matrix. In state-of-the-art simulations with a large number of states, one does not build this matrix in memory explicitly, but applies the operators to the state directly in the diagonalization routine.

def lanczos(m, seed, maxiter, tol, use_seed, calc_gs, force_maxiter = False): x1 = seed x2 = seed gs = seed a = np.zeros(100) b = np.zeros(100) z = np.zeros((100,100)) lvectors = [] control_max = maxiter; if(maxiter == -1): force_maxiter = False if(control_max == 0): gs = 1 maxiter = 1 return(e0,gs) x1[:] = 0 x2[:] = 0 gs[:] = 0 maxiter = 0 a[:] = 0.0 b[:] = 0.0 if(use_seed): x1 = seed else : x1 = np.random.random(x1.shape[0])*2-1. b[0] = np.sqrt(np.dot(x1,x1)) x1 = x1 / b[0] x2[:] = 0 b[0] = 1. e0 = 9999 nmax = min(99, gs.shape[0]) for iter in range(1,nmax+1): eini = e0 if(b[iter - 1] != 0.): aux = x1 x1 = -b[iter-1] * x2 x2 = aux / b[iter-1] aux = np.dot(m,x2) x1 = x1 + aux a[iter] = np.dot(x1, x2) x1 = x1 - x2*a[iter] b[iter] = np.sqrt(np.dot(x1, x1)) lvectors.append(x2) z.resize((iter+1,iter+1)) z[:,:] = 0 for i in range(0,iter-1): z[i,i+1] = b[i+1] z[i+1,i] = b[i+1] z[i,i] = a[i+1] z[iter-1,iter-1]=a[iter] d, v = np.linalg.eig(z) col = 0 n = 0 e0 = 9999 for e in d: if(e < e0): e0 = e col = n n+=1 e0 = d[col] print ("Iter = ",iter," Ener = ",e0) if((force_maxiter and iter >= control_max) or (iter >= gs.shape[0] or iter == 99 or abs(b[iter]) < tol) or \ ((not force_maxiter) and abs(eini-e0) <= tol)): # converged gs = 0. for i in range(0,iter): gs += v[i,col]*lvectors[i] print ("E0 = ", e0, np.sqrt(np.dot(gs,gs))) maxiter = iter return(e0,gs) # We return with ground states energy
seed = np.zeros(H.shape[0])
e0, gs = lanczos(H,seed,6,1.e-5,False,False)
Iter = 1 Ener = -0.32059440314178866 Iter = 2 Ener = -1.207943534608849 Iter = 3 Ener = -1.7312786649917817 Iter = 4 Ener = -2.463042387743914 Iter = 5 Ener = -2.758240572039436 Iter = 6 Ener = -2.7935078581431574 Iter = 7 Ener = -2.8001936054168652 Iter = 8 Ener = -2.802412181520129 Iter = 9 Ener = -2.8027617389669075 Iter = 10 Ener = -2.802773890249349 Iter = 11 Ener = -2.8027756307225253 E0 = -2.8027756307225253 1.0000000000000004
print(gs)
[ 0.06293272 -0.20780802 0.20781428 -0.06290635 0.20781929 -0.4785539 0.20780196 0.20781618 -0.20781206 0.06293183 -0.06290584 0.20780202 -0.20783061 -0.20782075 0.47853525 -0.20781988 0.06293281 -0.20782154 0.20779574 -0.06290309]
print(np.dot(gs,gs))
0.9999999999999994