Spenser Talkington
4 May 2023
= 14 #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
from scipy.sparse.linalg import eigsh as sparse_eigs
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
%%time
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 partial_basis(s):
= np.zeros((2**s,2**s),dtype=complex)
basis_states for i in range(0,2**s,1):
= vacuum(s)
state = bin(2**s+i)
b for j in range(1,s+1,1):
if(int(b[-j])==1):
= cD_(j-1,s)@state
state = state
basis_states[i] return basis_states
def partial_trace(psi,s,S):
= np.zeros((2**s,2**(S-s)),dtype=complex)
Psi for i in range(0,2**s,1):
for j in range(0,2**(S-s),1):
= np.kron(pbA[i],pbB[j])@psi
Psi[i,j] = Psi @ np.conjugate(Psi.T)
rho_A return rho_A
/opt/homebrew/lib/python3.11/site-packages/scipy/sparse/_index.py:100: SparseEfficiencyWarning: Changing the sparsity structure of a csc_matrix is expensive. lil_matrix is more efficient.
self._set_intXint(row, col, x.flat[0])
CPU times: user 49 s, sys: 8.17 s, total: 57.2 s
Wall time: 55.5 s
%%time
= 1
t = 2
V
= np.zeros(S+1)
S_EE = indicesFixedN(S//2,S)
idxs
= t*H_t + V*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
= sparse_eigs(H,k=1,sigma=-0.5*S) #make sure sigma is less than the lowest eigenvalue for convergence! -- an exact value is available for the Heisenberg model
eigs, vecs = vecs[:,0]
psi_gs = np.zeros(2**S,dtype=complex)
psi_gs_big = psi_gs
psi_gs_big[idxs]
for s in range(1,S//2+1,1):
= partial_basis(s)
pbA = partial_basis(S-s)
pbB = partial_trace(psi_gs_big,s,S)
rho_A = -np.real(np.trace(rho_A@matrixlog(rho_A)))
S_EE[s] -s-1] = S_EE[s] S_EE[
CPU times: user 34.5 s, sys: 25.4 s, total: 59.9 s
Wall time: 18.7 s
0,S,S+1),np.log2(np.exp(1))*S_EE,marker="o",label=r"$S_{GS}$ for $L=$"+str(S))
plt.scatter(np.linspace(= 1.0
a = np.pi
c = np.linspace(0,S,100)
x + c/6 * np.log(S/np.pi * np.sin(np.pi*x/S)),linestyle="dashed",label=r"$A+\frac{c}{6}\ln(\frac{L}{\pi}\sin(\pi\frac{L_A}{L}))$")
plt.plot(x,a 0,2.3)
plt.ylim(r"tV Model Ground State Entanglement ($V={}$, $c={}$)".format(V,np.round(c,2)))
plt.title(r"$S=-\mathrm{Tr}[\rho^A_{GS}\log_{2}(\rho^A_{GS})]$")
plt.ylabel(r"Bipartition Size ($L_A$)")
plt.xlabel( plt.legend()
/var/folders/jy/26sg472s4m3dk7z6s4trzt640000gn/T/ipykernel_98222/2363512847.py:5: RuntimeWarning: divide by zero encountered in log
plt.plot(x,a + c/6 * np.log(S/np.pi * np.sin(np.pi*x/S)),linestyle="dashed",label=r"$A+\frac{c}{6}\ln(\frac{L}{\pi}\sin(\pi\frac{L_A}{L}))$")
<matplotlib.legend.Legend at 0x121f68d90>