Reduced density matrix of an MPO

Hello,

I want to obtain the reduced density matrix of a subsystem given a total density matrix.

Here is a minimal working example.

N = 4
sites = siteinds("S=1/2", 2N; conserve_qns=false)
sites_p = prime(sites)

ψ = randomMPS(sites)

ρ_total = outer(ψ',ψ)
@show inds(ρ_total)

println("trace of ρ:", real(inner(ρ_total,ρ_total)))

ρ_S = MPO(N)
@show inds(ρ_S)

for i in 2:2:2N 
    del = dense(delta(sites[i],sites_p[i]))
    temp = ρ_total[i] * del 
    ρ_S[i÷2] = ρ_total[i-1] * temp 
end 

println("trace of ρ_S:", real(inner(ρ_S,ρ_S))) 

The above code works with a random MPS. If \psi is obtained from a tdvp evolution ( \psi is normalized during the evolution), and I carry out the same process, the trace of the resulting reduced density matrix (ρ_S) is not 1.

The reason I have this awkward, alternating structure of system and ancilla sites is to prepare the Gibbs state of the system using the purification method.

I have also attached a sketch of what I actually would like the code to do. A couple of questions: (1) Is \rho_S in my code the correct reduced density matrix? (2) Could you help me understand what I’m missing here?

Thanks for your help in advance.

First a shameless plug for: GitHub - ryanlevy/ITensorEntropyTools.jl: Tools for calculating Entanglement Entropy w/ ITensors.jl
(outer is very expensive and not meant to be used except for testing small systems)

Second println("trace of ρ:", real(inner(ρ_total,ρ_total))) is not the trace of rho (it’s closer to the purity), and thus maybe thats where your normalization is getting messed up.

Instead do tr(ρ_S), which i find to be 1 when using something that isn’t a product state.

1 Like

Hi, Ryan. Thanks so much for looking over the code. It was silly of me to compute the purity instead of the trace. I believe the trace error is now fixed. Thanks also for directing me to the entropy tools GitHub. It was very useful. I lifted a piece of code from one of your modules.

I am still unsure if the reduced density matrix I obtain from using your code instead of outer(.,.) is the correct one. Maybe I am making a mistake that I can’t seem to spot? I have computed the average energy of S in two ways. (1) from the purified \psi_{\rm{pure}} as \rm{Tr} [\bra{\psi_{\rm{pure}}} H \ket{\psi_{\rm{pure}}}] where H acts only S's degrees of freedom and (2) from the reduced density matrix as \rm{Tr} [\rho_S H]. Both these energies are not equal, shouldn’t they be?
Here is a sample code, where H is the Hamiltonian for long-range TFIM.

N = 4                           # Number of spins                    
J = 1.0                          # Coupling strength
h = 1.0                          # Transverse field
alpha = 2.0                      #exponential decay strength
cutoff = 1e-10

#-----------------------------------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------------------

function get_local_bell_state(N::Int,sites)
    @assert iseven(length(sites))
    N = length(sites)
    ψ0 = MPS(sites)

    for j in 1:2:N
        s1 = sites[j]
        s2 = sites[j+1]

        t = ITensor(s1,s2)

        t[s1 => "Up", s2 => "Up",] = 1/sqrt(2)
        t[s1 => "Dn", s2 => "Dn",] = 1/sqrt(2)

        U,S,V = svd(t,s1)
        ψ0[j] = U*S
        ψ0[j+1] = V 
    end 

    return ψ0 
end 

function getH(N, J, h, alpha, sites)
    os = OpSum()
    
    #Interaction energy 
    for j in 1:2:2N-3
        for k in j+2:2:2N 
            dist = abs(j-k)
            coupling = -J / (dist^alpha) 
            os += coupling, "X", j, "X", k 
        end
    end
    
    #Transverse field 
    for j in 1:2:2N
        os += -h, "Z", j 
    end 

    H = MPO(os, sites);
    return H
end

function getH_sys_sites(N, J, h, alpha, sys_sites)

    os = OpSum()

    #Interaction energy 
    for j in 1:N-1
        for k in j+1:N 
            dist = abs(j-k)
            coupling = -J / (dist^alpha) 
            os += coupling, "X", j, "X", k 
        end
    end

    #Transverse field 
    for j in 1:N
        os += -h, "Z", j 
    end 

    H = MPO(os, sys_sites);
    return H
end

#----------------------------------------------------------------------------------
#----------------------------------------------------------------------------------

sites = siteinds("S=1/2", 2N; conserve_qns=false)
sys_sites = sites[1:2:2N]

ψ = get_local_bell_state(2N,sites)

H = getH(N, J, h, alpha, sites)

# TDVP parameters
nsteps = 5
δβ = 0.1
β = 2.0 
num_β_steps = Int(round(β/δβ))
β_0 = 0.0

sweeps = Sweeps(5)
maxdim!(sweeps, 64)
cutoff!(sweeps, 1e-10)

println("---------- TDVP Evolution ----------")

ψ1 = nothing

for bstep in 0:num_β_steps
    β_current = β_0 + bstep * δβ
    ψ1 = ψ  # reset or reuse initial state
    τ = -(β_current) / (2nsteps)

    for n in 1:nsteps
        ψ1 = tdvp(
            H,
            τ,
            ψ1;
            updater=exponentiate_updater,
            nsweeps=5,
            maxdim=64,
            cutoff=1e-10,
            normalize=true
        )
    end
end 

#Ryan's code to get the reduced density matrix of S
function density_matrix_sites(ψ_::AbstractMPS, region;)
  """
    Obtain a density matrix, leaving some indices uncontracted
    Assumes very little about tag structure, only site/linkinds
    Currently assume ordered and orthogonality center is within region
  """
  region = sort(region)
  start, stop = region[1], region[end]
  s = siteinds(ψ_)
  ψ = orthogonalize(ψ_, start)
  if length(region) == 1
    return ψ[start] * prime(dag(ψ[start]), s[start])
  end

  ψH = dag(prime(ψ, linkinds(ψ)..., s[region]...))
  lᵢ₁ = linkinds(ψ, start - 1) #this is n,n+1 link
  ρ = prime(ψ[start], lᵢ₁)
  ρ *= ψH[start]

  for k in (start + 1):(stop - 1) # replace this with ITensorNetworks iterator
    ρ *= ψ[k]
    ρ *= ψH[k]
  end

  lⱼ = linkinds(ψ, stop)
  ρ *= prime(ψ[stop], lⱼ)
  ρ *= ψH[stop]

  return ρ
end

ψ_pure= ψ1

E_pure_state = inner(ψ_pure',H,ψ_pure) 

sites_to_keep = collect(1:2:2N)
ρ_S_p = density_matrix_sites(ψ_pure, sites_to_keep)
ρ_S = MPO(ρ_S_p,sys_sites)

H_sys = getH_sys_sites(N,J,h,alpha,sys_sites)
E_rdm = inner(H_sys,ρ_S) 

println("energy from rdm:", E_rdm)
println("energy from pure state:", E_pure_state)
println("diff in energy:", E_rdm-E_pure_state)

Thanks again for your help!

I’m not entirely sure but I suspect (having asked a colleague) that you should look at your normalization (the vectorized identity should have a norm not equal to 1) and double check that the energy you’re looking at is the appropriate one and doesn’t have two copies of the density matrix (as you have \psi and \psi' which would be two copies of \rho, where Tr[\rho H] has 1 copy of \psi)

Thanks, Ryan! I appreciate your help. I’ll dissect my code and post an update when I have one.

1 Like

I have fixed the error in the code. The distance in the function getH_sys_sites should be
dist = 2 *abs(j-k) . This resolves the issue of different energies. Thanks again for all your help!

1 Like