Custom local Hilbert space with swap gates

Hello,

I have been googling a bit, but I did not find a definite answer to my question.
The “apply” function to my understanding takes care of the JW-string, as well as of swap gates/non-local operators.
But is this also true when using a custom local Hilbert space?

In my case I want to use:

using ITensors
import ITensors: op

function ITensors.space(::SiteType"Lindblad"; conserve_qns=false, spin)
if conserve_qns
return [QN((“N”, 0), (“Sz”, 0spin)) => 1, QN((“N”, 1), (“Sz”, 1spin)) => 1,
QN((“N”, 1), (“Sz”, 1spin)) => 1, QN((“N”, 2), (“Sz”, 2spin)) => 1]
end
return 4
end

op(::OpName"N", ::SiteType"Lindblad") = [
0 0 0 0
0 1 0 0
0 0 0 0
0 0 0 1
]

op(::OpName"N_til", ::SiteType"Lindblad") = [
0 0 0 0
0 0 0 0
0 0 1 0
0 0 0 1
]

op(::OpName"N_tot", ::SiteType"Lindblad") = [
0 0 0 0
0 1 0 0
0 0 1 0
0 0 0 2
]

op(::OpName"C", ::SiteType"Lindblad") = [
0 1 0 0
0 0 0 0
0 0 0 1
0 0 0 0
]

op(::OpName"Cdag", ::SiteType"Lindblad") = [
0 0 0 0
1 0 0 0
0 0 0 0
0 0 1 0
]

op(::OpName"C_til", ::SiteType"Lindblad") = [
0 0 1 0
0 0 0 -1
0 0 0 0
0 0 0 0
]

op(::OpName"Cdag_til", ::SiteType"Lindblad") = [
0 0 0 0
0 0 0 0
1 0 0 0
0 -1 0 0
]

op(::OpName"F", ::SiteType"Lindblad") = [
1 0 0 0
0 -1 0 0
0 0 -1 0
0 0 0 1
]

Where each site allows for a “normal” and a “tilde” particle (|00>, |01>, |10>, |11>). So when applying c^dag_i c_i+1 or c^tild^dag_i c^tild_i+1 does “apply” know how to take care of the JW string? And if i use a non-local operator does it know how to swap correctly?
Also related to this → As you may notice I added a sign when applying the tilde creation/annihilation operator whenever the “normal” particle is also present, because i have to pass through this operator, but also here I am not sure if it is taken into account automatically maybe?

Ok I think i actually figured it out and apparently the answer is i have to take care of the JW string myself.
Is there a way i can put this in my “Lindblad.jl” module (where i defined this “Lindblad” site as mentioned above)?
Also follow up → Do i understand correctly that i dont have to care about swap gates? So I have to take care of the signs i collect there, but something like
ψ = apply(op(s1, “C†”) * op(s2, “F”) * op(s3, “C”), ψ)
works fine without first swapping sites 1 & 2, such that 1 & 3 are nearest neighbours.
Or am I just making a mistake here?

You’re right that the apply function does take care of fermionic operators. But to designate operators as fermionic, you must overload the has_fermion_string method as done for example in this file:
https://github.com/ITensor/ITensors.jl/blob/main/src/physics/site_types/fermion.jl

Thanks for the fast answer!
I tried what you suggested but it didnt work for me.

Lindblad_site.jl

using ITensors
import ITensors: op

function ITensors.space(::SiteType"Lindblad"; conserve_qns=false, spin)
if conserve_qns
return [QN((“N”, 0), (“Sz”, 0spin)) => 1, QN((“N”, 1), (“Sz”, 1spin)) => 1,
QN((“N”, 1), (“Sz”, 1spin)) => 1, QN((“N”, 2), (“Sz”, 2spin)) => 1]
end
return 4
end

ITensors.state(::StateName"empt", ::SiteType"Lindblad") = [1.0, 0.0, 0.0, 0.0]
ITensors.state(::StateName"norm", ::SiteType"Lindblad") = [0.0, 1.0, 0.0, 0.0]
ITensors.state(::StateName"tild", ::SiteType"Lindblad") = [0.0, 0.0, 1.0, 0.0]
ITensors.state(::StateName"double", ::SiteType"Lindblad") = [0.0, 0.0, 0.0, 1.0]

op(::OpName"Id", ::SiteType"Lindblad") = [
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
]

op(::OpName"N", ::SiteType"Lindblad") = [
0 0 0 0
0 1 0 0
0 0 0 0
0 0 0 1
]

op(::OpName"N_til", ::SiteType"Lindblad") = [
0 0 0 0
0 0 0 0
0 0 1 0
0 0 0 1
]

op(::OpName"N_tot", ::SiteType"Lindblad") = [
0 0 0 0
0 1 0 0
0 0 1 0
0 0 0 2
]

op(::OpName"C", ::SiteType"Lindblad") = [
0 1 0 0
0 0 0 0
0 0 0 1
0 0 0 0
]

op(::OpName"C†“, ::SiteType"Lindblad”) = [
0 0 0 0
1 0 0 0
0 0 0 0
0 0 1 0
]

op(::OpName"C_til", ::SiteType"Lindblad") = [
0 0 1 0
0 0 0 -1
0 0 0 0
0 0 0 0
]

op(::OpName"C†_til", ::SiteType"Lindblad") = [
0 0 0 0
0 0 0 0
1 0 0 0
0 -1 0 0
]

op(::OpName"F", ::SiteType"Lindblad") = [
1 0 0 0
0 -1 0 0
0 0 -1 0
0 0 0 1
]

ITensors.has_fermion_string(::OpName"C", ::SiteType"Lindblad") = true
ITensors.has_fermion_string(::OpName"C_til", ::SiteType"Lindblad") = true
ITensors.has_fermion_string(::OpName"C†“, ::SiteType"Lindblad”) = true
ITensors.has_fermion_string(::OpName"C†_til", ::SiteType"Lindblad") = true

test.jl

import LinearAlgebra

include(“./Lindblad_site.jl”)

N = 3

N_up = N

N_down = N

inds_up = siteinds(“Lindblad”, N_up; conserve_qns=true, spin = 1)

inds_down = siteinds(“Lindblad”, N_down; conserve_qns=true, spin = -1)

inds = vcat(inds_down, inds_up)

states = [“empt” for _ in 1:2*N]

states[1] = “norm”

states[3] = “norm”

ψ = MPS(ComplexF64, inds, states)

s1 = siteind(ψ,1)

s2 = siteind(ψ,2)

s3 = siteind(ψ,3)

ψ = apply(op(s3, “C”), ψ)

el = [2, 1, 1, 1, 1, 1]

V = ITensor(1.0)

for j=1:2*N

global V *= (ψ[j] * dag(state(inds[j], el[j])))

end

v = scalar(V)

In the “test.jl” i start from an MPS with a fermion on site one and on site three.
When annihilating the one on site three i should get a sign. (If the order of operators were swapped i should get it if i annihilate the one on the first site but still i dont get a sign)

Ok I am somewhat confused about the apply function now.

N = 3

inds_up = siteinds("Fermion", N)

states = ["Emp" for _ in 1:N]

states[2] = "Occ"

ψ = MPS(ComplexF64, inds_up, states)

s1 = siteind(ψ,1)

s2 = siteind(ψ,2)

s3 = siteind(ψ,3)

ops = op(s3, "Cdag")

ψ = apply(ops, ψ)

el = [1, 2, 2]

V = ITensor(1.0)

for j=1:N

global V *= (ψ[j] * dag(state(inds_up[j], el[j])))

end

v = scalar(V)

This i thought should give me “-1.0”, because there is a particle at site 2 and to apply C^dag_3 i have to get it past C^dag_2, but i get “1.0” instead. What am i misunderstanding here? Because the apply function should take care of this phase right?