using Test
using StaticArrays: SVector
using QuantumLattices.Essentials.QuantumSystems
using QuantumLattices.Essentials.QuantumSystems: heisenbergxyz, heisenbergpmz, gamma, dm
using QuantumLattices.Essentials.QuantumOperators: ID
using QuantumLattices.Essentials.Spatials: AbstractPID, PID, CPID, Point, Bond, rcoord, azimuthd
using QuantumLattices.Essentials.DegreesOfFreedom: Index, CompositeOID, OID, Hilbert, Operator, Operators, statistics, script, latexname, ishermitian
using QuantumLattices.Essentials.DegreesOfFreedom: IIDSpace, Coupling, Couplings, abbr, @subscript_str, @couplings, wildcard
using QuantumLattices.Interfaces: ⊗, ⋅, expand, permute, rank
using QuantumLattices.Prerequisites.Combinatorics: Permutations
using QuantumLattices.Prerequisites.VectorSpaces: shape, ndimshape

@testset "FID" begin
    fid = FID{:f}(orbital=1, spin=1)
    @test string(fid) == "FID{:f}(1, 1, 1)"
    @test statistics(fid) == statistics(typeof(fid)) == :f
    @test fid' == replace(fid, nambu=2)
    @test fid'' == replace(fid, nambu=1)

    fid = FID{:b}(1, 1, 0)
    @test string(fid) == "FID{:b}(1, 1, 0)"
    @test statistics(fid) == statistics(typeof(fid)) == :b
    @test fid' == fid
end

@testset "Fock" begin
    fock = Fock{:b}(norbital=1, nspin=2, nnambu=2)
    @test shape(fock) == (1:1, 1:2, 1:2)
    @test ndimshape(fock) == ndimshape(typeof(fock)) == 3
    @test CartesianIndex(FID{:b}(1, 1, 1), fock) == CartesianIndex(1, 1, 1)
    @test FID(CartesianIndex(1, 1, 1), fock) == FID{:b}(1, 1, 1)
    @test collect(fock) == [FID{:b}(1, 1, 1), FID{:b}(1, 2, 1), FID{:b}(1, 1, 2), FID{:b}(1, 2, 2)]
    @test statistics(fock) == statistics(typeof(fock)) == :b
    @test string(fock) == "Fock{:b}(norbital=1, nspin=2, nnambu=2)"

    @test summary(Fock{:b}(nspin=0, nnambu=1)) == "0-element Fock{:b}"
    @test summary(Fock{:b}(nspin=1, nnambu=1)) == "1-element Fock{:b}"
    @test summary(Fock{:f}(nspin=2, nnambu=1)) == "2-element Fock{:f}"

    @test match(FID{wildcard}, Fock{:f}) == match(FID{wildcard}, Fock{:b}) == true
    @test match(FID{:f}, Fock{:f}) == match(FID{:b}, Fock{:b}) == true
    @test match(FID{:b}, Fock{:f}) == match(FID{:f}, Fock{:b}) == false
end

@testset "latex" begin
    @test script(Val(:site), Index(PID(1), FID{:f}(2, 1, 1))) == 1
    @test script(Val(:orbital), Index(PID(1), FID{:f}(2, 1, 1))) == 2
    @test script(Val(:spint), Index(PID(1), FID{:f}(2, 3, 1))) == 3
    @test script(Val(:spsym), Index(PID(1), FID{:f}(2, 2, 1))) == "↑"
    @test script(Val(:spsym), Index(PID(1), FID{:f}(2, 1, 1))) == "↓"
    @test script(Val(:nambu), Index(PID(1), FID{:f}(2, 3, 1))) == ""
    @test script(Val(:nambu), Index(PID(1), FID{:f}(2, 3, 2))) == "\\dagger"

    @test latexname(Index{<:AbstractPID, <:FID{:f}}) == Symbol("Index{AbstractPID, FID{:f}}")
    @test latexname(CompositeOID{Index{<:AbstractPID, <:FID{:f}}}) == Symbol("CompositeOID{Index{AbstractPID, FID{:f}}}")
    @test latexname(Index{<:AbstractPID, <:FID{:b}}) == Symbol("Index{AbstractPID, FID{:b}}")
    @test latexname(CompositeOID{Index{<:AbstractPID, <:FID{:b}}}) == Symbol("CompositeOID{Index{AbstractPID, FID{:b}}}")
end

@testset "angle" begin
    @test angle(OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.0, 0.0], [1.0, 2.0]), [[1.0, 0.0], [0.0, 1.0]], [0.1, 0.0]) ≈ 2pi*0.1
    @test angle(OID(Index(CPID(1, 1), FID{:f}(1, 1, 2)), [0.0, 0.0], [1.0, 2.0]), [[1.0, 0.0], [0.0, 1.0]], [0.0, 0.2]) ≈ -2pi*0.4
end

@testset "FockOperator" begin
    id₁ = OID(Index(PID(2), FID{:f}(1, 1, 2)), SVector(0.5, 0.0), SVector(0.0, 0.0))
    id₂ = OID(Index(PID(2), FID{:f}(1, 1, 1)), SVector(0.5, 0.0), SVector(0.0, 0.0))
    id₃ = OID(Index(PID(1), FID{:f}(1, 2, 2)), SVector(0.0, 0.0), SVector(0.0, 0.0))
    id₄ = OID(Index(PID(1), FID{:f}(1, 2, 1)), SVector(0.0, 0.0), SVector(0.0, 0.0))

    opt = Operator(1.0, ID(id₁, id₂))
    @test opt|>isnormalordered

    opt = Operator(1.0, ID(id₁, id₂, id₃, id₄))
    @test opt|>isnormalordered == false
    @test repr(opt) == "c^{\\dagger}_{2, 1, ↓}c^{}_{2, 1, ↓}c^{\\dagger}_{1, 1, ↑}c^{}_{1, 1, ↑}"

    op₁ = Operator(1.5, ID(id₁, id₂))
    op₂ = Operator(2.0, ID(id₂, id₁))
    @test op₁*op₂ == nothing

    op₁ = Operator(1.5, ID(id₁, id₂))
    op₂ = Operator(2.0, ID(id₁, id₂))
    @test op₁*op₂ == Operator(3.0, ID(id₁, id₂, id₁, id₂))

    @test permute(id₁, id₂) == (Operator(1), Operator(-1, ID(id₂, id₁)))
    @test permute(id₂, id₁) == (Operator(1), Operator(-1, ID(id₁, id₂)))
    @test permute(id₁, id₄) == (Operator(-1, ID(id₄, id₁)),)
    @test permute(id₄, id₁) == (Operator(-1, ID(id₁, id₄)),)

    id₁ = OID(Index(PID(2), FID{:b}(1, 1, 2)), SVector(0.5, 0.0), SVector(0.0, 0.0))
    id₂ = OID(Index(PID(2), FID{:b}(1, 1, 1)), SVector(0.5, 0.0), SVector(0.0, 0.0))
    id₃ = OID(Index(PID(1), FID{:b}(1, 2, 2)), SVector(0.0, 0.0), SVector(0.0, 0.0))
    id₄ = OID(Index(PID(1), FID{:b}(1, 2, 1)), SVector(0.0, 0.0), SVector(0.0, 0.0))

    opt = Operator(1.0, ID(id₁, id₂))
    @test repr(opt) == "b^{\\dagger}_{2, 1, ↓}b^{}_{2, 1, ↓}"

    @test permute(id₁, id₂) == (Operator(+1), Operator(1, ID(id₂, id₁)))
    @test permute(id₂, id₁) == (Operator(-1), Operator(1, ID(id₁, id₂)))
    @test permute(id₁, id₄) == (Operator(1, ID(id₄, id₁)),)
    @test permute(id₄, id₁) == (Operator(1, ID(id₁, id₄)),)
end

@testset "FockCoupling" begin
    @test string(FockCoupling{2}(1.0)) == "FockCoupling{2}(value=1.0)"
    @test string(FockCoupling{2}(1.0, spins=(1, 2))) == "FockCoupling{2}(value=1.0, spins=[1 2])"
    @test repr(FockCoupling{2}(2.0)) == "2.0 {2}"

    fc₁ = FockCoupling{2}(1.5, spins=subscript"[x 1]")
    fc₂ = FockCoupling{2}(2.0, orbitals=subscript"[x y](x < y)")
    @test repr(fc₁) == "1.5 sp[x 1]"
    @test repr(fc₂) == "2.0 ob[x y](x < y)"
    fc = fc₁ * fc₂
    @test repr(fc) == "3.0 ob[* *]×[x y](x < y) ⊗ sp[x 1]×[* *]"

    fc₁ = FockCoupling{2}(1.5, spins=subscript"[x 1]")
    fc₂ = FockCoupling{2}(2.0, orbitals=subscript"[x y](x < y)")
    fc = fc₁ ⊗ fc₂
    @test repr(fc) == "3.0 ob[x y](x < y) ⊗ sp[x 1]"

    fc₁ = FockCoupling{2}(1.5, spins=(2, 1))
    fc₂ = FockCoupling{2}(2.0, spins=(1, 2))
    fc = fc₁ ⋅ fc₂
    @test repr(fc) == "3.0 sp[2 2]"

    fc = FockCoupling{2}(2.0, orbitals=(1, 2), nambus=(2, 1))
    bond = Bond(1, Point(CPID(1, 2), SVector(0.5), SVector(0.0)), Point(CPID(1, 1), SVector(0.0), SVector(0.0)))
    hilbert = Hilbert(pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2) for pid in [bond.epoint.pid, bond.spoint.pid])
    ex = expand(fc, bond, hilbert, Val(:Hopping))
    @test collect(ex) == [
        (2.0, ID(OID(Index(CPID(1, 1), FID{:f}(1, 1, 2)), SVector(0.0), SVector(0.0)), OID(Index(CPID(1, 2), FID{:f}(2, 1, 1)), SVector(0.5), SVector(0.0)))),
        (2.0, ID(OID(Index(CPID(1, 1), FID{:f}(1, 2, 2)), SVector(0.0), SVector(0.0)), OID(Index(CPID(1, 2), FID{:f}(2, 2, 1)), SVector(0.5), SVector(0.0))))
    ]

    fc = FockCoupling{4}(2.0, spins=(2, 2, 1, 1), nambus=(2, 1, 2, 1))
    point = Point(PID(1), SVector(0.0), SVector(0.0))
    hilbert = Hilbert(point.pid=>Fock{:b}(norbital=2, nspin=2, nnambu=2))
    ex = expand(fc, point, hilbert, Val(:info))
    @test collect(ex) == [
        (2.0, ID(OID(Index(PID(1), FID{:b}(1, 2, 2)), SVector(0.0), SVector(0.0)),
                 OID(Index(PID(1), FID{:b}(1, 2, 1)), SVector(0.0), SVector(0.0)),
                 OID(Index(PID(1), FID{:b}(1, 1, 2)), SVector(0.0), SVector(0.0)),
                 OID(Index(PID(1), FID{:b}(1, 1, 1)), SVector(0.0), SVector(0.0))
                 )),
        (2.0, ID(OID(Index(PID(1), FID{:b}(2, 2, 2)), SVector(0.0), SVector(0.0)),
                 OID(Index(PID(1), FID{:b}(2, 2, 1)), SVector(0.0), SVector(0.0)),
                 OID(Index(PID(1), FID{:b}(2, 1, 2)), SVector(0.0), SVector(0.0)),
                 OID(Index(PID(1), FID{:b}(2, 1, 1)), SVector(0.0), SVector(0.0))
                 ))
    ]

    fc = FockCoupling{4}(2.0, orbitals=subscript"[α α β β](α < β)", spins=(2, 1, 1, 2), nambus=(2, 2, 1, 1))
    point = Point(PID(1), SVector(0.5), SVector(0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=3, nspin=2, nnambu=2))
    ex = expand(fc, point, hilbert, Val(:info))
    @test collect(ex) == [
        (2.0, ID(OID(Index(PID(1), FID{:f}(1, 2, 2)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(1, 1, 2)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(2, 1, 1)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(2, 2, 1)), SVector(0.5), SVector(0.0))
                 )),
        (2.0, ID(OID(Index(PID(1), FID{:f}(1, 2, 2)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(1, 1, 2)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(3, 1, 1)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(3, 2, 1)), SVector(0.5), SVector(0.0))
                 )),
        (2.0, ID(OID(Index(PID(1), FID{:f}(2, 2, 2)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(2, 1, 2)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(3, 1, 1)), SVector(0.5), SVector(0.0)),
                 OID(Index(PID(1), FID{:f}(3, 2, 1)), SVector(0.5), SVector(0.0))
                 ))
    ]

    fc₁ = FockCoupling{2}(+1.0, spins=(2, 2), nambus=(2, 1))
    fc₂ = FockCoupling{2}(-1.0, spins=(1, 1), nambus=(2, 1))
    point = Point(CPID(1, 1), SVector(0.0), SVector(0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))
    ex = expand(fc₁*fc₂, point, hilbert, Val(:info))
    @test collect(ex) == [
        (-1.0, ID(OID(Index(CPID(1, 1), FID{:f}(1, 2, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(1, 2, 1)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(1, 1, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(1, 1, 1)), SVector(0.0), SVector(0.0))
                  )),
        (-1.0, ID(OID(Index(CPID(1, 1), FID{:f}(2, 2, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(2, 2, 1)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(1, 1, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(1, 1, 1)), SVector(0.0), SVector(0.0))
                  )),
        (-1.0, ID(OID(Index(CPID(1, 1), FID{:f}(1, 2, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(1, 2, 1)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(2, 1, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(2, 1, 1)), SVector(0.0), SVector(0.0))
                  )),
        (-1.0, ID(OID(Index(CPID(1, 1), FID{:f}(2, 2, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(2, 2, 1)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(2, 1, 2)), SVector(0.0), SVector(0.0)),
                  OID(Index(CPID(1, 1), FID{:f}(2, 1, 1)), SVector(0.0), SVector(0.0))
                  ))
    ]
end

@testset "σ⁰" begin
    @test σ⁰"sp" == FockCoupling{2}(1, spins=(1, 1)) + FockCoupling{2}(1, spins=(2, 2))
    @test σ⁰"ob" == FockCoupling{2}(1, orbitals=(1, 1)) + FockCoupling{2}(1, orbitals=(2, 2))
    @test σ⁰"ph" == FockCoupling{2}(1, nambus=(1, 2)) + FockCoupling{2}(1, nambus=(2, 1))
end

@testset "σˣ" begin
    @test σˣ"sp" == FockCoupling{2}(1, spins=(1, 2)) + FockCoupling{2}(1, spins=(2, 1))
    @test σˣ"ob" == FockCoupling{2}(1, orbitals=(1, 2)) + FockCoupling{2}(1, orbitals=(2, 1))
    @test σˣ"ph" == FockCoupling{2}(1, nambus=(1, 1)) + FockCoupling{2}(1, nambus=(2, 2))
end

@testset "σʸ" begin
    @test σʸ"sp" == FockCoupling{2}(1im, spins=(1, 2)) + FockCoupling{2}(-1im, spins=(2, 1))
    @test σʸ"ob" == FockCoupling{2}(1im, orbitals=(1, 2)) + FockCoupling{2}(-1im, orbitals=(2, 1))
    @test σʸ"ph" == FockCoupling{2}(1im, nambus=(1, 1)) + FockCoupling{2}(-1im, nambus=(2, 2))
end

@testset "σᶻ" begin
    @test σᶻ"sp" == FockCoupling{2}(-1, spins=(1, 1)) + FockCoupling{2}(1, spins=(2, 2))
    @test σᶻ"ob" == FockCoupling{2}(-1, orbitals=(1, 1)) + FockCoupling{2}(1, orbitals=(2, 2))
    @test σᶻ"ph" == FockCoupling{2}(-1, nambus=(1, 2)) + FockCoupling{2}(1, nambus=(2, 1))
end

@testset "σ⁺" begin
    @test σ⁺"sp" == Couplings(FockCoupling{2}(1, spins=(2, 1)))
    @test σ⁺"ob" == Couplings(FockCoupling{2}(1, orbitals=(2, 1)))
    @test σ⁺"ph" == Couplings(FockCoupling{2}(1, nambus=(2, 2)))
end

@testset "σ⁻" begin
    @test σ⁻"sp" == Couplings(FockCoupling{2}(1, spins=(1, 2)))
    @test σ⁻"ob" == Couplings(FockCoupling{2}(1, orbitals=(1, 2)))
    @test σ⁻"ph" == Couplings(FockCoupling{2}(1, nambus=(1, 1)))
end

@testset "fockcoupling" begin
    fc = fc"1.0 ob[α α β β](α < β) ⊗ sp[σ γ σ γ](σ ≠ γ) ⊗ ph[2 1 2 1]"
    @test repr(fc) == "1.0 ob[α α β β](α < β) ⊗ sp[σ γ σ γ](σ ≠ γ) ⊗ ph[2 1 2 1]"

    fc = fc"1.0 ob[α α β β] ⊗ sp[σ γ σ γ] ⊗ ph[2 1 2 1]"
    @test repr(fc) == "1.0 ob[α α β β] ⊗ sp[σ γ σ γ] ⊗ ph[2 1 2 1]"

    fc = fc"1.0 ob[α α β β](α < β) ⊗ sp[σ γ σ γ] ⊗ ph[2 1 2 1]"
    @test repr(fc) == "1.0 ob[α α β β](α < β) ⊗ sp[σ γ σ γ] ⊗ ph[2 1 2 1]"

    fc = fc"1.0 ob[α α β β](α < β) ⊗ ph[2 1 2 1]"
    @test repr(fc) == "1.0 ob[α α β β](α < β) ⊗ ph[2 1 2 1]"

    fc = fc"1.0 ob[α α β β](α < β) ⊗ sp[2 1 2 1]"
    @test repr(fc) == "1.0 ob[α α β β](α < β) ⊗ sp[2 1 2 1]"

    fc = fc"1.0 ob[1 1 1 1] ⊗ ph[2 1 2 1]"
    @test repr(fc) == "1.0 ob[1 1 1 1] ⊗ ph[2 1 2 1]"

    fc = fc"1.0 ph[2 1 2 1]"
    @test repr(fc) == "1.0 ph[2 1 2 1]"

    fc = fc"1.0im {2}"
    @test repr(fc) == "1.0im {2}"
end

@testset "Onsite" begin
    @test abbr(Onsite) == :st
    @test isnothing(ishermitian(Onsite))

    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))

    term = Onsite(:mu, 1.5, couplings=σˣ"sp"⊗σᶻ"ob", modulate=true)
    operators = Operators(
        Operator(+1.5, ID(OID(Index(PID(1), FID{:f}(2, 1, 2)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-1.5, ID(OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])))
    )
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators+operators'

    term = Onsite(:mu, 1.5, couplings=σᶻ"sp"⊗σᶻ"ob", modulate=true)
    operators = Operators(
        Operator(-0.75, ID(OID(Index(PID(1), FID{:f}(2, 1, 2)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(2, 1, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-0.75, ID(OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(+0.75, ID(OID(Index(PID(1), FID{:f}(2, 2, 2)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(+0.75, ID(OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0])))
    )
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators+operators'
end

@testset "Hopping" begin
    bond = Bond(1, Point(CPID('a', 1), (0.5, 0.5), (0.0, 0.0)), Point(CPID('b', 2), (0.0, 0.0), (0.0, 0.0)))
    hilbert = Hilbert(pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2) for pid in [bond.spoint.pid, bond.epoint.pid])
    term = Hopping(:t, 1.5, 1)
    operators = Operators(
        Operator(1.5, ID(OID(Index(CPID('b', 2), FID{:f}(2, 2, 2)), [0.0, 0.0], [0.0, 0.0]), OID(Index(CPID('a', 1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(1.5, ID(OID(Index(CPID('b', 2), FID{:f}(2, 1, 2)), [0.0, 0.0], [0.0, 0.0]), OID(Index(CPID('a', 1), FID{:f}(2, 1, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(1.5, ID(OID(Index(CPID('b', 2), FID{:f}(1, 1, 2)), [0.0, 0.0], [0.0, 0.0]), OID(Index(CPID('a', 1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(1.5, ID(OID(Index(CPID('b', 2), FID{:f}(1, 2, 2)), [0.0, 0.0], [0.0, 0.0]), OID(Index(CPID('a', 1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])))
    )
    @test term|>abbr == term|>typeof|>abbr == :hp
    @test term|>ishermitian == term|>typeof|>ishermitian == false
    @test expand(term, bond, hilbert, half=true) == operators
    @test expand(term, bond, hilbert, half=false) == operators+operators'
end

@testset "Pairing" begin
    bond = Bond(1, Point(PID(1), (0.5, 0.5), (0.0, 0.0)), Point(PID(2), (0.0, 0.0), (0.0, 0.0)))
    hilbert = Hilbert(pid=>Fock{:f}(norbital=1, nspin=2, nnambu=2) for pid in [bond.spoint.pid, bond.epoint.pid])
    term = Pairing(:Δ, 1.5, 1, couplings=@couplings(FockCoupling{2}(spins=(2, 2))), amplitude=bond->(bond|>rcoord|>azimuthd ≈ 45 ? 1 : -1))
    operators = Operators(
        Operator(-1.5, ID(OID(Index(PID(2), FID{:f}(1, 2, 1)), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(+1.5, ID(OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), FID{:f}(1, 2, 1)), [0.0, 0.0], [0.0, 0.0])))
    )
    @test term|>abbr == term|>typeof|>abbr == :pr
    @test term|>ishermitian == term|>typeof|>ishermitian == false
    @test expand(term, bond, hilbert, half=true) == operators
    @test expand(term, bond, hilbert, half=false) == operators+operators'

    point = Point(CPID('a', 1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=1, nspin=2, nnambu=2))
    term = Pairing(:Δ, 1.5, 0, couplings=FockCoupling{2}(spins=(2, 1))-FockCoupling{2}(spins=(1, 2)))
    operators = Operators(
        Operator(+1.5, ID(OID(Index(CPID('a', 1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-1.5, ID(OID(Index(CPID('a', 1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])))
    )
    @test term|>abbr == term|>typeof|>abbr == :pr
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators+operators'
end

@testset "Hubbard" begin
    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))
    term = Hubbard(:H, 2.5)
    operators = Operators(
        Operator(1.25, ID(
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(1.25, ID(
            OID(Index(PID(1), FID{:f}(2, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :hb
    @test term|>ishermitian == term|>typeof|>ishermitian == true
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators*2
end

@testset "InterOrbitalInterSpin" begin
    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))
    term = InterOrbitalInterSpin(:H, 2.5)
    operators = Operators(
        Operator(1.25, ID(
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(1.25, ID(
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :nons
    @test term|>ishermitian == term|>typeof|>ishermitian == true
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators*2
end

@testset "InterOrbitalIntraSpin" begin
    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))
    term = InterOrbitalIntraSpin(:H, 2.5)
    operators = Operators(
        Operator(1.25, ID(
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(1.25, ID(
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :noes
    @test term|>ishermitian == term|>typeof|>ishermitian == true
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators*2
end

@testset "SpinFlip" begin
    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))
    term = SpinFlip(:H, 2.5)
    operators = Operators(
        Operator(2.5, ID(
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :sf
    @test term|>ishermitian == term|>typeof|>ishermitian == false
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators+operators'
end

@testset "PairHopping" begin
    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Fock{:f}(norbital=2, nspin=2, nnambu=2))
    term = PairHopping(:H, 2.5)
    operators = Operators(
        Operator(2.5, ID(
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 1, 1)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(2, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :ph
    @test term|>ishermitian == term|>typeof|>ishermitian == false
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators+operators'
end

@testset "Coulomb" begin
    bond = Bond(1, Point(PID(1), (0.5, 0.5), (0.0, 0.0)), Point(PID(2), (0.0, 0.0), (0.0, 0.0)))
    hilbert = Hilbert(pid=>Fock{:f}(norbital=1, nspin=2, nnambu=2) for pid in [bond.spoint.pid, bond.epoint.pid])

    term = Coulomb(:V, 2.5, 1, couplings=σᶻ"sp"*σᶻ"sp")
    operators = Operators(
        Operator(-1.25, ID(
            OID(Index(PID(2), FID{:f}(1, 1, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 1, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(+1.25, ID(
            OID(Index(PID(2), FID{:f}(1, 1, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 1, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(-1.25, ID(
            OID(Index(PID(2), FID{:f}(1, 2, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 2, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(+1.25, ID(
            OID(Index(PID(2), FID{:f}(1, 2, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 2, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :cl
    @test isnothing(term|>ishermitian) && isnothing(term|>typeof|>ishermitian)
    @test expand(term, bond, hilbert, half=true) == operators
    @test expand(term, bond, hilbert, half=false) == operators+operators'

    term = Coulomb(:V, 2.5, 1, couplings=σˣ"sp"*σᶻ"sp")
    operators = Operators(
        Operator(-2.5, ID(
            OID(Index(PID(2), FID{:f}(1, 2, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 1, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(+2.5, ID(
            OID(Index(PID(2), FID{:f}(1, 1, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 2, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(+2.5, ID(
            OID(Index(PID(2), FID{:f}(1, 2, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 1, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 2, 1)), [0.5, 0.5], [0.0, 0.0])
            )),
        Operator(-2.5, ID(
            OID(Index(PID(2), FID{:f}(1, 1, 2)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(2), FID{:f}(1, 2, 1)), [0.0, 0.0], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 2)), [0.5, 0.5], [0.0, 0.0]),
            OID(Index(PID(1), FID{:f}(1, 1, 1)), [0.5, 0.5], [0.0, 0.0])
            ))
    )
    @test term|>abbr == term|>typeof|>abbr == :cl
    @test expand(term, bond, hilbert, half=true) == operators
    @test expand(term, bond, hilbert, half=false) == operators+operators'
end

@testset "SID" begin
    @test SID{1//2}('z', orbital=1)' == SID{1//2}('z', orbital=1)
    @test SID{1//2}('x', orbital=1)' == SID{1//2}('x', orbital=1)
    @test SID{1//2}('y', orbital=1)' == SID{1//2}('y', orbital=1)
    @test SID{1//2}('+', orbital=1)' == SID{1//2}('-', orbital=1)
    @test SID{1//2}('-', orbital=1)' == SID{1//2}('+', orbital=1)

    sid = SID{3//2}('x')
    @test string(sid) == "SID{3//2}(1, 'x')"
    @test replace(sid, tag='z') == SID{3//2}('z')
    @test totalspin(sid) == totalspin(typeof(sid)) == 3//2
    @test statistics(sid) == statistics(typeof(sid)) == :b
end

@testset "Matrix" begin
    @test isapprox(Matrix(SID{1//2}(1, 'z')), [[-0.5, 0.0] [0.0, 0.5]])
    @test isapprox(Matrix(SID{1//2}(1, 'x')), [[0.0, 0.5] [0.5, 0.0]])
    @test isapprox(Matrix(SID{1//2}(1, 'y')), [[0.0, -0.5im] [0.5im, 0.0]])
    @test isapprox(Matrix(SID{1//2}(1, '+')), [[0.0, 1.0] [0.0, 0.0]])
    @test isapprox(Matrix(SID{1//2}(1, '-')), [[0.0, 0.0] [1.0, 0.0]])

    @test isapprox(Matrix(SID{1}(1, 'z')), [[-1.0, 0.0, 0.0] [0.0, 0.0, 0.0] [0.0, 0.0, 1.0]])
    @test isapprox(Matrix(SID{1}(1, 'x')), [[0.0, √2/2, 0.0] [√2/2, 0.0, √2/2] [0.0, √2/2, 0.0]])
    @test isapprox(Matrix(SID{1}(1, 'y')), [[0.0, -√2im/2, 0.0] [√2im/2, 0.0, -√2im/2] [0.0, √2im/2, 0.0]])
    @test isapprox(Matrix(SID{1}(1, '+')), [[0.0, √2, 0.0] [0.0, 0.0, √2] [0.0, 0.0, 0.0]])
    @test isapprox(Matrix(SID{1}(1, '-')), [[0.0, 0.0, 0.0] [√2, 0.0, 0.0] [0.0, √2, 0.0]])
end

@testset "Spin" begin
    spin = Spin{1}(norbital=2)
    @test shape(spin) == (1:2, 1:5)
    @test ndimshape(spin) == ndimshape(typeof(spin)) == 2
    @test CartesianIndex(SID{1}(1, 'z'), spin) == CartesianIndex(1, 3)
    @test SID(CartesianIndex(1, 1), spin) == SID{1}(1, 'x')
    @test summary(spin) == "10-element Spin{1}"
    @test string(spin) == "Spin{1}(norbital=2)"
    @test totalspin(spin) == totalspin(typeof(spin)) == 1
    @test collect(spin) == [
        SID{1}(1, 'x'), SID{1}(2, 'x'), SID{1}(1, 'y'), SID{1}(2, 'y'), SID{1}(1, 'z'), SID{1}(2, 'z'),
        SID{1}(1, '+'), SID{1}(2, '+'), SID{1}(1, '-'), SID{1}(2, '-')
    ]
    @test shape(IIDSpace(SID{1//2}(:a, 'x'), Spin{1//2}(2))) == (1:2, 1:1)
    @test shape(IIDSpace(SID{1//2}(2, 'y'), Spin{1//2}(2))) == (2:2, 2:2)
    @test shape(IIDSpace(SID{1//2}(:a, wildcard), Spin{1//2}(2))) == (1:2, 1:5)
    @test shape(IIDSpace(SID{1//2}(2, wildcard), Spin{1//2}(2))) == (2:2, 1:5)

    @test match(SID{wildcard}, Spin{1//2}) == true
    @test match(SID{1//2}, Spin{1//2}) == true
    @test match(SID{1//2}, Spin{1}) == match(SID{1}, Spin{1//2}) == false
end

@testset "latex" begin
    index = Index(PID(1), SID{1//2}(2, 'z'))
    @test script(Val(:site), index) == 1
    @test script(Val(:orbital), index) == 2
    @test script(Val(:tag), index) == 'z'

    @test latexname(Index{<:AbstractPID, <:SID}) == Symbol("Index{AbstractPID, SID}")
    @test latexname(CompositeOID{<:Index{<:AbstractPID, <:SID}}) == Symbol("CompositeOID{Index{AbstractPID, SID}}")
end

@testset "SpinOperator" begin
    opt = Operator(1.0, ID(
        OID(Index(PID(1), SID{1//2}('+')), [0.0, 0.0], [0.0, 0.0]),
        OID(Index(PID(1), SID{1//2}('-')), [0.0, 0.0], [0.0, 0.0])
        ))
    @test opt' == Operator(1.0, ID(
        OID(Index(PID(1), SID{1//2}('+')), [0.0, 0.0], [0.0, 0.0]),
        OID(Index(PID(1), SID{1//2}('-')), [0.0, 0.0], [0.0, 0.0])
        ))
    @test ishermitian(opt)
    @test repr(opt) == "S^{+}_{1}S^{-}_{1}"
end

@testset "permute" begin
    soptrep(opt::Operator) = opt.value * prod([Matrix(opt.id[i].index.iid) for i = 1:rank(opt)])
    for S in (1//2, 1, 3//2)
        oids = [OID(Index(PID(1), SID{S}(2, tag)), [0.0, 0.0], [0.0, 0.0]) for tag in ('x', 'y', 'z', '+', '-')]
        for (id₁, id₂) in Permutations{2}(oids)
            left = soptrep(Operator(1, ID(id₁, id₂)))
            right = sum([soptrep(opt) for opt in permute(id₁, id₂)])
            @test isapprox(left, right)
        end
    end
    id₁ = OID(Index(PID(1), SID{1//2}(2, 'z')), [0.0, 0.0], [0.0, 0.0])
    id₂ = OID(Index(PID(2), SID{1//2}(2, 'z')), [0.0, 0.0], [0.0, 0.0])
    @test permute(id₁, id₂) == (Operator(1, ID(id₂, id₁)),)
end

@testset "SpinCoupling" begin
    @test string(SpinCoupling(1.0, ('+', '-'))) == "SpinCoupling(value=1.0, tags=S⁺S⁻)"
    @test string(SpinCoupling(1.0, ('z', 'z'))) == "SpinCoupling(value=1.0, tags=SᶻSᶻ)"
    @test string(SpinCoupling(1.0, ('-', '+'), orbitals=(1, 2))) == "SpinCoupling(value=1.0, tags=S⁻S⁺, orbitals=[1 2])"
    @test repr(SpinCoupling(2.0, ('x', 'y'))) == "2.0 SˣSʸ"

    sc₁ = SpinCoupling(1.5, ('+', '-'), orbitals=subscript"[α β](α > β)")
    sc₂ = SpinCoupling(2.0, ('+', '-'), orbitals=subscript"[α β](α < β)")
    @test repr(sc₁) == "1.5 S⁺S⁻ ob[α β](α > β)"
    @test repr(sc₂) == "2.0 S⁺S⁻ ob[α β](α < β)"

    sc = sc₁ * sc₂
    @test repr(sc) == "3.0 S⁺S⁻S⁺S⁻ ob[α β](α > β)×[α β](α < β)"

    sc = SpinCoupling(2.0, ('+', '-'), orbitals=(1, 2))
    bond = Bond(1, Point(CPID(1, 2), [0.5], [0.0]), Point(CPID(1, 1), [0.0], [0.0]))
    hilbert = Hilbert(pid=>Spin{1}(norbital=2) for pid in [bond.epoint.pid, bond.spoint.pid])
    ex = expand(sc, bond, hilbert, Val(:SpinTerm))
    @test collect(ex) == [(2.0, ID(
        OID(Index(CPID(1, 1), SID{1}(1, '+')), [0.0], [0.0]),
        OID(Index(CPID(1, 2), SID{1}(2, '-')), [0.5], [0.0])
        ))]

    sc = SpinCoupling(2.0, ('+', '-', '+', '-'), orbitals=subscript"[α α β β](α < β)")
    point = Point(PID(1), [0.0], [0.0])
    hilbert = Hilbert(point.pid=>Spin{1}(norbital=3))
    ex = expand(sc, point, hilbert, Val(:info))
    @test collect(ex) == [
        (2.0, ID(OID(Index(PID(1), SID{1}(1, '+')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(1, '-')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(2, '+')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(2, '-')), [0.0], [0.0])
                 )),
        (2.0, ID(OID(Index(PID(1), SID{1}(1, '+')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(1, '-')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(3, '+')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(3, '-')), [0.0], [0.0])
                 )),
        (2.0, ID(OID(Index(PID(1), SID{1}(2, '+')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(2, '-')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(3, '+')), [0.0], [0.0]),
                 OID(Index(PID(1), SID{1}(3, '-')), [0.0], [0.0])
                 ))
    ]
end

@testset "heisenberg" begin
    @test heisenberg"+-z ob[1 2]" == heisenberg"ob[1 2]" == heisenbergpmz(orbitals=(1, 2)) == Couplings(
        SpinCoupling(1//1, ('z', 'z'), orbitals=(1, 2)),
        SpinCoupling(1//2, ('+', '-'), orbitals=(1, 2)),
        SpinCoupling(1//2, ('-', '+'), orbitals=(1, 2))
    )
    @test heisenberg"" == heisenbergpmz() == Couplings(SpinCoupling(1//1, ('z', 'z')), SpinCoupling(1//2, ('+', '-')), SpinCoupling(1//2, ('-', '+')))
    @test heisenberg"xyz" == heisenbergxyz() == Couplings(SpinCoupling(1, ('x', 'x')), SpinCoupling(1, ('y', 'y')), SpinCoupling(1, ('z', 'z')))
end

@testset "ising" begin
    @test ising"x" == Couplings(SpinCoupling(1, ('x', 'x')))
    @test ising"y" == Couplings(SpinCoupling(1, ('y', 'y')))
    @test ising"z" == Couplings(SpinCoupling(1, ('z', 'z')))

    @test ising"x ob[α β](α < β)" == Couplings(SpinCoupling(1, ('x', 'x'), orbitals=subscript"[α β](α < β)"))
    @test ising"y ob[α β](α < β)" == Couplings(SpinCoupling(1, ('y', 'y'), orbitals=subscript"[α β](α < β)"))
    @test ising"z ob[α β](α < β)" == Couplings(SpinCoupling(1, ('z', 'z'), orbitals=subscript"[α β](α < β)"))
end

@testset "gamma" begin
    @test gamma"x ob[1 1]" == gamma('y', 'z', orbitals=(1, 1))== SpinCoupling(1, ('y', 'z'), orbitals=(1, 1)) + SpinCoupling(1, ('z', 'y'), orbitals=(1, 1))
    @test gamma"y" == gamma('z', 'x') == SpinCoupling(1, ('z', 'x')) + SpinCoupling(1, ('x', 'z'))
    @test gamma"z" == gamma('x', 'y') == SpinCoupling(1, ('x', 'y')) + SpinCoupling(1, ('y', 'x'))
end

@testset "dm" begin
    @test dm"x ob[1 1]" == dm('y', 'z', orbitals=(1, 1)) == SpinCoupling(1, ('y', 'z'), orbitals=(1, 1)) - SpinCoupling(1, ('z', 'y'), orbitals=(1, 1))
    @test dm"y" == dm('z', 'x') == SpinCoupling(1, ('z', 'x')) - SpinCoupling(1, ('x', 'z'))
    @test dm"z" == dm('x', 'y') == SpinCoupling(1, ('x', 'y')) - SpinCoupling(1, ('y', 'x'))
end

@testset "sᵅ" begin
    @test sˣ"ob[1]" == Couplings(SpinCoupling(1, ('x',), orbitals=(1,)))
    @test sʸ"" == Couplings(SpinCoupling(1, ('y',)))
    @test sᶻ"" == Couplings(SpinCoupling(1, ('z',)))
end

@testset "spincoupling" begin
    sc = sc"1.0 S⁺S⁻ ob[α β](α < β)"
    @test repr(sc) == "1.0 S⁺S⁻ ob[α β](α < β)"

    sc = sc"1.0 S⁺S⁻ ob[α β]"
    @test repr(sc) == "1.0 S⁺S⁻ ob[α β]"

    sc = sc"1.0 S⁺S⁻ ob[1 2]"
    @test repr(sc) == "1.0 S⁺S⁻ ob[1 2]"

    sc = sc"1.0 S⁺S⁻"
    @test repr(sc) == "1.0 S⁺S⁻"
end

@testset "SpinTerm" begin
    point = Point(PID(1), (0.5, 0.5), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>Spin{1//2}(norbital=2))
    term = SpinTerm(:h, 1.5, 0, sᶻ"")
    operators = Operators(
        Operator(1.5, ID(OID(Index(PID(1), SID{1//2}(1, 'z')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(1.5, ID(OID(Index(PID(1), SID{1//2}(2, 'z')), [0.5, 0.5], [0.0, 0.0])))
    )
    @test term|>abbr == term|>typeof|>abbr == :sp
    @test term|>ishermitian == term|>typeof|>ishermitian == true
    @test expand(term, point, hilbert) == operators

    bond = Bond(1, Point(CPID('a', 1), (0.0, 0.0), (0.0, 0.0)), Point(CPID('b', 1), (0.5, 0.5), (0.0, 0.0)))
    hilbert = Hilbert(pid=>Spin{1//2}(norbital=2) for pid in [bond.spoint.pid, bond.epoint.pid])
    term = SpinTerm(:J, 1.5, 1, heisenberg"")
    operators = Operators(
        Operator(1.50, ID(OID(Index(CPID('b', 1), SID{1//2}(2, 'z')), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), SID{1//2}(2, 'z')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(0.75, ID(OID(Index(CPID('b', 1), SID{1//2}(2, '-')), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), SID{1//2}(2, '+')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(0.75, ID(OID(Index(CPID('b', 1), SID{1//2}(1, '-')), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), SID{1//2}(1, '+')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(0.75, ID(OID(Index(CPID('b', 1), SID{1//2}(1, '+')), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), SID{1//2}(1, '-')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(1.50, ID(OID(Index(CPID('b', 1), SID{1//2}(1, 'z')), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), SID{1//2}(1, 'z')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(0.75, ID(OID(Index(CPID('b', 1), SID{1//2}(2, '+')), [0.5, 0.5], [0.0, 0.0]), OID(Index(CPID('a', 1), SID{1//2}(2, '-')), [0.0, 0.0], [0.0, 0.0])))
    )
    @test expand(term, bond, hilbert) == operators
end

@testset "NID" begin
    @test NID('u')' == NID('u')
    @test NID('p')' == NID('p')

    nid = NID('p')
    @test statistics(nid) == statistics(nid) == :b
end

@testset "Phonon" begin
    pn = Phonon(3)
    @test shape(pn) == (1:2, 1:3)
    @test ndimshape(pn) == ndimshape(typeof(pn)) == 2
    for i = 1:length(pn)
        @test NID(CartesianIndex(pn[i], pn), pn) == pn[i]
    end
    @test collect(pn) == [NID('u', 'x'), NID('p', 'x'), NID('u', 'y'), NID('p', 'y'), NID('u', 'z'), NID('p', 'z')]

    @test shape(IIDSpace(NID('u'), Phonon(3))) == (1:1, 1:3)
    @test shape(IIDSpace(NID('u', 'x'), Phonon(3))) == (1:1, 1:1)
    @test shape(IIDSpace(NID('u', 'y'), Phonon(3))) == (1:1, 2:2)
    @test shape(IIDSpace(NID('u', 'z'), Phonon(3))) == (1:1, 3:3)

    @test shape(IIDSpace(NID('p'), Phonon(2))) == (2:2, 1:2)
    @test shape(IIDSpace(NID('p', 'x'), Phonon(3))) == (2:2, 1:1)
    @test shape(IIDSpace(NID('p', 'y'), Phonon(3))) == (2:2, 2:2)
    @test shape(IIDSpace(NID('p', 'z'), Phonon(3))) == (2:2, 3:3)
end

@testset "latex" begin
    index = Index(PID(1), NID('u', 'x'))
    @test script(Val(:BD), index, nlatex) == 'u'
    @test script(Val(:BD), OID(index, [0.0, 0.0], [0.0, 0.0]), nlatex) == 'u'
    @test script(Val(:site), index) == 1
    @test script(Val(:dir), index) == 'x'

    index = Index(PID(2), NID('p', 'y'))
    @test script(Val(:BD), index, nlatex) == 'p'
    @test script(Val(:BD), OID(index, [0.0, 0.0], [0.0, 0.0]), nlatex) == 'p'
    @test script(Val(:site), index) == 2
    @test script(Val(:dir), index) == 'y'

    latexname(Index{<:AbstractPID, <:NID}) == Symbol("Index{AbstractPID, NID}")
    latexname(CompositeOID{<:Index{<:AbstractPID, <:NID}}) == Symbol("CompositeOID{Index{AbstractPID, NID}}")
end

@testset "PhononOperator" begin
    opt = Operator(1.0, ID(
        OID(Index(PID(1), NID('p', 'x')), [0.0, 0.0], [0.0, 0.0]),
        OID(Index(PID(1), NID('p', 'x')), [0.0, 0.0], [0.0, 0.0])
        ))
    @test opt' == Operator(1.0, ID(
        OID(Index(PID(1), NID('p', 'x')), [0.0, 0.0], [0.0, 0.0]),
        OID(Index(PID(1), NID('p', 'x')), [0.0, 0.0], [0.0, 0.0])
        ))
    @test ishermitian(opt)
    @test repr(opt) == "(p^{x}_{1})^2"
end

@testset "permute" begin
    id₁ = OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0])
    id₂ = OID(Index(PID(1), NID('p', 'x')), [0.0, 0.0], [0.0, 0.0])
    @test permute(id₁, id₂) == (Operator(+1im), Operator(1, ID(id₂, id₁)))
    @test permute(id₂, id₁) == (Operator(-1im), Operator(1, ID(id₁, id₂)))

    id₁ = OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0])
    id₂ = OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0])
    @test permute(id₁, id₂) == (Operator(1, ID(id₂, id₁)),)

    id₁ = OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0])
    id₂ = OID(Index(PID(1), NID('p', 'y')), [0.0, 0.0], [0.0, 0.0])
    @test permute(id₁, id₂) == (Operator(1, ID(id₂, id₁)),)
end

@testset "PhononCoupling" begin
    pnc = PhononCoupling(1.0, ('p', 'p'))
    @test string(pnc) == "PhononCoupling(value=1.0, tags=[p p])"
    @test repr(pnc) == "1.0 [p p]"

    pnc = PhononCoupling(1.0, ('p', 'p'), dirs=('x', 'x'))
    @test string(pnc) == "PhononCoupling(value=1.0, tags=[p p], dirs=[x x])"
    @test repr(pnc) == "1.0 [p p] dr[x x]"

    pnc = PhononCoupling(2.0, ('p', 'p'))
    point = Point(PID(1), [0.5, 0.0], [0.0, 0.0])
    hilbert = Hilbert(point.pid=>Phonon(2))
    ex = expand(pnc, point, hilbert, Val(:PhononKinetic))
    @test collect(ex) == [
        (2.0, ID(OID(Index(PID(1), NID('p', 'x')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('p', 'x')), [0.5, 0.0], [0.0, 0.0]))),
        (2.0, ID(OID(Index(PID(1), NID('p', 'y')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('p', 'y')), [0.5, 0.0], [0.0, 0.0])))
    ]

    pnc = PhononCoupling(1.0, ('u', 'u'))
    bond = Bond(1, Point(PID(2), [0.5, 0.0], [0.0, 0.0]), Point(PID(1), [0.0, 0.0], [0.0, 0.0]))
    hilbert = Hilbert(pid=>Phonon(2) for pid in [bond.epoint.pid, bond.spoint.pid])
    ex = expand(pnc, bond, hilbert, Val(:PhononPotential))
    @test shape(ex) == (1:2, 1:3)
    @test collect(ex) ==[
        (+1.0, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        (+0.0, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        (-2.0, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.0], [0.0, 0.0]))),
        (+0.0, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.0], [0.0, 0.0]))),
        (+1.0, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.0], [0.0, 0.0]))),
        (+0.0, ID(OID(Index(PID(2), NID('u', 'y')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.0], [0.0, 0.0])))
        ]

    @test kinetic"" == Couplings(PhononCoupling(1, ('p', 'p')))
    @test potential"" == Couplings(PhononCoupling(1, ('u', 'u')))
end

@testset "PhononKinetic" begin
    term = PhononKinetic(:T, 2.0)
    @test abbr(term) == abbr(typeof(term)) == :pnk
    @test ishermitian(term) == ishermitian(typeof(term)) == true

    point = Point(PID(1), [0.5, 0.0], [0.0, 0.0])
    hilbert = Hilbert(point.pid=>Phonon(2))
    operators = Operators(
        Operator(2.0, ID(OID(Index(PID(1), NID('p', 'x')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('p', 'x')), [0.5, 0.0], [0.0, 0.0]))),
        Operator(2.0, ID(OID(Index(PID(1), NID('p', 'y')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('p', 'y')), [0.5, 0.0], [0.0, 0.0])))
    )
    @test expand(term, point, hilbert) == operators
end

@testset "PhononPotential" begin
    term = PhononPotential(:V, 2.0, 1)
    @test abbr(term) == abbr(typeof(term)) == :pnp
    @test ishermitian(term) == ishermitian(typeof(term)) == true

    bond = Bond(1, Point(PID(2), [0.5, 0.0], [0.0, 0.0]), Point(PID(1), [0.0, 0.0], [0.0, 0.0]))
    hilbert = Hilbert(pid=>Phonon(2) for pid in [bond.epoint.pid, bond.spoint.pid])
    operators = Operators(
        Operator(+2.0, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-4.0, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.0], [0.0, 0.0]))),
        Operator(+2.0, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.0], [0.0, 0.0]))),
    )
    @test expand(term, bond, hilbert) == operators

    bond = Bond(1, Point(PID(2), [0.0, 0.5], [0.0, 0.0]), Point(PID(1), [0.0, 0.0], [0.0, 0.0]))
    hilbert = Hilbert(pid=>Phonon(2) for pid in [bond.epoint.pid, bond.spoint.pid])
    operators = Operators(
        Operator(+2.0, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-4.0, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.0, 0.5], [0.0, 0.0]))),
        Operator(+2.0, ID(OID(Index(PID(2), NID('u', 'y')), [0.0, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.0, 0.5], [0.0, 0.0])))
    )
    @test expand(term, bond, hilbert) == operators

    bond = Bond(1, Point(PID(2), [0.5, 0.5], [0.0, 0.0]), Point(PID(1), [0.0, 0.0], [0.0, 0.0]))
    hilbert = Hilbert(pid=>Phonon(2) for pid in [bond.epoint.pid, bond.spoint.pid])
    operators = Operators(
        Operator(+1.0, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(+1.0, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-2.0, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-2.0, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(+1.0, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(+1.0, ID(OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0])))
    )
    @test expand(term, bond, hilbert) ≈ operators
end

@testset "DMPhonon" begin
    @test dmphonon"" == Couplings(Coupling(1, ID(NID('u', wildcard), SID{wildcard}(1, wildcard))))

    term = DMPhonon(:dmp, 2.0, 1)
    @test abbr(term) == abbr(typeof(term)) == :dmp
    @test ishermitian(term) == ishermitian(typeof(term)) == false

    bond = Bond(1, Point(PID(2), [0.5, 0.5], [0.0, 0.0]), Point(PID(1), [0.0, 0.0], [0.0, 0.0]))
    hilbert = Hilbert(pid=>Phonon(2)⊗Spin{1//2}(1) for pid in [bond.epoint.pid, bond.spoint.pid])
    operators = Operators(
        Operator(√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'y')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), NID('u', 'y')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'y')), [0.5, 0.5], [0.0, 0.0]))),
        Operator(√2/2, ID(OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(1), NID('u', 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(2), NID('u', 'x')), [0.5, 0.5], [0.0, 0.0]), OID(Index(PID(1), SID{1//2}(1, 'x')), [0.0, 0.0], [0.0, 0.0]))),
        Operator(-√2/2, ID(OID(Index(PID(1), NID('u', 'y')), [0.0, 0.0], [0.0, 0.0]), OID(Index(PID(2), SID{1//2}(1, 'x')), [0.5, 0.5], [0.0, 0.0])))
    )
    @test expand(term, bond, hilbert) ≈ operators
end


function handle_step(obj::FieldODEStepHandler, arg0::FieldODEStateInterpolator, arg1::jboolean)
    return jcall(obj, "handleStep", void, (FieldODEStateInterpolator, jboolean), arg0, arg1)
end

function init(obj::FieldODEStepHandler, arg0::FieldODEStateAndDerivative, arg1::RealFieldElement)
    return jcall(obj, "init", void, (FieldODEStateAndDerivative, RealFieldElement), arg0, arg1)
end


#=
SimulationTest:
- Julia version:
- Author: inaba
- Date: 2021-11-20
=#
module SimulationTest
using Test
using LightGraphs
using Statistics: mean

include("../src/Simulation.jl")
const sim = Simulation  # alias

@testset "Agent" begin
    @testset "Deterministic test" begin
        agent = sim.Agent(1)
        @test agent.id == 1
        @test agent.next_is_cooperator == false
        @test agent.next_is_punisher == false
        @test agent.payoff == 0.0
    end

    # is_cooperator と is_punisher が50%程度の確率で設定されていることを確認。
    @testset "Probabilistic test" begin
        agents = [sim.Agent(id) for id in 1:1000]
        average_cooperator_rate = mean([agent.is_cooperator for agent in agents])
        average_punisher_rate = mean([agent.is_punisher for agent in agents])
        @test 0.45 <= average_cooperator_rate <= 0.55
        @test 0.45 <= average_punisher_rate <= 0.55
    end
end

@testset "Model" begin
    @testset "neighbours" begin
        agents = [sim.Agent(id) for id in 1:5]
        graph = cycle_graph(length(agents))
        model = sim.Model(agents, graph, g = 0.0, p = 1.0, a = 0.0)
        # 1の隣人は 4, 5, 2, 3 であることを確認
        @test sort(model.neighbours[1]) == sort([4, 5, 2, 3])
        # 2の隣人は 5, 1, 3, 4 であることを確認
        @test sort(model.neighbours[2]) == sort([5, 1, 3, 4])
        # 3の隣人は 1, 2, 4, 5 であることを確認
        @test sort(model.neighbours[3]) == sort([1, 2, 4, 5])
        # 4の隣人は 2, 3, 5, 1 であることを確認
        @test sort(model.neighbours[4]) == sort([2, 3, 5, 1])
        # 5の隣人は 3, 4, 1, 2 であることを確認
        @test sort(model.neighbours[5]) == sort([3, 4, 1, 2])
    end

    @testset "calc_payoffs!" begin
        agents = [sim.Agent(id) for id in 1:10]

        # agentの戦略を固定する。(戦略を確率論的にではなく決定論的に決めるのが目的なので、どの戦略にするかは恣意的に決めて良い)
        for agent in agents
            agent.is_cooperator = (agent.id % 3 == 2)
            agent.is_punisher = (agent.id % 3 == 1)
        end

        graph = cycle_graph(length(agents))
        model = sim.Model(agents, graph, g = 0.0, p = 1.0, a = 0.0)
        sim.calc_payoffs!(model)
    end
end
end


# Simple test of function run_mut_evolution()
# include("../src/CGP.jl") # Assumes that CGP.jl is loaded
# The following lines allow this file to included from CGP.jl, CGP.jl/test, or CGP.jl/src.
#include("../src/Env_evolution.jl")
#include("../src/Assignment.jl")
cwd = pwd()
date = "7_25"
if cwd[end-5:end] == "CGP.jl"
  csvfile = "$cwd/data/$date/test_env_evolveA.csv"
else
  csvfile = "../data/$date/test_env_evolveA.csv"
end
iterations = 1
numinputs = 3
numoutputs = 6
nodearity = 2
numinteriors = 15
ngoals = 1
levelsback=15
#hamming_rng = true:true
maxsteps = 100000:100000
gl_repetitions = 3
num_flip_bits = 2:2
perm_heuristic=true
perturb_goal_range = false:true
avgfitness = true
fit_limit_list = [numoutputs-2.0, numoutputs-1.0, Float64(numoutputs)]
#run_env_evolution( iterations, numinputs, numoutputs, numinteriors, goallistlength, maxsteps, levelsback, csvfile )
df = run_env_evolution( iterations, numinputs, numoutputs, numinteriors, ngoals, maxsteps, levelsback,
    gl_repetitions, num_flip_bits, avgfitness, perm_heuristic, perturb_goal_range, fit_limit_list, csvfile )


"""
Default parameters for all convenience methods that are exported to the end user.

See `OptRunController` for the description.
"""
const DefaultParameters = ParamsDict(
    :NumDimensions  => :NotSpecified, # Dimension of problem to be optimized
    :SearchRange    => (-1.0, 1.0), # Default search range to use per dimension unless specified
    :SearchSpace    => false, # Search space can be directly specified and will then take precedence over NumDimensions and SearchRange.
    :FitnessScheme  => MinimizingFitnessScheme, # fitness scheme to be used
    :TargetFitness => nothing, # optimal (target) fitness, if known

    :Method => :adaptive_de_rand_1_bin_radiuslimited,

    :MaxTime        => 0.0,
    :MaxFuncEvals   => 0,
    :MaxSteps       => 10000,
    :MaxStepsWithoutProgress => 10000,
    :MinDeltaFitnessTolerance => 1e-50,
    :FitnessTolerance => 1e-8,

    :MaxNumStepsWithoutFuncEvals => 100,

    :NumRepetitions => 1,     # Number of repetitions to run for each optimizer for each problem

    :TraceMode      => :compact,  # Print tracing information during the optimization
    :TraceInterval  => 0.50,  # Minimum number of seconds between consecutive trace messages printed to STDOUT
    :SaveTrace      => false,
    :SaveFitnessTraceToCsv => false, # Save a csv file with information about the major fitness improvement events (only the first event in each fitness magnitude class is saved)
    :SaveParameters => false, # Save parameters to a json file for later scrutiny

    :CallbackFunction => x -> x, # Function to callback to, here just the identity function.
    :CallbackInterval  => -1.0,  # Minimum number of seconds between consecutive callbacks. If <0.0 we never callback (which is the default).

    :RandomizeRngSeed => true, # Randomize the RngSeed value before using any random numbers.
    :RngSeed        => 1234,   # The specific random seed to set before any random numbers are generated. The seed is randomly selected if RandomizeRngSeed is true, and this parameter is updated with its actual value.

    :PopulationSize => 50
)

function check_and_create_search_space(params::Parameters)
    # If an explicit SearchSpace has been given we use that. It takes precedence.
    if haskey(params, :SearchSpace)
        ss = params[:SearchSpace]
        if isa(ss, SearchSpace)
            return ss
        elseif isa(ss, typeof([(0.0, 1.0)]))
            return RectSearchSpace(ss)
        elseif ss == false
            # silently fallthrough to the other means of search space specification
        else
            throw(ArgumentError("Using $(typeof(ss)) for SearchSpace is not supported"))
        end
    end

    # If no explicit search space has been given we must create one from the
    # other related parameters.
    if haskey(params, :SearchRange)
        sr = params[:SearchRange]
        # Check that a valid search range has been stated and create the search_space
        # based on it, or bail out.
        if isa(sr, Tuple{<:Real, <:Real})
            ndim = params[:NumDimensions]
            if ndim == :NotSpecified
                throw(ArgumentError("You MUST specify NumDimensions= in a solution when giving a SearchRange=$(sr)"))
            end
            return RectSearchSpace(params[:NumDimensions], sr)
        elseif isa(sr, Vector{Tuple{<:Real, <:Real}})
            return RectSearchSpace(sr)
        else
            throw(ArgumentError("Using $(typeof(sr)) for SearchRange is not supported."))
        end
    end

    # No valid search space have been specified => bail.
    throw(ArgumentError("Invalid search space specification ("*
                        "SearchSpace = $(params[:SearchSpace]), "*
                        "SearchRange = $(params[:SearchRange]), "*
                        "NumDimensions = $(params[:NumDimensions]))"))
end

function check_valid!(params::Parameters)
    # Check that max_time is larger than zero if it has been specified.
    if haskey(params, :MaxTime)
        if !isa(params[:MaxTime], Number) || params[:MaxTime] < 0.0
            throw(ArgumentError("MaxTime parameter must be a non-negative number"))
        elseif params[:MaxTime] > 0.0
            params[:MaxTime] = convert(Float64, params[:MaxTime])
            params[:MaxFuncEvals] = 0
            params[:MaxSteps] = 0
        end
    end

    # Check that a valid number of fevals has been specified. Print warning if higher than 1e8.
    if haskey(params,:MaxFuncEvals)
        if !isa(params[:MaxFuncEvals], Integer) || params[:MaxFuncEvals] < 0.0
            throw(ArgumentError("MaxFuncEvals parameter MUST be a non-negative integer"))
        elseif params[:MaxFuncEvals] > 0.0
            if params[:MaxFuncEvals] >= 1e8
                @warn("Number of allowed function evals is $(params[:MaxFuncEvals]); this can take a LONG time")
            end
            params[:MaxFuncEvals] = convert(Int, params[:MaxFuncEvals])
            params[:MaxSteps] = 0
        end
    end

    # Check that a valid number of iterations has been specified. Print warning if higher than 1e8.
    if haskey(params, :MaxSteps)
        if !isa(params[:MaxSteps], Number) || params[:MaxSteps] < 0.0
            throw(ArgumentError("The number of iterations (MaxSteps) MUST be a non-negative number"))
        elseif params[:MaxSteps] > 0.0
            if params[:MaxSteps] >= 1e8
                @warn("Number of allowed iterations is $(params[:MaxSteps]); this can take a LONG time")
            end
            params[:MaxSteps] = convert(Int, params[:MaxSteps])
        end
    end

    # Check that a valid population size has been given.
    if params[:PopulationSize] < 2
        # FIXME why? What if we use popsize of 1 for optimizers that improve on one solution?
        throw(ArgumentError("The population size MUST be at least 2"))
    end

    method = params[:Method]
    # Check that a valid method has been specified and then set up the optimizer
    if !isa(method, Symbol) || method ∉ BlackBoxOptim.MethodNames
        throw(ArgumentError("The method specified, $(method), is NOT among the valid methods:   $(MethodNames)"))
    end
end


#
# This file is a part of MolecularGraph.jl
# Licensed under the MIT License http://opensource.org/licenses/MIT
#

""" The argument `func` is a parser function which has a parser state as an
argument, process tokens found in the given text, and returns nothing if no
valid tokens were found.
"""

function lglowand!(state::SmartsParserState, func)
    """ LogicalLowAnd <- Or (';' Or)*
    """
    cond = []
    c = lgor!(state, func)
    while c !== nothing
        push!(cond, c)
        if read(state) == ';'
            forward!(state)
            c = lgor!(state, func)
        else
            break
        end
    end
    if isempty(cond)
        return
    elseif length(cond) == 1
        return cond[1]
    else
        return :and => Tuple(cond)
    end
end

function lgor!(state::SmartsParserState, func)
    """ Or <- And (',' And)*
    """
    cond = []
    c = lghighand!(state, func)
    while c !== nothing
        push!(cond, c)
        if read(state) == ','
            forward!(state)
            c = lghighand!(state, func)
        else
            break
        end
    end
    if isempty(cond)
        return
    elseif length(cond) == 1
        return cond[1]
    else
        return :or => Tuple(cond)
    end
end

function lghighand!(state::SmartsParserState, func)
    """ And <- Not ('&'? Not)*
    """
    cond = []
    c = lgnot!(state, func)
    while c !== nothing
        push!(cond, c)
        if read(state) == '&'
            forward!(state)
        end
        c = lgnot!(state, func)
    end
    if isempty(cond)
        return
    elseif length(cond) == 1
        return cond[1]
    else
        return :and => Tuple(cond)
    end
end

function lgnot!(state::SmartsParserState, func)
    """ Not <- '!'? Element
    """
    if read(state) == '!'
        forward!(state)
        cond = func(state)
        if cond === nothing
            # TODO: need backtracking in the case like [!=!c]
            backtrack!(state)
            return
        else
            return :not => cond
        end
    else
        return func(state)
    end
end


# Elias Kuepper, Holger Teichgraeber, 2019

 ######################
 # CapacityExpansion
 # Capacity Expansion Probelem formulation
 #
 #####################
using TimeSeriesClustering
using CSV
using YAML
using DataFrames
using StatsBase
using JLD2
using FileIO
using JuMP

include(joinpath("utils","datastructs.jl"))
include(joinpath("utils","optvariable.jl"))
include(joinpath("utils","utils.jl"))
include(joinpath("utils","load_data.jl"))
include(joinpath("optim_problems","run_opt.jl"))
include(joinpath("optim_problems","set.jl"))
include(joinpath("optim_problems","opt.jl"))


# functions for passing out info from simulations, similar to the info return from openai gym
# maintained by @zsunberg

"""
    a, ai = action_info(policy, x)

Return a tuple containing the action determined by policy 'p' at state or belief 'x' and information (usually a `NamedTuple`, `Dict` or `nothing`) from the calculation of that action.

By default, returns `nothing` as info.
"""
function action_info(p::Policy, x)
    return action(p, x), nothing
end

"""
    policy, si = solve_info(solver, problem)

Return a tuple containing the policy determined by a solver and information (usually a `NamedTuple`, `Dict` or `nothing`) from the calculation of that policy.

By default, returns `nothing` as info.
"""
function solve_info(s::Solver, problem::Union{POMDP,MDP})
    return solve(s, problem), nothing
end

"""
    bp, i = update_info(updater, b, a, o)

Return a tuple containing the new belief and information (usually a `NamedTuple`, `Dict` or `nothing`) from the belief update.

By default, returns `nothing` as info.
"""
function update_info(up::Updater, b, a, o)
    return update(up, b, a, o), nothing
end

# once POMDPs v0.8 is released, this should be a jldoctest
"""
    add_infonode(ddn::DDNStructure)

Create a new DDNStructure object with a new node labeled :info for returning miscellaneous informationabout a simulation step.

Typically, the object in info is associative (i.e. a `Dict` or `NamedTuple`) with keys corresponding to different pieces of information.

# Example (using POMDPs v0.8)

```julia
using POMDPs, POMDPModelTools, POMDPPolicies, POMDPSimulators, Random

struct MyMDP <: MDP{Int, Int} end

# add the info node to the DDN
POMDPs.DDNStructure(::Type{MyMDP}) = mdp_ddn() |> add_infonode

# the dynamics involve two random numbers - here we record the values for each in info
function POMDPs.gen(m::MyMDP, s, a, rng)
    r1 = rand(rng)
    r2 = randn(rng)
    return (sp=s+a+r1+r2, r=s^2, info=(r1=r1, r2=r2))
end

m = MyMDP()
@show nodenames(DDNStructure(m))
p = FunctionPolicy(s->1)
for (s,info) in stepthrough(m, p, 1, "s,info", max_steps=5, rng=MersenneTwister(2))
    @show s
    @show info
end
```
"""
function add_infonode(ddn) # for DDNStructure, but it is not declared in v0.7.3, so there is not annotation
    add_node(ddn, :info, ConstantDDNNode(nothing), nodenames(ddn))
end

function add_infonode(ddn::POMDPs.DDNStructureV7{nodenames}) where nodenames
    return POMDPs.DDNStructureV7{(nodenames..., :info)}()
end

###############################################################
# Note all generate functions will be deprecated in POMDPs v0.8
###############################################################

if DDNStructure(MDP) isa POMDPs.DDNStructureV7
    """
    Return a tuple containing the next state and reward and information (usually a `NamedTuple`, `Dict` or `nothing`) from that step.

    By default, returns `nothing` as info.
    """
    function generate_sri(p::MDP, s, a, rng::AbstractRNG)
        return generate_sr(p, s, a, rng)..., nothing
    end

    """
    Return a tuple containing the next state, observation, and reward and information (usually a `NamedTuple`, `Dict` or `nothing`) from that step.

    By default, returns `nothing` as info.
    """
    function generate_sori(p::POMDP, s, a, rng::AbstractRNG)
        return generate_sor(p, s, a, rng)..., nothing
    end

    POMDPs.gen(::DDNOut{(:sp,:o,:r,:i)}, m, s, a, rng) = generate_sori(m, s, a, rng)
    POMDPs.gen(::DDNOut{(:sp,:o,:r,:info)}, m, s, a, rng) = generate_sori(m, s, a, rng)
    POMDPs.gen(::DDNOut{(:sp,:r,:i)}, m, s, a, rng) = generate_sri(m, s, a, rng)
    POMDPs.gen(::DDNOut{(:sp,:r,:info)}, m, s, a, rng) = generate_sri(m, s, a, rng)
else
    @deprecate generate_sri(args...) gen(DDNOut(:sp,:r,:info), args...)
    @deprecate generate_sori(args...) gen(DDNOut(:sp,:o,:r,:info), args...)
end


#############################################################################
#Copyright (c) 2020 Charles Le Losq
#
#The MIT License (MIT)
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the #Software without restriction, including without limitation the rights to use, copy, #modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, #and to permit persons to whom the Software is furnished to do so, subject to the #following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#############################################################################
VERSION >= v"1.0.0" && __precompile__()

module Gcvspl

unixpath = "../deps/src/gcvspline/libgcvspl"
const gcvlib = joinpath(dirname(@__FILE__), @static Sys.isunix() ? unixpath : winpath)

#function __init__()
#	if (Libdl.dlopen_e(gcvlib) == C_NULL)
#        error("GCVSPL not properly compiled. Run Pkg.build(\"gcvspl\"). Windows auto-build is not setup, you might want to build the library manually.")
#	end
#end

include("gcvspl_wrapper.jl")

export gcv_spl, spl_der

end # module


function value(obj::TimeVectorFunction, arg0::AbsoluteDate)
    return jcall(obj, "value", Vector{jdouble}, (AbsoluteDate,), arg0)
end

function value(obj::TimeVectorFunction, arg0::FieldAbsoluteDate)
    return jcall(obj, "value", Vector{RealFieldElement}, (FieldAbsoluteDate,), arg0)
end


using Dagger
import Dagger: reduceblock
using DistributedArrays

using BenchmarkTools

AmulB_compatible(x,y) =
    try; size(x), size(y), x * y; true; catch err false end

AmulB_compatible(x::Computed,y::Computed) = AmulB_compatible(domain(x.result), domain(y.result))

function AcmulB_compatible(x,y)
    AmulB_compatible(domain(x.result)', domain(y.result))
end

function matrix_suite(ctx, X, f=x->x)
    suite = BenchmarkGroup(["matrix"])

    suite["alloc"] = @benchmarkable (compute($(f(X))); gc())

    Y = compute(ctx, f(X))
    suite["X+1.0"]  = @benchmarkable (compute($ctx, $(f(Y+1))); gc())
    suite["X+X"]    = @benchmarkable (compute($ctx, $(f(Y+Y))); gc())
    suite["X'"]     = @benchmarkable (compute($ctx, $(f(Y'))); gc())
    suite["sin(X)"] = @benchmarkable (compute($ctx, $(f(sin(Y)))); gc())

    if domain(Y.result) == domain(Y.result)'
        @show domain(Y.result)
        suite["X+X'"]   = @benchmarkable (compute($ctx, $(f(Y+Y'))); gc())
    end
    if AmulB_compatible(Y, Y)
        suite["X*X"]    = @benchmarkable (compute($ctx, $(f(Y*Y))); gc())
    end
    if AcmulB_compatible(Y, Y)
        suite["X'*X"]   = @benchmarkable (compute($ctx, $(f(Y'*Y))); gc())
    end
    suite
end

function matrix_variants(NW, f=x->x)
    w = workers()[1:NW]
    suite = BenchmarkGroup()

    matrix(ch, sz) = zeros(Chunks(ch), sz)

    suite["trivial_one_each"] = matrix_suite(Context(w), matrix(1, NW), f)
    suite["trivial_two_each"] = matrix_suite(Context(w), matrix(1, 2NW), f)
    suite["trivial_1k_each"] = matrix_suite(Context(w), matrix(1, 1000NW), f)

 #  suite["128M_one_each"] =
 #      matrix_suite(Context(w), matrix((4000,4000), (4000*NW, 4000)), f)

 #  suite["128M_two_each"] =
 #      matrix_suite(Context(w), matrix((4000,4000), (NW*4000, 2*4000)), f)

 #  suite["128M_uneven"] =
 #      matrix_suite(Context(w), matrix((4000,4000), (5000*NW, 5000)), f)
    suite
end

function matrix_suite_nworkers(NW)
    suite = BenchmarkGroup()
    suite["in-memory"] = matrix_variants(2, x->x)
    suite["on-disk"]   = matrix_variants(2, x -> Dagger.save(x, "tmp$(randstring(5))"))
    suite
end

using JLD
function main()
    if length(ARGS) == 0
        error("Usage: julia -p NWorkers benchmark.jl [tune-file] [output-file]")
    end

    nw = nworkers()
    suite = matrix_suite_nworkers(nw)
    if length(ARGS) > 0
        tune_file = ARGS[1]
        if isfile(tune_file)
            println("Using benchmark tuning data in $tune_file")
            loadparams!(suite, JLD.load(tune_file, "suite"), :evals, :samples)
        else
            println("Creating benchmark tuning file $tune_file")
            tune!(suite)
            JLD.save(tune_file, "suite", params(suite))
        end
    else
        println("Tuning benchmarks. Tip: provide a tune_file argument to save this step.")
        tune!(suite)
    end

    println("running benchmarks")
    res = run(suite)
    @show(res)
    output = get(ARGS, 2, "result-$(nw)worker-$(Dates.now()).jld")
    println("saving results in $output")
    JLD.save(output, "result", res)
    println("Done.")

    if length(ARGS) > 2
        compare_file = ARGS[3]
        println("Comparing with results in $compare_file")
        reference_results = JLD.load(compare_file, "result")
        @show judge(reference_results, res)
    end

end

main()


## Functions for subsetting data objects

#SubDataTypes

# Definition is the same, but importantly this keeps a Subarray
mutable struct SubComMatrix{D <: Real} <: AbstractComMatrix{D}
    occurrences::SubArray{D,2}
    speciesnames::SubArray{<:AbstractString,1}
    sitenames::SubArray{<:AbstractString,1}
end

mutable struct SubSpeciesData{D <: Real} <: SEThings{D}
    commatrix::SubComMatrix{D}
    traits::DataFrames.SubDataFrame
end

mutable struct SubGridData <: SEGrid
    indices::SubArray{Int,2}
    grid::GridTopology
end

mutable struct SubPointData <: SEPoints
    coords::SubArray{Float64,2}
end

mutable struct SubLocations{T<:Union{SubGridData, SubPointData}} <: SELocations{T}
    coords::T
    sitestats::DataFrames.SubDataFrame
end

mutable struct SubAssemblage{D <: Real, P <: SubLocations} <: SEAssemblage{D, SubSpeciesData{D}, P} # A type to keep subtypes together, ensuring that they are all aligned at all times
    site::P
    occ::SubSpeciesData{D}
end

# TODO delete
mutable struct SubSiteData{S<:SubLocations} <: SESpatialData{S}
    site::S
end

# TODO not sure this is necessary anymore - perhaps remove, or update with a string (for names)

# creating views
view(occ::SEThings; species = 1:nspecies(occ), sites = 1:nsites(occ)) = SubSpeciesData(view(occ.commatrix, sites = sites, species = species), view(occ.traits,species, :))
# The SELocations things are missing as of yet - need to go by the dropbyindex functionality
function view(com::AbstractComMatrix; species = 1:nspecies(com), sites = 1:nsites(com))
    sit = asindices(sites, sitenames(com))
    spec = asindices(species, speciesnames(com))
    SubComMatrix(view(com.occurrences, spec, sit), view(com.speciesnames, spec), view(com.sitenames, sit)) #TODO change the order of these in the object to fit the array index order
end

view(pd::SEPoints, sites) = SubPointData(view(pd.coords, sites, :))
view(gd::SEGrid, sites) = SubGridData(view(gd.indices, sites, :), gd.grid)
view(lo::SELocations, sites) = SubLocations{SubGridData}(view(lo.coords, sites), view(lo.sitestats, sites, :))
view(sp::SESpatialData, sites = 1:nsites(sp)) = SubSiteData(view(sp.site, sites))

 function view(asm::SEAssemblage{D}; species = 1:nspecies(asm), sites = 1:nsites(asm), dropsites = false, dropspecies = false, dropempty = false) where D
    sit = asindices(sites, sitenames(asm))
    spec = asindices(species, speciesnames(asm))
    occ = view(asm.occ, species = spec, sites = sit)
    site = view(asm.site, sit)

    if dropsites || dropempty
        hasspecies = occupied(occ)
        occ = view(occ, sites = hasspecies)
        site = view(site, hasspecies)
    end

    if dropspecies || dropempty
        occ = view(occ, species = occurring(occ))
    end

    SubAssemblage{D, typeof(site)}(site, occ)
end

Assemblage(assm::SubAssemblage) = copy(assm)

copy(asm::SEAssemblage) = Assemblage(copy(asm.site), copy(asm.occ))
copy(sp::SESpatialData) = SiteData(copy(sp.site))
copy(pd::SEPoints) = PointData(copy(pd.coords), copy(pd.sitestats))
copy(pd::AbstractComMatrix) = ComMatrix(copy(pd.occurrences), copy(pd.speciesnames), copy(pd.sitenames))
copy(occ::SEThings) = SpeciesData(copy(occ.commatrix), my_dataframe_copy(occ.traits))
copy(lo::SELocations) = Locations(copy(lo.coords), my_dataframe_copy(lo.sitestats))

function copy(gd::SEGrid)
    indices = copy(gd.indices)
    grid = subsetgrid(indices, gd.grid)
    x_shift = Int.((xmin(grid) - xmin(gd.grid)) / xcellsize(gd.grid))
    y_shift = Int.((ymin(grid) - ymin(gd.grid)) / ycellsize(gd.grid))
    indices[:,1] .-= x_shift
    indices[:,2] .-= y_shift
    GridData(indices, grid)
end

# because I cannot define a new copy method for DataFrames
function my_dataframe_copy(sdf::AbstractDataFrame)
    ret = DataFrame()
    for n in propertynames(sdf)
        ret[!,n] = sdf[:,n]
    end
    ret
end
# Helper functions

function subsetgrid(indices, grid)
   xmin = xrange(grid)[minimum(indices[:,1])]
   ymin = yrange(grid)[minimum(indices[:,2])]
   xcells = maxrange(indices[:,1]) + 1
   ycells = maxrange(indices[:,2]) + 1
   GridTopology(range(xmin, step = xcellsize(grid), length = xcells), range(ymin, step = ycellsize(grid), length = ycells))
end

function show(io::IO, com::SubComMatrix)
    sp = createsummaryline(speciesnames(com))
    si = createsummaryline(sitenames(com))
    println(io, "SubComMatrix with $(nspecies(com)) species in $(nsites(com)) sites\n\nSpecies names:\n$(sp)\n\nSite names:\n$(si)")
end

function show(io::IO, com::SubAssemblage)
    sp = createsummaryline(speciesnames(com))
    si = createsummaryline(sitenames(com))
    println(io, "SubAssemblage with $(nspecies(com)) species in $(nsites(com)) sites\n\nSpecies names:\n$(sp)\n\nSite names:\n$(si)")
end


using AprilTags
using ImageCore
using FileIO
using ImageMagick
using ImageDraw
using ColorTypes
using FixedPointNumbers
using Test

@testset "AprilTags" begin
    # To test without extra dependancies
    # image = rand(UInt8, 480,640)
    # image[50:79,50:79]     = kron(reinterpret(UInt8,getAprilTagImage(0)), ones(UInt8, 3,3))
    # image[150:179,150:179] = kron(reinterpret(UInt8,getAprilTagImage(1)), ones(UInt8, 3,3))
    # image[250:279,250:279] = kron(reinterpret(UInt8,getAprilTagImage(2)), ones(UInt8, 3,3))
    # image = Gray.(reinterpret(N0f8, image))
    #
    #
    # imageCol = RGB.(image)
    # refpoints = [[63.9, 63.9],
    #             [163.9, 163.9],
    #             [263.9, 263.9]]

    image = load(dirname(Base.source_path()) *"/../data/tagtest.jpg")
    imageCol = load(dirname(Base.source_path()) *"/../data/colortag.jpg")
    refpoints = [[404.5, 176.1],
                 [134.0, 216.1],
                 [412.0, 130.1]]

    @testset "Low-level API" begin
        # test wrappers
        #create april tag detector
        td = apriltag_detector_create()

        #create tag family
        tf = tag36h11_create()

        #add family to detector
        apriltag_detector_add_family(td, tf)

        #create image8 object for april tags
        image8 = convert(AprilTags.image_u8_t, image)

        # run detector on image
        detections = apriltag_detector_detect(td, image8)

        # copy detections
        tags = getTagDetections(detections)

        #extract tag centres
        cpoints = map(tag->[tag.c[2],tag.c[1]],tags)
        @test cpoints ≈ refpoints atol=0.5

        # test convertions
        image8_from_u8 = convert(AprilTags.image_u8_t, reinterpret(UInt8, image)[:,:])
        @test image8_from_u8.width == image8.width
        @test image8_from_u8.height == image8.height
        @test image8_from_u8.stride == image8.stride
        #TODO: maybe add test for content of pointer

        apriltag_detections_destroy(detections)
        # Cleanup: free the detector and tag family when done.
        apriltag_detector_destroy(td)
        tag36h11_destroy(tf)
    end

    @testset "High-level API" begin
        # test the high level convenience functions
        detector = AprilTagDetector()
        tags2 = detector(image)

        @test length(detector(gray.(image))) == 3
        @test length(detector(reinterpret(UInt8,image)[:,:])) == 3

        tagsth = AprilTags.threadcalldetect(detector, image)
        @test length(tagsth) == 3

        #test on random image, should detect zero tags
        @test length(detector(rand(Gray{N0f8},100,100))) == 0

        #getters -- compare with default
        @test detector.nThreads == 1
        @test detector.quad_decimate == 2.0 # TODO: default changed to 2 - consider changing back
        @test detector.quad_sigma == 0.0
        @test detector.refine_edges == 1
        @test detector.decode_sharpening == 0.25

        #setters -- set new values
        detector.nThreads = 4
        detector.quad_decimate = 2.0
        detector.quad_sigma = 0.1
        detector.refine_edges = 0
        detector.decode_sharpening = 0.2
        # detector.refine_decode = 1
        # detector.refine_pose = 1

        #getters -- compare with set values just now
        @test detector.nThreads == 4
        @test detector.quad_decimate == 2.0
        @test detector.quad_sigma == 0.1f0
        @test detector.refine_edges == 0
        @test detector.decode_sharpening == 0.2
        # @test detector.refine_decode == 1
        # @test detector.refine_pose == 1

        #test @show overload
        # @test sprint((t,s)->show(t,"text/plain",s), detector) == "AprilTagDetector\nnThreads: 4\nquad_decimate: 1.0\nquad_sigma: 0.0\nrefine_edges: 1\nrefine_decode: 1\nrefine_pose: 1\n"
        @test sprint((t,s)->show(t,"text/plain",s), detector) == "AprilTagDetector\nnThreads: 4\nquad_decimate: 2.0\nquad_sigma: 0.1\nrefine_edges: 0\ndecode_sharpening: 0.2\n"

        cpoints = map(tag->[tag.c[2],tag.c[1]],tags2)
        freeDetector!(detector)
        @test cpoints ≈ refpoints atol=0.5

        pose = homography_to_pose(tags2[1].H, -520., 520., 320., 240.)
        # TODO create better ref pose
        refpose = [0.65  0.12 -0.75  -5.43;
                   0.39 -0.90  0.19  -6.20;
                  -0.65 -0.41 -0.63 -19.62;
                   0.0   0.0   0.0    1.0]
        @test pose[1:3,1:3] ≈ refpose[1:3,1:3] atol = 0.05
        @test pose[1:3,4] ≈ refpose[1:3,4] atol = 0.1

        #test drawing functions
        fx = 524.040
        fy = 524.040
        cx = 319.254
        cy = 251.227
        K = [fx 0  cx;
              0 fy cy]
        imCol = RGB.(image)
        foreach(tag->drawTagBox!(imCol, tag), tags2)
        # test one pixel to be correctly drawn
        t1xy = round.(Int,tags2[1].p[1])
        @test imCol[t1xy[2],t1xy[1]] == RGB{N0f8}(0.0, 1.0, 0.0)
        #thicker lines also
        foreach(tag->drawTagBox!(imCol,tag, width = 2, drawReticle = false), tags2)
        foreach(tag->drawTagBox!(imCol,tag, width = 3, drawReticle = true), tags2)
        #TODO: verify that drawing is correct

        # test for bounds error in drawing functions with thicker lines
        foreach(tag->drawTagBox!(imCol,tag, width = 1000, drawReticle = true), tags2)

        foreach(tag->drawTagAxes!(imCol,tag, K), tags2)
        #TODO: verify that drawing tag axis is correct

        # test constructors for other families
        detector2 = AprilTagDetector(AprilTags.tag25h9)
        freeDetector!(detector2)

        detector2 = AprilTagDetector(AprilTags.tag16h5)
        freeDetector!(detector2)

        detector2 = AprilTagDetector(AprilTags.tag36h11)
        freeDetector!(detector2)

        # detector2 = AprilTagDetector(AprilTags.tag36h10)
        # freeDetector!(detector2)

        reftag36h11_0 = Gray{N0f8}[ 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 0.0 1.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0;
                                    1.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0 0.0 1.0;
                                    1.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0]
        @test reftag36h11_0 == getAprilTagImage(0)

        # reftag36h10_0 = Gray{N0f8}[ 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0;
        #                             1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
        #                             1.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0;
        #                             1.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0;
        #                             1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0;
        #                             1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0;
        #                             1.0 0.0 0.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0;
        #                             1.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0;
        #                             1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
        #                             1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0]
        # @test reftag36h10_0 == getAprilTagImage(0, AprilTags.tag36h10)

        reftag16h5_0 = Gray{N0f8}[  1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0;
                                    1.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0]
        @test reftag16h5_0 == getAprilTagImage(0, AprilTags.tag16h5)

        reftag25h9_0 = Gray{N0f8}[  1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0;
                                    1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0;
                                    1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0;
                                    1.0 0.0 1.0 1.0 1.0 1.0 1.0 0.0 1.0;
                                    1.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0;
                                    1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0;
                                    1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0]
        @test reftag25h9_0 == getAprilTagImage(0, AprilTags.tag25h9)

        # TODO test, just placeholder for now
        detector = AprilTagDetector()
        detector.quad_decimate = 1.0 #NOTE see line 84
        fx = 524.040
        fy = 524.040
        cx = 319.254
        cy = 251.227
        taglength = 0.172
        (tags, poses) = detectAndPose(detector, image, fx, fy, cx, cy, taglength)
        # TODO test here, this is just result used as test.
        @test all(isapprox.(poses[1], [ 0.630532  -0.391638  0.670111  -0.461887;
                                        0.118082   0.901717  0.415889   0.494238;
                                       -0.767128  -0.183103  0.614807   1.69053],
                                      atol = 0.01))
        freeDetector!(detector)

    end

    include("homography.jl")

    @testset "Errors" begin
        #testing freed detectors errors
        detector = AprilTagDetector()
        freeDetector!(detector)
        @test_throws ErrorException tags = detector(image)
        @test_throws ErrorException tags = AprilTags.threadcalldetect(detector, image)
        @test_throws ErrorException AprilTags.setnThreads(detector, 4)
        @test_throws ErrorException AprilTags.setquad_decimate(detector, 1.0)
        @test_throws ErrorException AprilTags.setquad_sigma(detector,0.0)
        @test_throws ErrorException AprilTags.setrefine_edges(detector,1)
        @test_throws ErrorException AprilTags.setdecode_sharpening(detector,0.2)
        # @test_throws ErrorException AprilTags.setrefine_decode(detector,1)
        # @test_throws ErrorException AprilTags.setrefine_pose(detector,1)

        @test_throws ErrorException AprilTags.getnThreads(detector)
        @test_throws ErrorException AprilTags.getquad_decimate(detector)
        @test_throws ErrorException AprilTags.getquad_sigma(detector)
        @test_throws ErrorException AprilTags.getrefine_edges(detector)
        @test_throws ErrorException AprilTags.getdecode_sharpening(detector)
        # @test_throws ErrorException AprilTags.getrefine_decode(detector)
        # @test_throws ErrorException AprilTags.getrefine_pose(detector)

        @test freeDetector!(detector) == nothing
        #testing NULL tag families errors
        detector = AprilTagDetector()
        detector.tf = C_NULL
        @test_throws ErrorException tags = detector(image)
        @test_throws ErrorException tags = AprilTags.threadcalldetect(detector, image)
        @test freeDetector!(detector) == nothing
    end

    @testset "Color Image Conversion" begin
        detector = AprilTagDetector()
        detector.quad_decimate = 1.0 #NOTE see line 84
        tags = detector(imageCol)
        @test length(tags) == 1
        freeDetector!(detector)
    end
end


using Random
"""
Prioritized Sweeping (core)

Implementation of Algorithm 2 Prioritized Sweeping with reversed full backups in
[Van Seijen, Harm, and Richard S. Sutton. "Efficient planning in MDPs by small backups."
Proceedings of the 30th International Conference on International Conference on Machine Learning. Vol. 28. 2013]

Note 1
------
The Tsas' estimation is NOT done through Perfect Integration,
but through the estimators :
TParticleFilterJump (particle filtering derived for an outlier detection task)

The Rsas' estimation is done with Perfect Integration.

The fact the estimations for Tsas' and Rsas' are different prevents us from using the
    Small Backups method of [Van Seijen and Sutton, 2013].
We use reverse full backups instead.

Note 2
------
Perfect Integration with Small Backups is not supported right now.
"""

mutable struct PrioritizedSweeping{TREstimate,TTEstimate}
    nr_states::Int
    nr_actions::Int
    γ::Float64
    init_R::Float64
    initvalue::Float64
    nr_upd_cycles::Int
    counter::Int
    QSA::Array{Float64, 2}
    VS::Array{Float64, 1}
    US::Array{Float64, 1}
    Restimate::TREstimate
    Testimate::TTEstimate
    prioQ::PriorityQueue
    do_debug_print::Bool
end
function PrioritizedSweeping(; nr_states = 10, nr_actions = 4, γ = .9, init_R = 1.,
    initvalue = init_R / (1. - γ),
    nr_upd_cycles = 3, counter = 0,
    QSA = zeros(Float64, nr_states, nr_actions) .+ initvalue,
    VS = zeros(Float64, nr_states) .+ initvalue,
    US = zeros(Float64, nr_states) .+ initvalue,
    Restimatetype = RIntegratorNextStateReward,
    Testimatetype = TParticleFilterJump,
    prioQ = PriorityQueue(Base.Order.Reverse, zip(Int[], Float64[])),
    nparticles = 3, surpriseprobability = .01, stochasticity = .01, seedlearner = 3,
    etaleak = .9,
    do_debug_print = false)

    if Testimatetype == TParticleFilterJump
        Testimate = TParticleFilterJump(nr_states = nr_states,
                                nr_actions = nr_actions,
                                nparticles = nparticles,
                                surpriseprobability = surpriseprobability,
                                stochasticity = stochasticity,
                                seed = seedlearner)
    elseif Testimatetype == TLeakyIntegrator
        Testimate = TLeakyIntegrator(nr_states = nr_states,
                                        nr_actions = nr_actions,
                                        etaleak = etaleak)
    elseif Testimatetype == TLeakyGlaescher
        Testimate = TLeakyGlaescher(nr_states = nr_states,
                                    nr_actions = nr_actions,
                                    η = etaleak)
    end
    # ---- Set up Rsas' estimator ----
    if Restimatetype == RIntegratorNextStateReward
        Restimate = RIntegratorNextStateReward(nr_states = nr_states, init_R = init_R)
    end

    PrioritizedSweeping(nr_states, nr_actions, γ, init_R, initvalue, nr_upd_cycles, counter,
                QSA, VS, US, Restimate, Testimate, prioQ, do_debug_print)
end
export PrioritizedSweeping
""" Add state and priority value to queue """
function addtoqueue!(prioQ, state, prio)
    if haskey(prioQ, state)
        prioQ[state] = prio
    else
        enqueue!(prioQ, state, prio)
    end
    prioQ
end
""" Go through queue """
function processqueue!(learner::PrioritizedSweeping)
    while length(learner.prioQ) > 0 && learner.counter < learner.nr_upd_cycles
        learner.counter += 1
        sprime = dequeue!(learner.prioQ)
        ΔV = learner.VS[sprime] - learner.US[sprime]
        learner.US[sprime] = learner.VS[sprime]
        updatepredecessorsq!(learner, sprime, ΔV)
    end
    learner.counter = 0
end
""" Update Q values of predecessors - For Testimate that uses counts (Ns1a0s0) """
function updatepredecessorsq!(learner::PrioritizedSweeping{TR, TT} where {TR, TT<:TNs1a0s0},
                            sprime, ΔV)
    if length(learner.Testimate.Ns1a0s0[sprime]) > 0
        for ((a0, s0), n) in learner.Testimate.Ns1a0s0[sprime]
            if n > 0.
                learner.QSA[s0, a0] += learner.γ * ΔV * n/learner.Testimate.Nsa[a0, s0]
                updateV!(learner, s0)
            end
        end
    end
end
""" Update Q values of predecessors - For Testimate that uses probabilites (Ps1a0s0) """
function updatepredecessorsq!(learner::PrioritizedSweeping{TR, TT} where {TR, TT<:TPs1a0s0},
                            sprime, ΔV)
    if length(learner.Testimate.Ps1a0s0[sprime]) > 0
        for ((a0, s0), n) in learner.Testimate.Ps1a0s0[sprime]
            if learner.Testimate.Ps1a0s0[sprime][a0, s0] > 0.
                learner.QSA[s0, a0] += learner.γ * ΔV * n
                updateV!(learner, s0)
            end
        end
    end
end

""" Full backup  -- R[s']"""
function updateq!(learner::PrioritizedSweeping{RIntegratorNextStateReward, TT} where TT,
                state, action, next_state, reward, done)
    # Update all s-a space (It is less efficient and it won't make much difference,
    # but we do it like this, because it is the correct way: The reason is that
    # we have R(s') and not R(s,a), as they had in the van Seijen paper!)
    shuffledstates = shuffle(collect(1:learner.nr_states))
    for s in shuffledstates
        for a in 1:learner.nr_actions
            next_values, next_transprobs, next_states = getnextstates(learner, s, a)
            Rbar = [learner.Restimate.R[sprime] for sprime in next_states]
            learner.QSA[s, a] = sum(next_transprobs.* (Rbar + learner.γ * next_values))
        end
        updateV!(learner, s)
    end
end
""" Get next states, trans_probs to next states, and values of next states
    -- For Testimate that uses probabilities (Ps1a0s0) """
function getnextstates(learner::PrioritizedSweeping{TR, TT} where {TR, TT<:TPs1a0s0}, state, action)
    next_states = [s for s in 1:learner.nr_states if haskey(learner.Testimate.Ps1a0s0[s],(action, state))]
    next_values = [learner.US[s] for s in next_states]
    # @show nextvs
    next_transprobs = [learner.Testimate.Ps1a0s0[s][action, state] for s in next_states]
    # @show nextps
    next_values, next_transprobs, next_states
end
""" Get next states, trans_probs to next states, and values of next states
    -- For Testimate that uses counts (Ns1a0s0)
    NOT USED IN PAPER
    """
function getnextstates(learner::PrioritizedSweeping{TR, TT} where {TR, TT<:TNs1a0s0}, state, action)
    next_states = [s for s in 1:learner.nr_states if haskey(learner.Testimate.Ns1a0s0[s],(action, state))]
    next_values = [learner.US[s] for s in next_states]
    # @show nextvs
    next_transprobs = [learner.Testimate.Ns1a0s0[s][action, state]/learner.Testimate.Nsa[action, state] for s in next_states]
    # @show nextps
    next_values, next_transprobs, next_states
end
""" Update V value and add to queue """
function updateV!(learner::PrioritizedSweeping, state)
    learner.VS[state] = maximum(learner.QSA[state, :])
    prio = abs(learner.VS[state] - learner.US[state])
    # @show learner.VS[s], learner.US[s], p
    addtoqueue!(learner.prioQ, state, prio)
    # @show learner.prioQ
    if learner.do_debug_print
        println("prio=$(round(prio,digits=3))")
    end
end

function do_debug_print_beforeupdate(learner::PrioritizedSweeping,
                                    state, action, done)
    if done
        println(" **************** DONE!!! ************** ")
    end
    println("Before update:")
    if in(:Ps1a0s0, fieldnames(typeof(learner.Testimate)))
        for s in 1:learner.nr_states
            @show learner.Testimate.Ps1a0s0[s][(action, state)]
        end
    else
        for s in 1:learner.nr_states
            @show learner.Testimate.Ns1a0s0[s][(action, state)]
        end
    @show learner.Testimate.Nsa[action, state]
    end
    @show learner.Restimate.R
    for s in 1:size(learner.QSA, 1)
        @show learner.QSA[s, :]
    end
end
function do_debug_print_afterupdate(learner::PrioritizedSweeping,
                                state, action)
    println("After update:")
    if in(:Ps1a0s0, fieldnames(typeof(learner.Testimate)))
        for s in 1:learner.nr_states
            @show learner.Testimate.Ps1a0s0[s][(action, state)]
        end
    else
        for s in 1:learner.nr_states
            @show learner.Testimate.Ns1a0s0[s][(action, state)]
        end
        @show learner.Testimate.Nsa[action, state]
    end
    @show learner.Restimate.R
    for s in 1:size(learner.QSA, 1)
        @show learner.QSA[s, :]
    end
end
function do_debug_print_final(learner::PrioritizedSweeping)
    println("Final:")
    for s in 1:size(learner.QSA,1)
        @show learner.QSA[s, :]
    end
    println("--------------")
end

""" Update: main function """
function update!(learner::PrioritizedSweeping, state, action, next_state, reward, done)

    if learner.do_debug_print
        do_debug_print_beforeupdate(learner, state, action, done)
    end

    # Update Tsas'
    updatet!(learner.Testimate, state, action, next_state, done)
    # Update Rsas'
    updater!(learner.Restimate, state, action, next_state, reward)
    # Update Q values
    updateq!(learner, state, action, next_state, reward, done)

    if learner.do_debug_print
        do_debug_print_afterupdate(learner, state, action)
    end

    # Go through priotity queue
    processqueue!(learner)

    if learner.do_debug_print
        do_debug_print_final(learner)
    end
end
export update!