diff --git a/test/runtests.jl b/test/runtests.jl index 14ea961..2955417 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,16 +1,7 @@ using Test using TestExtras -using Random -# using TensorKit: TensorKitSectors using TensorKitSectors -using TensorOperations -using Base.Iterators: take, product -using LinearAlgebra: LinearAlgebra -const TKS = TensorKitSectors - -include("testsetup.jl") -using .TestSetup include("newsectors.jl") using .NewSectors @@ -46,9 +37,10 @@ const sectorlist = ( TimeReversed{FermionParity ⊠ SU2Irrep ⊠ NewSU2Irrep}, ) -@testset "$(TensorKitSectors.type_repr(I))" for I in sectorlist - @include("sectors.jl") -end +include("testsuite.jl") +using .SectorTestSuite + +foreach(SectorTestSuite.test_sector, sectorlist) @testset "Deligne product" begin sectorlist′ = (Trivial, sectorlist...) diff --git a/test/sectors.jl b/test/sectors.jl index 5f56cfa..58fed46 100644 --- a/test/sectors.jl +++ b/test/sectors.jl @@ -1,99 +1,106 @@ -Istr = TKS.type_repr(I) -@testset "Sector $Istr: Basic properties" begin +using TensorOperations +using LinearAlgebra + +@testsuite "Basic properties" I -> begin s = (randsector(I), randsector(I), randsector(I)) - @test eval(Meta.parse(sprint(show, I))) == I - @test eval(Meta.parse(TKS.type_repr(I))) == I - @test eval(Meta.parse(sprint(show, s[1]))) == s[1] - @test @constinferred(hash(s[1])) == hash(deepcopy(s[1])) - @test @constinferred(unit(s[1])) == @constinferred(unit(I)) - @constinferred dual(s[1]) - @constinferred dim(s[1]) - @constinferred frobenius_schur_phase(s[1]) - @constinferred frobenius_schur_indicator(s[1]) - @constinferred Nsymbol(s...) - @constinferred Asymbol(s...) - B = @constinferred Bsymbol(s...) - F = @constinferred Fsymbol(s..., s...) + @test Base.eval(Main, Meta.parse(sprint(show, I))) == I + @test Base.eval(Main, Meta.parse(TensorKitSectors.type_repr(I))) == I + @test Base.eval(Main, Meta.parse(sprint(show, s[1]))) == s[1] + @test @testinferred(hash(s[1])) == hash(deepcopy(s[1])) + @test @testinferred(unit(s[1])) == @testinferred(unit(I)) + @testinferred dual(s[1]) + @testinferred dim(s[1]) + @testinferred frobenius_schur_phase(s[1]) + @testinferred frobenius_schur_indicator(s[1]) + @testinferred Nsymbol(s...) + @testinferred Asymbol(s...) + B = @testinferred Bsymbol(s...) + F = @testinferred Fsymbol(s..., s...) if BraidingStyle(I) isa HasBraiding - R = @constinferred Rsymbol(s...) + R = @testinferred Rsymbol(s...) if FusionStyle(I) === SimpleFusion() - @test typeof(R * F) <: @constinferred sectorscalartype(I) + @test typeof(R * F) <: @testinferred sectorscalartype(I) else - @test Base.promote_op(*, eltype(R), eltype(F)) <: @constinferred sectorscalartype(I) + @test Base.promote_op(*, eltype(R), eltype(F)) <: @testinferred sectorscalartype(I) end else if FusionStyle(I) === SimpleFusion() - @test typeof(F) <: @constinferred sectorscalartype(I) + @test typeof(F) <: @testinferred sectorscalartype(I) else - @test eltype(F) <: @constinferred sectorscalartype(I) + @test eltype(F) <: @testinferred sectorscalartype(I) end end - it = @constinferred s[1] ⊗ s[2] - @constinferred ⊗(s..., s...) + @testinferred(s[1] ⊗ s[2]) + @testinferred(⊗(s..., s...)) end -@testset "Sector $Istr: Value iterator" begin + +@testsuite "Value iterator" I -> begin @test eltype(values(I)) == I sprev = unit(I) for (i, s) in enumerate(values(I)) - @test !isless(s, sprev) # confirm compatibility with sort order - @test s == @constinferred (values(I)[i]) + @test !isless(s, sprev) + @test s == @testinferred(values(I)[i]) @test findindex(values(I), s) == i sprev = s i >= 10 && break end @test unit(I) == first(values(I)) @test length(allunits(I)) == 1 - @test (@constinferred findindex(values(I), unit(I))) == 1 + @test (@testinferred findindex(values(I), unit(I))) == 1 for s in smallset(I) - @test (@constinferred values(I)[findindex(values(I), s)]) == s + @test (@testinferred values(I)[findindex(values(I), s)]) == s end end -if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @testset "Sector $Istr: fusion tensor and F-move and R-move" begin - for a in smallset(I), b in smallset(I) - for c in ⊗(a, b) - X1 = permutedims(fusiontensor(a, b, c), (2, 1, 3, 4)) - X2 = fusiontensor(b, a, c) - l = dim(a) * dim(b) * dim(c) - R = LinearAlgebra.transpose(Rsymbol(a, b, c)) - sz = (l, convert(Int, Nsymbol(a, b, c))) - @test reshape(X1, sz) ≈ reshape(X2, sz) * R - end - end - for a in smallset(I), b in smallset(I), c in smallset(I) - for e in ⊗(a, b), f in ⊗(b, c) - for d in intersect(⊗(e, c), ⊗(a, f)) - X1 = fusiontensor(a, b, e) - X2 = fusiontensor(e, c, d) - Y1 = fusiontensor(b, c, f) - Y2 = fusiontensor(a, f, d) - @tensor f1[-1, -2, -3, -4] := conj(Y2[a, f, d, -4]) * - conj(Y1[b, c, f, -3]) * X1[a, b, e, -1] * X2[e, c, d, -2] - if FusionStyle(I) isa MultiplicityFreeFusion - f2 = fill(Fsymbol(a, b, c, d, e, f) * dim(d), (1, 1, 1, 1)) - else - f2 = Fsymbol(a, b, c, d, e, f) * dim(d) - end - @test isapprox(f1, f2; atol = 1.0e-12, rtol = 1.0e-12) + +@testsuite "fusion tensor and F-move" I -> begin + (BraidingStyle(I) isa Bosonic && hasfusiontensor(I)) || return nothing + for a in smallset(I), b in smallset(I), c in smallset(I) + for e in ⊗(a, b), f in ⊗(b, c) + for d in intersect(⊗(e, c), ⊗(a, f)) + X1 = fusiontensor(a, b, e) + X2 = fusiontensor(e, c, d) + Y1 = fusiontensor(b, c, f) + Y2 = fusiontensor(a, f, d) + @tensor f1[-1, -2, -3, -4] := conj(Y2[a, f, d, -4]) * + conj(Y1[b, c, f, -3]) * X1[a, b, e, -1] * X2[e, c, d, -2] + if FusionStyle(I) isa MultiplicityFreeFusion + f2 = fill(Fsymbol(a, b, c, d, e, f) * dim(d), (1, 1, 1, 1)) + else + f2 = Fsymbol(a, b, c, d, e, f) * dim(d) end + @test isapprox(f1, f2; atol = 1.0e-12, rtol = 1.0e-12) end end end end -if hasfusiontensor(I) - @testset "Orthogonality of fusiontensors" begin - for a in smallset(I), b in smallset(I) - cs = vec(collect(a ⊗ b)) - CGCs = map(c -> reshape(fusiontensor(a, b, c), :, dim(c)), cs) - M = map(Iterators.product(CGCs, CGCs)) do (cgc1, cgc2) - return LinearAlgebra.norm(cgc1' * cgc2) - end - @test isapprox(M' * M, LinearAlgebra.Diagonal(dim.(cs)); atol = 1.0e-12) + +@testsuite "fusion tensor and F-move and R-move" I -> begin + (BraidingStyle(I) isa Bosonic && hasfusiontensor(I)) || return nothing + for a in smallset(I), b in smallset(I) + for c in ⊗(a, b) + X1 = permutedims(fusiontensor(a, b, c), (2, 1, 3, 4)) + X2 = fusiontensor(b, a, c) + l = dim(a) * dim(b) * dim(c) + R = LinearAlgebra.transpose(Rsymbol(a, b, c)) + sz = (l, convert(Int, Nsymbol(a, b, c))) + @test reshape(X1, sz) ≈ reshape(X2, sz) * R end end end -@testset "Sector $Istr: Unitarity of F-move" begin +@testsuite "Orthogonality of fusiontensors" I -> begin + hasfusiontensor(I) || return nothing + for a in smallset(I), b in smallset(I) + cs = vec(collect(a ⊗ b)) + CGCs = map(c -> reshape(fusiontensor(a, b, c), :, dim(c)), cs) + M = map(Iterators.product(CGCs, CGCs)) do (cgc1, cgc2) + return LinearAlgebra.norm(cgc1' * cgc2) + end + @test isapprox(M' * M, LinearAlgebra.Diagonal(dim.(cs)); atol = 1.0e-12) + end +end + +@testsuite "Unitarity of F-move" I -> begin for a in smallset(I), b in smallset(I), c in smallset(I) for d in ⊗(a, b, c) es = collect(intersect(⊗(a, b), map(dual, ⊗(c, dual(d))))) @@ -105,10 +112,7 @@ end Fblocks = Vector{Any}() for e in es, f in fs Fs = Fsymbol(a, b, c, d, e, f) - push!( - Fblocks, - reshape(Fs, (size(Fs, 1) * size(Fs, 2), size(Fs, 3) * size(Fs, 4))) - ) + push!(Fblocks, reshape(Fs, (size(Fs, 1) * size(Fs, 2), size(Fs, 3) * size(Fs, 4)))) end F = hvcat(length(fs), Fblocks...) end @@ -116,20 +120,22 @@ end end end end -@testset "Sector $Istr: Triangle equation" begin + +@testsuite "Triangle equation" I -> begin for a in smallset(I), b in smallset(I) @test triangle_equation(a, b; atol = 1.0e-12, rtol = 1.0e-12) end end -@testset "Sector $Istr: Pentagon equation" begin + +@testsuite "Pentagon equation" I -> begin for a in smallset(I), b in smallset(I), c in smallset(I), d in smallset(I) @test pentagon_equation(a, b, c, d; atol = 1.0e-12, rtol = 1.0e-12) end end -if BraidingStyle(I) isa HasBraiding - @testset "Sector $Istr: Hexagon equation" begin - for a in smallset(I), b in smallset(I), c in smallset(I) - @test hexagon_equation(a, b, c; atol = 1.0e-12, rtol = 1.0e-12) - end + +@testsuite "Hexagon equation" I -> begin + BraidingStyle(I) isa HasBraiding || return nothing + for a in smallset(I), b in smallset(I), c in smallset(I) + @test hexagon_equation(a, b, c; atol = 1.0e-12, rtol = 1.0e-12) end end diff --git a/test/testsetup.jl b/test/testsetup.jl deleted file mode 100644 index a3fd255..0000000 --- a/test/testsetup.jl +++ /dev/null @@ -1,48 +0,0 @@ -""" - module TestSetup - -Basic utility for testing sectors. -""" -module TestSetup - -export smallset, randsector, hasfusiontensor - -using TensorKitSectors, Test, Random -using TestExtras -using Base.Iterators: take, product - -smallset(::Type{I}) where {I <: Sector} = take(values(I), 5) -function smallset(::Type{ProductSector{Tuple{I1, I2}}}) where {I1, I2} - iter = product(smallset(I1), smallset(I2)) - s = collect(i ⊠ j for (i, j) in iter if dim(i) * dim(j) <= 6) - return length(s) > 6 ? rand(s, 6) : s -end -function smallset(::Type{ProductSector{Tuple{I1, I2, I3}}}) where {I1, I2, I3} - iter = product(smallset(I1), smallset(I2), smallset(I3)) - s = collect(i ⊠ j ⊠ k for (i, j, k) in iter if dim(i) * dim(j) * dim(k) <= 6) - return length(s) > 6 ? rand(s, 6) : s -end -function randsector(::Type{I}) where {I <: Sector} - s = collect(smallset(I)) - a = rand(s) - while isunit(a) # don't use trivial label - a = rand(s) - end - return a -end -randsector(::Type{I}) where {I <: Union{Trivial, PlanarTrivial}} = unit(I) - -function hasfusiontensor(I::Type{<:Sector}) - try - fusiontensor(unit(I), unit(I), unit(I)) - return true - catch e - if e isa MethodError - return false - else - rethrow(e) - end - end -end - -end # module TestSetup diff --git a/test/testsuite.jl b/test/testsuite.jl new file mode 100644 index 0000000..5932a9c --- /dev/null +++ b/test/testsuite.jl @@ -0,0 +1,167 @@ +""" + module SectorTestSuite + +Test suite and utilities that ensure a reusable way of verifying the required interface for a `Sector` type. +Framework based on the GPUArrays testsuite. + +Downstream packages may include this test suite as follows: + +```julia +import TensorKitSectors +testsuite_path = joinpath( + dirname(dirname(pathof(TensorKitSectors))), # TensorKitSectors root + "test", "testsuite.jl" +) +include(testsuite_path) + +SectorTestSuite.test_sectortype(MySectorType) +``` + +Additionally, this test suite exports the following convenience testing utilities: +* [`smallset`](@ref) +* [`randsector`](@ref) +* [`hasfusiontensor`](@ref) +""" +module SectorTestSuite + +export smallset, randsector, hasfusiontensor + +using Test +using TestExtras +using TensorKitSectors +using TensorKitSectors: type_repr +using Random +using Base.Iterators: take, product + +const tests = Dict() + +""" + @testsuite name I -> begin + # test code here + end + +Register a sector testsuite. +The body is executed with a single argument `I`, the concrete `Sector` type under test. +""" +macro testsuite(name, ex) + safe_name = lowercase(replace(replace(name, " " => "_"), "/" => "_")) + fn = Symbol("test_$(safe_name)") + return quote + $(esc(fn))(I) = $(esc(ex))(I) + @assert !haskey(tests, $name) + tests[$name] = $fn + end +end + +""" + test_sectortype(I::Type) + +Runs the entire TensorKitSectors test suite on sector type `I`. +""" +function test_sector(I::Type) + return @testset "$(type_repr(I))" begin + for (name, fun) in tests + code = quote + $fun($I) + end + @eval @testset $name $code + end + end +end + +""" + @testinferred [AllowedTypes] ex + +Like `Test.@inferred`, but registers failures through a test, rather than an error. +""" +macro testinferred(ex) + return _inferred(ex, __module__) +end +macro testinferred(ex, allow) + return _inferred(ex, __module__, allow) +end + +# Implementation copied from Test._inferred: +function _inferred(ex, mod, allow = :(Union{})) + if Meta.isexpr(ex, :ref) + ex = Expr(:call, :getindex, ex.args...) + end + Meta.isexpr(ex, :call)|| error("@testinferred requires a call expression") + farg = ex.args[1] + if isa(farg, Symbol) && farg !== :.. && first(string(farg)) == '.' + farg = Symbol(string(farg)[2:end]) + ex = Expr( + :call, GlobalRef(Test, :_materialize_broadcasted), + farg, ex.args[2:end]... + ) + end + result = let ex = ex + quote + let allow = $(esc(allow)) + allow isa Type || throw(ArgumentError("@inferred requires a type as second argument")) + $( + if any(@nospecialize(a) -> (Meta.isexpr(a, :kw) || Meta.isexpr(a, :parameters)), ex.args) + # Has keywords + # Create the call expression with escaped user expressions + call_expr = :($(esc(ex.args[1]))(args...; kwargs...)) + quote + args, kwargs, result = $(esc(Expr(:call, _args_and_call, ex.args[2:end]..., ex.args[1]))) + # wrap in dummy hygienic-scope to work around scoping issues with `call_expr` already having `esc` on the necessary parts + inftype = $(Expr(:var"hygienic-scope", Base.gen_call_with_extracted_types(mod, Base.infer_return_type, call_expr; is_source_reflection = false), Test)) + end + else + # No keywords + quote + args = ($([esc(ex.args[i]) for i in 2:length(ex.args)]...),) + result = $(esc(ex.args[1]))(args...) + inftype = Base.infer_return_type($(esc(ex.args[1])), Base.typesof(args...)) + end + end + ) + rettype = result isa Type ? Type{result} : typeof(result) + @test rettype <: allow || rettype == Base.typesplit(inftype, allow) + result + end + end + end + return Base.remove_linenums!(result) +end + +smallset(::Type{I}) where {I <: Sector} = take(values(I), 5) +function smallset(::Type{ProductSector{Tuple{I1, I2}}}) where {I1, I2} + iter = product(smallset(I1), smallset(I2)) + s = collect(i ⊠ j for (i, j) in iter if dim(i) * dim(j) <= 6) + return length(s) > 6 ? rand(s, 6) : s +end +function smallset(::Type{ProductSector{Tuple{I1, I2, I3}}}) where {I1, I2, I3} + iter = product(smallset(I1), smallset(I2), smallset(I3)) + s = collect(i ⊠ j ⊠ k for (i, j, k) in iter if dim(i) * dim(j) * dim(k) <= 6) + return length(s) > 6 ? rand(s, 6) : s +end + +function randsector(::Type{I}) where {I <: Sector} + s = collect(smallset(I)) + a = Random.rand(s) + while isunit(a) # don't use trivial label + a = Random.rand(s) + end + return a +end +randsector(::Type{I}) where {I <: Union{Trivial, PlanarTrivial}} = unit(I) + +function hasfusiontensor(I::Type{<:Sector}) + try + fusiontensor(unit(I), unit(I), unit(I)) + return true + catch e + if e isa MethodError + return false + else + rethrow(e) + end + end +end + +include("sectors.jl") + +end # module SectorTestSuite