Hello everyone,
I am currently trying to construct a hopping term between nearest neighbors for spinless Fermions on a one dimensional chain with L sites manually, i.e. without using OpSum (I want to learn the manual construction since my actual Hamiltonian contains exponentials of operators, which, to my understanding, do not work in OpSum). This is my Hamiltonian:
I follow the first half of the paper by Crosswhite and Bacon (https://arxiv.org/pdf/0708.1221) for constructing MPOs by using finite automata. To apply this formalism, I need the terms to be site ordered (at least to my understanding), which is why I rewrite the Hamiltonian
and then construct the boundary matrices W_0 and W_L as well as the bulk matrices W_bulk by writing down the finite state automata and the corresponding matrix product diagram. Below is the code for three functions, one for constructing the MPO with OpSum, one for constructing it manually and one for constructing it manually and neglecting the sign change:
using ITensors
using ITensorMPS
using Random
function hopping_opsum(sites::Vector{<:Index}, t::Real=1.0)::MPO
os = OpSum()
for j in 1:(length(sites)-1)
os += -t, "câ ", j, "c", j + 1
os += -t, "câ ", j + 1, "c", j # or +t, "c", j, "câ ", j + 1 due to anticommutation
end
return MPO(os, sites)
end
function hopping_manual(sites::Vector{<:Index}, t::Real=1.0)::MPO
# Local operators
id = ComplexF64[1 0; 0 1]
n = ComplexF64[0 0; 0 1]
c_dag = ComplexF64[0 0; 1 0]
c = ComplexF64[0 1; 0 0]
# Virtual bond indices (dimension 4), excluding boundaries
L = length(sites)
links = [Index(4, "link,l=$i") for i in 1:(L - 1)]
W = Vector{ITensor}(undef, L) # undef: do not initialize yet
# First site
W[1] = ITensor(links[1], prime(sites[1]), sites[1])
for i in 1:2, j in 1:2
W[1][1, i, j] = id[i, j]
W[1][2, i, j] = -t * c_dag[i, j]
W[1][3, i, j] = +t * c[i, j] # !! sign change due to anticommutation
end
# Bulk sites
for l in 2:(L-1) # n is already used
W[l] = ITensor(links[l-1], links[l], prime(sites[l]), sites[l])
for i in 1:2, j in 1:2
W[l][1, 1, i, j] = id[i, j]
W[l][1, 2, i, j] = -t * c_dag[i, j]
W[l][1, 3, i, j] = +t * c[i, j] # !! sign change due to anticommutation
W[l][2, 4, i, j] = c[i, j]
W[l][3, 4, i, j] = c_dag[i, j]
W[l][4, 4, i, j] = id[i, j]
end
end
# last site
W[L] = ITensor(links[L-1], prime(sites[L]), sites[L])
for i in 1:2, j in 1:2
W[L][2, i, j] = c[i, j]
W[L][3, i, j] = c_dag[i, j]
W[L][4, i, j] = id[i, j]
end
return MPO(W)
end
function hopping_manual_changed_sign(sites::Vector{<:Index}, t::Real=1.0)::MPO
# Local operators
id = ComplexF64[1 0; 0 1]
n = ComplexF64[0 0; 0 1]
c_dag = ComplexF64[0 0; 1 0]
c = ComplexF64[0 1; 0 0]
# Virtual bond indices (dimension 4), excluding boundaries
L = length(sites)
links = [Index(4, "link,l=$i") for i in 1:(L - 1)]
W = Vector{ITensor}(undef, L) # undef: do not initialize yet
# First site
W[1] = ITensor(links[1], prime(sites[1]), sites[1])
for i in 1:2, j in 1:2
W[1][1, i, j] = id[i, j]
W[1][2, i, j] = -t * c_dag[i, j]
W[1][3, i, j] = -t * c[i, j] # !!
end
# Bulk sites
for l in 2:(L-1) # n is already used
W[l] = ITensor(links[l-1], links[l], prime(sites[l]), sites[l])
for i in 1:2, j in 1:2
W[l][1, 1, i, j] = id[i, j]
W[l][1, 2, i, j] = -t * c_dag[i, j]
W[l][1, 3, i, j] = -t * c[i, j] # !!
W[l][2, 4, i, j] = c[i, j]
W[l][3, 4, i, j] = c_dag[i, j]
W[l][4, 4, i, j] = id[i, j]
end
end
# last site
W[L] = ITensor(links[L-1], prime(sites[L]), sites[L])
for i in 1:2, j in 1:2
W[L][2, i, j] = c[i, j]
W[L][3, i, j] = c_dag[i, j]
W[L][4, i, j] = id[i, j]
end
return MPO(W)
end
let
# setup for simple system
Random.seed!(42)
L = 3
sites = siteinds("Fermion", L)
H_opsum = hopping_opsum(sites)
H_manual = hopping_manual(sites)
H_manual_changed = hopping_manual_changed_sign(sites)
# compute energies for a random s
s = randomMPS(sites, 2)
@show energy_opsum = inner(s', H_opsum, s)
@show energy_manual = inner(s', H_manual, s)
@show energy_manual_changed = inner(s', H_manual_changed, s)
# compute action of Hamiltonians on s
s_opsum = apply(H_opsum, s)
s_manual = apply(H_manual, s)
s_manual_changed = apply(H_manual_changed, s)
# compare results with opsum as reference
@show norm(s_opsum - s_manual)
@show norm(s_opsum - s_manual_changed)
end
nothing
The lines affected by the sign change are marked with # !!. The let block is for quickly checking, if the MPOs encode the same operator, i.e. if they act the same on a random MPS. Executing this code within the REPL yields:
```
julia> include(âhopping.jlâ)
energy_opsum = inner(sâ, H_opsum, s) = -0.07088577164847563
energy_manual = inner(sâ, H_manual, s) = -1.629062980549775e-17 + 0.0im
energy_manual_changed = inner(sâ, H_manual_changed, s) = -0.07088577164847563 + 0.
0im
norm(s_opsum - s_manual) = 1.085567382692007
norm(s_opsum - s_manual_changed) = 1.7775626050888642e-16
```
which brings me to the following questions:
-
Is the general syntax that I use to manually construct the MPO correct? I have googled a lot during the last weeks on how to achieve this in ITensors, but have found no nice reference/example.
-
Why does the function with the âwrongâ sign convention work correctly, i.e. match the result from OpSum? It feels like I have not yet fully understood how to construct any Hamiltonian including only nearest neighbor interactions with the finite automata formalism.
-
Does the formalism change extensively when implementing periodic boundary conditions and Abelian symmetries?
I am very new to Julia and ITensor, so please forgive all the silly mistakes I probably did. I look forward to hearing from you!