following 1905.03312 and Hatem Barghathi's March Meeting talk
Spenser Talkington
4 May 2023
= 10 #sites S
import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import expm as matrixexp
from scipy.linalg import logm as matrixlog
import scipy.sparse as sparse
from functools import lru_cache
%config InlineBackend.figure_format = "svg"
Fast implementation of fermion operators courtesy of Brandon Monsen.
# New version with sparse matrices. These are defined via Jordan-Wigner
@lru_cache(maxsize=None)
def cD_(n, L=10):
= sparse.csc_matrix([[0,1],[0,0]])
S = sparse.csc_matrix([[1,0],[0,-1]])
pauli_z = sparse.identity(2,format="csc")
id2
# This construction is written explicitly in the slides. I am just copying them
# matrices to the left of the index n are pauli z matrices, and to the right are identity.
# at index n, it is the S^+ operator.
= S if n==0 else pauli_z
temp
for idx in range(1, L):
if idx < n:
= sparse.kron(temp, pauli_z)
temp elif idx == n:
= sparse.kron(temp, S)
temp else:
= sparse.kron(temp, id2)
temp
# re-type casting to a sparse matrix removes extraneous zeroes and greatly speeds up the code
return sparse.csc_matrix(temp.toarray())
@lru_cache(maxsize=None)
# Identical to cD_ except each matrix is replaced with its transpose
def c_(n, L=10):
= sparse.csc_matrix([[0,0],[1,0]])
S = sparse.csc_matrix([[1,0],[0,-1]])
pauli_z = sparse.identity(2,format="csc")
id2
= S if n==0 else pauli_z
temp
for idx in range(1, L):
if idx < n:
= sparse.kron(temp, pauli_z)
temp elif idx == n:
= sparse.kron(temp, S)
temp else:
= sparse.kron(temp, id2)
temp
return sparse.csc_matrix(temp.toarray())
#Helper function for later that defines a number operator to save space in the code & help readability
@lru_cache(maxsize=None)
def n_(n,L=10):
return cD_(n,L) @ c_(n,L)
def indicesFixedN(N, L):
# My testing suggests that the eigenvectors of the number operators are the pure cartesian bases vectors and the
# number operator eigenvalue of eigenvector n is the bit sum of (2**L-1-n) in binary.
= []
temp for idx in range(2**L):
if bit_sum(2**L-1-idx) == N:
temp.append(idx)return np.array(temp)
def bit_sum(n):
return bin(n).count("1")
def vacuum(L):
= np.zeros(2**L)
temp -1]=1
temp[return temp
def rot(sites,fermions):
#rotate into the fixed particle number sector
= indicesFixedN(fermions,sites)
indices = np.shape(indices)[0]
num = sparse.csc_matrix(np.zeros((2**sites,2**sites)))
rot for i in range(0,num,1):
= 1
rot[i,indices[i]] return num,rot
= rot(S,S//2)
dim,rotate = sparse.identity(2**S, format = "csc")#*****dim, format = "csc")
identity
def number(sites):
#create a dictionary for the number operators rotated into the fixed particle number sector
= {}
numbers for i in range(0,sites,1):
str(i)] = n_(i,sites) #*****(rotate @ n_(i,sites) @ rotate.T)[0:dim,0:dim]
numbers[return numbers
= number(S)
numbers
def t_Hamiltonian(sites,t=1):
#build the Hamiltonian without disorder
= sparse.csc_matrix(np.zeros((2**sites,2**sites)))
t_total for i in range(0,sites-1,1):
+= - t * (cD_(i,sites)@c_(i+1,sites) + cD_(i+1,sites)@c_(i,sites))
t_total += (-1)**((sites/2)%2) * t * (cD_(sites-1,sites)@c_(0,sites) + cD_(0,sites)@c_(sites-1,sites)) #(anti)periodic BCs (for (even) odd number of fermions)
t_total = t_total #*****(rotate @ t_total @ rotate.T)[0:dim,0:dim]
t_reduced return t_reduced
def V_Hamiltonian(sites,V=1):
#build the Hamiltonian without disorder
= sparse.csc_matrix(np.zeros((2**S,2**S))) #******np.zeros((dim,dim))
V_total for i in range(0,sites-1,1):
+= V*(numbers[str(i)])@(numbers[str(i+1)])
V_total += V * (numbers[str(sites-1)])@(numbers[str(0)]) #(anti)periodic BCs (for (even) odd number of fermions)
V_total return V_total
= t_Hamiltonian(S)
H_t = V_Hamiltonian(S) H_V
def half_basis(S):
= {}
basis_states for i in range(0,2**(S//2),1):
= vacuum(S//2)
state = bin(2**(S//2)+i)
b for j in range(1,S//2+1,1):
if(int(b[-j])==1):
= cD_(j-1,S//2)@state
state str(i)] = state
basis_states[return basis_states
= half_basis(S)
basis_states
def half_trace(rho,S): #trace rho over the first s sites of a length S chain
= np.zeros((2**(S//2),2**(S//2)),dtype=complex) #not bothering to reduce dimensionality or take advantage of rho being real
rho_A for i in range(0,2**(S//2),1):
= np.kron(np.identity(2**(S//2),dtype=complex),basis_states[str(i)])
psi_j += psi_j@rho@np.conjugate(psi_j.T)
rho_A return rho_A
%%time
= 1
t = 101
interactions = np.linspace(-10,10,interactions)#np.logspace(np.log10(1*10**-2),np.log10(1*10**2),interactions)
Vs
= np.zeros(interactions)
S_EE = indicesFixedN(S//2,S)
idxs
for j in range(0,interactions,1):
= t*H_t + Vs[j]*H_V #create a Hamiltonian
H = (rotate @ H @ rotate.T)[0:dim,0:dim] #*********rotate into fixed particle basis
H #find ground state, express in big basis
= np.linalg.eigh(H.toarray())
eigs, vecs = vecs[:,0]
psi_gs = np.zeros(2**S,dtype=complex)
psi_gs_big = psi_gs
psi_gs_big[idxs] #entropy calculation
= np.outer(np.conjugate(psi_gs_big),psi_gs_big) #since this is a pure state
rho = half_trace(rho,S)
rho_A += -np.real(np.trace(rho_A@matrixlog(rho_A)))
S_EE[j] #print(j)
/opt/homebrew/lib/python3.11/site-packages/scipy/linalg/_matfuncs_inv_ssq.py:836: LogmNearlySingularWarning: The logm input matrix may be nearly singular.
warnings.warn(near_singularity_msg, LogmNearlySingularWarning)
CPU times: user 4min 7s, sys: 6min, total: 10min 7s
Wall time: 51.5 s
= S_EE S_EE10
#plt.plot(Vs,np.log2(np.exp(1))*S_EE4,marker=".",label=r"$L=4$")
1))*S_EE6,marker=".",label=r"$L=6$")
plt.plot(Vs,np.log2(np.exp(1))*S_EE8,marker=".",label=r"$L=8$")
plt.plot(Vs,np.log2(np.exp(1))*S_EE10,marker=".",label=r"$L=10$")
plt.plot(Vs,np.log2(np.exp(#plt.plot(Vs,np.log2(np.exp(1))*S_EE12,marker=".",label=r"$L=12$")
"V")
plt.xlabel(r"$S=-\mathrm{Tr}[\rho^A_{GS}\log_{2}(\rho^A_{GS})]$")
plt.ylabel(
plt.legend()-2)
plt.axvline(2)
plt.axvline(-9,2.65,"Cluster Solid")
plt.text(-1.5,2.6,"Luttinger\n Liquid")
plt.text(3,2.65,"Charge Density Wave")
plt.text("tV Model Entropy Crossovers")
plt.title("tV.pdf") plt.savefig(