From 7d352d2aa88494198a06c4f5f80b32e91210240b Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 6 Jul 2026 17:02:50 +0200 Subject: [PATCH 01/13] implement testsuite --- test/TensorKitTestSuite.jl | 241 ++++++++++ test/setup.jl | 93 +--- test/testsuite/fusiontrees.jl | 560 ++++++++++++++++++++++ test/testsuite/spaces.jl | 110 +++++ test/testsuite/tensors.jl | 872 ++++++++++++++++++++++++++++++++++ 5 files changed, 1791 insertions(+), 85 deletions(-) create mode 100644 test/TensorKitTestSuite.jl create mode 100644 test/testsuite/fusiontrees.jl create mode 100644 test/testsuite/spaces.jl create mode 100644 test/testsuite/tensors.jl diff --git a/test/TensorKitTestSuite.jl b/test/TensorKitTestSuite.jl new file mode 100644 index 000000000..bdfaec0d9 --- /dev/null +++ b/test/TensorKitTestSuite.jl @@ -0,0 +1,241 @@ +""" + module TensorKitTestSuite + +Test suite and utilities that ensure a reusable way of verifying that a custom `Sector` +type is correctly supported by fusion trees, `GradedSpace`, and `TensorMap`. + +Downstream packages may include this test suite as follows: + +```julia +import TensorKit +testsuite_path = joinpath( + dirname(dirname(pathof(TensorKit))), # TensorKit root + "test", "TensorKitTestSuite.jl" +) +include(testsuite_path) +using .TensorKitTestSuite + +TensorKitTestSuite.test_fusiontrees(MySector) +TensorKitTestSuite.test_spaces(MySector) +TensorKitTestSuite.test_tensors((V1, V2, V3, V4, V5)) # 5 mutually compatible spaces +``` + +The three entry points above are independent and may be run selectively. + +Additionally, this test suite exports the following convenience testing utilities: +* [`smallset`](@ref) +* [`randsector`](@ref) +* [`random_fusion`](@ref) +* [`hasfusiontensor`](@ref) +* [`force_planar`](@ref) +""" +module TensorKitTestSuite + +export test_fusiontrees, test_spaces, test_tensors +export smallset, randsector, random_fusion, hasfusiontensor, force_planar + +using Test +using TestExtras +using Random +using Random: randperm +using LinearAlgebra +using TupleTools +using Combinatorics: permutations +using TensorOperations +using MatrixAlgebraKit: left_polar, isunitary +using Base.Iterators: take, product + +using TensorKit +using TensorKit: type_repr, FusionTreeBlock, ℙ, PlanarTrivial, hassector, HomSpace +import TensorKit as TK +using TensorKitSectors + +const testgroups = Dict{Symbol, Dict{String, Expr}}( + :fusiontrees => Dict{String, Expr}(), + :spaces => Dict{String, Expr}(), + :tensors => Dict{String, Expr}(), +) + +# cannot just esc() the body, because that would make it a closure, compile it at a fixed world age and break constprop=true +# workaround here is to store an unevaluated `Expr` and `Core.eval` it in a fresh `let` block every time +# this is at the cost of not reusing compiled code +function _run_testsuite_entry(lambda::Expr, arg) + param, body = lambda.args[1], lambda.args[2] + letex = Expr(:let, Expr(:(=), param, arg), body) # let block makes param local to the body + return Core.eval(@__MODULE__, letex) +end + +""" + @testsuite testgroup name I -> begin + # test code here + end + +Register a testsuite entry under `testgroup` (one of `:fusiontrees`, `:spaces`,`:tensors`). +The body is executed with a single argument: the concrete `Sector` type +under test (for `:fusiontrees`/`:spaces`), or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). + +Important: Whatever is passed as `name` becomes part of the generated function that must be called to run that body. +In particular, a `safe_name` is made where `name`'s spaces are replaced by underscores, and everything becomes lowercase. +One then calls `test__`. This way, individual entries can be invoked without running the whole test group. +""" +macro testsuite(testgroup, name, ex) #TODO: rename + Meta.isexpr(ex, :(->)) || error("@testsuite requires an `arg -> body` expression") + testgroupsym = testgroup isa QuoteNode ? testgroup.value : testgroup + safe_name = lowercase(replace(name, r"[^A-Za-z0-9]+" => "_")) + fn = Symbol("test_", testgroupsym, "_", safe_name) + group = QuoteNode(testgroupsym) + return quote + @assert !haskey(testgroups[$group], $name) "duplicate testsuite name: $($name) ($($group))" + testgroups[$group][$name] = $(QuoteNode(ex)) + $(esc(fn))(arg) = _run_testsuite_entry(testgroups[$group][$name], arg) + nothing + end +end + +""" + test_fusiontrees(I::Type{<:Sector}) + +Runs the entire fusion-tree manipulation test suite (single and double fusion trees) +on sector type `I`. +""" +function test_fusiontrees(I::Type{<:Sector}) + return @testset "$(type_repr(I))" begin + for (name, lambda) in testgroups[:fusiontrees] + @testset "$name" begin + _run_testsuite_entry(lambda, I) + end + end + end +end + +""" + test_spaces(I::Type{<:Sector}) + +Runs the `GradedSpace` test suite on sector type `I`. +""" +function test_spaces(I::Type{<:Sector}) #TODO: change since it's just 1 testsuite, or change to get the hom-space tests in here + return @testset "$(type_repr(I))" begin + for (name, lambda) in testgroups[:spaces] + @testset "$name" begin + _run_testsuite_entry(lambda, I) + end + end + end +end + +""" + test_tensors(V) + +Runs the tensor operation test suite (construction, contractions, linear algebra, +index manipulations, braiding, `HomSpace`) on `V`, a 5-tuple of mutually +compatible `ElementarySpace`s. See `setup.jl` for space design considerations. +""" +function test_tensors(V::NTuple{5, GradedSpace{I, NTuple{N, Int}}}) where {I <: Sector, N} + return @testset "$(type_repr(I))" begin + for (name, lambda) in testgroups[:tensors] + @testset "$name" begin + _run_testsuite_entry(lambda, V) + end + end + end +end + +# Sector utilities +# ---------------- +# TODO: replace with the ones from TensorKitSectors.SectorTestSuite +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 + +function hasfusiontensor(I::Type{<:Sector}) + try + u = first(allunits(I)) + fusiontensor(u, u, u) + return true + catch e + if e isa MethodError + return false + else + rethrow(e) + end + end +end + +""" + random_fusion(I::Type{<:Sector}, ::Val{N}) where {N} + +Returns an `N`-tuple of sectors of type `I` that can consistently be used as the +uncoupled sectors of a fusion tree, i.e. consecutive sectors have a non-empty fusion product. +""" +function random_fusion(I::Type{<:Sector}, ::Val{N}) where {N} #TODO: merge with TKS random_fusion + N == 1 && return (randsector(I),) + tail = random_fusion(I, Val(N - 1)) + s = randsector(I) + counter = 0 + while isempty(⊗(s, first(tail))) && counter < 20 + counter += 1 + s = (counter < 20) ? randsector(I) : leftunit(first(tail)) + end + return (s, tail...) +end + +""" + force_planar(obj) + +Replace an object with a planar equivalent -- i.e. one that disallows braiding. +""" +force_planar(V::ComplexSpace) = isdual(V) ? (ℙ^dim(V))' : ℙ^dim(V) +function force_planar(V::GradedSpace) + return GradedSpace((c ⊠ PlanarTrivial() => dim(V, c) for c in sectors(V))..., isdual(V)) +end +force_planar(V::ProductSpace) = mapreduce(force_planar, ⊗, V) +function force_planar(tsrc::TensorMap{<:Any, ComplexSpace}) + tdst = similar(tsrc, force_planar(codomain(tsrc)) ← force_planar(domain(tsrc))) + copyto!(block(tdst, PlanarTrivial()), block(tsrc, Trivial())) + return tdst +end +function force_planar(tsrc::TensorMap{<:Any, <:GradedSpace}) + tdst = similar(tsrc, force_planar(codomain(tsrc)) ← force_planar(domain(tsrc))) + for (c, b) in blocks(tsrc) + copyto!(block(tdst, c ⊠ PlanarTrivial()), b) + end + return tdst +end + +# # helper function to check that d - dim(c) < dim(V) <= d where c is the largest sector +# # to allow for truncations to have some margin with larger sectors +# function dim_isapprox(V::ElementarySpace, d::Int) +# dim_c_max = maximum(dim, sectors(V); init = 1) +# return max(0, d - dim_c_max) ≤ dim(V) ≤ d + dim_c_max +# end +# function dim_isapprox(V::ProductSpace, d::Int) +# dim_c_max = maximum(dim, blocksectors(V); init = 1) +# return max(0, d - dim_c_max) ≤ dim(V) ≤ d + dim_c_max +# end + +_isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) +_isunitary(x; kwargs...) = isunitary(x; kwargs...) +_isone(x; kwargs...) = isapprox(x, one(x); kwargs...) + +include("testsuite/fusiontrees.jl") +include("testsuite/spaces.jl") +include("testsuite/tensors.jl") + +end # module TensorKitTestSuite diff --git a/test/setup.jl b/test/setup.jl index 9c8244dab..019938ba1 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -9,20 +9,23 @@ export sectorlist, fast_sectorlist export default_spacelist, factorization_spacelist, ad_spacelist export test_ad_rrule export _isunitary, _isone +export TensorKitTestSuite using Random using Test: @test using TensorKit -using TensorKit: ℙ, PlanarTrivial using TensorKitSectors using TensorOperations: IndexTuple, Index2Tuple -using Base.Iterators: take, product using TupleTools using MatrixAlgebraKit: MatrixAlgebraKit, diagview using ChainRulesCore: NoTangent using ChainRulesTestUtils: ChainRulesTestUtils, test_rrule using Zygote: Zygote, rrule_via_ad +include(joinpath(@__DIR__, "TensorKitTestSuite.jl")) +using .TensorKitTestSuite +using .TensorKitTestSuite: _isunitary, _isone + Random.seed!(123456) # IndexTuple utility @@ -63,91 +66,11 @@ end default_tol(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 default_tol(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 -# Sector utility +# Sector lists # -------------- -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 -function hasfusiontensor(I::Type{<:Sector}) - try - u = first(allunits(I)) - fusiontensor(u, u, u) - return true - catch e - if e isa MethodError - return false - else - rethrow(e) - end - end -end - -""" - force_planar(obj) - -Replace an object with a planar equivalent -- i.e. one that disallows braiding. -""" -force_planar(V::ComplexSpace) = isdual(V) ? (ℙ^dim(V))' : ℙ^dim(V) -function force_planar(V::GradedSpace) - return GradedSpace((c ⊠ PlanarTrivial() => dim(V, c) for c in sectors(V))..., isdual(V)) -end -force_planar(V::ProductSpace) = mapreduce(force_planar, ⊗, V) -function force_planar(tsrc::TensorMap{<:Any, ComplexSpace}) - tdst = similar(tsrc, force_planar(codomain(tsrc)) ← force_planar(domain(tsrc))) - copyto!(block(tdst, PlanarTrivial()), block(tsrc, Trivial())) - return tdst -end -function force_planar(tsrc::TensorMap{<:Any, <:GradedSpace}) - tdst = similar(tsrc, force_planar(codomain(tsrc)) ← force_planar(domain(tsrc))) - for (c, b) in blocks(tsrc) - copyto!(block(tdst, c ⊠ PlanarTrivial()), b) - end - return tdst -end - -function random_fusion(I::Type{<:Sector}, ::Val{N}) where {N} # for fusion tree tests - N == 1 && return (randsector(I),) - tail = random_fusion(I, Val(N - 1)) - s = randsector(I) - counter = 0 - while isempty(⊗(s, first(tail))) && counter < 20 - counter += 1 - s = (counter < 20) ? randsector(I) : leftunit(first(tail)) - end - return (s, tail...) -end - -# # helper function to check that d - dim(c) < dim(V) <= d where c is the largest sector -# # to allow for truncations to have some margin with larger sectors -# function dim_isapprox(V::ElementarySpace, d::Int) -# dim_c_max = maximum(dim, sectors(V); init = 1) -# return max(0, d - dim_c_max) ≤ dim(V) ≤ d + dim_c_max -# end -# function dim_isapprox(V::ProductSpace, d::Int) -# dim_c_max = maximum(dim, blocksectors(V); init = 1) -# return max(0, d - dim_c_max) ≤ dim(V) ≤ d + dim_c_max -# end - -_isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) -_isunitary(x; kwargs...) = isunitary(x; kwargs...) -_isone(x; kwargs...) = isapprox(x, one(x); kwargs...) +# TODO: make the changes to make this compatible with also importing TKS's SectorTestSuite +# there's some overlap in names, and TKS's SectorTestSuite has some of these functions already uniquefusionsectorlist = ( Z2Irrep, Z3Irrep, Z4Irrep, Z3Irrep ⊠ Z4Irrep, U1Irrep, diff --git a/test/testsuite/fusiontrees.jl b/test/testsuite/fusiontrees.jl new file mode 100644 index 000000000..3a57f0c95 --- /dev/null +++ b/test/testsuite/fusiontrees.jl @@ -0,0 +1,560 @@ +# Fusion tree manipulations + +# Single fusion trees +# ------------------- +@testsuite :fusiontrees "iterate and printing" I -> begin + N = 5 + out = random_fusion(I, Val(N)) + isdual = ntuple(n -> rand(Bool), N) + incoupled = rand(collect(⊗(out...))) # renamed to not break infix `in` in this scope + numtrees = length(fusiontrees(out, incoupled, isdual)) + @test numtrees == count(n -> true, fusiontrees(out, incoupled, isdual)) + while !(0 < numtrees < 30) && !(one(incoupled) in ⊗(out...)) + out = ntuple(n -> randsector(I), N) + incoupled = rand(collect(⊗(out...))) + numtrees = length(fusiontrees(out, incoupled, isdual)) + @test numtrees == count(n -> true, fusiontrees(out, incoupled, isdual)) + end + it = @constinferred fusiontrees(out, incoupled, isdual) + @constinferred Nothing iterate(it) + f, s = iterate(it) + @constinferred Nothing iterate(it, s) + @test f == @constinferred first(it) + @test eval(Meta.parse(sprint(show, f; context = (:module => @__MODULE__)))) == f +end + +@testsuite :fusiontrees "constructor properties" I -> begin + for u in allunits(I) + @constinferred FusionTree((), u, (), (), ()) + @constinferred FusionTree((u,), u, (false,), (), ()) + @constinferred FusionTree((u, u), u, (false, false), (), (1,)) + @constinferred FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) + @constinferred FusionTree( + (u, u, u, u), u, (false, false, false, false), (u, u), (1, 1, 1) + ) + @test_throws MethodError FusionTree((u, u, u), u, (false, false), (u,), (1, 1)) + @test_throws MethodError FusionTree( + (u, u, u), u, (false, false, false), (u, u), (1, 1) + ) + @test_throws MethodError FusionTree( + (u, u, u), u, (false, false, false), (u,), (1, 1, 1) + ) + @test_throws MethodError FusionTree((u, u, u), u, (false, false, false), (), (1,)) + + f = FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) + @test sectortype(f) == I + @test length(f) == 3 + @test FusionStyle(f) == FusionStyle(I) + @test BraidingStyle(f) == BraidingStyle(I) + + if FusionStyle(I) isa UniqueFusion + @constinferred FusionTree((), u, ()) + @constinferred FusionTree((u,), u, (false,)) + @constinferred FusionTree((u, u), u, (false, false)) + @constinferred FusionTree((u, u, u), u) + if UnitStyle(I) isa SimpleUnit + @constinferred FusionTree((u, u, u, u)) + else + @test_throws ArgumentError FusionTree((u, u, u, u)) + end + @test_throws MethodError FusionTree((u, u), u, (false, false, false)) + else + @test_throws ArgumentError FusionTree((), u, ()) + @test_throws ArgumentError FusionTree((u,), u, (false,)) + @test_throws ArgumentError FusionTree((u, u), u, (false, false)) + @test_throws ArgumentError FusionTree((u, u, u), u) + if I <: ProductSector && UnitStyle(I) isa GenericUnit + @test_throws DomainError FusionTree((u, u, u, u)) + else + @test_throws ArgumentError FusionTree((u, u, u, u)) + end + end + end +end + +# Basic associativity manipulations of individual fusion trees +@testsuite :fusiontrees "split and join" I -> begin + N = 6 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdual = ntuple(n -> rand(Bool), N) + f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) + for i in 0:N + f₁, f₂ = @constinferred TK.split(f, $i) + @test length(f₁) == i + @test length(f₂) == N - i + 1 + f′ = @constinferred TK.join(f₁, f₂) + @test f′ == f + end +end + +@testsuite :fusiontrees "multi Fmove" I -> begin + N = 6 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdualrest = ntuple(n -> rand(Bool), N - 1) + for isdual in ((false, isdualrest...), (true, isdualrest...)) + trees = collect(fusiontrees(uncoupled, coupled, isdual)) + trees = trees[randperm(length(trees))[1:rand(1:min(5, length(trees)))]] # limit number of tests? + for f in trees + a = f.uncoupled[1] + isduala = f.isdual[1] + c = f.coupled + f′s, coeffs = @constinferred TK.multi_Fmove(f) + @test norm(coeffs) ≈ 1 atol = 1.0e-12 # expansion should have unit norm + d = Dict(f => -one(eltype(eltype(coeffs)))) + for (f′, coeff) in zip(f′s, coeffs) + @test coeff ≈ TK.multi_associator(f, f′) + f′′s, coeff′s = @constinferred TK.multi_Fmove_inv(a, c, f′, isduala) + if FusionStyle(I) isa MultiplicityFreeFusion + @test norm(coeff′s) ≈ 1 atol = 1.0e-12 # expansion should have unit norm + else + for i in 1:Nsymbol(a, f′.coupled, c) + @test norm(getindex.(coeff′s, i)) ≈ 1 atol = 1.0e-12 # expansion should have unit norm for every possible fusion channel at the top vertex + end + end + for (f′′, coeff′) in zip(f′′s, coeff′s) + @test coeff′ ≈ conj(TK.multi_associator(f′′, f′)) + d[f′′] = get(d, f′′, zero(eltype(coeff′))) + sum(coeff .* coeff′) + end + end + @test norm(values(d)) < 1.0e-12 + end + end + + if hasfusiontensor(I) # because no permutations are involved, this also works for fermionic braiding + N = 4 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdualrest = ntuple(n -> rand(Bool), N - 1) + for isdual in ((false, isdualrest...), (true, isdualrest...)) + trees = collect(fusiontrees(uncoupled, coupled, isdual)) + trees = trees[randperm(length(trees))[1:rand(1:min(5, length(trees)))]] # limit number of tests? + for f in trees + ftensor = fusiontensor(f) + ftensor′ = zero(ftensor) + a = f.uncoupled[1] + isduala = f.isdual[1] + c = f.coupled + f′s, coeffs = @constinferred TK.multi_Fmove(f) + for (f′, coeff) in zip(f′s, coeffs) + f′tensor = fusiontensor(f′) + for i in 1:Nsymbol(a, f′.coupled, c) + f′′ = FusionTree{I}((a, f′.coupled), c, (isduala, false), (), (i,)) + f′′tensor = fusiontensor(f′′) + ftensor′ += coeff[i] * tensorcontract(1:(N + 1), f′tensor, [(2:N)..., -1], f′′tensor, [1, -1, N + 1]) + end + end + @test ftensor′ ≈ ftensor atol = 1.0e-12 + end + end + end +end + +@testsuite :fusiontrees "insertat" I -> begin + # just check some basic consistency properties here + # correctness should follow from multi_Fmove tests + N = 4 + out2 = random_fusion(I, Val(N)) + in2 = rand(collect(⊗(out2...))) + isdual2 = ntuple(n -> rand(Bool), N) + f2 = rand(collect(fusiontrees(out2, in2, isdual2))) + for i in 1:N + out1 = random_fusion(I, Val(N)) # guaranteed good fusion + out1 = Base.setindex(out1, in2, i) # can lead to poor fusion + while isempty(⊗(out1...)) # TODO: better way to do this? + out1 = random_fusion(I, Val(N)) + out1 = Base.setindex(out1, in2, i) + end + in1 = rand(collect(⊗(out1...))) + isdual1 = ntuple(n -> rand(Bool), N) + isdual1 = Base.setindex(isdual1, false, i) + f1 = rand(collect(fusiontrees(out1, in1, isdual1))) + + trees = @constinferred TK.insertat(f1, i, f2) + @test norm(values(trees)) ≈ 1 + + if hasfusiontensor(I) + Af1 = fusiontensor(f1) + Af2 = fusiontensor(f2) + Af = tensorcontract( + 1:(2N), Af1, + [1:(i - 1); -1; N - 1 .+ ((i + 1):(N + 1))], + Af2, [i - 1 .+ (1:N); -1] + ) + Af′ = zero(Af) + for (f, coeff) in trees + Af′ .+= coeff .* fusiontensor(f) + end + @test Af ≈ Af′ + end + end +end + +@testsuite :fusiontrees "merging" I -> begin + N = 3 + out1 = random_fusion(I, Val(N)) + out2 = random_fusion(I, Val(N)) + in1 = rand(collect(⊗(out1...))) + in2 = rand(collect(⊗(out2...))) + tp = ⊗(in1, in2) # messy solution but it works + while isempty(tp) + out1 = random_fusion(I, Val(N)) + out2 = random_fusion(I, Val(N)) + in1 = rand(collect(⊗(out1...))) + in2 = rand(collect(⊗(out2...))) + tp = ⊗(in1, in2) + end + + f1 = rand(collect(fusiontrees(out1, in1))) + f2 = rand(collect(fusiontrees(out2, in2))) + + d = @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) + @test norm(values(d)) ≈ 1 + if !(FusionStyle(I) isa GenericFusion) + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) + end + @test dim(in1) * dim(in2) ≈ sum( + abs2(coeff) * dim(c) for c in in1 ⊗ in2 + for μ in 1:Nsymbol(in1, in2, c) + for (f, coeff) in TK.merge(f1, f2, c, μ) + ) + + if hasfusiontensor(I) + for c in in1 ⊗ in2 + for μ in 1:Nsymbol(in1, in2, c) + Af1 = fusiontensor(f1) + Af2 = fusiontensor(f2) + Af0 = fusiontensor(FusionTree((in1, in2), c, (false, false), (), (μ,))) + _Af = tensorcontract( + 1:(N + 2), Af1, [1:N; -1], Af0, [-1; N + 1; N + 2] + ) + Af = tensorcontract( + 1:(2N + 1), Af2, [N .+ (1:N); -1], _Af, [1:N; -1; 2N + 1] + ) + Af′ = zero(Af) + for (f, coeff) in TK.merge(f1, f2, c, μ) + Af′ .+= coeff .* fusiontensor(f) + end + @test Af ≈ Af′ + end + end + end +end + +# Duality tests +@testsuite :fusiontrees "elementary planar trace" I -> begin + N = 5 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdual = ntuple(n -> rand(Bool), N) + f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) + for i in 0:N # insert a (b b̄ ← 1) vertex in the tree after ith uncoupled sector and then trace it away + f₁, f₂ = TK.split(f, i) + c = f₁.coupled + funit = FusionTree{I}((c, rightunit(c)), c, (false, false), (), (1,)) + f′ = TK.join(TK.join(f₁, funit), f₂) + for b in smallset(I) + leftunit(b) == rightunit(c) || continue + out = Dict(f => -sqrtdim(b) * one(fusionscalartype(I))) + fbb = FusionTree{I}((b, dual(b)), leftunit(b), (false, true), (), (1,)) + for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) + d = @constinferred TK.elementary_trace(f′′, i + 1) + for (tree, coeff2) in d + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + out = Dict(f => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) + fbb = FusionTree{I}((b, dual(b)), leftunit(b), (true, false), (), (1,)) + for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) + for (tree, coeff2) in TK.elementary_trace(f′′, i + 1) + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + end + end + # insert f′ in between the two legs of a (b b̄ ← 1) vertex and then trace the outer legs away + f′ = TK.join(f, FusionTree{I}((coupled, dual(coupled)), leftunit(coupled), (false, true), (), (1,))) + for b in smallset(I) + rightunit(b) == leftunit(coupled) || continue + fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (false, false, true), (b,), (1, 1)) + out = Dict(f′ => -sqrtdim(b) * one(fusionscalartype(I))) + for (f′′, coeff) in TK.insertat(fbb, 2, f′) + d = @constinferred TK.elementary_trace(f′′, N + 3) + for (tree, coeff2) in d + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (true, false, false), (b,), (1, 1)) + out = Dict(f′ => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) + for (f′′, coeff) in TK.insertat(fbb, 2, f′) + for (tree, coeff2) in TK.elementary_trace(f′′, N + 3) + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + end +end + +# Double fusion trees +# -------------------- +function _random_doubletree_setup(I::Type{<:Sector}) + N = I <: ProductSector ? 3 : 4 + A = nothing + + if UnitStyle(I) isa SimpleUnit + out = random_fusion(I, Val(N)) + numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) + while !(0 < numtrees < 100) + out = random_fusion(I, Val(N)) + numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) + end + incoming = rand(collect(⊗(out...))) + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + else + out = random_fusion(I, Val(N)) + out2 = random_fusion(I, Val(N)) + tp = ⊗(out...) + tp2 = ⊗(out2...) + while isempty(intersect(tp, tp2)) # guarantee fusion to same coloring + out2 = random_fusion(I, Val(N)) + tp2 = ⊗(out2...) + end + @test_throws ArgumentError fusiontrees((out..., map(dual, out)...)) + incoming = rand(collect(intersect(tp, tp2))) + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) # no permuting + end + + if FusionStyle(I) isa UniqueFusion + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + src = (f1, f2) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A = fusiontensor(src) + end + else + src = FusionTreeBlock{I}((out, out), (ntuple(n -> rand(Bool), N), ntuple(n -> rand(Bool), N))) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A = map(fusiontensor, fusiontrees(src)) + end + end + return N, src, A +end + +@testsuite :fusiontrees "bending" I -> begin + _, src, A = _random_doubletree_setup(I) + # single bend + dst, U = @constinferred TK.bendright(src) + dst2, U2 = @constinferred TK.bendleft(dst) + @test src == dst2 + @test _isone(U2 * U) + # double bend + dst1, U1 = @constinferred TK.bendleft(src) + dst2, U2 = @constinferred TK.bendleft(dst1) + dst3, U3 = @constinferred TK.bendright(dst2) + dst4, U4 = @constinferred TK.bendright(dst3) + @test src == dst4 + @test _isone(U4 * U3 * U2 * U1) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) + p₁ = ntuple(i -> all_inds[i], numout(dst2)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) + U = U2 * U1 + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst2)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(A″ .* U[:, i]) + end + end + end +end + +@testsuite :fusiontrees "folding" I -> begin + _, src, A = _random_doubletree_setup(I) + # single bend + dst, U = @constinferred TK.foldleft(src) + dst2, U2 = @constinferred TK.foldright(dst) + @test src == dst2 + @test _isone(U2 * U) + # double bend + dst1, U1 = @constinferred TK.foldright(src) + dst2, U2 = @constinferred TK.foldright(dst1) + dst3, U3 = @constinferred TK.foldleft(dst2) + dst4, U4 = @constinferred TK.foldleft(dst3) + @test src == dst4 + @test _isone(U4 * U3 * U2 * U1) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + all_inds = TupleTools.circshift((ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...), -2) + p₁ = ntuple(i -> all_inds[i], numout(dst2)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) + U = U2 * U1 + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst2) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst2)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(A″ .* U[:, i]) + end + end + end +end + +@testsuite :fusiontrees "repartitioning" I -> begin + N, src, A = _random_doubletree_setup(I) + for n in 0:(2 * N) + dst, U = @constinferred TK.repartition(src, $n) + # @test _isunitary(U) + + dst′, U′ = repartition(dst, N) + @test _isone(U * U′) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) + p₁ = ntuple(i -> all_inds[i], numout(dst)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst)], numin(dst))) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(A″ .* U[:, i]) + end + end + end + end +end + +@testsuite :fusiontrees "transposition" I -> begin + N, src, A = _random_doubletree_setup(I) + for n in 0:(2N) + i0 = rand(1:(2N)) + p = mod1.(i0 .+ (1:(2N)), 2N) + ip = mod1.(-i0 .+ (1:(2N)), 2N) + p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) + p1, p2 = p′[1:n], p′[(2N):-1:(n + 1)] + ip′ = tuple(getindex.(Ref(vcat(1:n, (2N):-1:(n + 1))), ip)...) + ip1, ip2 = ip′[1:N], ip′[(2N):-1:(N + 1)] + + dst, U = @constinferred transpose(src, (p1, p2)) + dst′, U′ = @constinferred transpose(dst, (ip1, ip2)) + @test _isone(U * U′) + + if BraidingStyle(I) isa Bosonic + dst″, U″ = permute(src, (p1, p2)) + @test U″ ≈ U + end + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(U[:, i] .* A″) + end + end + end + end +end + +@testsuite :fusiontrees "permutation and braiding" I -> begin + BraidingStyle(I) isa HasBraiding || return nothing + N, src, A = _random_doubletree_setup(I) + for n in 0:(2N) + p = (randperm(2 * N)...,) + p1, p2 = p[1:n], p[(n + 1):(2N)] + ip = invperm(p) + ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] + levels = ntuple(identity, 2N) + l1, l2 = levels[1:N], levels[(N + 1):(2N)] + ilevels = TupleTools.getindices(levels, p) + il1, il2 = ilevels[1:n], ilevels[(n + 1):(2N)] + + if BraidingStyle(I) isa SymmetricBraiding + dst, U = @constinferred TensorKit.permute(src, (p1, p2)) + else + dst, U = @constinferred TensorKit.braid(src, (p1, p2), (l1, l2)) + end + + # check norm-preserving + if FusionStyle(I) isa UniqueFusion + @test abs(U) ≈ 1 + else + dim1 = map(fusiontrees(src)) do (f1, f2) + return dim(f1.coupled) + end + dim2 = map(fusiontrees(dst)) do (f1, f2) + return dim(f1.coupled) + end + @test vec(sum(abs2.(U) .* dim2; dims = 1)) ≈ dim1 + end + + # check reversible + if BraidingStyle(I) isa SymmetricBraiding + dst′, U′ = @constinferred TensorKit.permute(dst, (ip1, ip2)) + else + dst′, U′ = @constinferred TensorKit.braid(dst, (ip1, ip2), (il1, il2)) + end + @test _isone(U * U′) + + # check fusiontensor compatibility + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + Aj = sum(A″ .* U[:, i]) + @test Ai ≈ Aj + end + end + end + end +end + +@testsuite :fusiontrees "planar trace" I -> begin + N, src, A = _random_doubletree_setup(I) + if FusionStyle(I) isa UniqueFusion + f1, f1 = src + dst, U = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) + d1 = zip((dst,), (U,)) + else + f1, f1 = first(fusiontrees(src)) + src′ = FusionTreeBlock{I}((f1.uncoupled, f1.uncoupled), (f1.isdual, f1.isdual)) + dst, U = transpose(src′, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) + d1 = zip(fusiontrees(dst), U[:, 1]) + end + + f1front, = TK.split(f1, N - 1) + T = sectorscalartype(I) + d2 = Dict{typeof((f1front, f1front)), T}() + for ((f1′, f2′), coeff′) in d1 + for ((f1′′, f2′′), coeff′′) in TK.planar_trace( + (f1′, f2′), ((2:N...,), (1, ((2N):-1:(N + 3))...)), ((N + 1,), (N + 2,)) + ) + coeff = coeff′ * coeff′′ + d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff + end + end + for ((f1_, f2_), coeff) in d2 + if (f1_, f2_) == (f1front, f1front) + @test coeff ≈ dim(f1.coupled) / dim(f1front.coupled) + else + @test abs(coeff) < 1.0e-12 + end + end +end diff --git a/test/testsuite/spaces.jl b/test/testsuite/spaces.jl new file mode 100644 index 000000000..d92cfc10d --- /dev/null +++ b/test/testsuite/spaces.jl @@ -0,0 +1,110 @@ +# GradedSpace +# =========== +# Ported from the sector-parametrized part of test/symmetries/spaces.jl. + +""" + eval_show(x) + +Use `show` to generate a string representation of `x`, then parse and evaluate the resulting expression. +""" +function eval_show(x) #TODO: move this function to setup so it doesn't repeat + str = sprint(show, x; context = (:module => @__MODULE__)) + ex = Meta.parse(str) + return eval(ex) +end + +@testsuite :spaces "graded space" I -> begin + if Base.IteratorSize(values(I)) === Base.IsInfinite() + set = unique(vcat(allunits(I)..., [randsector(I) for k in 1:10])) + gen = (c => 2 for c in set) + else + gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) + end + V = GradedSpace(gen) + @test eval(Meta.parse(type_repr(typeof(V)))) == typeof(V) + @test eval_show(V) == V + @test eval_show(V') == V' + @test V' == GradedSpace(gen; dual = true) + @test V == @constinferred GradedSpace(gen...) + @test V' == @constinferred GradedSpace(gen...; dual = true) + @test V == @constinferred GradedSpace(tuple(gen...)) + @test V' == @constinferred GradedSpace(tuple(gen...); dual = true) + @test V == @constinferred GradedSpace(Dict(gen)) + @test V' == @constinferred GradedSpace(Dict(gen); dual = true) + @test V == @inferred Vect[I](gen) + @test V' == @constinferred Vect[I](gen; dual = true) + @test V == @constinferred Vect[I](gen...) + @test V' == @constinferred Vect[I](gen...; dual = true) + @test V == @constinferred Vect[I](Dict(gen)) + @test V' == @constinferred Vect[I](Dict(gen); dual = true) + @test V == @constinferred typeof(V)(c => dim(V, c) for c in sectors(V)) + if I isa ZNIrrep + @test V == @constinferred typeof(V)(V.dims) + @test V' == @constinferred typeof(V)(V.dims; dual = true) + end + @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') + @test V == GradedSpace(reverse(collect(gen))...) + @test eval_show(V) == V + @test eval_show(typeof(V)) == typeof(V) + # space with no sectors + @test dim(@constinferred(zerospace(V))) == 0 + # space with unit(s), always test as if multifusion + W = @constinferred GradedSpace(unit => 1 for unit in allunits(I)) + dict = Dict(unit => 1 for unit in allunits(I)) + @test W == GradedSpace(dict) + @test W == GradedSpace(push!(dict, randsector(I) => 0)) + @test @constinferred(zerospace(V)) == GradedSpace(unit => 0 for unit in allunits(I)) + randunit = rand(collect(allunits(I))) + @test_throws ArgumentError("Sector $(randunit) appears multiple times") GradedSpace(randunit => 1, randunit => 3) + + @test isunitspace(W) + @test @constinferred(unitspace(V)) == W == unitspace(typeof(V)) + if UnitStyle(I) isa SimpleUnit + @test @constinferred(leftunitspace(V)) == W == @constinferred(rightunitspace(V)) + else + @test_throws ArgumentError leftunitspace(V) + @test_throws ArgumentError rightunitspace(V) + end + @test eval_show(W) == W + @test isa(V, VectorSpace) + @test isa(V, ElementarySpace) + @test isa(InnerProductStyle(V), HasInnerProduct) + @test isa(InnerProductStyle(V), EuclideanInnerProduct) + @test isa(V, GradedSpace) + @test isa(V, GradedSpace{I}) + @test @constinferred(dual(V)) == @constinferred(conj(V)) == @constinferred(adjoint(V)) != V + @test @constinferred(field(V)) == ℂ + @test @constinferred(sectortype(V)) == I + slist = @constinferred sectors(V) + @test @constinferred(hassector(V, first(slist))) + @test @constinferred(dim(V)) == sum(dim(s) * dim(V, s) for s in slist) + @test @constinferred(reduceddim(V)) == sum(dim(V, s) for s in slist) + @constinferred dim(V, first(slist)) + if hasfusiontensor(I) + @test @constinferred(axes(V)) == Base.OneTo(dim(V)) + end + @test @constinferred(⊕(V, zerospace(V))) == V + @test @constinferred(⊕(V, V)) == Vect[I](c => 2dim(V, c) for c in sectors(V)) + @test @constinferred(⊕(V, V, V, V)) == Vect[I](c => 4dim(V, c) for c in sectors(V)) + @test @constinferred(⊕(V, unitspace(V))) == Vect[I](c => isunit(c) + dim(V, c) for c in sectors(V)) + @test @constinferred(fuse(V, unitspace(V))) == V + d = Dict{I, Int}() + for a in sectors(V), b in sectors(V) + for c in a ⊗ b + d[c] = get(d, c, 0) + dim(V, a) * dim(V, b) * Nsymbol(a, b, c) + end + end + @test @constinferred(fuse(V, V)) == GradedSpace(d) + @test @constinferred(flip(V)) == Vect[I](conj(c) => dim(V, c) for c in sectors(V))' + @test flip(V) ≅ V + @test flip(V) ≾ V + @test flip(V) ≿ V + @test @constinferred(⊕(V, V)) == @constinferred supremum(V, ⊕(V, V)) + @test V == @constinferred infimum(V, ⊕(V, V)) + @test V ≺ ⊕(V, V) + @test !(V ≻ ⊕(V, V)) + + u = first(allunits(I)) + @test infimum(V, GradedSpace(u => 3)) == GradedSpace(u => 2) + @test_throws SpaceMismatch (⊕(V, V')) +end diff --git a/test/testsuite/tensors.jl b/test/testsuite/tensors.jl new file mode 100644 index 000000000..20eed70c1 --- /dev/null +++ b/test/testsuite/tensors.jl @@ -0,0 +1,872 @@ +# Tensor operations +# ================= + +@testsuite :tensors "tensor constructions" V -> begin + I = sectortype(first(V)) + V1, V2, V3, V4, V5 = V + @timedtestset "Basic tensor properties" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + for T in (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat) + t = @constinferred zeros(T, W) + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test norm(t) == 0 + @test codomain(t) == W + @test space(t) == (W ← one(W)) + @test domain(t) == one(W) + @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} + # Array type input + t = @constinferred zeros(Vector{T}, W) + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test norm(t) == 0 + @test codomain(t) == W + @test space(t) == (W ← one(W)) + @test domain(t) == one(W) + @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} + # blocks + bs = @constinferred blocks(t) + if !isempty(blocksectors(t)) # multifusion space ending on module gives empty data + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(W)) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t, first(blocksectors(t))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t) + @test typeof(c) === sectortype(t) + end + end + end + @timedtestset "Tensor Dict conversion" begin + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + for T in (Int, Float32, ComplexF64) + t = @constinferred rand(T, W) + d = convert(Dict, t) + @test t == convert(TensorMap, d) + end + end + if hasfusiontensor(I) || I == Trivial + @timedtestset "Tensor Array conversion" begin + W1 = V1 ← one(V1) + W2 = one(V2) ← V2 + W3 = V1 ⊗ V2 ← one(V1) + W4 = V1 ← V2 + W5 = one(V1) ← V1 ⊗ V2 + W6 = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + for W in (W1, W2, W3, W4, W5, W6) + for T in (Int, Float32, ComplexF64) + if T == Int + t = TensorMap{T}(undef, W) + for (_, b) in blocks(t) + rand!(b, -20:20) + end + else + t = @constinferred randn(T, W) + end + a = @constinferred convert(Array, t) + b = reshape(a, dim(codomain(W)), dim(domain(W))) + @test t ≈ @constinferred TensorMap(a, W) + @test t ≈ @constinferred TensorMap(b, W) + @test t === @constinferred TensorMap(t.data, W) + end + end + for T in (Int, Float32, ComplexF64) + t = randn(T, V1 ⊗ V2 ← zerospace(V1)) + a = convert(Array, t) + @test norm(a) == 0 + end + end + end + if hasfusiontensor(I) + @timedtestset "Real and imaginary parts" begin + W = V1 ⊗ V2 + for T in (Float64, ComplexF64, ComplexF32) + t = @constinferred randn(T, W, W) + + tr = @constinferred real(t) + @test scalartype(tr) <: Real + @test real(convert(Array, t)) == convert(Array, tr) + + ti = @constinferred imag(t) + @test scalartype(ti) <: Real + @test imag(convert(Array, t)) == convert(Array, ti) + + tc = @inferred complex(t) + @test scalartype(tc) <: Complex + @test complex(convert(Array, t)) == convert(Array, tc) + + tc2 = @inferred complex(tr, ti) + @test tc2 ≈ tc + end + end + end + @timedtestset "Tensor conversion" begin + W = V1 ⊗ V2 + t = @constinferred randn(W ← W) + @test typeof(convert(TensorMap, t')) == typeof(t) + tc = complex(t) + @test convert(typeof(tc), t) == tc + @test typeof(convert(typeof(tc), t)) == typeof(tc) + @test typeof(convert(typeof(tc), t')) == typeof(tc) + @test Base.promote_typeof(t, tc) == typeof(tc) + @test Base.promote_typeof(tc, t) == typeof(tc + t) + end +end + +@testsuite :tensors "contractions" V -> begin + I = sectortype(first(V)) + symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding + V1, V2, V3, V4, V5 = V + @timedtestset "Full trace: test self-consistency" begin + if symmetricbraiding + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') + t2 = permute(t, ((1, 2), (4, 3))) + s = @constinferred tr(t2) + @test conj(s) ≈ tr(t2') + if !isdual(V1) + t2 = twist!(t2, 1) + end + if isdual(V2) + t2 = twist!(t2, 2) + end + ss = tr(t2) + @tensor s2 = t[a, b, b, a] + @tensor t3[a, b] := t[a, c, c, b] + @tensor s3 = t3[a, a] + @test ss ≈ s2 + @test ss ≈ s3 + end + t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) # avoid permutes + ss = @constinferred tr(t) + @test conj(ss) ≈ tr(t') + @planar s2 = t[a b; a b] + @planar t3[a; b] := t[a c; b c] + @planar s3 = t3[a; a] + + @test ss ≈ s2 + @test ss ≈ s3 + end + @timedtestset "Partial trace: test self-consistency" begin + if symmetricbraiding + t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) + @tensor t2[a; b] := t[c d b; c d a] + @tensor t4[a b; c d] := t[e d c; e b a] + @tensor t5[a; b] := t4[a c; b c] + @test t2 ≈ t5 + end + t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # compatible with module fusion + @planar t2[a; b] := t[c a d; c b d] + @planar t4[a b; c d] := t[e a b; e c d] + @planar t5[a; b] := t4[a c; b c] + @test t2 ≈ t5 + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Trace: test via conversion" begin + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') + @tensor t2[a, b] := t[c, d, b, d, c, a] + @tensor t3[a, b] := convert(Array, t)[c, d, b, d, c, a] + @test t3 ≈ convert(Array, t2) + end + end + #TODO: find version that works for all multifusion cases + symmetricbraiding && @timedtestset "Trace and contraction" begin + t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3) + t2 = rand(ComplexF64, V2' ⊗ V4 ⊗ V1') + t3 = t1 ⊗ t2 + @tensor ta[a, b] := t1[x, y, a] * t2[y, b, x] + @tensor tb[a, b] := t3[x, y, a, y, b, x] + @test ta ≈ tb + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Tensor contraction: test via conversion" begin + A1 = randn(ComplexF64, V1' * V2', V3') + A2 = randn(ComplexF64, V3 * V4, V5) + rhoL = randn(ComplexF64, V1, V1) + rhoR = randn(ComplexF64, V5, V5)' # test adjoint tensor + H = randn(ComplexF64, V2 * V4, V2 * V4) + @tensor HrA12[a, s1, s2, c] := rhoL[a, a'] * conj(A1[a', t1, b]) * + A2[b, t2, c'] * rhoR[c', c] * H[s1, s2, t1, t2] + + @tensor HrA12array[a, s1, s2, c] := convert(Array, rhoL)[a, a'] * + conj(convert(Array, A1)[a', t1, b]) * convert(Array, A2)[b, t2, c'] * + convert(Array, rhoR)[c', c] * convert(Array, H)[s1, s2, t1, t2] + + @test HrA12array ≈ convert(Array, HrA12) + end + end + @timedtestset "Tensor product: test via norm preservation" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V1, V5') + t2 = rand(T, V2 ⊗ V3, V4') + t = @constinferred (t1 ⊗ t2) + @test norm(t) ≈ norm(t1) * norm(t2) + end + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Tensor product: test via conversion" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V1, V5') + t2 = rand(T, V2 ⊗ V3, V4') + t = @constinferred (t1 ⊗ t2) + d1 = dim(codomain(t1)) + d2 = dim(codomain(t2)) + d3 = dim(domain(t1)) + d4 = dim(domain(t2)) + At = convert(Array, t) + @test reshape(At, (d1, d2, d3, d4)) ≈ + reshape(convert(Array, t1), (d1, 1, d3, 1)) .* + reshape(convert(Array, t2), (1, d2, 1, d4)) + end + end + end + symmetricbraiding && @timedtestset "Tensor product: test via tensor contraction" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V1, V5') + t2 = rand(T, V2 ⊗ V3, V4') + t = @constinferred (t1 ⊗ t2) + @tensor t′[1 2 3; 4 5] := t1[1; 4] * t2[2 3; 5] + @test t ≈ t′ + end + end + @timedtestset "Tensor absorption" begin + # absorbing small into large + t1 = zeros((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') + t2 = rand(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + t3 = @constinferred absorb(t1, t2) + @test norm(t3) ≈ norm(t2) + @test norm(t1) == 0 + t4 = @constinferred absorb!(t1, t2) + @test t1 === t4 + @test t3 ≈ t4 + + # absorbing large into small + t1 = rand((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') + t2 = zeros(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + t3 = @constinferred absorb(t2, t1) + @test norm(t3) < norm(t1) + @test norm(t2) == 0 + t4 = @constinferred absorb!(t2, t1) + @test t2 === t4 + @test t3 ≈ t4 + end +end + +@testsuite :tensors "linear algebra" V -> begin + I = sectortype(first(V)) + V1, V2, V3, V4, V5 = V + @timedtestset "Basic linear algebra" begin + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + for T in (Float32, ComplexF64) + t = @constinferred rand(T, W) + @test scalartype(t) == T + @test space(t) == W + @test space(t') == W' + @test dim(t) == dim(space(t)) + @test codomain(t) == codomain(W) + @test domain(t) == domain(W) + # blocks for adjoint + bs = @constinferred blocks(t') + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(W')) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t', first(blocksectors(t'))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t') + @test typeof(c) === sectortype(t) + # linear algebra + @test isa(@constinferred(norm(t)), real(T)) + @test norm(t)^2 ≈ dot(t, t) + α = rand(T) + @test norm(α * t) ≈ abs(α) * norm(t) + @test norm(t + t, 2) ≈ 2 * norm(t, 2) + @test norm(t + t, 1) ≈ 2 * norm(t, 1) + @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) + p = 3 * rand(Float64) + @test norm(t + t, p) ≈ 2 * norm(t, p) + @test norm(t) ≈ norm(t') + + t2 = @constinferred rand!(similar(t)) + β = rand(T) + @test @constinferred(dot(β * t2, α * t)) ≈ conj(β) * α * conj(dot(t, t2)) + @test dot(t2, t) ≈ conj(dot(t, t2)) + @test dot(t2, t) ≈ conj(dot(t2', t')) + @test dot(t2, t) ≈ dot(t', t2') + + if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) + i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved + i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) + @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) + @test i2 * i1 == @constinferred(id(Vector{T}, V2 ⊗ V1)) + end + + w = @constinferred isometry(T, V1 ⊗ (rightunitspace(V1) ⊕ rightunitspace(V1)), V1) + @test dim(w) == 2 * dim(V1 ← V1) + @test w' * w == id(Vector{T}, V1) + @test w * w' == (w * w')^2 + end + end + if hasfusiontensor(I) + @timedtestset "Basic linear algebra: test via conversion" begin + W = V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)' + for T in (Float32, ComplexF64) + t = rand(T, W) + t2 = @constinferred rand!(similar(t)) + @test norm(t, 2) ≈ norm(convert(Array, t), 2) + @test dot(t2, t) ≈ dot(convert(Array, t2), convert(Array, t)) + α = rand(T) + @test convert(Array, α * t) ≈ α * convert(Array, t) + @test convert(Array, t + t) ≈ 2 * convert(Array, t) + end + end + end + @timedtestset "Multiplication of isometries: test properties" begin + W1 = V1 ⊗ V2 ⊗ V3 + W2 = (V4 ⊗ V5)' + for T in (Float64, ComplexF64) + t1 = randisometry(T, W1, W2) + t2 = randisometry(T, W2 ← W2) + @test isisometric(t1) + @test isunitary(t2) + P = t1 * t1' + @test P * P ≈ P + end + end + @timedtestset "Multiplication and inverse: test compatibility" begin + W1 = V1 ⊗ V2 ⊗ V3 + W2 = (V4 ⊗ V5)' + for T in (Float64, ComplexF64) + t1 = rand(T, W1, W1) + t2 = rand(T, W2 ← W2) + t = rand(T, W1, W2) + @test t1 * (t1 \ t) ≈ t + @test (t / t2) * t2 ≈ t + @test t1 \ one(t1) ≈ inv(t1) + @test one(t1) / t1 ≈ pinv(t1) + @test_throws SpaceMismatch inv(t) + @test_throws SpaceMismatch t2 \ t + @test_throws SpaceMismatch t / t1 + tp = pinv(t) * t + @test tp ≈ tp * tp + end + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Multiplication and inverse: test via conversion" begin + W1 = V1 ⊗ V2 ⊗ V3 + W2 = (V4 ⊗ V5)' + for T in (Float32, Float64, ComplexF32, ComplexF64) + t1 = rand(T, W1 ← W1) + t2 = rand(T, W2, W2) + t = rand(T, W1 ← W2) + d1 = dim(W1) + d2 = dim(W2) + At1 = reshape(convert(Array, t1), d1, d1) + At2 = reshape(convert(Array, t2), d2, d2) + At = reshape(convert(Array, t), d1, d2) + @test reshape(convert(Array, t1 * t), d1, d2) ≈ At1 * At + @test reshape(convert(Array, t1' * t), d1, d2) ≈ At1' * At + @test reshape(convert(Array, t2 * t'), d2, d1) ≈ At2 * At' + @test reshape(convert(Array, t2' * t'), d2, d1) ≈ At2' * At' + + @test reshape(convert(Array, inv(t1)), d1, d1) ≈ inv(At1) + @test reshape(convert(Array, pinv(t)), d2, d1) ≈ pinv(At) + + if T == Float32 || T == ComplexF32 + continue + end + + @test reshape(convert(Array, t1 \ t), d1, d2) ≈ At1 \ At + @test reshape(convert(Array, t1' \ t), d1, d2) ≈ At1' \ At + @test reshape(convert(Array, t2 \ t'), d2, d1) ≈ At2 \ At' + @test reshape(convert(Array, t2' \ t'), d2, d1) ≈ At2' \ At' + + @test reshape(convert(Array, t2 / t), d2, d1) ≈ At2 / At + @test reshape(convert(Array, t2' / t), d2, d1) ≈ At2' / At + @test reshape(convert(Array, t1 / t'), d1, d2) ≈ At1 / At' + @test reshape(convert(Array, t1' / t'), d1, d2) ≈ At1' / At' + end + end + end + @timedtestset "diag/diagm" begin + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + t = randn(ComplexF64, W) + d = LinearAlgebra.diag(t) + D = LinearAlgebra.diagm(codomain(t), domain(t), d) + @test LinearAlgebra.isdiag(D) + @test LinearAlgebra.diag(D) == d + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Tensor functions" begin + W = V1 ⊗ V2 + for T in (Float64, ComplexF64) + t = randn(T, W, W) + s = dim(W) + expt = @constinferred exp(t) + @test reshape(convert(Array, expt), (s, s)) ≈ + exp(reshape(convert(Array, t), (s, s))) + + @test (@constinferred sqrt(t))^2 ≈ t + @test reshape(convert(Array, sqrt(t^2)), (s, s)) ≈ + sqrt(reshape(convert(Array, t^2), (s, s))) + + @test exp(@constinferred log(expt)) ≈ expt + @test reshape(convert(Array, log(expt)), (s, s)) ≈ + log(reshape(convert(Array, expt), (s, s))) + + @test (@constinferred cos(t))^2 + (@constinferred sin(t))^2 ≈ id(W) + @test (@constinferred tan(t)) ≈ sin(t) / cos(t) + @test (@constinferred cot(t)) ≈ cos(t) / sin(t) + @test (@constinferred cosh(t))^2 - (@constinferred sinh(t))^2 ≈ id(W) + @test (@constinferred tanh(t)) ≈ sinh(t) / cosh(t) + @test (@constinferred coth(t)) ≈ cosh(t) / sinh(t) + + t1 = sin(t) + @test sin(@constinferred asin(t1)) ≈ t1 + t2 = cos(t) + @test cos(@constinferred acos(t2)) ≈ t2 + t3 = sinh(t) + @test sinh(@constinferred asinh(t3)) ≈ t3 + t4 = cosh(t) + @test cosh(@constinferred acosh(t4)) ≈ t4 + t5 = tan(t) + @test tan(@constinferred atan(t5)) ≈ t5 + t6 = cot(t) + @test cot(@constinferred acot(t6)) ≈ t6 + t7 = tanh(t) + @test tanh(@constinferred atanh(t7)) ≈ t7 + t8 = coth(t) + @test coth(@constinferred acoth(t8)) ≈ t8 + t = randn(T, W, V1) # not square + for f in + ( + cos, sin, tan, cot, cosh, sinh, tanh, coth, atan, acot, asinh, + sqrt, log, asin, acos, acosh, atanh, acoth, + ) + @test_throws SpaceMismatch f(t) + end + end + end + end + @timedtestset "Sylvester equation" begin + for T in (Float32, ComplexF64) + tA = rand(T, V1 ⊗ V2, V1 ⊗ V2) + tB = rand(T, (V3 ⊗ V4 ⊗ V5)', (V3 ⊗ V4 ⊗ V5)') + tA = 3 // 2 * left_polar(tA)[1] + tB = 1 // 5 * left_polar(tB)[1] + tC = rand(T, V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + t = @constinferred sylvester(tA, tB, tC) + @test codomain(t) == V1 ⊗ V2 + @test domain(t) == (V3 ⊗ V4 ⊗ V5)' + @test norm(tA * t + t * tB + tC) < + (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + matrix(x) = reshape(convert(Array, x), dim(codomain(x)), dim(domain(x))) + @test matrix(t) ≈ sylvester(matrix(tA), matrix(tB), matrix(tC)) + end + end + end +end + +@testsuite :tensors "index manipulations" V -> begin + I = sectortype(first(V)) + hasbraiding = BraidingStyle(I) isa HasBraiding + symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding + V1, V2, V3, V4, V5 = V + @timedtestset "Trivial space insertion and removal" begin + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + for T in (Float32, ComplexF64) + t = @constinferred rand(T, W) + t2 = @constinferred insertleftunit(t) + @test t2 == @constinferred insertrightunit(t) + @test space(t2) == insertleftunit(space(t)) + @test @constinferred(removeunit(t2, $(numind(t2)))) == t + t3 = @constinferred insertleftunit(t; copy = true) + @test t3 == @constinferred insertrightunit(t; copy = true) + @test @constinferred(removeunit(t3, $(numind(t3)))) == t + + @test numind(t2) == numind(t) + 1 + @test scalartype(t2) === T + @test t.data === t2.data + + @test t.data !== t3.data + for (c, b) in blocks(t) + @test b == block(t3, c) + end + + t4 = @constinferred insertrightunit(t, 3; dual = true) + @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) + for (c, b) in blocks(t) + @test b == block(t4, c) + end + @test @constinferred(removeunit(t4, 4)) == t + + t5 = @constinferred insertleftunit(t, 4; dual = true) + @test numin(t5) == numin(t) + 1 && numout(t5) == numout(t) + for (c, b) in blocks(t) + @test b == block(t5, c) + end + @test @constinferred(removeunit(t5, 4)) == t + end + end + symmetricbraiding && @timedtestset "Permutations: test via inner product invariance" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + t = rand(ComplexF64, W) + t′ = randn!(similar(t)) + for k in 0:5 + for p in permutations(1:5) + p1 = ntuple(n -> p[n], k) + p2 = ntuple(n -> p[k + n], 5 - k) + t2 = @constinferred permute(t, (p1, p2)) + @test norm(t2) ≈ norm(t) + t2′ = permute(t′, (p1, p2)) + @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) + end + + t3 = @constinferred repartition(t, $k) + @test norm(t3) ≈ norm(t) + t3′ = @constinferred repartition!(similar(t3), t′) + @test norm(t3′) ≈ norm(t′) + @test dot(t′, t) ≈ dot(t3′, t3) + end + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Permutations: test via conversion" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + t = rand(ComplexF64, W) + a = convert(Array, t) + for k in 0:5 + for p in permutations(1:5) + p1 = ntuple(n -> p[n], k) + p2 = ntuple(n -> p[k + n], 5 - k) + t2 = permute(t, (p1, p2)) + a2 = convert(Array, t2) + @test a2 ≈ permutedims(a, (p1..., p2...)) + @test convert(Array, transpose(t2)) ≈ + permutedims(a2, (5, 4, 3, 2, 1)) + end + + t3 = repartition(t, k) + a3 = convert(Array, t3) + @test a3 ≈ permutedims( + a, (ntuple(identity, k)..., reverse(ntuple(i -> i + k, 5 - k))...) + ) + end + end + end + hasbraiding && @timedtestset "Index flipping: test flipping inverse" begin + t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)') + for i in 1:5 + @test t ≈ flip(flip(t, i), i; inv = true) + @test t ≈ flip(flip(t, i; inv = true), i) + end + end + symmetricbraiding && @timedtestset "Index flipping: test via explicit flip" begin + t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) + F1 = unitary(flip(V1), V1) + + @tensor tf[a, b; c, d] := F1[a, a'] * t[a', b; c, d] + @test flip(t, 1) ≈ tf + @tensor tf[a, b; c, d] := conj(F1[b, b']) * t[a, b'; c, d] + @test twist!(flip(t, 2), 2) ≈ tf + @tensor tf[a, b; c, d] := F1[c, c'] * t[a, b; c', d] + @test flip(t, 3) ≈ tf + @tensor tf[a, b; c, d] := conj(F1[d, d']) * t[a, b; c, d'] + @test twist!(flip(t, 4), 4) ≈ tf + end + symmetricbraiding && @timedtestset "Index flipping: test via contraction" begin + t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V4) + t2 = rand(ComplexF64, V2' ⊗ V5 ← V4' ⊗ V1) + @tensor ta[a, b] := t1[x, y, a, z] * t2[y, b, z, x] + @tensor tb[a, b] := flip(t1, 1)[x, y, a, z] * flip(t2, 4)[y, b, z, x] + @test ta ≈ tb + @tensor tb[a, b] := flip(t1, (2, 4))[x, y, a, z] * flip(t2, (1, 3))[y, b, z, x] + @test ta ≈ tb + @tensor tb[a, b] := flip(t1, (1, 2, 4))[x, y, a, z] * flip(t2, (1, 3, 4))[y, b, z, x] + @tensor tb[a, b] := flip(t1, (1, 3))[x, y, a, z] * flip(t2, (2, 4))[y, b, z, x] + @test flip(ta, (1, 2)) ≈ tb + end + hasbraiding && !symmetricbraiding && @timedtestset "Braid AdjointTensorMap: adjoint identity" begin + t = rand(ComplexF64, V1 ⊗ V2 ← V3) + p = ((2,), (1, 3)) + levels = (1, 3, 2) + t1 = copy(braid(t', p, levels)) + t2 = braid(copy(t'), p, levels) + @test t1 ≈ t2 + end +end + +@testsuite :tensors "braiding tensor" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa NoBraiding && return nothing + Vspace = first(V) + T = ComplexF64 + t = randn(T, Vspace ⊗ Vspace' ⊗ Vspace' ⊗ Vspace ← Vspace ⊗ Vspace') + @timedtestset "planaradd! with BraidingTensor" begin + b = BraidingTensor(Vspace, Vspace') + bb = TensorMap(b) + # Cyclic rotations of the planar leg cycle (cod1, cod2, dom2, dom1). + # Use transpose (F-symbols only) as reference, since permute requires SymmetricBraiding. + # rotation 0 (identity) + @planar t1[-1 -2; -3 -4] := b[-1 -2; -3 -4] + @test t1 ≈ bb + # rotation 1: single-tree cycle (4,1,2,3) → (p1=(2,4), p2=(1,3)) + @planar t2[-1 -2; -3 -4] := b[-3 -1; -4 -2] + @test t2 ≈ transpose(bb, ((2, 4), (1, 3))) + # rotation 2: single-tree cycle (3,4,1,2) → (p1=(4,3), p2=(2,1)) + @planar t3[-1 -2; -3 -4] := b[-4 -3; -2 -1] + @test t3 ≈ transpose(bb, ((4, 3), (2, 1))) + # rotation 3: single-tree cycle (2,3,4,1) → (p1=(3,1), p2=(4,2)) + @planar t4[-1 -2; -3 -4] := b[-2 -4; -1 -3] + @test t4 ≈ transpose(bb, ((3, 1), (4, 2))) + # adjoint BraidingTensor (rotation 0) + ba = b' + @planar t5[-1 -2; -3 -4] := ba[-1 -2; -3 -4] + @test t5 ≈ TensorMap(ba) + end + + @timedtestset "τ as left factor, all legs contracted" begin + # BraidingTensor(V, V') on leading codomain indices + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -2 -1] * t[1 2 -3 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 2; -1 1] * t[1 2 -3 -4; -5 -6] + @test t1 ≈ braid(t, ((2, 1, 3, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V') on inner codomain indices + ττ = TensorMap(BraidingTensor(Vspace', Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ braid(t, ((1, 3, 2, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V) on trailing codomain indices + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -4 -3] * t[-1 -2 1 2; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-4 2; -3 1] * t[-1 -2 1 2; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + end + + @timedtestset "τ as left factor, mixed open legs" begin + # BraidingTensor(V', V) with mixed index pattern + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V') with mixed index pattern (inverse of previous) + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V) with mixed index pattern + ττ = TensorMap(BraidingTensor(Vspace, Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + end + + @timedtestset "τ as right factor" begin + # BraidingTensor(V', V) on all domain indices + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[1 2; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ[1 2; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[-6 -5; 2 1] + @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[2 -6; 1 -5] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V') adjoint on all domain indices + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[1 2; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ'[1 2; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[-6 -5; 2 1] + @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[2 -6; 1 -5] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V) with mixed domain legs + ττ = TensorMap(BraidingTensor(Vspace, Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[-4 -6; 1 2] + @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * ττ[-4 -6; 1 2] + @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[2 1; -6 -4] + @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ'[-6 2; -4 1] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + end + + @timedtestset "τ with fully contracted output" begin + # scalar output + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[(); (-1, -2)] := τ[2 1; 3 4] * t[1 2 3 4; -1 -2] + @planar t2[(); (-1, -2)] := ττ[2 1; 3 4] * t[1 2 3 4; -1 -2] + @planar t3[(); (-1, -2)] := τ[4 3; 1 2] * t[1 2 3 4; -1 -2] + @planar t4[(); (-1, -2)] := τ'[1 4; 2 3] * t[1 2 3 4; -1 -2] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # rank-1 output + ττ = TensorMap(BraidingTensor(Vspace, Vspace)) + @planar t1[-1; -2] := τ[2 1; 3 4] * t[-1 1 2 3; -2 4] + @planar t2[-1; -2] := ττ[2 1; 3 4] * t[-1 1 2 3; -2 4] + @planar t3[-1; -2] := τ[4 3; 1 2] * t[-1 1 2 3; -2 4] + @planar t4[-1; -2] := τ'[1 4; 2 3] * t[-1 1 2 3; -2 4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # rank-2 output + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2] := τ[2 1; 3 4] * t[-1 -2 1 2; 4 3] + @planar t2[-1 -2] := ττ[2 1; 3 4] * t[-1 -2 1 2; 4 3] + @planar t3[-1 -2] := τ[4 3; 1 2] * t[-1 -2 1 2; 4 3] + @planar t4[-1 -2] := τ'[1 4; 2 3] * t[-1 -2 1 2; 4 3] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + end + + @timedtestset "τ with one open codomain leg" begin + # BraidingTensor(V, V') with one open codomain leg + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2; -3 -4] := τ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] + @planar t2[-1 -2; -3 -4] := ττ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] + @planar t3[-1 -2; -3 -4] := τ[2 1; 3 -1] * t[1 2 3 -2; -3 -4] + @planar t4[-1 -2; -3 -4] := τ'[3 2; -1 1] * t[1 2 3 -2; -3 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V') adjoint with one open codomain leg + ττ = TensorMap(BraidingTensor(Vspace', Vspace')) + @planar t1[-1 -2; -3 -4] := τ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] + @planar t2[-1 -2; -3 -4] := ττ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] + @planar t3[-1 -2; -3 -4] := τ'[2 1; 3 -2] * t[-1 1 2 3; -3 -4] + @planar t4[-1 -2; -3 -4] := τ[3 2; -2 1] * t[-1 1 2 3; -3 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V) with one open codomain leg + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3; -4] := τ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] + @planar t2[-1 -2 -3; -4] := ττ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] + @planar t3[-1 -2 -3; -4] := τ[2 1; 3 -3] * t[-1 -2 1 2; -4 3] + @planar t4[-1 -2 -3; -4] := τ'[3 2; -3 1] * t[-1 -2 1 2; -4 3] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + end + + @timedtestset "τ as right factor with open domain leg" begin + # BraidingTensor(V', V) as right factor with one open domain leg + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[1 2; -4 3] + @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ[1 2; -4 3] + @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[3 -4; 2 1] + @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[2 3; 1 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V') adjoint as right factor with one open domain leg + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[1 2; -4 3] + @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ'[1 2; -4 3] + @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[3 -4; 2 1] + @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[2 3; 1 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + end + + @timedtestset "BraidingTensor × BraidingTensor" begin + # b1 domain == b2 codomain == V⊗V', straight-through (planar) contraction + b1 = BraidingTensor(Vspace, Vspace') # space: V'⊗V ← V⊗V' + b2 = BraidingTensor(Vspace', Vspace) # space: V⊗V' ← V'⊗V + bb1 = TensorMap(b1) + bb2 = TensorMap(b2) + @planar t1[-1 -2; -3 -4] := b1[-1 -2; 1 2] * b2[1 2; -3 -4] + @planar t2[-1 -2; -3 -4] := bb1[-1 -2; 1 2] * bb2[1 2; -3 -4] + @test t1 ≈ t2 + end +end + +@testsuite :tensors "hom space" V -> begin + V1, V2, V3, V4, V5 = V + W = HomSpace(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + @test W == ((V3 ⊗ V4 ⊗ V5)' → V1 ⊗ V2) + @test W == (V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)') + @test W' == (V1 ⊗ V2 → (V3 ⊗ V4 ⊗ V5)') + @test codomain(W) == V1 ⊗ V2 + @test domain(W)' == V3 ⊗ V4 ⊗ V5 + @test _eval_show(W) == W + @test _eval_show(typeof(W)) == typeof(W) + @test spacetype(W) == typeof(V1) + @test sectortype(W) == sectortype(V1) + @test W[1] == V1 + @test W[2] == V2 + @test W[3] == V5 + @test W[4] == V4 + @test W[5] == V3 + @test all(W .== (V1, V2, V5, V4, V3)) + @test @constinferred(map(isdual, W)) == ntuple(i -> isdual(W[i]), length(W)) + @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') + @test W == deepcopy(W) + cod = codomain(W) + dom = domain(W) + @test (cod ← dom ⊗ rightunitspace(dom[3])) == + @constinferred(insertleftunit(W)) == + @constinferred(insertrightunit(W)) + @test @constinferred(removeunit(insertleftunit(W), $(numind(W) + 1))) == W + @test (cod ← dom ⊗ rightunitspace(dom[3])') == + @constinferred(insertleftunit(W; conj = true)) == + @constinferred(insertrightunit(W; conj = true)) + @test (leftunitspace(cod[1]) ⊗ cod ← dom) == + @constinferred(insertleftunit(W, 1)) == + @constinferred(insertrightunit(W, 0)) + @test (cod ⊗ rightunitspace(cod[2]) ← dom) == + @constinferred(insertrightunit(W, 2)) + @test (cod ← leftunitspace(dom[1]) ⊗ dom) == + @constinferred(insertleftunit(W, 3)) + @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W + if UnitStyle(sectortype(W)) isa SimpleUnit + @test @constinferred(insertrightunit(one(V1) ← V1, 0)) == (unitspace(V1) ← V1) + @test_throws BoundsError insertleftunit(one(V1) ← V1, 0) + else + @test_throws ArgumentError insertrightunit(one(V1) ← V1, 0) + @test_throws ArgumentError insertleftunit(one(V1) ← V1, 0) + end + @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') + @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) + @test permute(W, ((2, 5, 4), (1, 3))) == (V2 ⊗ V3 ⊗ V4 ← V1' ⊗ V5') # cyclic permutation +end From d9cc856aee0051259b92f1c184618b44a1b09479 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 6 Jul 2026 17:04:12 +0200 Subject: [PATCH 02/13] apply testsuite to current tests --- test/symmetries/doubletree.jl | 257 +----------------------- test/symmetries/singletree.jl | 312 +---------------------------- test/symmetries/spaces.jl | 147 +------------- test/tensors/braidingtensor.jl | 225 +-------------------- test/tensors/construction.jl | 110 +--------- test/tensors/contractions.jl | 135 +------------ test/tensors/indexmanipulations.jl | 126 +----------- test/tensors/linalg.jl | 214 +------------------- 8 files changed, 25 insertions(+), 1501 deletions(-) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index f0f7484a0..00dd32532 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -1,262 +1,17 @@ using Test, TestExtras using TensorKit -using TensorKit: FusionTreeBlock import TensorKit as TK -using Random: randperm -using TensorOperations -using TupleTools # TODO: remove this once type_repr works for all included types using TensorKitSectors @timedtestset "Double fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - Istr = TensorKit.type_repr(I) - N = I <: ProductSector ? 3 : 4 - - if UnitStyle(I) isa SimpleUnit - out = random_fusion(I, Val(N)) - numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) - while !(0 < numtrees < 100) - out = random_fusion(I, Val(N)) - numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) - end - incoming = rand(collect(⊗(out...))) - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) - else - out = random_fusion(I, Val(N)) - out2 = random_fusion(I, Val(N)) - tp = ⊗(out...) - tp2 = ⊗(out2...) - while isempty(intersect(tp, tp2)) # guarantee fusion to same coloring - out2 = random_fusion(I, Val(N)) - tp2 = ⊗(out2...) - end - @test_throws ArgumentError fusiontrees((out..., map(dual, out)...)) - incoming = rand(collect(intersect(tp, tp2))) - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) # no permuting - end - - if FusionStyle(I) isa UniqueFusion - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) - src = (f1, f2) - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - A = fusiontensor(src) - end - else - src = FusionTreeBlock{I}((out, out), (ntuple(n -> rand(Bool), N), ntuple(n -> rand(Bool), N))) - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - A = map(fusiontensor, fusiontrees(src)) - end - end - - @testset "Double fusion tree: bending" begin - # single bend - dst, U = @constinferred TK.bendright(src) - dst2, U2 = @constinferred TK.bendleft(dst) - @test src == dst2 - @test _isone(U2 * U) - # double bend - dst1, U1 = @constinferred TK.bendleft(src) - dst2, U2 = @constinferred TK.bendleft(dst1) - dst3, U3 = @constinferred TK.bendright(dst2) - dst4, U4 = @constinferred TK.bendright(dst3) - @test src == dst4 - @test _isone(U4 * U3 * U2 * U1) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) - p₁ = ntuple(i -> all_inds[i], numout(dst2)) - p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) - U = U2 * U1 - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) - A″ = map(fusiontensor, fusiontrees(dst2)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(A″ .* U[:, i]) - end - end - end - end - - @testset "Double fusion tree: folding" begin - # single bend - dst, U = @constinferred TK.foldleft(src) - dst2, U2 = @constinferred TK.foldright(dst) - @test src == dst2 - @test _isone(U2 * U) - # double bend - dst1, U1 = @constinferred TK.foldright(src) - dst2, U2 = @constinferred TK.foldright(dst1) - dst3, U3 = @constinferred TK.foldleft(dst2) - dst4, U4 = @constinferred TK.foldleft(dst3) - @test src == dst4 - @test _isone(U4 * U3 * U2 * U1) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - all_inds = TupleTools.circshift((ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...), -2) - p₁ = ntuple(i -> all_inds[i], numout(dst2)) - p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) - U = U2 * U1 - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst2) - else - A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) - A″ = map(fusiontensor, fusiontrees(dst2)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(A″ .* U[:, i]) - end - end - end - end - - @testset "Double fusion tree: repartitioning" begin - for n in 0:(2 * N) - dst, U = @constinferred TK.repartition(src, $n) - # @test _isunitary(U) - - dst′, U′ = repartition(dst, N) - @test _isone(U * U′) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) - p₁ = ntuple(i -> all_inds[i], numout(dst)) - p₂ = reverse(ntuple(i -> all_inds[i + numout(dst)], numin(dst))) - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) - A″ = map(fusiontensor, fusiontrees(dst)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(A″ .* U[:, i]) - end - end - end - end - end - - @testset "Double fusion tree: transposition" begin - for n in 0:(2N) - i0 = rand(1:(2N)) - p = mod1.(i0 .+ (1:(2N)), 2N) - ip = mod1.(-i0 .+ (1:(2N)), 2N) - p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) - p1, p2 = p′[1:n], p′[(2N):-1:(n + 1)] - ip′ = tuple(getindex.(Ref(vcat(1:n, (2N):-1:(n + 1))), ip)...) - ip1, ip2 = ip′[1:N], ip′[(2N):-1:(N + 1)] - - dst, U = @constinferred transpose(src, (p1, p2)) - dst′, U′ = @constinferred transpose(dst, (ip1, ip2)) - @test _isone(U * U′) - - if BraidingStyle(I) isa Bosonic - dst″, U″ = permute(src, (p1, p2)) - @test U″ ≈ U - end - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) - A″ = map(fusiontensor, fusiontrees(dst)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(U[:, i] .* A″) - end - end - end - end - end - - BraidingStyle(I) isa HasBraiding && @testset "Double fusion tree: permutation and braiding" begin - for n in 0:(2N) - p = (randperm(2 * N)...,) - p1, p2 = p[1:n], p[(n + 1):(2N)] - ip = invperm(p) - ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] - levels = ntuple(identity, 2N) - l1, l2 = levels[1:N], levels[(N + 1):(2N)] - ilevels = TupleTools.getindices(levels, p) - il1, il2 = ilevels[1:n], ilevels[(n + 1):(2N)] - - if BraidingStyle(I) isa SymmetricBraiding - dst, U = @constinferred TensorKit.permute(src, (p1, p2)) - else - dst, U = @constinferred TensorKit.braid(src, (p1, p2), (l1, l2)) - end - - # check norm-preserving - if FusionStyle(I) isa UniqueFusion - @test abs(U) ≈ 1 - else - dim1 = map(fusiontrees(src)) do (f1, f2) - return dim(f1.coupled) - end - dim2 = map(fusiontrees(dst)) do (f1, f2) - return dim(f1.coupled) - end - @test vec(sum(abs2.(U) .* dim2; dims = 1)) ≈ dim1 - end - - # check reversible - if BraidingStyle(I) isa SymmetricBraiding - dst′, U′ = @constinferred TensorKit.permute(dst, (ip1, ip2)) - else - dst′, U′ = @constinferred TensorKit.braid(dst, (ip1, ip2), (il1, il2)) - end - @test _isone(U * U′) - - # check fusiontensor compatibility - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) - A″ = map(fusiontensor, fusiontrees(dst)) - for (i, Ai) in enumerate(A′) - Aj = sum(A″ .* U[:, i]) - @test Ai ≈ Aj - end - end - end - end - end - - @testset "Double fusion tree: planar trace" begin - if FusionStyle(I) isa UniqueFusion - f1, f1 = src - dst, U = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - d1 = zip((dst,), (U,)) - else - f1, f1 = first(fusiontrees(src)) - src′ = FusionTreeBlock{I}((f1.uncoupled, f1.uncoupled), (f1.isdual, f1.isdual)) - dst, U = transpose(src′, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - d1 = zip(fusiontrees(dst), U[:, 1]) - end - - f1front, = TK.split(f1, N - 1) - T = sectorscalartype(I) - d2 = Dict{typeof((f1front, f1front)), T}() - for ((f1′, f2′), coeff′) in d1 - for ((f1′′, f2′′), coeff′′) in TK.planar_trace( - (f1′, f2′), ((2:N...,), (1, ((2N):-1:(N + 3))...)), ((N + 1,), (N + 2,)) - ) - coeff = coeff′ * coeff′′ - d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff - end - end - for ((f1_, f2_), coeff) in d2 - if (f1_, f2_) == (f1front, f1front) - @test coeff ≈ dim(f1.coupled) / dim(f1front.coupled) - else - @test abs(coeff) < 1.0e-12 - end - end - end + TensorKitTestSuite.test_fusiontrees_bending(I) + TensorKitTestSuite.test_fusiontrees_folding(I) + TensorKitTestSuite.test_fusiontrees_repartitioning(I) + TensorKitTestSuite.test_fusiontrees_transposition(I) + TensorKitTestSuite.test_fusiontrees_permutation_and_braiding(I) + TensorKitTestSuite.test_fusiontrees_planar_trace(I) TK.empty_globalcaches!() end diff --git a/test/symmetries/singletree.jl b/test/symmetries/singletree.jl index cabb7dd74..bf36884a2 100644 --- a/test/symmetries/singletree.jl +++ b/test/symmetries/singletree.jl @@ -1,315 +1,17 @@ using Test, TestExtras using TensorKit -using TensorKit: FusionTreeBlock, × import TensorKit as TK -using Random: randperm -using TensorOperations -using MatrixAlgebraKit: isunitary -using LinearAlgebra -using TupleTools # TODO: remove this once type_repr works for all included types using TensorKitSectors @timedtestset "Single fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - Istr = TensorKit.type_repr(I) - N = 5 - out = random_fusion(I, Val(N)) - isdual = ntuple(n -> rand(Bool), N) - in = rand(collect(⊗(out...))) - numtrees = length(fusiontrees(out, in, isdual)) - @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - while !(0 < numtrees < 30) && !(one(in) in ⊗(out...)) - out = ntuple(n -> randsector(I), N) - in = rand(collect(⊗(out...))) - numtrees = length(fusiontrees(out, in, isdual)) - @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - end - it = @constinferred fusiontrees(out, in, isdual) - @constinferred Nothing iterate(it) - f, s = iterate(it) - @constinferred Nothing iterate(it, s) - @test f == @constinferred first(it) - - @testset "Fusion tree: printing" begin - @test eval(Meta.parse(sprint(show, f; context = (:module => @__MODULE__)))) == f - end - - @testset "Fusion tree: constructor properties" begin - for u in allunits(I) - @constinferred FusionTree((), u, (), (), ()) - @constinferred FusionTree((u,), u, (false,), (), ()) - @constinferred FusionTree((u, u), u, (false, false), (), (1,)) - @constinferred FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) - @constinferred FusionTree( - (u, u, u, u), u, (false, false, false, false), (u, u), (1, 1, 1) - ) - @test_throws MethodError FusionTree((u, u, u), u, (false, false), (u,), (1, 1)) - @test_throws MethodError FusionTree( - (u, u, u), u, (false, false, false), (u, u), (1, 1) - ) - @test_throws MethodError FusionTree( - (u, u, u), u, (false, false, false), (u,), (1, 1, 1) - ) - @test_throws MethodError FusionTree((u, u, u), u, (false, false, false), (), (1,)) - - f = FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) - @test sectortype(f) == I - @test length(f) == 3 - @test FusionStyle(f) == FusionStyle(I) - @test BraidingStyle(f) == BraidingStyle(I) - - if FusionStyle(I) isa UniqueFusion - @constinferred FusionTree((), u, ()) - @constinferred FusionTree((u,), u, (false,)) - @constinferred FusionTree((u, u), u, (false, false)) - @constinferred FusionTree((u, u, u), u) - if UnitStyle(I) isa SimpleUnit - @constinferred FusionTree((u, u, u, u)) - else - @test_throws ArgumentError FusionTree((u, u, u, u)) - end - @test_throws MethodError FusionTree((u, u), u, (false, false, false)) - else - @test_throws ArgumentError FusionTree((), u, ()) - @test_throws ArgumentError FusionTree((u,), u, (false,)) - @test_throws ArgumentError FusionTree((u, u), u, (false, false)) - @test_throws ArgumentError FusionTree((u, u, u), u) - if I <: ProductSector && UnitStyle(I) isa GenericUnit - @test_throws DomainError FusionTree((u, u, u, u)) - else - @test_throws ArgumentError FusionTree((u, u, u, u)) - end - end - end - end - - # Basic associativity manipulations of individual fusion trees - @testset "Fusion tree: split and join" begin - N = 6 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdual = ntuple(n -> rand(Bool), N) - f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) - for i in 0:N - f₁, f₂ = @constinferred TK.split(f, $i) - @test length(f₁) == i - @test length(f₂) == N - i + 1 - f′ = @constinferred TK.join(f₁, f₂) - @test f′ == f - end - end - - @testset "Fusion tree: multi_Fmove" begin - N = 6 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdualrest = ntuple(n -> rand(Bool), N - 1) - for isdual in ((false, isdualrest...), (true, isdualrest...)) - trees = collect(fusiontrees(uncoupled, coupled, isdual)) - trees = trees[randperm(length(trees))[1:rand(1:min(5, length(trees)))]] # limit number of tests? - for f in trees - a = f.uncoupled[1] - isduala = f.isdual[1] - c = f.coupled - f′s, coeffs = @constinferred TK.multi_Fmove(f) - @test norm(coeffs) ≈ 1 atol = 1.0e-12 # expansion should have unit norm - d = Dict(f => -one(eltype(eltype(coeffs)))) - for (f′, coeff) in zip(f′s, coeffs) - @test coeff ≈ TK.multi_associator(f, f′) - f′′s, coeff′s = @constinferred TK.multi_Fmove_inv(a, c, f′, isduala) - if FusionStyle(I) isa MultiplicityFreeFusion - @test norm(coeff′s) ≈ 1 atol = 1.0e-12 # expansion should have unit norm - else - for i in 1:Nsymbol(a, f′.coupled, c) - @test norm(getindex.(coeff′s, i)) ≈ 1 atol = 1.0e-12 # expansion should have unit norm for every possible fusion channel at the top vertex - end - end - for (f′′, coeff′) in zip(f′′s, coeff′s) - @test coeff′ ≈ conj(TK.multi_associator(f′′, f′)) - d[f′′] = get(d, f′′, zero(eltype(coeff′))) + sum(coeff .* coeff′) - end - end - @test norm(values(d)) < 1.0e-12 - end - end - - if hasfusiontensor(I) # because no permutations are involved, this also works for fermionic braiding - N = 4 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdualrest = ntuple(n -> rand(Bool), N - 1) - for isdual in ((false, isdualrest...), (true, isdualrest...)) - trees = collect(fusiontrees(uncoupled, coupled, isdual)) - trees = trees[randperm(length(trees))[1:rand(1:min(5, length(trees)))]] # limit number of tests? - for f in trees - ftensor = fusiontensor(f) - ftensor′ = zero(ftensor) - a = f.uncoupled[1] - isduala = f.isdual[1] - c = f.coupled - f′s, coeffs = @constinferred TK.multi_Fmove(f) - for (f′, coeff) in zip(f′s, coeffs) - f′tensor = fusiontensor(f′) - for i in 1:Nsymbol(a, f′.coupled, c) - f′′ = FusionTree{I}((a, f′.coupled), c, (isduala, false), (), (i,)) - f′′tensor = fusiontensor(f′′) - ftensor′ += coeff[i] * tensorcontract(1:(N + 1), f′tensor, [(2:N)..., -1], f′′tensor, [1, -1, N + 1]) - end - end - @test ftensor′ ≈ ftensor atol = 1.0e-12 - end - end - end - end - - @testset "Fusion tree: insertat" begin - # just check some basic consistency properties here - # correctness should follow from multi_Fmove tests - N = 4 - out2 = random_fusion(I, Val(N)) - in2 = rand(collect(⊗(out2...))) - isdual2 = ntuple(n -> rand(Bool), N) - f2 = rand(collect(fusiontrees(out2, in2, isdual2))) - for i in 1:N - out1 = random_fusion(I, Val(N)) # guaranteed good fusion - out1 = Base.setindex(out1, in2, i) # can lead to poor fusion - while isempty(⊗(out1...)) # TODO: better way to do this? - out1 = random_fusion(I, Val(N)) - out1 = Base.setindex(out1, in2, i) - end - in1 = rand(collect(⊗(out1...))) - isdual1 = ntuple(n -> rand(Bool), N) - isdual1 = Base.setindex(isdual1, false, i) - f1 = rand(collect(fusiontrees(out1, in1, isdual1))) - - trees = @constinferred TK.insertat(f1, i, f2) - @test norm(values(trees)) ≈ 1 - - if hasfusiontensor(I) - Af1 = fusiontensor(f1) - Af2 = fusiontensor(f2) - Af = tensorcontract( - 1:(2N), Af1, - [1:(i - 1); -1; N - 1 .+ ((i + 1):(N + 1))], - Af2, [i - 1 .+ (1:N); -1] - ) - Af′ = zero(Af) - for (f, coeff) in trees - Af′ .+= coeff .* fusiontensor(f) - end - @test Af ≈ Af′ - end - end - end - - @testset "Fusion tree: merging" begin - N = 3 - out1 = random_fusion(I, Val(N)) - out2 = random_fusion(I, Val(N)) - in1 = rand(collect(⊗(out1...))) - in2 = rand(collect(⊗(out2...))) - tp = ⊗(in1, in2) # messy solution but it works - while isempty(tp) - out1 = random_fusion(I, Val(N)) - out2 = random_fusion(I, Val(N)) - in1 = rand(collect(⊗(out1...))) - in2 = rand(collect(⊗(out2...))) - tp = ⊗(in1, in2) - end - - f1 = rand(collect(fusiontrees(out1, in1))) - f2 = rand(collect(fusiontrees(out2, in2))) - - d = @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) - @test norm(values(d)) ≈ 1 - if !(FusionStyle(I) isa GenericFusion) - @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) - end - @test dim(in1) * dim(in2) ≈ sum( - abs2(coeff) * dim(c) for c in in1 ⊗ in2 - for μ in 1:Nsymbol(in1, in2, c) - for (f, coeff) in TK.merge(f1, f2, c, μ) - ) - - if hasfusiontensor(I) - for c in in1 ⊗ in2 - for μ in 1:Nsymbol(in1, in2, c) - Af1 = fusiontensor(f1) - Af2 = fusiontensor(f2) - Af0 = fusiontensor(FusionTree((in1, in2), c, (false, false), (), (μ,))) - _Af = tensorcontract( - 1:(N + 2), Af1, [1:N; -1], Af0, [-1; N + 1; N + 2] - ) - Af = tensorcontract( - 1:(2N + 1), Af2, [N .+ (1:N); -1], _Af, [1:N; -1; 2N + 1] - ) - Af′ = zero(Af) - for (f, coeff) in TK.merge(f1, f2, c, μ) - Af′ .+= coeff .* fusiontensor(f) - end - @test Af ≈ Af′ - end - end - end - end - - # Duality tests - @testset "Fusion tree: elementary planar trace" begin - N = 5 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdual = ntuple(n -> rand(Bool), N) - f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) - for i in 0:N # insert a (b b̄ ← 1) vertex in the tree after ith uncoupled sector and then trace it away - f₁, f₂ = TK.split(f, i) - c = f₁.coupled - funit = FusionTree{I}((c, rightunit(c)), c, (false, false), (), (1,)) - f′ = TK.join(TK.join(f₁, funit), f₂) - for b in smallset(I) - leftunit(b) == rightunit(c) || continue - out = Dict(f => -sqrtdim(b) * one(fusionscalartype(I))) - fbb = FusionTree{I}((b, dual(b)), leftunit(b), (false, true), (), (1,)) - for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) - d = @constinferred TK.elementary_trace(f′′, i + 1) - for (tree, coeff2) in d - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - out = Dict(f => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) - fbb = FusionTree{I}((b, dual(b)), leftunit(b), (true, false), (), (1,)) - for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) - for (tree, coeff2) in TK.elementary_trace(f′′, i + 1) - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - end - end - # insert f′ in between the two legs of a (b b̄ ← 1) vertex and then trace the outer legs away - f′ = TK.join(f, FusionTree{I}((coupled, dual(coupled)), leftunit(coupled), (false, true), (), (1,))) - for b in smallset(I) - rightunit(b) == leftunit(coupled) || continue - fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (false, false, true), (b,), (1, 1)) - out = Dict(f′ => -sqrtdim(b) * one(fusionscalartype(I))) - for (f′′, coeff) in TK.insertat(fbb, 2, f′) - d = @constinferred TK.elementary_trace(f′′, N + 3) - for (tree, coeff2) in d - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (true, false, false), (b,), (1, 1)) - out = Dict(f′ => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) - for (f′′, coeff) in TK.insertat(fbb, 2, f′) - for (tree, coeff2) in TK.elementary_trace(f′′, N + 3) - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - end - end - + TensorKitTestSuite.test_fusiontrees_iterate_and_printing(I) + TensorKitTestSuite.test_fusiontrees_constructor_properties(I) + TensorKitTestSuite.test_fusiontrees_split_and_join(I) + TensorKitTestSuite.test_fusiontrees_multi_fmove(I) + TensorKitTestSuite.test_fusiontrees_insertat(I) + TensorKitTestSuite.test_fusiontrees_merging(I) + TensorKitTestSuite.test_fusiontrees_elementary_planar_trace(I) TK.empty_globalcaches!() end diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 6296bc335..dbe1ed1a8 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -11,7 +11,7 @@ using TensorKitSectors Use `show` to generate a string representation of `x`, then parse and evaluate the resulting expression. """ -function eval_show(x) +function eval_show(x) #TODO: move this to setup.jl str = sprint(show, x; context = (:module => @__MODULE__)) ex = Meta.parse(str) return eval(ex) @@ -187,100 +187,8 @@ end @test @constinferred(axes(V)) == Base.OneTo(d) end -@timedtestset "ElementarySpace: $(type_repr(Vect[I]))" for I in sectorlist - if Base.IteratorSize(values(I)) === Base.IsInfinite() - set = unique(vcat(allunits(I)..., [randsector(I) for k in 1:10])) - gen = (c => 2 for c in set) - else - gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) - end - V = GradedSpace(gen) - @test eval(Meta.parse(type_repr(typeof(V)))) == typeof(V) - @test eval_show(V) == V - @test eval_show(V') == V' - @test V' == GradedSpace(gen; dual = true) - @test V == @constinferred GradedSpace(gen...) - @test V' == @constinferred GradedSpace(gen...; dual = true) - @test V == @constinferred GradedSpace(tuple(gen...)) - @test V' == @constinferred GradedSpace(tuple(gen...); dual = true) - @test V == @constinferred GradedSpace(Dict(gen)) - @test V' == @constinferred GradedSpace(Dict(gen); dual = true) - @test V == @inferred Vect[I](gen) - @test V' == @constinferred Vect[I](gen; dual = true) - @test V == @constinferred Vect[I](gen...) - @test V' == @constinferred Vect[I](gen...; dual = true) - @test V == @constinferred Vect[I](Dict(gen)) - @test V' == @constinferred Vect[I](Dict(gen); dual = true) - @test V == @constinferred typeof(V)(c => dim(V, c) for c in sectors(V)) - if I isa ZNIrrep - @test V == @constinferred typeof(V)(V.dims) - @test V' == @constinferred typeof(V)(V.dims; dual = true) - end - @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') - @test V == GradedSpace(reverse(collect(gen))...) - @test eval_show(V) == V - @test eval_show(typeof(V)) == typeof(V) - # space with no sectors - @test dim(@constinferred(zerospace(V))) == 0 - # space with unit(s), always test as if multifusion - W = @constinferred GradedSpace(unit => 1 for unit in allunits(I)) - dict = Dict(unit => 1 for unit in allunits(I)) - @test W == GradedSpace(dict) - @test W == GradedSpace(push!(dict, randsector(I) => 0)) - @test @constinferred(zerospace(V)) == GradedSpace(unit => 0 for unit in allunits(I)) - randunit = rand(collect(allunits(I))) - @test_throws ArgumentError("Sector $(randunit) appears multiple times") GradedSpace(randunit => 1, randunit => 3) - - @test isunitspace(W) - @test @constinferred(unitspace(V)) == W == unitspace(typeof(V)) - if UnitStyle(I) isa SimpleUnit - @test @constinferred(leftunitspace(V)) == W == @constinferred(rightunitspace(V)) - else - @test_throws ArgumentError leftunitspace(V) - @test_throws ArgumentError rightunitspace(V) - end - @test eval_show(W) == W - @test isa(V, VectorSpace) - @test isa(V, ElementarySpace) - @test isa(InnerProductStyle(V), HasInnerProduct) - @test isa(InnerProductStyle(V), EuclideanInnerProduct) - @test isa(V, GradedSpace) - @test isa(V, GradedSpace{I}) - @test @constinferred(dual(V)) == @constinferred(conj(V)) == @constinferred(adjoint(V)) != V - @test @constinferred(field(V)) == ℂ - @test @constinferred(sectortype(V)) == I - slist = @constinferred sectors(V) - @test @constinferred(hassector(V, first(slist))) - @test @constinferred(dim(V)) == sum(dim(s) * dim(V, s) for s in slist) - @test @constinferred(reduceddim(V)) == sum(dim(V, s) for s in slist) - @constinferred dim(V, first(slist)) - if hasfusiontensor(I) - @test @constinferred(axes(V)) == Base.OneTo(dim(V)) - end - @test @constinferred(⊕(V, zerospace(V))) == V - @test @constinferred(⊕(V, V)) == Vect[I](c => 2dim(V, c) for c in sectors(V)) - @test @constinferred(⊕(V, V, V, V)) == Vect[I](c => 4dim(V, c) for c in sectors(V)) - @test @constinferred(⊕(V, unitspace(V))) == Vect[I](c => isunit(c) + dim(V, c) for c in sectors(V)) - @test @constinferred(fuse(V, unitspace(V))) == V - d = Dict{I, Int}() - for a in sectors(V), b in sectors(V) - for c in a ⊗ b - d[c] = get(d, c, 0) + dim(V, a) * dim(V, b) * Nsymbol(a, b, c) - end - end - @test @constinferred(fuse(V, V)) == GradedSpace(d) - @test @constinferred(flip(V)) == Vect[I](conj(c) => dim(V, c) for c in sectors(V))' - @test flip(V) ≅ V - @test flip(V) ≾ V - @test flip(V) ≿ V - @test @constinferred(⊕(V, V)) == @constinferred supremum(V, ⊕(V, V)) - @test V == @constinferred infimum(V, ⊕(V, V)) - @test V ≺ ⊕(V, V) - @test !(V ≻ ⊕(V, V)) - - u = first(allunits(I)) - @test infimum(V, GradedSpace(u => 3)) == GradedSpace(u => 2) - @test_throws SpaceMismatch (⊕(V, V')) +@timedtestset "ElementarySpace: $(type_repr(Vect[I]))" verbose = true for I in sectorlist + TensorKitTestSuite.test_spaces_graded_space(I) end @timedtestset "ProductSpace{ℂ}" begin @@ -428,53 +336,8 @@ end end @timedtestset "HomSpace" begin - for (V1, V2, V3, V4, V5) in ad_spacelist(fast_tests) - W = HomSpace(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - @test W == ((V3 ⊗ V4 ⊗ V5)' → V1 ⊗ V2) - @test W == (V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)') - @test W' == (V1 ⊗ V2 → (V3 ⊗ V4 ⊗ V5)') - @test codomain(W) == V1 ⊗ V2 - @test domain(W)' == V3 ⊗ V4 ⊗ V5 - @test eval_show(W) == W - @test eval_show(typeof(W)) == typeof(W) - @test spacetype(W) == typeof(V1) - @test sectortype(W) == sectortype(V1) - @test W[1] == V1 - @test W[2] == V2 - @test W[3] == V5 - @test W[4] == V4 - @test W[5] == V3 - @test all(W .== (V1, V2, V5, V4, V3)) - @test @constinferred(map(isdual, W)) == ntuple(i -> isdual(W[i]), length(W)) - @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') - @test W == deepcopy(W) - cod = codomain(W) - dom = domain(W) - @test (cod ← dom ⊗ rightunitspace(dom[3])) == - @constinferred(insertleftunit(W)) == - @constinferred(insertrightunit(W)) - @test @constinferred(removeunit(insertleftunit(W), $(numind(W) + 1))) == W - @test (cod ← dom ⊗ rightunitspace(dom[3])') == - @constinferred(insertleftunit(W; conj = true)) == - @constinferred(insertrightunit(W; conj = true)) - @test (leftunitspace(cod[1]) ⊗ cod ← dom) == - @constinferred(insertleftunit(W, 1)) == - @constinferred(insertrightunit(W, 0)) - @test (cod ⊗ rightunitspace(cod[2]) ← dom) == - @constinferred(insertrightunit(W, 2)) - @test (cod ← leftunitspace(dom[1]) ⊗ dom) == - @constinferred(insertleftunit(W, 3)) - @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W - if UnitStyle(sectortype(W)) isa SimpleUnit - @test @constinferred(insertrightunit(one(V1) ← V1, 0)) == (unitspace(V1) ← V1) - @test_throws BoundsError insertleftunit(one(V1) ← V1, 0) - else - @test_throws ArgumentError insertrightunit(one(V1) ← V1, 0) - @test_throws ArgumentError insertleftunit(one(V1) ← V1, 0) - end - @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') - @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) - @test permute(W, ((2, 5, 4), (1, 3))) == (V2 ⊗ V3 ⊗ V4 ← V1' ⊗ V5') # cyclic permutation + for V in ad_spacelist(fast_tests) + TensorKitTestSuite.test_tensors_hom_space(V) end end diff --git a/test/tensors/braidingtensor.jl b/test/tensors/braidingtensor.jl index bcf887b80..87958cda3 100644 --- a/test/tensors/braidingtensor.jl +++ b/test/tensors/braidingtensor.jl @@ -4,234 +4,15 @@ using TensorKit: type_repr spacelist = default_spacelist(fast_tests) -for V_tuple in spacelist - I = sectortype(first(V_tuple)) +for V in spacelist + I = sectortype(first(V)) Istr = type_repr(I) BraidingStyle(I) isa NoBraiding && continue - V = first(V_tuple) - T = ComplexF64 - t = randn(T, V ⊗ V' ⊗ V' ⊗ V ← V ⊗ V') println("---------------------------------------") println("BraidingTensor planar contractions with symmetry: $Istr") println("---------------------------------------") @timedtestset "BraidingTensor planar contractions with symmetry: $Istr" verbose = true begin - @timedtestset "planaradd! with BraidingTensor" begin - b = BraidingTensor(V, V') - bb = TensorMap(b) - # Cyclic rotations of the planar leg cycle (cod1, cod2, dom2, dom1). - # Use transpose (F-symbols only) as reference, since permute requires SymmetricBraiding. - # rotation 0 (identity) - @planar t1[-1 -2; -3 -4] := b[-1 -2; -3 -4] - @test t1 ≈ bb - # rotation 1: single-tree cycle (4,1,2,3) → (p1=(2,4), p2=(1,3)) - @planar t2[-1 -2; -3 -4] := b[-3 -1; -4 -2] - @test t2 ≈ transpose(bb, ((2, 4), (1, 3))) - # rotation 2: single-tree cycle (3,4,1,2) → (p1=(4,3), p2=(2,1)) - @planar t3[-1 -2; -3 -4] := b[-4 -3; -2 -1] - @test t3 ≈ transpose(bb, ((4, 3), (2, 1))) - # rotation 3: single-tree cycle (2,3,4,1) → (p1=(3,1), p2=(4,2)) - @planar t4[-1 -2; -3 -4] := b[-2 -4; -1 -3] - @test t4 ≈ transpose(bb, ((3, 1), (4, 2))) - # adjoint BraidingTensor (rotation 0) - ba = b' - @planar t5[-1 -2; -3 -4] := ba[-1 -2; -3 -4] - @test t5 ≈ TensorMap(ba) - end - - @timedtestset "τ as left factor, all legs contracted" begin - # BraidingTensor(V, V') on leading codomain indices - ττ = TensorMap(BraidingTensor(V, V')) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -2 -1] * t[1 2 -3 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 2; -1 1] * t[1 2 -3 -4; -5 -6] - @test t1 ≈ braid(t, ((2, 1, 3, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V') on inner codomain indices - ττ = TensorMap(BraidingTensor(V', V')) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ braid(t, ((1, 3, 2, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V) on trailing codomain indices - ττ = TensorMap(BraidingTensor(V', V)) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -4 -3] * t[-1 -2 1 2; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-4 2; -3 1] * t[-1 -2 1 2; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end - - @timedtestset "τ as left factor, mixed open legs" begin - # BraidingTensor(V', V) with mixed index pattern - ττ = TensorMap(BraidingTensor(V', V)) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V') with mixed index pattern (inverse of previous) - ττ = TensorMap(BraidingTensor(V, V')) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V) with mixed index pattern - ττ = TensorMap(BraidingTensor(V, V)) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end - - @timedtestset "τ as right factor" begin - # BraidingTensor(V', V) on all domain indices - ττ = TensorMap(BraidingTensor(V', V)) - @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[1 2; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ[1 2; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[-6 -5; 2 1] - @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[2 -6; 1 -5] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V') adjoint on all domain indices - ττ = TensorMap(BraidingTensor(V, V')) - @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[1 2; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ'[1 2; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[-6 -5; 2 1] - @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[2 -6; 1 -5] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V) with mixed domain legs - ττ = TensorMap(BraidingTensor(V, V)) - @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[-4 -6; 1 2] - @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * ττ[-4 -6; 1 2] - @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[2 1; -6 -4] - @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ'[-6 2; -4 1] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end - - @timedtestset "τ with fully contracted output" begin - # scalar output - ττ = TensorMap(BraidingTensor(V', V)) - @planar t1[(); (-1, -2)] := τ[2 1; 3 4] * t[1 2 3 4; -1 -2] - @planar t2[(); (-1, -2)] := ττ[2 1; 3 4] * t[1 2 3 4; -1 -2] - @planar t3[(); (-1, -2)] := τ[4 3; 1 2] * t[1 2 3 4; -1 -2] - @planar t4[(); (-1, -2)] := τ'[1 4; 2 3] * t[1 2 3 4; -1 -2] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # rank-1 output - ττ = TensorMap(BraidingTensor(V, V)) - @planar t1[-1; -2] := τ[2 1; 3 4] * t[-1 1 2 3; -2 4] - @planar t2[-1; -2] := ττ[2 1; 3 4] * t[-1 1 2 3; -2 4] - @planar t3[-1; -2] := τ[4 3; 1 2] * t[-1 1 2 3; -2 4] - @planar t4[-1; -2] := τ'[1 4; 2 3] * t[-1 1 2 3; -2 4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # rank-2 output - ττ = TensorMap(BraidingTensor(V, V')) - @planar t1[-1 -2] := τ[2 1; 3 4] * t[-1 -2 1 2; 4 3] - @planar t2[-1 -2] := ττ[2 1; 3 4] * t[-1 -2 1 2; 4 3] - @planar t3[-1 -2] := τ[4 3; 1 2] * t[-1 -2 1 2; 4 3] - @planar t4[-1 -2] := τ'[1 4; 2 3] * t[-1 -2 1 2; 4 3] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end - - @timedtestset "τ with one open codomain leg" begin - # BraidingTensor(V, V') with one open codomain leg - ττ = TensorMap(BraidingTensor(V, V')) - @planar t1[-1 -2; -3 -4] := τ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] - @planar t2[-1 -2; -3 -4] := ττ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] - @planar t3[-1 -2; -3 -4] := τ[2 1; 3 -1] * t[1 2 3 -2; -3 -4] - @planar t4[-1 -2; -3 -4] := τ'[3 2; -1 1] * t[1 2 3 -2; -3 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V') adjoint with one open codomain leg - ττ = TensorMap(BraidingTensor(V', V')) - @planar t1[-1 -2; -3 -4] := τ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] - @planar t2[-1 -2; -3 -4] := ττ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] - @planar t3[-1 -2; -3 -4] := τ'[2 1; 3 -2] * t[-1 1 2 3; -3 -4] - @planar t4[-1 -2; -3 -4] := τ[3 2; -2 1] * t[-1 1 2 3; -3 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V) with one open codomain leg - ττ = TensorMap(BraidingTensor(V', V)) - @planar t1[-1 -2 -3; -4] := τ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] - @planar t2[-1 -2 -3; -4] := ττ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] - @planar t3[-1 -2 -3; -4] := τ[2 1; 3 -3] * t[-1 -2 1 2; -4 3] - @planar t4[-1 -2 -3; -4] := τ'[3 2; -3 1] * t[-1 -2 1 2; -4 3] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end - - @timedtestset "τ as right factor with open domain leg" begin - # BraidingTensor(V', V) as right factor with one open domain leg - ττ = TensorMap(BraidingTensor(V', V)) - @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[1 2; -4 3] - @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ[1 2; -4 3] - @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[3 -4; 2 1] - @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[2 3; 1 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V') adjoint as right factor with one open domain leg - ττ = TensorMap(BraidingTensor(V, V')) - @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[1 2; -4 3] - @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ'[1 2; -4 3] - @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[3 -4; 2 1] - @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[2 3; 1 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end - - @timedtestset "BraidingTensor × BraidingTensor" begin - # b1 domain == b2 codomain == V⊗V', straight-through (planar) contraction - b1 = BraidingTensor(V, V') # space: V'⊗V ← V⊗V' - b2 = BraidingTensor(V', V) # space: V⊗V' ← V'⊗V - bb1 = TensorMap(b1) - bb2 = TensorMap(b2) - @planar t1[-1 -2; -3 -4] := b1[-1 -2; 1 2] * b2[1 2; -3 -4] - @planar t2[-1 -2; -3 -4] := bb1[-1 -2; 1 2] * bb2[1 2; -3 -4] - @test t1 ≈ t2 - end + TensorKitTestSuite.test_tensors_braiding_tensor(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/construction.jl b/test/tensors/construction.jl index c6047b8d8..8b46d50e8 100644 --- a/test/tensors/construction.jl +++ b/test/tensors/construction.jl @@ -12,115 +12,7 @@ for V in spacelist println("Tensor constructions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor constructions with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - @timedtestset "Basic tensor properties" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - for T in (fast_tests ? (Float64, ComplexF64) : (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat)) - t = @constinferred zeros(T, W) - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test norm(t) == 0 - @test codomain(t) == W - @test space(t) == (W ← one(W)) - @test domain(t) == one(W) - @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} - # Array type input - t = @constinferred zeros(Vector{T}, W) - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test norm(t) == 0 - @test codomain(t) == W - @test space(t) == (W ← one(W)) - @test domain(t) == one(W) - @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} - # blocks - bs = @constinferred blocks(t) - if !isempty(blocksectors(t)) # multifusion space ending on module gives empty data - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(W)) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t, first(blocksectors(t))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t) - @test typeof(c) === sectortype(t) - end - end - end - @timedtestset "Tensor Dict conversion" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - for T in (Int, Float32, ComplexF64) - t = @constinferred rand(T, W) - d = convert(Dict, t) - @test t == convert(TensorMap, d) - end - end - if hasfusiontensor(I) || I == Trivial - @timedtestset "Tensor Array conversion" begin - W1 = V1 ← one(V1) - W2 = one(V2) ← V2 - W3 = V1 ⊗ V2 ← one(V1) - W4 = V1 ← V2 - W5 = one(V1) ← V1 ⊗ V2 - W6 = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 - for W in (W1, W2, W3, W4, W5, W6) - for T in (Int, Float32, ComplexF64) - if T == Int - t = TensorMap{T}(undef, W) - for (_, b) in blocks(t) - rand!(b, -20:20) - end - else - t = @constinferred randn(T, W) - end - a = @constinferred convert(Array, t) - b = reshape(a, dim(codomain(W)), dim(domain(W))) - @test t ≈ @constinferred TensorMap(a, W) - @test t ≈ @constinferred TensorMap(b, W) - @test t === @constinferred TensorMap(t.data, W) - end - end - for T in (Int, Float32, ComplexF64) - t = randn(T, V1 ⊗ V2 ← zerospace(V1)) - a = convert(Array, t) - @test norm(a) == 0 - end - end - end - if hasfusiontensor(I) - @timedtestset "Real and imaginary parts" begin - W = V1 ⊗ V2 - for T in (Float64, ComplexF64, ComplexF32) - t = @constinferred randn(T, W, W) - - tr = @constinferred real(t) - @test scalartype(tr) <: Real - @test real(convert(Array, t)) == convert(Array, tr) - - ti = @constinferred imag(t) - @test scalartype(ti) <: Real - @test imag(convert(Array, t)) == convert(Array, ti) - - tc = @inferred complex(t) - @test scalartype(tc) <: Complex - @test complex(convert(Array, t)) == convert(Array, tc) - - tc2 = @inferred complex(tr, ti) - @test tc2 ≈ tc - end - end - end - @timedtestset "Tensor conversion" begin - W = V1 ⊗ V2 - t = @constinferred randn(W ← W) - @test typeof(convert(TensorMap, t')) == typeof(t) - tc = complex(t) - @test convert(typeof(tc), t) == tc - @test typeof(convert(typeof(tc), t)) == typeof(tc) - @test typeof(convert(typeof(tc), t')) == typeof(tc) - @test Base.promote_typeof(t, tc) == typeof(tc) - @test Base.promote_typeof(tc, t) == typeof(tc + t) - end + TensorKitTestSuite.test_tensors_tensor_constructions(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/contractions.jl b/test/tensors/contractions.jl index e457c39d5..ca16eb28d 100644 --- a/test/tensors/contractions.jl +++ b/test/tensors/contractions.jl @@ -8,144 +8,11 @@ spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V)) Istr = type_repr(I) - symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding println("---------------------------------------") println("Tensor contractions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor contractions with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - @timedtestset "Full trace: test self-consistency" begin - if symmetricbraiding - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') - t2 = permute(t, ((1, 2), (4, 3))) - s = @constinferred tr(t2) - @test conj(s) ≈ tr(t2') - if !isdual(V1) - t2 = twist!(t2, 1) - end - if isdual(V2) - t2 = twist!(t2, 2) - end - ss = tr(t2) - @tensor s2 = t[a, b, b, a] - @tensor t3[a, b] := t[a, c, c, b] - @tensor s3 = t3[a, a] - @test ss ≈ s2 - @test ss ≈ s3 - end - t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) # avoid permutes - ss = @constinferred tr(t) - @test conj(ss) ≈ tr(t') - @planar s2 = t[a b; a b] - @planar t3[a; b] := t[a c; b c] - @planar s3 = t3[a; a] - - @test ss ≈ s2 - @test ss ≈ s3 - end - @timedtestset "Partial trace: test self-consistency" begin - if symmetricbraiding - t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) - @tensor t2[a; b] := t[c d b; c d a] - @tensor t4[a b; c d] := t[e d c; e b a] - @tensor t5[a; b] := t4[a c; b c] - @test t2 ≈ t5 - end - t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # compatible with module fusion - @planar t2[a; b] := t[c a d; c b d] - @planar t4[a b; c d] := t[e a b; e c d] - @planar t5[a; b] := t4[a c; b c] - @test t2 ≈ t5 - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Trace: test via conversion" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') - @tensor t2[a, b] := t[c, d, b, d, c, a] - @tensor t3[a, b] := convert(Array, t)[c, d, b, d, c, a] - @test t3 ≈ convert(Array, t2) - end - end - #TODO: find version that works for all multifusion cases - symmetricbraiding && @timedtestset "Trace and contraction" begin - t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3) - t2 = rand(ComplexF64, V2' ⊗ V4 ⊗ V1') - t3 = t1 ⊗ t2 - @tensor ta[a, b] := t1[x, y, a] * t2[y, b, x] - @tensor tb[a, b] := t3[x, y, a, y, b, x] - @test ta ≈ tb - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor contraction: test via conversion" begin - A1 = randn(ComplexF64, V1' * V2', V3') - A2 = randn(ComplexF64, V3 * V4, V5) - rhoL = randn(ComplexF64, V1, V1) - rhoR = randn(ComplexF64, V5, V5)' # test adjoint tensor - H = randn(ComplexF64, V2 * V4, V2 * V4) - @tensor HrA12[a, s1, s2, c] := rhoL[a, a'] * conj(A1[a', t1, b]) * - A2[b, t2, c'] * rhoR[c', c] * H[s1, s2, t1, t2] - - @tensor HrA12array[a, s1, s2, c] := convert(Array, rhoL)[a, a'] * - conj(convert(Array, A1)[a', t1, b]) * convert(Array, A2)[b, t2, c'] * - convert(Array, rhoR)[c', c] * convert(Array, H)[s1, s2, t1, t2] - - @test HrA12array ≈ convert(Array, HrA12) - end - end - @timedtestset "Tensor product: test via norm preservation" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V1, V5') - t2 = rand(T, V2 ⊗ V3, V4') - t = @constinferred (t1 ⊗ t2) - @test norm(t) ≈ norm(t1) * norm(t2) - end - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor product: test via conversion" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V1, V5') - t2 = rand(T, V2 ⊗ V3, V4') - t = @constinferred (t1 ⊗ t2) - d1 = dim(codomain(t1)) - d2 = dim(codomain(t2)) - d3 = dim(domain(t1)) - d4 = dim(domain(t2)) - At = convert(Array, t) - @test reshape(At, (d1, d2, d3, d4)) ≈ - reshape(convert(Array, t1), (d1, 1, d3, 1)) .* - reshape(convert(Array, t2), (1, d2, 1, d4)) - end - end - end - symmetricbraiding && @timedtestset "Tensor product: test via tensor contraction" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V1, V5') - t2 = rand(T, V2 ⊗ V3, V4') - t = @constinferred (t1 ⊗ t2) - @tensor t′[1 2 3; 4 5] := t1[1; 4] * t2[2 3; 5] - @test t ≈ t′ - end - end - @timedtestset "Tensor absorption" begin - # absorbing small into large - t1 = zeros((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') - t2 = rand(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - t3 = @constinferred absorb(t1, t2) - @test norm(t3) ≈ norm(t2) - @test norm(t1) == 0 - t4 = @constinferred absorb!(t1, t2) - @test t1 === t4 - @test t3 ≈ t4 - - # absorbing large into small - t1 = rand((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') - t2 = zeros(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - t3 = @constinferred absorb(t2, t1) - @test norm(t3) < norm(t1) - @test norm(t2) == 0 - t4 = @constinferred absorb!(t2, t1) - @test t2 === t4 - @test t3 ≈ t4 - end + TensorKitTestSuite.test_tensors_contractions(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index 836418b3f..d955d3460 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -1,7 +1,6 @@ using Test, TestExtras using TensorKit using TensorKit: type_repr -using Combinatorics: permutations spacelist = default_spacelist(fast_tests) @@ -9,134 +8,11 @@ spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V)) Istr = type_repr(I) - hasbraiding = BraidingStyle(I) isa HasBraiding - symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding println("---------------------------------------") println("Tensor index manipulations with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor index manipulations with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - @timedtestset "Trivial space insertion and removal" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) - t2 = @constinferred insertleftunit(t) - @test t2 == @constinferred insertrightunit(t) - @test space(t2) == insertleftunit(space(t)) - @test @constinferred(removeunit(t2, $(numind(t2)))) == t - t3 = @constinferred insertleftunit(t; copy = true) - @test t3 == @constinferred insertrightunit(t; copy = true) - @test @constinferred(removeunit(t3, $(numind(t3)))) == t - - @test numind(t2) == numind(t) + 1 - @test scalartype(t2) === T - @test t.data === t2.data - - @test t.data !== t3.data - for (c, b) in blocks(t) - @test b == block(t3, c) - end - - t4 = @constinferred insertrightunit(t, 3; dual = true) - @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) - for (c, b) in blocks(t) - @test b == block(t4, c) - end - @test @constinferred(removeunit(t4, 4)) == t - - t5 = @constinferred insertleftunit(t, 4; dual = true) - @test numin(t5) == numin(t) + 1 && numout(t5) == numout(t) - for (c, b) in blocks(t) - @test b == block(t5, c) - end - @test @constinferred(removeunit(t5, 4)) == t - end - end - symmetricbraiding && @timedtestset "Permutations: test via inner product invariance" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - t = rand(ComplexF64, W) - t′ = randn!(similar(t)) - for k in 0:5 - for p in permutations(1:5) - p1 = ntuple(n -> p[n], k) - p2 = ntuple(n -> p[k + n], 5 - k) - t2 = @constinferred permute(t, (p1, p2)) - @test norm(t2) ≈ norm(t) - t2′ = permute(t′, (p1, p2)) - @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) - end - - t3 = @constinferred repartition(t, $k) - @test norm(t3) ≈ norm(t) - t3′ = @constinferred repartition!(similar(t3), t′) - @test norm(t3′) ≈ norm(t′) - @test dot(t′, t) ≈ dot(t3′, t3) - end - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Permutations: test via conversion" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - t = rand(ComplexF64, W) - a = convert(Array, t) - for k in 0:5 - for p in permutations(1:5) - p1 = ntuple(n -> p[n], k) - p2 = ntuple(n -> p[k + n], 5 - k) - t2 = permute(t, (p1, p2)) - a2 = convert(Array, t2) - @test a2 ≈ permutedims(a, (p1..., p2...)) - @test convert(Array, transpose(t2)) ≈ - permutedims(a2, (5, 4, 3, 2, 1)) - end - - t3 = repartition(t, k) - a3 = convert(Array, t3) - @test a3 ≈ permutedims( - a, (ntuple(identity, k)..., reverse(ntuple(i -> i + k, 5 - k))...) - ) - end - end - end - hasbraiding && @timedtestset "Index flipping: test flipping inverse" begin - t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)') - for i in 1:5 - @test t ≈ flip(flip(t, i), i; inv = true) - @test t ≈ flip(flip(t, i; inv = true), i) - end - end - symmetricbraiding && @timedtestset "Index flipping: test via explicit flip" begin - t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) - F1 = unitary(flip(V1), V1) - - @tensor tf[a, b; c, d] := F1[a, a'] * t[a', b; c, d] - @test flip(t, 1) ≈ tf - @tensor tf[a, b; c, d] := conj(F1[b, b']) * t[a, b'; c, d] - @test twist!(flip(t, 2), 2) ≈ tf - @tensor tf[a, b; c, d] := F1[c, c'] * t[a, b; c', d] - @test flip(t, 3) ≈ tf - @tensor tf[a, b; c, d] := conj(F1[d, d']) * t[a, b; c, d'] - @test twist!(flip(t, 4), 4) ≈ tf - end - symmetricbraiding && @timedtestset "Index flipping: test via contraction" begin - t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V4) - t2 = rand(ComplexF64, V2' ⊗ V5 ← V4' ⊗ V1) - @tensor ta[a, b] := t1[x, y, a, z] * t2[y, b, z, x] - @tensor tb[a, b] := flip(t1, 1)[x, y, a, z] * flip(t2, 4)[y, b, z, x] - @test ta ≈ tb - @tensor tb[a, b] := flip(t1, (2, 4))[x, y, a, z] * flip(t2, (1, 3))[y, b, z, x] - @test ta ≈ tb - @tensor tb[a, b] := flip(t1, (1, 2, 4))[x, y, a, z] * flip(t2, (1, 3, 4))[y, b, z, x] - @tensor tb[a, b] := flip(t1, (1, 3))[x, y, a, z] * flip(t2, (2, 4))[y, b, z, x] - @test flip(ta, (1, 2)) ≈ tb - end - hasbraiding && !symmetricbraiding && @timedtestset "Braid AdjointTensorMap: adjoint identity" begin - t = rand(ComplexF64, V1 ⊗ V2 ← V3) - p = ((2,), (1, 3)) - levels = (1, 3, 2) - t1 = copy(braid(t', p, levels)) - t2 = braid(copy(t'), p, levels) - @test t1 ≈ t2 - end + TensorKitTestSuite.test_tensors_index_manipulations(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/linalg.jl b/test/tensors/linalg.jl index 738e85939..ba4107732 100644 --- a/test/tensors/linalg.jl +++ b/test/tensors/linalg.jl @@ -8,223 +8,11 @@ spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V)) Istr = type_repr(I) - symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding println("---------------------------------------") println("Tensor linear algebra with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor linear algebra with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - @timedtestset "Basic linear algebra" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) - @test scalartype(t) == T - @test space(t) == W - @test space(t') == W' - @test dim(t) == dim(space(t)) - @test codomain(t) == codomain(W) - @test domain(t) == domain(W) - # blocks for adjoint - bs = @constinferred blocks(t') - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(W')) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t', first(blocksectors(t'))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t') - @test typeof(c) === sectortype(t) - # linear algebra - @test isa(@constinferred(norm(t)), real(T)) - @test norm(t)^2 ≈ dot(t, t) - α = rand(T) - @test norm(α * t) ≈ abs(α) * norm(t) - @test norm(t + t, 2) ≈ 2 * norm(t, 2) - @test norm(t + t, 1) ≈ 2 * norm(t, 1) - @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) - p = 3 * rand(Float64) - @test norm(t + t, p) ≈ 2 * norm(t, p) - @test norm(t) ≈ norm(t') - - t2 = @constinferred rand!(similar(t)) - β = rand(T) - @test @constinferred(dot(β * t2, α * t)) ≈ conj(β) * α * conj(dot(t, t2)) - @test dot(t2, t) ≈ conj(dot(t, t2)) - @test dot(t2, t) ≈ conj(dot(t2', t')) - @test dot(t2, t) ≈ dot(t', t2') - - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) - i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved - i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) - @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) - @test i2 * i1 == @constinferred(id(Vector{T}, V2 ⊗ V1)) - end - - w = @constinferred isometry(T, V1 ⊗ (rightunitspace(V1) ⊕ rightunitspace(V1)), V1) - @test dim(w) == 2 * dim(V1 ← V1) - @test w' * w == id(Vector{T}, V1) - @test w * w' == (w * w')^2 - end - end - if hasfusiontensor(I) - @timedtestset "Basic linear algebra: test via conversion" begin - W = V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)' - for T in (Float32, ComplexF64) - t = rand(T, W) - t2 = @constinferred rand!(similar(t)) - @test norm(t, 2) ≈ norm(convert(Array, t), 2) - @test dot(t2, t) ≈ dot(convert(Array, t2), convert(Array, t)) - α = rand(T) - @test convert(Array, α * t) ≈ α * convert(Array, t) - @test convert(Array, t + t) ≈ 2 * convert(Array, t) - end - end - end - @timedtestset "Multiplication of isometries: test properties" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = (V4 ⊗ V5)' - for T in (Float64, ComplexF64) - t1 = randisometry(T, W1, W2) - t2 = randisometry(T, W2 ← W2) - @test isisometric(t1) - @test isunitary(t2) - P = t1 * t1' - @test P * P ≈ P - end - end - @timedtestset "Multiplication and inverse: test compatibility" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = (V4 ⊗ V5)' - for T in (Float64, ComplexF64) - t1 = rand(T, W1, W1) - t2 = rand(T, W2 ← W2) - t = rand(T, W1, W2) - @test t1 * (t1 \ t) ≈ t - @test (t / t2) * t2 ≈ t - @test t1 \ one(t1) ≈ inv(t1) - @test one(t1) / t1 ≈ pinv(t1) - @test_throws SpaceMismatch inv(t) - @test_throws SpaceMismatch t2 \ t - @test_throws SpaceMismatch t / t1 - tp = pinv(t) * t - @test tp ≈ tp * tp - end - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Multiplication and inverse: test via conversion" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = (V4 ⊗ V5)' - for T in (Float32, Float64, ComplexF32, ComplexF64) - t1 = rand(T, W1 ← W1) - t2 = rand(T, W2, W2) - t = rand(T, W1 ← W2) - d1 = dim(W1) - d2 = dim(W2) - At1 = reshape(convert(Array, t1), d1, d1) - At2 = reshape(convert(Array, t2), d2, d2) - At = reshape(convert(Array, t), d1, d2) - @test reshape(convert(Array, t1 * t), d1, d2) ≈ At1 * At - @test reshape(convert(Array, t1' * t), d1, d2) ≈ At1' * At - @test reshape(convert(Array, t2 * t'), d2, d1) ≈ At2 * At' - @test reshape(convert(Array, t2' * t'), d2, d1) ≈ At2' * At' - - @test reshape(convert(Array, inv(t1)), d1, d1) ≈ inv(At1) - @test reshape(convert(Array, pinv(t)), d2, d1) ≈ pinv(At) - - if T == Float32 || T == ComplexF32 - continue - end - - @test reshape(convert(Array, t1 \ t), d1, d2) ≈ At1 \ At - @test reshape(convert(Array, t1' \ t), d1, d2) ≈ At1' \ At - @test reshape(convert(Array, t2 \ t'), d2, d1) ≈ At2 \ At' - @test reshape(convert(Array, t2' \ t'), d2, d1) ≈ At2' \ At' - - @test reshape(convert(Array, t2 / t), d2, d1) ≈ At2 / At - @test reshape(convert(Array, t2' / t), d2, d1) ≈ At2' / At - @test reshape(convert(Array, t1 / t'), d1, d2) ≈ At1 / At' - @test reshape(convert(Array, t1' / t'), d1, d2) ≈ At1' / At' - end - end - end - @timedtestset "diag/diagm" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - t = randn(ComplexF64, W) - d = LinearAlgebra.diag(t) - D = LinearAlgebra.diagm(codomain(t), domain(t), d) - @test LinearAlgebra.isdiag(D) - @test LinearAlgebra.diag(D) == d - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor functions" begin - W = V1 ⊗ V2 - for T in (Float64, ComplexF64) - t = randn(T, W, W) - s = dim(W) - expt = @constinferred exp(t) - @test reshape(convert(Array, expt), (s, s)) ≈ - exp(reshape(convert(Array, t), (s, s))) - - @test (@constinferred sqrt(t))^2 ≈ t - @test reshape(convert(Array, sqrt(t^2)), (s, s)) ≈ - sqrt(reshape(convert(Array, t^2), (s, s))) - - @test exp(@constinferred log(expt)) ≈ expt - @test reshape(convert(Array, log(expt)), (s, s)) ≈ - log(reshape(convert(Array, expt), (s, s))) - - @test (@constinferred cos(t))^2 + (@constinferred sin(t))^2 ≈ id(W) - @test (@constinferred tan(t)) ≈ sin(t) / cos(t) - @test (@constinferred cot(t)) ≈ cos(t) / sin(t) - @test (@constinferred cosh(t))^2 - (@constinferred sinh(t))^2 ≈ id(W) - @test (@constinferred tanh(t)) ≈ sinh(t) / cosh(t) - @test (@constinferred coth(t)) ≈ cosh(t) / sinh(t) - - t1 = sin(t) - @test sin(@constinferred asin(t1)) ≈ t1 - t2 = cos(t) - @test cos(@constinferred acos(t2)) ≈ t2 - t3 = sinh(t) - @test sinh(@constinferred asinh(t3)) ≈ t3 - t4 = cosh(t) - @test cosh(@constinferred acosh(t4)) ≈ t4 - t5 = tan(t) - @test tan(@constinferred atan(t5)) ≈ t5 - t6 = cot(t) - @test cot(@constinferred acot(t6)) ≈ t6 - t7 = tanh(t) - @test tanh(@constinferred atanh(t7)) ≈ t7 - t8 = coth(t) - @test coth(@constinferred acoth(t8)) ≈ t8 - t = randn(T, W, V1) # not square - for f in - ( - cos, sin, tan, cot, cosh, sinh, tanh, coth, atan, acot, asinh, - sqrt, log, asin, acos, acosh, atanh, acoth, - ) - @test_throws SpaceMismatch f(t) - end - end - end - end - @timedtestset "Sylvester equation" begin - for T in (Float32, ComplexF64) - tA = rand(T, V1 ⊗ V2, V1 ⊗ V2) - tB = rand(T, (V3 ⊗ V4 ⊗ V5)', (V3 ⊗ V4 ⊗ V5)') - tA = 3 // 2 * left_polar(tA)[1] - tB = 1 // 5 * left_polar(tB)[1] - tC = rand(T, V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - t = @constinferred sylvester(tA, tB, tC) - @test codomain(t) == V1 ⊗ V2 - @test domain(t) == (V3 ⊗ V4 ⊗ V5)' - @test norm(tA * t + t * tB + tC) < - (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - matrix(x) = reshape(convert(Array, x), dim(codomain(x)), dim(domain(x))) - @test matrix(t) ≈ sylvester(matrix(tA), matrix(tB), matrix(tC)) - end - end - end + TensorKitTestSuite.test_tensors_linear_algebra(V) end TensorKit.empty_globalcaches!() end From f469892cfa465f0d5115e58afc3b132579bf45e0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 6 Jul 2026 17:46:09 +0200 Subject: [PATCH 03/13] use TKS's testsuite for sector utility --- test/TensorKitTestSuite.jl | 78 ++++++++++---------------------------- test/setup.jl | 6 +-- 2 files changed, 23 insertions(+), 61 deletions(-) diff --git a/test/TensorKitTestSuite.jl b/test/TensorKitTestSuite.jl index bdfaec0d9..d4c659015 100644 --- a/test/TensorKitTestSuite.jl +++ b/test/TensorKitTestSuite.jl @@ -21,18 +21,16 @@ TensorKitTestSuite.test_tensors((V1, V2, V3, V4, V5)) # 5 mutually compatible sp ``` The three entry points above are independent and may be run selectively. - -Additionally, this test suite exports the following convenience testing utilities: -* [`smallset`](@ref) -* [`randsector`](@ref) -* [`random_fusion`](@ref) -* [`hasfusiontensor`](@ref) +This module additionally exports: * [`force_planar`](@ref) + +Sector-level helpers are reused from `TensorKitSectors.SectorTestSuite` internally, +but deliberately *not* re-exported here. """ module TensorKitTestSuite export test_fusiontrees, test_spaces, test_tensors -export smallset, randsector, random_fusion, hasfusiontensor, force_planar +export force_planar using Test using TestExtras @@ -43,13 +41,21 @@ using TupleTools using Combinatorics: permutations using TensorOperations using MatrixAlgebraKit: left_polar, isunitary -using Base.Iterators: take, product using TensorKit using TensorKit: type_repr, FusionTreeBlock, ℙ, PlanarTrivial, hassector, HomSpace import TensorKit as TK using TensorKitSectors +# Reuse TensorKitSectors's own sector-level test helpers +sectortestsuite_path = joinpath( + dirname(dirname(pathof(TensorKitSectors))), "test", "testsuite.jl" +) +include(sectortestsuite_path) +using .SectorTestSuite: smallset, randsector, hasfusiontensor +using .SectorTestSuite: can_fuse, F_unitarity_test, R_unitarity_test +import .SectorTestSuite: random_fusion # TODO: is the method added below needed? + const testgroups = Dict{Symbol, Dict{String, Expr}}( :fusiontrees => Dict{String, Expr}(), :spaces => Dict{String, Expr}(), @@ -78,7 +84,7 @@ Important: Whatever is passed as `name` becomes part of the generated function t In particular, a `safe_name` is made where `name`'s spaces are replaced by underscores, and everything becomes lowercase. One then calls `test__`. This way, individual entries can be invoked without running the whole test group. """ -macro testsuite(testgroup, name, ex) #TODO: rename +macro testsuite(testgroup, name, ex) Meta.isexpr(ex, :(->)) || error("@testsuite requires an `arg -> body` expression") testgroupsym = testgroup isa QuoteNode ? testgroup.value : testgroup safe_name = lowercase(replace(name, r"[^A-Za-z0-9]+" => "_")) @@ -142,58 +148,16 @@ end # Sector utilities # ---------------- -# TODO: replace with the ones from TensorKitSectors.SectorTestSuite -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 - -function hasfusiontensor(I::Type{<:Sector}) - try - u = first(allunits(I)) - fusiontensor(u, u, u) - return true - catch e - if e isa MethodError - return false - else - rethrow(e) - end - end -end - """ random_fusion(I::Type{<:Sector}, ::Val{N}) where {N} -Returns an `N`-tuple of sectors of type `I` that can consistently be used as the -uncoupled sectors of a fusion tree, i.e. consecutive sectors have a non-empty fusion product. +Returns an `NTuple{N,I}` of sectors that can consistently be used as the uncoupled +sectors of a fusion tree, i.e. consecutive sectors have a non-empty fusion product. +Thin wrapper around `SectorTestSuite.random_fusion(I, N::Int)`. """ -function random_fusion(I::Type{<:Sector}, ::Val{N}) where {N} #TODO: merge with TKS random_fusion - N == 1 && return (randsector(I),) - tail = random_fusion(I, Val(N - 1)) - s = randsector(I) - counter = 0 - while isempty(⊗(s, first(tail))) && counter < 20 - counter += 1 - s = (counter < 20) ? randsector(I) : leftunit(first(tail)) - end - return (s, tail...) +function random_fusion(I::Type{<:Sector}, ::Val{N}) where {N} + v = random_fusion(I, N) + return ntuple(i -> v[i], Val(N)) end """ diff --git a/test/setup.jl b/test/setup.jl index 019938ba1..9c3c31ad3 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -24,7 +24,9 @@ using Zygote: Zygote, rrule_via_ad include(joinpath(@__DIR__, "TensorKitTestSuite.jl")) using .TensorKitTestSuite +# not exported by TensorKitTestSuite to avoid clash with TensorKitSectors.SectorTestSuite using .TensorKitTestSuite: _isunitary, _isone +using .TensorKitTestSuite: smallset, randsector, hasfusiontensor, random_fusion Random.seed!(123456) @@ -68,10 +70,6 @@ default_tol(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 # Sector lists # -------------- - -# TODO: make the changes to make this compatible with also importing TKS's SectorTestSuite -# there's some overlap in names, and TKS's SectorTestSuite has some of these functions already - uniquefusionsectorlist = ( Z2Irrep, Z3Irrep, Z4Irrep, Z3Irrep ⊠ Z4Irrep, U1Irrep, FermionParity, FermionParity ⊠ FermionParity, FermionNumber, # fermionic From bad9878d8569f28c62293a0b0164f0e682c27984 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 6 Jul 2026 18:05:56 +0200 Subject: [PATCH 04/13] homogenise eval_show --- test/TensorKitTestSuite.jl | 14 +++++++++++++- test/setup.jl | 2 +- test/symmetries/spaces.jl | 12 ------------ test/testsuite/spaces.jl | 13 ------------- test/testsuite/tensors.jl | 4 ++-- 5 files changed, 16 insertions(+), 29 deletions(-) diff --git a/test/TensorKitTestSuite.jl b/test/TensorKitTestSuite.jl index d4c659015..62976e150 100644 --- a/test/TensorKitTestSuite.jl +++ b/test/TensorKitTestSuite.jl @@ -23,6 +23,7 @@ TensorKitTestSuite.test_tensors((V1, V2, V3, V4, V5)) # 5 mutually compatible sp The three entry points above are independent and may be run selectively. This module additionally exports: * [`force_planar`](@ref) +* [`eval_show`](@ref) Sector-level helpers are reused from `TensorKitSectors.SectorTestSuite` internally, but deliberately *not* re-exported here. @@ -30,7 +31,7 @@ but deliberately *not* re-exported here. module TensorKitTestSuite export test_fusiontrees, test_spaces, test_tensors -export force_planar +export force_planar, eval_show using Test using TestExtras @@ -198,6 +199,17 @@ _isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) _isunitary(x; kwargs...) = isunitary(x; kwargs...) _isone(x; kwargs...) = isapprox(x, one(x); kwargs...) +""" + eval_show(x) + +Use `show` to generate a string representation of `x`, then parse and evaluate the resulting expression. +""" +function eval_show(x) + str = sprint(show, x; context = (:module => @__MODULE__)) + ex = Meta.parse(str) + return eval(ex) +end + include("testsuite/fusiontrees.jl") include("testsuite/spaces.jl") include("testsuite/tensors.jl") diff --git a/test/setup.jl b/test/setup.jl index 9c3c31ad3..80318f68c 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -2,7 +2,7 @@ module TestSetup export randindextuple, randcircshift, _repartition, trivtuple export default_tol -export smallset, randsector, hasfusiontensor, force_planar +export smallset, randsector, hasfusiontensor, force_planar, eval_show export random_fusion export sectorlist, fast_sectorlist # export dim_isapprox diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index dbe1ed1a8..8774bd449 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -5,18 +5,6 @@ using TensorKit: hassector, type_repr, HomSpace, sectorequal, sectorhash # TODO: remove this once type_repr works for all included types using TensorKitSectors - -""" - eval_show(x) - -Use `show` to generate a string representation of `x`, then parse and evaluate the resulting expression. -""" -function eval_show(x) #TODO: move this to setup.jl - str = sprint(show, x; context = (:module => @__MODULE__)) - ex = Meta.parse(str) - return eval(ex) -end - @timedtestset "Fields" begin @test isa(ℝ, Field) @test isa(ℂ, Field) diff --git a/test/testsuite/spaces.jl b/test/testsuite/spaces.jl index d92cfc10d..26b7105b4 100644 --- a/test/testsuite/spaces.jl +++ b/test/testsuite/spaces.jl @@ -1,17 +1,4 @@ # GradedSpace -# =========== -# Ported from the sector-parametrized part of test/symmetries/spaces.jl. - -""" - eval_show(x) - -Use `show` to generate a string representation of `x`, then parse and evaluate the resulting expression. -""" -function eval_show(x) #TODO: move this function to setup so it doesn't repeat - str = sprint(show, x; context = (:module => @__MODULE__)) - ex = Meta.parse(str) - return eval(ex) -end @testsuite :spaces "graded space" I -> begin if Base.IteratorSize(values(I)) === Base.IsInfinite() diff --git a/test/testsuite/tensors.jl b/test/testsuite/tensors.jl index 20eed70c1..9c441e9b0 100644 --- a/test/testsuite/tensors.jl +++ b/test/testsuite/tensors.jl @@ -829,8 +829,8 @@ end @test W' == (V1 ⊗ V2 → (V3 ⊗ V4 ⊗ V5)') @test codomain(W) == V1 ⊗ V2 @test domain(W)' == V3 ⊗ V4 ⊗ V5 - @test _eval_show(W) == W - @test _eval_show(typeof(W)) == typeof(W) + @test eval_show(W) == W + @test eval_show(typeof(W)) == typeof(W) @test spacetype(W) == typeof(V1) @test sectortype(W) == sectortype(V1) @test W[1] == V1 From 91127f9f04a9dc3f3bf98ce265ebc797b4d1b710 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 7 Jul 2026 14:29:52 +0200 Subject: [PATCH 05/13] avoid testsuite being recognised as tests --- test/runtests.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 7e76a60e7..0d36a7004 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,6 +5,10 @@ testsuite = ParallelTestRunner.find_tests(@__DIR__) # Exclude non-test files delete!(testsuite, "setup") # shared setup module +delete!(testsuite, "TensorKitTestSuite") # reusable test suite module, not a test file itself +delete!(testsuite, "testsuite/fusiontrees") # only meant to be `include`d inside TensorKitTestSuite +delete!(testsuite, "testsuite/spaces") +delete!(testsuite, "testsuite/tensors") # CUDA tests: only run if CUDA is functional using CUDA: CUDA From cbaf351fe97afe43f1cb8a93dffce6ebe26ed4aa Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 7 Jul 2026 14:31:10 +0200 Subject: [PATCH 06/13] fix x ambiguity --- test/TensorKitTestSuite.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/TensorKitTestSuite.jl b/test/TensorKitTestSuite.jl index 62976e150..ad47ad64d 100644 --- a/test/TensorKitTestSuite.jl +++ b/test/TensorKitTestSuite.jl @@ -47,6 +47,7 @@ using TensorKit using TensorKit: type_repr, FusionTreeBlock, ℙ, PlanarTrivial, hassector, HomSpace import TensorKit as TK using TensorKitSectors +using TensorKitSectors: × # Reuse TensorKitSectors's own sector-level test helpers sectortestsuite_path = joinpath( From 10f8a4e74246afa0996f446c75695c5e0dd9b3e9 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 7 Jul 2026 16:20:13 +0200 Subject: [PATCH 07/13] move testsuite file --- test/setup.jl | 2 +- test/{ => testsuite}/TensorKitTestSuite.jl | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/{ => testsuite}/TensorKitTestSuite.jl (100%) diff --git a/test/setup.jl b/test/setup.jl index 80318f68c..81a0926c5 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -22,7 +22,7 @@ using ChainRulesCore: NoTangent using ChainRulesTestUtils: ChainRulesTestUtils, test_rrule using Zygote: Zygote, rrule_via_ad -include(joinpath(@__DIR__, "TensorKitTestSuite.jl")) +include(joinpath(@__DIR__, "testsuite", "TensorKitTestSuite.jl")) using .TensorKitTestSuite # not exported by TensorKitTestSuite to avoid clash with TensorKitSectors.SectorTestSuite using .TensorKitTestSuite: _isunitary, _isone diff --git a/test/TensorKitTestSuite.jl b/test/testsuite/TensorKitTestSuite.jl similarity index 100% rename from test/TensorKitTestSuite.jl rename to test/testsuite/TensorKitTestSuite.jl From eff5ab67b303db5f5ed56672736130fb2fa49c24 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 7 Jul 2026 16:29:15 +0200 Subject: [PATCH 08/13] make separate entry points for single and double fusion tree tests --- test/symmetries/doubletree.jl | 13 +++++---- test/symmetries/singletree.jl | 14 +++++----- test/testsuite/TensorKitTestSuite.jl | 40 +++++++++++++++++++--------- test/testsuite/fusiontrees.jl | 26 +++++++++--------- 4 files changed, 54 insertions(+), 39 deletions(-) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index 00dd32532..483d5b26e 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -5,13 +5,12 @@ import TensorKit as TK # TODO: remove this once type_repr works for all included types using TensorKitSectors - @timedtestset "Double fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - TensorKitTestSuite.test_fusiontrees_bending(I) - TensorKitTestSuite.test_fusiontrees_folding(I) - TensorKitTestSuite.test_fusiontrees_repartitioning(I) - TensorKitTestSuite.test_fusiontrees_transposition(I) - TensorKitTestSuite.test_fusiontrees_permutation_and_braiding(I) - TensorKitTestSuite.test_fusiontrees_planar_trace(I) + TensorKitTestSuite.test_double_fusiontrees_bending(I) + TensorKitTestSuite.test_double_fusiontrees_folding(I) + TensorKitTestSuite.test_double_fusiontrees_repartitioning(I) + TensorKitTestSuite.test_double_fusiontrees_transposition(I) + TensorKitTestSuite.test_double_fusiontrees_permutation_and_braiding(I) + TensorKitTestSuite.test_double_fusiontrees_planar_trace(I) TK.empty_globalcaches!() end diff --git a/test/symmetries/singletree.jl b/test/symmetries/singletree.jl index bf36884a2..faa2cd5e9 100644 --- a/test/symmetries/singletree.jl +++ b/test/symmetries/singletree.jl @@ -6,12 +6,12 @@ import TensorKit as TK using TensorKitSectors @timedtestset "Single fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - TensorKitTestSuite.test_fusiontrees_iterate_and_printing(I) - TensorKitTestSuite.test_fusiontrees_constructor_properties(I) - TensorKitTestSuite.test_fusiontrees_split_and_join(I) - TensorKitTestSuite.test_fusiontrees_multi_fmove(I) - TensorKitTestSuite.test_fusiontrees_insertat(I) - TensorKitTestSuite.test_fusiontrees_merging(I) - TensorKitTestSuite.test_fusiontrees_elementary_planar_trace(I) + TensorKitTestSuite.test_single_fusiontrees_iterate_and_printing(I) + TensorKitTestSuite.test_single_fusiontrees_constructor_properties(I) + TensorKitTestSuite.test_single_fusiontrees_split_and_join(I) + TensorKitTestSuite.test_single_fusiontrees_multi_fmove(I) + TensorKitTestSuite.test_single_fusiontrees_insertat(I) + TensorKitTestSuite.test_single_fusiontrees_merging(I) + TensorKitTestSuite.test_single_fusiontrees_elementary_planar_trace(I) TK.empty_globalcaches!() end diff --git a/test/testsuite/TensorKitTestSuite.jl b/test/testsuite/TensorKitTestSuite.jl index ad47ad64d..c34851a74 100644 --- a/test/testsuite/TensorKitTestSuite.jl +++ b/test/testsuite/TensorKitTestSuite.jl @@ -15,12 +15,13 @@ testsuite_path = joinpath( include(testsuite_path) using .TensorKitTestSuite -TensorKitTestSuite.test_fusiontrees(MySector) +TensorKitTestSuite.test_single_fusiontrees(MySector) +TensorKitTestSuite.test_double_fusiontrees(MySector) TensorKitTestSuite.test_spaces(MySector) TensorKitTestSuite.test_tensors((V1, V2, V3, V4, V5)) # 5 mutually compatible spaces ``` -The three entry points above are independent and may be run selectively. +The four entry points above are independent and may be run selectively. This module additionally exports: * [`force_planar`](@ref) * [`eval_show`](@ref) @@ -30,7 +31,7 @@ but deliberately *not* re-exported here. """ module TensorKitTestSuite -export test_fusiontrees, test_spaces, test_tensors +export test_single_fusiontrees, test_double_fusiontrees, test_spaces, test_tensors export force_planar, eval_show using Test @@ -59,7 +60,8 @@ using .SectorTestSuite: can_fuse, F_unitarity_test, R_unitarity_test import .SectorTestSuite: random_fusion # TODO: is the method added below needed? const testgroups = Dict{Symbol, Dict{String, Expr}}( - :fusiontrees => Dict{String, Expr}(), + :single_fusiontrees => Dict{String, Expr}(), + :double_fusiontrees => Dict{String, Expr}(), :spaces => Dict{String, Expr}(), :tensors => Dict{String, Expr}(), ) @@ -78,9 +80,9 @@ end # test code here end -Register a testsuite entry under `testgroup` (one of `:fusiontrees`, `:spaces`,`:tensors`). -The body is executed with a single argument: the concrete `Sector` type -under test (for `:fusiontrees`/`:spaces`), or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). +Register a testsuite entry under `testgroup` (one of `:single_fusiontrees`, `:double_fusiontrees`, `:spaces`,`:tensors`). +The body is executed with a single argument: the concrete `Sector` type under test +(for `:single_fusiontrees`, `:double_fusiontrees` and `:spaces`), or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). Important: Whatever is passed as `name` becomes part of the generated function that must be called to run that body. In particular, a `safe_name` is made where `name`'s spaces are replaced by underscores, and everything becomes lowercase. @@ -101,14 +103,28 @@ macro testsuite(testgroup, name, ex) end """ - test_fusiontrees(I::Type{<:Sector}) + test_single_fusiontrees(I::Type{<:Sector}) -Runs the entire fusion-tree manipulation test suite (single and double fusion trees) -on sector type `I`. +Runs the single fusion-tree manipulation test suite on sector type `I`. """ -function test_fusiontrees(I::Type{<:Sector}) +function test_single_fusiontrees(I::Type{<:Sector}) return @testset "$(type_repr(I))" begin - for (name, lambda) in testgroups[:fusiontrees] + for (name, lambda) in testgroups[:single_fusiontrees] + @testset "$name" begin + _run_testsuite_entry(lambda, I) + end + end + end +end + +""" + test_double_fusiontrees(I::Type{<:Sector}) + +Runs the double fusion-tree manipulation test suite on sector type `I`. +""" +function test_double_fusiontrees(I::Type{<:Sector}) + return @testset "$(type_repr(I))" begin + for (name, lambda) in testgroups[:double_fusiontrees] @testset "$name" begin _run_testsuite_entry(lambda, I) end diff --git a/test/testsuite/fusiontrees.jl b/test/testsuite/fusiontrees.jl index 3a57f0c95..ab10a51af 100644 --- a/test/testsuite/fusiontrees.jl +++ b/test/testsuite/fusiontrees.jl @@ -2,7 +2,7 @@ # Single fusion trees # ------------------- -@testsuite :fusiontrees "iterate and printing" I -> begin +@testsuite :single_fusiontrees "iterate and printing" I -> begin N = 5 out = random_fusion(I, Val(N)) isdual = ntuple(n -> rand(Bool), N) @@ -23,7 +23,7 @@ @test eval(Meta.parse(sprint(show, f; context = (:module => @__MODULE__)))) == f end -@testsuite :fusiontrees "constructor properties" I -> begin +@testsuite :single_fusiontrees "constructor properties" I -> begin for u in allunits(I) @constinferred FusionTree((), u, (), (), ()) @constinferred FusionTree((u,), u, (false,), (), ()) @@ -73,7 +73,7 @@ end end # Basic associativity manipulations of individual fusion trees -@testsuite :fusiontrees "split and join" I -> begin +@testsuite :single_fusiontrees "split and join" I -> begin N = 6 uncoupled = random_fusion(I, Val(N)) coupled = rand(collect(⊗(uncoupled...))) @@ -88,7 +88,7 @@ end end end -@testsuite :fusiontrees "multi Fmove" I -> begin +@testsuite :single_fusiontrees "multi Fmove" I -> begin N = 6 uncoupled = random_fusion(I, Val(N)) coupled = rand(collect(⊗(uncoupled...))) @@ -151,7 +151,7 @@ end end end -@testsuite :fusiontrees "insertat" I -> begin +@testsuite :single_fusiontrees "insertat" I -> begin # just check some basic consistency properties here # correctness should follow from multi_Fmove tests N = 4 @@ -191,7 +191,7 @@ end end end -@testsuite :fusiontrees "merging" I -> begin +@testsuite :single_fusiontrees "merging" I -> begin N = 3 out1 = random_fusion(I, Val(N)) out2 = random_fusion(I, Val(N)) @@ -243,7 +243,7 @@ end end # Duality tests -@testsuite :fusiontrees "elementary planar trace" I -> begin +@testsuite :single_fusiontrees "elementary planar trace" I -> begin N = 5 uncoupled = random_fusion(I, Val(N)) coupled = rand(collect(⊗(uncoupled...))) @@ -346,7 +346,7 @@ function _random_doubletree_setup(I::Type{<:Sector}) return N, src, A end -@testsuite :fusiontrees "bending" I -> begin +@testsuite :double_fusiontrees "bending" I -> begin _, src, A = _random_doubletree_setup(I) # single bend dst, U = @constinferred TK.bendright(src) @@ -378,7 +378,7 @@ end end end -@testsuite :fusiontrees "folding" I -> begin +@testsuite :double_fusiontrees "folding" I -> begin _, src, A = _random_doubletree_setup(I) # single bend dst, U = @constinferred TK.foldleft(src) @@ -410,7 +410,7 @@ end end end -@testsuite :fusiontrees "repartitioning" I -> begin +@testsuite :double_fusiontrees "repartitioning" I -> begin N, src, A = _random_doubletree_setup(I) for n in 0:(2 * N) dst, U = @constinferred TK.repartition(src, $n) @@ -436,7 +436,7 @@ end end end -@testsuite :fusiontrees "transposition" I -> begin +@testsuite :double_fusiontrees "transposition" I -> begin N, src, A = _random_doubletree_setup(I) for n in 0:(2N) i0 = rand(1:(2N)) @@ -470,7 +470,7 @@ end end end -@testsuite :fusiontrees "permutation and braiding" I -> begin +@testsuite :double_fusiontrees "permutation and braiding" I -> begin BraidingStyle(I) isa HasBraiding || return nothing N, src, A = _random_doubletree_setup(I) for n in 0:(2N) @@ -526,7 +526,7 @@ end end end -@testsuite :fusiontrees "planar trace" I -> begin +@testsuite :double_fusiontrees "planar trace" I -> begin N, src, A = _random_doubletree_setup(I) if FusionStyle(I) isa UniqueFusion f1, f1 = src From 77c20613b9ef749de72691e208a4ad511819db40 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 8 Jul 2026 11:40:43 +0200 Subject: [PATCH 09/13] split up tensor tests --- test/tensors/braidingtensor.jl | 9 +- test/tensors/construction.jl | 6 +- test/tensors/contractions.jl | 10 +- test/tensors/indexmanipulations.jl | 8 +- test/tensors/linalg.jl | 9 +- test/testsuite/tensors.jl | 1587 +++++++++++++++------------- 6 files changed, 872 insertions(+), 757 deletions(-) diff --git a/test/tensors/braidingtensor.jl b/test/tensors/braidingtensor.jl index 87958cda3..d11532b7c 100644 --- a/test/tensors/braidingtensor.jl +++ b/test/tensors/braidingtensor.jl @@ -12,7 +12,14 @@ for V in spacelist println("BraidingTensor planar contractions with symmetry: $Istr") println("---------------------------------------") @timedtestset "BraidingTensor planar contractions with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_braiding_tensor(V) + TensorKitTestSuite.test_tensors_braiding_tensor_planaradd(V) + TensorKitTestSuite.test_tensors_braiding_tensor_left_full_contraction(V) + TensorKitTestSuite.test_tensors_braiding_tensor_left_partial_contraction(V) + TensorKitTestSuite.test_tensors_braiding_tensor_right_full_contraction(V) + TensorKitTestSuite.test_tensors_braiding_tensor_full_contraction_output(V) + TensorKitTestSuite.test_tensors_braiding_tensor_open_codomain_leg(V) + TensorKitTestSuite.test_tensors_braiding_tensor_open_domain_leg(V) + TensorKitTestSuite.test_tensors_contraction_between_braiding_tensors(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/construction.jl b/test/tensors/construction.jl index 8b46d50e8..a34b492ef 100644 --- a/test/tensors/construction.jl +++ b/test/tensors/construction.jl @@ -12,7 +12,11 @@ for V in spacelist println("Tensor constructions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor constructions with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_tensor_constructions(V) + TensorKitTestSuite.test_tensors_basic_properties(V) + TensorKitTestSuite.test_tensors_dict_conversion(V) + TensorKitTestSuite.test_tensors_array_conversion(V) + TensorKitTestSuite.test_tensors_real_and_imaginary_parts(V) + TensorKitTestSuite.test_tensors_tensor_conversion(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/contractions.jl b/test/tensors/contractions.jl index ca16eb28d..627112bed 100644 --- a/test/tensors/contractions.jl +++ b/test/tensors/contractions.jl @@ -12,7 +12,15 @@ for V in spacelist println("Tensor contractions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor contractions with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_contractions(V) + TensorKitTestSuite.test_tensors_full_trace(V) + TensorKitTestSuite.test_tensors_partial_trace(V) + TensorKitTestSuite.test_tensors_trace_via_conversion(V) + TensorKitTestSuite.test_tensors_trace_and_contraction(V) + TensorKitTestSuite.test_tensors_contraction_via_conversion(V) + TensorKitTestSuite.test_tensors_tensor_product_norm_preservation(V) + TensorKitTestSuite.test_tensors_tensor_product_via_conversion(V) + TensorKitTestSuite.test_tensors_tensor_product_via_contraction(V) + TensorKitTestSuite.test_tensors_absorption(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index d955d3460..0693ab4f9 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -12,7 +12,13 @@ for V in spacelist println("Tensor index manipulations with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor index manipulations with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_index_manipulations(V) + TensorKitTestSuite.test_tensors_trivial_space_insertion_and_removal(V) + TensorKitTestSuite.test_tensors_permutations_via_inner_product_invariance(V) + TensorKitTestSuite.test_tensors_permutations_via_conversion(V) + TensorKitTestSuite.test_tensors_index_flipping_inverse(V) + TensorKitTestSuite.test_tensors_index_flipping_explicit(V) + TensorKitTestSuite.test_tensors_index_flipping_via_contraction(V) + TensorKitTestSuite.test_tensors_braid_adjoint_identity(V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/linalg.jl b/test/tensors/linalg.jl index ba4107732..5fdce8627 100644 --- a/test/tensors/linalg.jl +++ b/test/tensors/linalg.jl @@ -12,7 +12,14 @@ for V in spacelist println("Tensor linear algebra with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor linear algebra with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_linear_algebra(V) + TensorKitTestSuite.test_tensors_basic_linear_algebra(V) + TensorKitTestSuite.test_tensors_linear_algebra_conversion(V) + TensorKitTestSuite.test_tensors_multiplication_of_isometries(V) + TensorKitTestSuite.test_tensors_multiplication_and_inverse_compatibility(V) + TensorKitTestSuite.test_tensors_multiplication_and_inverse_conversion(V) + TensorKitTestSuite.test_tensors_diag_and_diagm(V) + TensorKitTestSuite.test_tensors_tensor_functions(V) + TensorKitTestSuite.test_tensors_sylvester_equation(V) end TensorKit.empty_globalcaches!() end diff --git a/test/testsuite/tensors.jl b/test/testsuite/tensors.jl index 9c441e9b0..fa0668ab7 100644 --- a/test/testsuite/tensors.jl +++ b/test/testsuite/tensors.jl @@ -1,826 +1,909 @@ # Tensor operations # ================= -@testsuite :tensors "tensor constructions" V -> begin - I = sectortype(first(V)) +# tensor constructions +#--------------------- +@testsuite :tensors "basic properties" V -> begin V1, V2, V3, V4, V5 = V - @timedtestset "Basic tensor properties" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - for T in (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat) - t = @constinferred zeros(T, W) - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test norm(t) == 0 - @test codomain(t) == W - @test space(t) == (W ← one(W)) - @test domain(t) == one(W) - @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} - # Array type input - t = @constinferred zeros(Vector{T}, W) - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test norm(t) == 0 - @test codomain(t) == W - @test space(t) == (W ← one(W)) - @test domain(t) == one(W) - @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} - # blocks - bs = @constinferred blocks(t) - if !isempty(blocksectors(t)) # multifusion space ending on module gives empty data - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(W)) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t, first(blocksectors(t))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t) - @test typeof(c) === sectortype(t) - end + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + for T in (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat) + t = @constinferred zeros(T, W) + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test norm(t) == 0 + @test codomain(t) == W + @test space(t) == (W ← one(W)) + @test domain(t) == one(W) + @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} + # Array type input + t = @constinferred zeros(Vector{T}, W) + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test norm(t) == 0 + @test codomain(t) == W + @test space(t) == (W ← one(W)) + @test domain(t) == one(W) + @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} + # blocks + bs = @constinferred blocks(t) + if !isempty(blocksectors(t)) # multifusion space ending on module gives empty data + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(W)) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t, first(blocksectors(t))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t) + @test typeof(c) === sectortype(t) end end - @timedtestset "Tensor Dict conversion" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - for T in (Int, Float32, ComplexF64) - t = @constinferred rand(T, W) - d = convert(Dict, t) - @test t == convert(TensorMap, d) - end +end + +@testsuite :tensors "dict conversion" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + for T in (Int, Float32, ComplexF64) + t = @constinferred rand(T, W) + d = convert(Dict, t) + @test t == convert(TensorMap, d) end - if hasfusiontensor(I) || I == Trivial - @timedtestset "Tensor Array conversion" begin - W1 = V1 ← one(V1) - W2 = one(V2) ← V2 - W3 = V1 ⊗ V2 ← one(V1) - W4 = V1 ← V2 - W5 = one(V1) ← V1 ⊗ V2 - W6 = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 - for W in (W1, W2, W3, W4, W5, W6) - for T in (Int, Float32, ComplexF64) - if T == Int - t = TensorMap{T}(undef, W) - for (_, b) in blocks(t) - rand!(b, -20:20) - end - else - t = @constinferred randn(T, W) - end - a = @constinferred convert(Array, t) - b = reshape(a, dim(codomain(W)), dim(domain(W))) - @test t ≈ @constinferred TensorMap(a, W) - @test t ≈ @constinferred TensorMap(b, W) - @test t === @constinferred TensorMap(t.data, W) +end + +@testsuite :tensors "array conversion" V -> begin + I = sectortype(first(V)) + hasfusiontensor(I) || return nothing # trivial is also tested + V1, V2, V3, V4, V5 = V + W1 = V1 ← one(V1) + W2 = one(V2) ← V2 + W3 = V1 ⊗ V2 ← one(V1) + W4 = V1 ← V2 + W5 = one(V1) ← V1 ⊗ V2 + W6 = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + for W in (W1, W2, W3, W4, W5, W6) + for T in (Int, Float32, ComplexF64) + if T == Int + t = TensorMap{T}(undef, W) + for (_, b) in blocks(t) + rand!(b, -20:20) end + else + t = @constinferred randn(T, W) end - for T in (Int, Float32, ComplexF64) - t = randn(T, V1 ⊗ V2 ← zerospace(V1)) - a = convert(Array, t) - @test norm(a) == 0 - end + a = @constinferred convert(Array, t) + b = reshape(a, dim(codomain(W)), dim(domain(W))) + @test t ≈ @constinferred TensorMap(a, W) + @test t ≈ @constinferred TensorMap(b, W) + @test t === @constinferred TensorMap(t.data, W) end end - if hasfusiontensor(I) - @timedtestset "Real and imaginary parts" begin - W = V1 ⊗ V2 - for T in (Float64, ComplexF64, ComplexF32) - t = @constinferred randn(T, W, W) - - tr = @constinferred real(t) - @test scalartype(tr) <: Real - @test real(convert(Array, t)) == convert(Array, tr) - - ti = @constinferred imag(t) - @test scalartype(ti) <: Real - @test imag(convert(Array, t)) == convert(Array, ti) - - tc = @inferred complex(t) - @test scalartype(tc) <: Complex - @test complex(convert(Array, t)) == convert(Array, tc) - - tc2 = @inferred complex(tr, ti) - @test tc2 ≈ tc - end - end + for T in (Int, Float32, ComplexF64) + t = randn(T, V1 ⊗ V2 ← zerospace(V1)) + a = convert(Array, t) + @test norm(a) == 0 end - @timedtestset "Tensor conversion" begin - W = V1 ⊗ V2 - t = @constinferred randn(W ← W) - @test typeof(convert(TensorMap, t')) == typeof(t) - tc = complex(t) - @test convert(typeof(tc), t) == tc - @test typeof(convert(typeof(tc), t)) == typeof(tc) - @test typeof(convert(typeof(tc), t')) == typeof(tc) - @test Base.promote_typeof(t, tc) == typeof(tc) - @test Base.promote_typeof(tc, t) == typeof(tc + t) +end + +@testsuite :tensors "real and imaginary parts" V -> begin + I = sectortype(first(V)) + hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + for T in (Float64, ComplexF64, ComplexF32) + t = @constinferred randn(T, W, W) + + tr = @constinferred real(t) + @test scalartype(tr) <: Real + @test real(convert(Array, t)) == convert(Array, tr) + + ti = @constinferred imag(t) + @test scalartype(ti) <: Real + @test imag(convert(Array, t)) == convert(Array, ti) + + tc = @inferred complex(t) + @test scalartype(tc) <: Complex + @test complex(convert(Array, t)) == convert(Array, tc) + + tc2 = @inferred complex(tr, ti) + @test tc2 ≈ tc end end -@testsuite :tensors "contractions" V -> begin +@testsuite :tensors "tensor conversion" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + t = @constinferred randn(W ← W) + @test typeof(convert(TensorMap, t')) == typeof(t) + tc = complex(t) + @test convert(typeof(tc), t) == tc + @test typeof(convert(typeof(tc), t)) == typeof(tc) + @test typeof(convert(typeof(tc), t')) == typeof(tc) + @test Base.promote_typeof(t, tc) == typeof(tc) + @test Base.promote_typeof(tc, t) == typeof(tc + t) +end + +# tensor contractions +#-------------------- +@testsuite :tensors "full trace" V -> begin # self-consistency I = sectortype(first(V)) - symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding V1, V2, V3, V4, V5 = V - @timedtestset "Full trace: test self-consistency" begin - if symmetricbraiding - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') - t2 = permute(t, ((1, 2), (4, 3))) - s = @constinferred tr(t2) - @test conj(s) ≈ tr(t2') - if !isdual(V1) - t2 = twist!(t2, 1) - end - if isdual(V2) - t2 = twist!(t2, 2) - end - ss = tr(t2) - @tensor s2 = t[a, b, b, a] - @tensor t3[a, b] := t[a, c, c, b] - @tensor s3 = t3[a, a] - @test ss ≈ s2 - @test ss ≈ s3 + if BraidingStyle(I) isa SymmetricBraiding + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') + t2 = permute(t, ((1, 2), (4, 3))) + s = @constinferred tr(t2) + @test conj(s) ≈ tr(t2') + if !isdual(V1) + t2 = twist!(t2, 1) end - t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) # avoid permutes - ss = @constinferred tr(t) - @test conj(ss) ≈ tr(t') - @planar s2 = t[a b; a b] - @planar t3[a; b] := t[a c; b c] - @planar s3 = t3[a; a] - + if isdual(V2) + t2 = twist!(t2, 2) + end + ss = tr(t2) + @tensor s2 = t[a, b, b, a] + @tensor t3[a, b] := t[a, c, c, b] + @tensor s3 = t3[a, a] @test ss ≈ s2 @test ss ≈ s3 end - @timedtestset "Partial trace: test self-consistency" begin - if symmetricbraiding - t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) - @tensor t2[a; b] := t[c d b; c d a] - @tensor t4[a b; c d] := t[e d c; e b a] - @tensor t5[a; b] := t4[a c; b c] - @test t2 ≈ t5 - end - t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # compatible with module fusion - @planar t2[a; b] := t[c a d; c b d] - @planar t4[a b; c d] := t[e a b; e c d] - @planar t5[a; b] := t4[a c; b c] + t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) # avoid permutes + ss = @constinferred tr(t) + @test conj(ss) ≈ tr(t') + @planar s2 = t[a b; a b] + @planar t3[a; b] := t[a c; b c] + @planar s3 = t3[a; a] + + @test ss ≈ s2 + @test ss ≈ s3 +end + +@testsuite :tensors "partial trace" V -> begin # self-consistency + I = sectortype(first(V)) + V1, V2, V3, V4, V5 = V + if BraidingStyle(I) isa SymmetricBraiding + t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) + @tensor t2[a; b] := t[c d b; c d a] + @tensor t4[a b; c d] := t[e d c; e b a] + @tensor t5[a; b] := t4[a c; b c] @test t2 ≈ t5 end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Trace: test via conversion" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') - @tensor t2[a, b] := t[c, d, b, d, c, a] - @tensor t3[a, b] := convert(Array, t)[c, d, b, d, c, a] - @test t3 ≈ convert(Array, t2) - end + t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # compatible with module fusion + @planar t2[a; b] := t[c a d; c b d] + @planar t4[a b; c d] := t[e a b; e c d] + @planar t5[a; b] := t4[a c; b c] + @test t2 ≈ t5 +end + +@testsuite :tensors "trace via conversion" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa Bosonic && hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') + @tensor t2[a, b] := t[c, d, b, d, c, a] + @tensor t3[a, b] := convert(Array, t)[c, d, b, d, c, a] + @test t3 ≈ convert(Array, t2) +end + +#TODO: find version that works for all multifusion cases +@testsuite :tensors "trace and contraction" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa SymmetricBraiding || return nothing + V1, V2, V3, V4, V5 = V + t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3) + t2 = rand(ComplexF64, V2' ⊗ V4 ⊗ V1') + t3 = t1 ⊗ t2 + @tensor ta[a, b] := t1[x, y, a] * t2[y, b, x] + @tensor tb[a, b] := t3[x, y, a, y, b, x] + @test ta ≈ tb +end + +@testsuite :tensors "contraction via conversion" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa Bosonic && hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + A1 = randn(ComplexF64, V1' * V2', V3') + A2 = randn(ComplexF64, V3 * V4, V5) + rhoL = randn(ComplexF64, V1, V1) + rhoR = randn(ComplexF64, V5, V5)' # test adjoint tensor + H = randn(ComplexF64, V2 * V4, V2 * V4) + @tensor HrA12[a, s1, s2, c] := rhoL[a, a'] * conj(A1[a', t1, b]) * + A2[b, t2, c'] * rhoR[c', c] * H[s1, s2, t1, t2] + + @tensor HrA12array[a, s1, s2, c] := convert(Array, rhoL)[a, a'] * + conj(convert(Array, A1)[a', t1, b]) * convert(Array, A2)[b, t2, c'] * + convert(Array, rhoR)[c', c] * convert(Array, H)[s1, s2, t1, t2] + + @test HrA12array ≈ convert(Array, HrA12) +end + +@testsuite :tensors "tensor product norm preservation" V -> begin + for T in (Float32, ComplexF64) + t1 = rand(T, V1, V5') + t2 = rand(T, V2 ⊗ V3, V4') + t = @constinferred (t1 ⊗ t2) + @test norm(t) ≈ norm(t1) * norm(t2) end - #TODO: find version that works for all multifusion cases - symmetricbraiding && @timedtestset "Trace and contraction" begin - t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3) - t2 = rand(ComplexF64, V2' ⊗ V4 ⊗ V1') - t3 = t1 ⊗ t2 - @tensor ta[a, b] := t1[x, y, a] * t2[y, b, x] - @tensor tb[a, b] := t3[x, y, a, y, b, x] - @test ta ≈ tb +end + +@testsuite :tensors "tensor product via conversion" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa Bosonic && hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + for T in (Float32, ComplexF64) + t1 = rand(T, V1, V5') + t2 = rand(T, V2 ⊗ V3, V4') + t = @constinferred (t1 ⊗ t2) + d1 = dim(codomain(t1)) + d2 = dim(codomain(t2)) + d3 = dim(domain(t1)) + d4 = dim(domain(t2)) + At = convert(Array, t) + @test reshape(At, (d1, d2, d3, d4)) ≈ + reshape(convert(Array, t1), (d1, 1, d3, 1)) .* + reshape(convert(Array, t2), (1, d2, 1, d4)) end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor contraction: test via conversion" begin - A1 = randn(ComplexF64, V1' * V2', V3') - A2 = randn(ComplexF64, V3 * V4, V5) - rhoL = randn(ComplexF64, V1, V1) - rhoR = randn(ComplexF64, V5, V5)' # test adjoint tensor - H = randn(ComplexF64, V2 * V4, V2 * V4) - @tensor HrA12[a, s1, s2, c] := rhoL[a, a'] * conj(A1[a', t1, b]) * - A2[b, t2, c'] * rhoR[c', c] * H[s1, s2, t1, t2] - - @tensor HrA12array[a, s1, s2, c] := convert(Array, rhoL)[a, a'] * - conj(convert(Array, A1)[a', t1, b]) * convert(Array, A2)[b, t2, c'] * - convert(Array, rhoR)[c', c] * convert(Array, H)[s1, s2, t1, t2] - - @test HrA12array ≈ convert(Array, HrA12) - end +end + +@testsuite :tensors "tensor product via contraction" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa SymmetricBraiding || return nothing + V1, V2, V3, V4, V5 = V + for T in (Float32, ComplexF64) + t1 = rand(T, V1, V5') + t2 = rand(T, V2 ⊗ V3, V4') + t = @constinferred (t1 ⊗ t2) + @tensor t′[1 2 3; 4 5] := t1[1; 4] * t2[2 3; 5] + @test t ≈ t′ end - @timedtestset "Tensor product: test via norm preservation" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V1, V5') - t2 = rand(T, V2 ⊗ V3, V4') - t = @constinferred (t1 ⊗ t2) - @test norm(t) ≈ norm(t1) * norm(t2) +end + +@testsuite :tensors "absorption" V -> begin + V1, V2, V3, V4, V5 = V + # absorbing small into large + t1 = zeros((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') + t2 = rand(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + t3 = @constinferred absorb(t1, t2) + @test norm(t3) ≈ norm(t2) + @test norm(t1) == 0 + t4 = @constinferred absorb!(t1, t2) + @test t1 === t4 + @test t3 ≈ t4 + + # absorbing large into small + t1 = rand((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') + t2 = zeros(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + t3 = @constinferred absorb(t2, t1) + @test norm(t3) < norm(t1) + @test norm(t2) == 0 + t4 = @constinferred absorb!(t2, t1) + @test t2 === t4 + @test t3 ≈ t4 +end + +# linear algebra +#--------------- + +@testsuite :tensors "basic linear algebra" V -> begin + I = sectortype(first(V)) + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + for T in (Float32, ComplexF64) + t = @constinferred rand(T, W) + @test scalartype(t) == T + @test space(t) == W + @test space(t') == W' + @test dim(t) == dim(space(t)) + @test codomain(t) == codomain(W) + @test domain(t) == domain(W) + # blocks for adjoint + bs = @constinferred blocks(t') + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(W')) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t', first(blocksectors(t'))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t') + @test typeof(c) === sectortype(t) + # linear algebra + @test isa(@constinferred(norm(t)), real(T)) + @test norm(t)^2 ≈ dot(t, t) + α = rand(T) + @test norm(α * t) ≈ abs(α) * norm(t) + @test norm(t + t, 2) ≈ 2 * norm(t, 2) + @test norm(t + t, 1) ≈ 2 * norm(t, 1) + @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) + p = 3 * rand(Float64) + @test norm(t + t, p) ≈ 2 * norm(t, p) + @test norm(t) ≈ norm(t') + + t2 = @constinferred rand!(similar(t)) + β = rand(T) + @test @constinferred(dot(β * t2, α * t)) ≈ conj(β) * α * conj(dot(t, t2)) + @test dot(t2, t) ≈ conj(dot(t, t2)) + @test dot(t2, t) ≈ conj(dot(t2', t')) + @test dot(t2, t) ≈ dot(t', t2') + + if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) + i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved + i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) + @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) + @test i2 * i1 == @constinferred(id(Vector{T}, V2 ⊗ V1)) end + + w = @constinferred isometry(T, V1 ⊗ (rightunitspace(V1) ⊕ rightunitspace(V1)), V1) + @test dim(w) == 2 * dim(V1 ← V1) + @test w' * w == id(Vector{T}, V1) + @test w * w' == (w * w')^2 end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor product: test via conversion" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V1, V5') - t2 = rand(T, V2 ⊗ V3, V4') - t = @constinferred (t1 ⊗ t2) - d1 = dim(codomain(t1)) - d2 = dim(codomain(t2)) - d3 = dim(domain(t1)) - d4 = dim(domain(t2)) - At = convert(Array, t) - @test reshape(At, (d1, d2, d3, d4)) ≈ - reshape(convert(Array, t1), (d1, 1, d3, 1)) .* - reshape(convert(Array, t2), (1, d2, 1, d4)) - end - end +end + +@testsuite :tensors "linear algebra conversion" V -> begin + I = sectortype(first(V)) + hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)' + for T in (Float32, ComplexF64) + t = rand(T, W) + t2 = @constinferred rand!(similar(t)) + @test norm(t, 2) ≈ norm(convert(Array, t), 2) + @test dot(t2, t) ≈ dot(convert(Array, t2), convert(Array, t)) + α = rand(T) + @test convert(Array, α * t) ≈ α * convert(Array, t) + @test convert(Array, t + t) ≈ 2 * convert(Array, t) end - symmetricbraiding && @timedtestset "Tensor product: test via tensor contraction" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V1, V5') - t2 = rand(T, V2 ⊗ V3, V4') - t = @constinferred (t1 ⊗ t2) - @tensor t′[1 2 3; 4 5] := t1[1; 4] * t2[2 3; 5] - @test t ≈ t′ - end +end + +@testsuite :tensors "multiplication of isometries" V -> begin + V1, V2, V3, V4, V5 = V + W1 = V1 ⊗ V2 ⊗ V3 + W2 = (V4 ⊗ V5)' + for T in (Float64, ComplexF64) + t1 = randisometry(T, W1, W2) + t2 = randisometry(T, W2 ← W2) + @test isisometric(t1) + @test isunitary(t2) + P = t1 * t1' + @test P * P ≈ P end - @timedtestset "Tensor absorption" begin - # absorbing small into large - t1 = zeros((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') - t2 = rand(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - t3 = @constinferred absorb(t1, t2) - @test norm(t3) ≈ norm(t2) - @test norm(t1) == 0 - t4 = @constinferred absorb!(t1, t2) - @test t1 === t4 - @test t3 ≈ t4 - - # absorbing large into small - t1 = rand((V1 ⊕ V1) ⊗ (V2 ⊕ V2), (V3 ⊗ (V4 ⊕ V4) ⊗ V5)') - t2 = zeros(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - t3 = @constinferred absorb(t2, t1) - @test norm(t3) < norm(t1) - @test norm(t2) == 0 - t4 = @constinferred absorb!(t2, t1) - @test t2 === t4 - @test t3 ≈ t4 +end + +@testsuite :tensors "multiplication and inverse compatibility" V -> begin + V1, V2, V3, V4, V5 = V + W1 = V1 ⊗ V2 ⊗ V3 + W2 = (V4 ⊗ V5)' + for T in (Float64, ComplexF64) + t1 = rand(T, W1, W1) + t2 = rand(T, W2 ← W2) + t = rand(T, W1, W2) + @test t1 * (t1 \ t) ≈ t + @test (t / t2) * t2 ≈ t + @test t1 \ one(t1) ≈ inv(t1) + @test one(t1) / t1 ≈ pinv(t1) + @test_throws SpaceMismatch inv(t) + @test_throws SpaceMismatch t2 \ t + @test_throws SpaceMismatch t / t1 + tp = pinv(t) * t + @test tp ≈ tp * tp end end -@testsuite :tensors "linear algebra" V -> begin +@testsuite :tensors "multiplication and inverse conversion" V -> begin I = sectortype(first(V)) + BraidingStyle(I) isa Bosonic && hasfusiontensor(I) || return nothing V1, V2, V3, V4, V5 = V - @timedtestset "Basic linear algebra" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) - @test scalartype(t) == T - @test space(t) == W - @test space(t') == W' - @test dim(t) == dim(space(t)) - @test codomain(t) == codomain(W) - @test domain(t) == domain(W) - # blocks for adjoint - bs = @constinferred blocks(t') - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(W')) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t', first(blocksectors(t'))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t') - @test typeof(c) === sectortype(t) - # linear algebra - @test isa(@constinferred(norm(t)), real(T)) - @test norm(t)^2 ≈ dot(t, t) - α = rand(T) - @test norm(α * t) ≈ abs(α) * norm(t) - @test norm(t + t, 2) ≈ 2 * norm(t, 2) - @test norm(t + t, 1) ≈ 2 * norm(t, 1) - @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) - p = 3 * rand(Float64) - @test norm(t + t, p) ≈ 2 * norm(t, p) - @test norm(t) ≈ norm(t') - - t2 = @constinferred rand!(similar(t)) - β = rand(T) - @test @constinferred(dot(β * t2, α * t)) ≈ conj(β) * α * conj(dot(t, t2)) - @test dot(t2, t) ≈ conj(dot(t, t2)) - @test dot(t2, t) ≈ conj(dot(t2', t')) - @test dot(t2, t) ≈ dot(t', t2') - - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) - i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved - i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) - @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) - @test i2 * i1 == @constinferred(id(Vector{T}, V2 ⊗ V1)) - end - - w = @constinferred isometry(T, V1 ⊗ (rightunitspace(V1) ⊕ rightunitspace(V1)), V1) - @test dim(w) == 2 * dim(V1 ← V1) - @test w' * w == id(Vector{T}, V1) - @test w * w' == (w * w')^2 - end - end - if hasfusiontensor(I) - @timedtestset "Basic linear algebra: test via conversion" begin - W = V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)' - for T in (Float32, ComplexF64) - t = rand(T, W) - t2 = @constinferred rand!(similar(t)) - @test norm(t, 2) ≈ norm(convert(Array, t), 2) - @test dot(t2, t) ≈ dot(convert(Array, t2), convert(Array, t)) - α = rand(T) - @test convert(Array, α * t) ≈ α * convert(Array, t) - @test convert(Array, t + t) ≈ 2 * convert(Array, t) - end + W1 = V1 ⊗ V2 ⊗ V3 + W2 = (V4 ⊗ V5)' + for T in (Float32, Float64, ComplexF32, ComplexF64) + t1 = rand(T, W1 ← W1) + t2 = rand(T, W2, W2) + t = rand(T, W1 ← W2) + d1 = dim(W1) + d2 = dim(W2) + At1 = reshape(convert(Array, t1), d1, d1) + At2 = reshape(convert(Array, t2), d2, d2) + At = reshape(convert(Array, t), d1, d2) + @test reshape(convert(Array, t1 * t), d1, d2) ≈ At1 * At + @test reshape(convert(Array, t1' * t), d1, d2) ≈ At1' * At + @test reshape(convert(Array, t2 * t'), d2, d1) ≈ At2 * At' + @test reshape(convert(Array, t2' * t'), d2, d1) ≈ At2' * At' + + @test reshape(convert(Array, inv(t1)), d1, d1) ≈ inv(At1) + @test reshape(convert(Array, pinv(t)), d2, d1) ≈ pinv(At) + + if T == Float32 || T == ComplexF32 + continue end + + @test reshape(convert(Array, t1 \ t), d1, d2) ≈ At1 \ At + @test reshape(convert(Array, t1' \ t), d1, d2) ≈ At1' \ At + @test reshape(convert(Array, t2 \ t'), d2, d1) ≈ At2 \ At' + @test reshape(convert(Array, t2' \ t'), d2, d1) ≈ At2' \ At' + + @test reshape(convert(Array, t2 / t), d2, d1) ≈ At2 / At + @test reshape(convert(Array, t2' / t), d2, d1) ≈ At2' / At + @test reshape(convert(Array, t1 / t'), d1, d2) ≈ At1 / At' + @test reshape(convert(Array, t1' / t'), d1, d2) ≈ At1' / At' end - @timedtestset "Multiplication of isometries: test properties" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = (V4 ⊗ V5)' - for T in (Float64, ComplexF64) - t1 = randisometry(T, W1, W2) - t2 = randisometry(T, W2 ← W2) - @test isisometric(t1) - @test isunitary(t2) - P = t1 * t1' - @test P * P ≈ P +end + +@testsuite :tensors "diag and diagm" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + t = randn(ComplexF64, W) + d = LinearAlgebra.diag(t) + D = LinearAlgebra.diagm(codomain(t), domain(t), d) + @test LinearAlgebra.isdiag(D) + @test LinearAlgebra.diag(D) == d +end + +@testsuite :tensors "tensor functions" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa Bosonic && hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + for T in (Float64, ComplexF64) + t = randn(T, W, W) + s = dim(W) + expt = @constinferred exp(t) + @test reshape(convert(Array, expt), (s, s)) ≈ + exp(reshape(convert(Array, t), (s, s))) + + @test (@constinferred sqrt(t))^2 ≈ t + @test reshape(convert(Array, sqrt(t^2)), (s, s)) ≈ + sqrt(reshape(convert(Array, t^2), (s, s))) + + @test exp(@constinferred log(expt)) ≈ expt + @test reshape(convert(Array, log(expt)), (s, s)) ≈ + log(reshape(convert(Array, expt), (s, s))) + + @test (@constinferred cos(t))^2 + (@constinferred sin(t))^2 ≈ id(W) + @test (@constinferred tan(t)) ≈ sin(t) / cos(t) + @test (@constinferred cot(t)) ≈ cos(t) / sin(t) + @test (@constinferred cosh(t))^2 - (@constinferred sinh(t))^2 ≈ id(W) + @test (@constinferred tanh(t)) ≈ sinh(t) / cosh(t) + @test (@constinferred coth(t)) ≈ cosh(t) / sinh(t) + + t1 = sin(t) + @test sin(@constinferred asin(t1)) ≈ t1 + t2 = cos(t) + @test cos(@constinferred acos(t2)) ≈ t2 + t3 = sinh(t) + @test sinh(@constinferred asinh(t3)) ≈ t3 + t4 = cosh(t) + @test cosh(@constinferred acosh(t4)) ≈ t4 + t5 = tan(t) + @test tan(@constinferred atan(t5)) ≈ t5 + t6 = cot(t) + @test cot(@constinferred acot(t6)) ≈ t6 + t7 = tanh(t) + @test tanh(@constinferred atanh(t7)) ≈ t7 + t8 = coth(t) + @test coth(@constinferred acoth(t8)) ≈ t8 + t = randn(T, W, V1) # not square + for f in + ( + cos, sin, tan, cot, cosh, sinh, tanh, coth, atan, acot, asinh, + sqrt, log, asin, acos, acosh, atanh, acoth, + ) + @test_throws SpaceMismatch f(t) end end - @timedtestset "Multiplication and inverse: test compatibility" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = (V4 ⊗ V5)' - for T in (Float64, ComplexF64) - t1 = rand(T, W1, W1) - t2 = rand(T, W2 ← W2) - t = rand(T, W1, W2) - @test t1 * (t1 \ t) ≈ t - @test (t / t2) * t2 ≈ t - @test t1 \ one(t1) ≈ inv(t1) - @test one(t1) / t1 ≈ pinv(t1) - @test_throws SpaceMismatch inv(t) - @test_throws SpaceMismatch t2 \ t - @test_throws SpaceMismatch t / t1 - tp = pinv(t) * t - @test tp ≈ tp * tp +end + +@testsuite :tensors "sylvester equation" V -> begin + I = sectortype(first(V)) + V1, V2, V3, V4, V5 = V + for T in (Float32, ComplexF64) + tA = rand(T, V1 ⊗ V2, V1 ⊗ V2) + tB = rand(T, (V3 ⊗ V4 ⊗ V5)', (V3 ⊗ V4 ⊗ V5)') + tA = 3 // 2 * left_polar(tA)[1] + tB = 1 // 5 * left_polar(tB)[1] + tC = rand(T, V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') + t = @constinferred sylvester(tA, tB, tC) + @test codomain(t) == V1 ⊗ V2 + @test domain(t) == (V3 ⊗ V4 ⊗ V5)' + @test norm(tA * t + t * tB + tC) < + (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + matrix(x) = reshape(convert(Array, x), dim(codomain(x)), dim(domain(x))) + @test matrix(t) ≈ sylvester(matrix(tA), matrix(tB), matrix(tC)) end end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Multiplication and inverse: test via conversion" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = (V4 ⊗ V5)' - for T in (Float32, Float64, ComplexF32, ComplexF64) - t1 = rand(T, W1 ← W1) - t2 = rand(T, W2, W2) - t = rand(T, W1 ← W2) - d1 = dim(W1) - d2 = dim(W2) - At1 = reshape(convert(Array, t1), d1, d1) - At2 = reshape(convert(Array, t2), d2, d2) - At = reshape(convert(Array, t), d1, d2) - @test reshape(convert(Array, t1 * t), d1, d2) ≈ At1 * At - @test reshape(convert(Array, t1' * t), d1, d2) ≈ At1' * At - @test reshape(convert(Array, t2 * t'), d2, d1) ≈ At2 * At' - @test reshape(convert(Array, t2' * t'), d2, d1) ≈ At2' * At' - - @test reshape(convert(Array, inv(t1)), d1, d1) ≈ inv(At1) - @test reshape(convert(Array, pinv(t)), d2, d1) ≈ pinv(At) - - if T == Float32 || T == ComplexF32 - continue - end +end - @test reshape(convert(Array, t1 \ t), d1, d2) ≈ At1 \ At - @test reshape(convert(Array, t1' \ t), d1, d2) ≈ At1' \ At - @test reshape(convert(Array, t2 \ t'), d2, d1) ≈ At2 \ At' - @test reshape(convert(Array, t2' \ t'), d2, d1) ≈ At2' \ At' +# index manipulations +#-------------------- - @test reshape(convert(Array, t2 / t), d2, d1) ≈ At2 / At - @test reshape(convert(Array, t2' / t), d2, d1) ≈ At2' / At - @test reshape(convert(Array, t1 / t'), d1, d2) ≈ At1 / At' - @test reshape(convert(Array, t1' / t'), d1, d2) ≈ At1' / At' - end +@testsuite :tensors "trivial space insertion and removal" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' + for T in (Float32, ComplexF64) + t = @constinferred rand(T, W) + t2 = @constinferred insertleftunit(t) + @test t2 == @constinferred insertrightunit(t) + @test space(t2) == insertleftunit(space(t)) + @test @constinferred(removeunit(t2, $(numind(t2)))) == t + t3 = @constinferred insertleftunit(t; copy = true) + @test t3 == @constinferred insertrightunit(t; copy = true) + @test @constinferred(removeunit(t3, $(numind(t3)))) == t + + @test numind(t2) == numind(t) + 1 + @test scalartype(t2) === T + @test t.data === t2.data + + @test t.data !== t3.data + for (c, b) in blocks(t) + @test b == block(t3, c) end - end - @timedtestset "diag/diagm" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - t = randn(ComplexF64, W) - d = LinearAlgebra.diag(t) - D = LinearAlgebra.diagm(codomain(t), domain(t), d) - @test LinearAlgebra.isdiag(D) - @test LinearAlgebra.diag(D) == d - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor functions" begin - W = V1 ⊗ V2 - for T in (Float64, ComplexF64) - t = randn(T, W, W) - s = dim(W) - expt = @constinferred exp(t) - @test reshape(convert(Array, expt), (s, s)) ≈ - exp(reshape(convert(Array, t), (s, s))) - - @test (@constinferred sqrt(t))^2 ≈ t - @test reshape(convert(Array, sqrt(t^2)), (s, s)) ≈ - sqrt(reshape(convert(Array, t^2), (s, s))) - - @test exp(@constinferred log(expt)) ≈ expt - @test reshape(convert(Array, log(expt)), (s, s)) ≈ - log(reshape(convert(Array, expt), (s, s))) - - @test (@constinferred cos(t))^2 + (@constinferred sin(t))^2 ≈ id(W) - @test (@constinferred tan(t)) ≈ sin(t) / cos(t) - @test (@constinferred cot(t)) ≈ cos(t) / sin(t) - @test (@constinferred cosh(t))^2 - (@constinferred sinh(t))^2 ≈ id(W) - @test (@constinferred tanh(t)) ≈ sinh(t) / cosh(t) - @test (@constinferred coth(t)) ≈ cosh(t) / sinh(t) - - t1 = sin(t) - @test sin(@constinferred asin(t1)) ≈ t1 - t2 = cos(t) - @test cos(@constinferred acos(t2)) ≈ t2 - t3 = sinh(t) - @test sinh(@constinferred asinh(t3)) ≈ t3 - t4 = cosh(t) - @test cosh(@constinferred acosh(t4)) ≈ t4 - t5 = tan(t) - @test tan(@constinferred atan(t5)) ≈ t5 - t6 = cot(t) - @test cot(@constinferred acot(t6)) ≈ t6 - t7 = tanh(t) - @test tanh(@constinferred atanh(t7)) ≈ t7 - t8 = coth(t) - @test coth(@constinferred acoth(t8)) ≈ t8 - t = randn(T, W, V1) # not square - for f in - ( - cos, sin, tan, cot, cosh, sinh, tanh, coth, atan, acot, asinh, - sqrt, log, asin, acos, acosh, atanh, acoth, - ) - @test_throws SpaceMismatch f(t) - end - end + + t4 = @constinferred insertrightunit(t, 3; dual = true) + @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) + for (c, b) in blocks(t) + @test b == block(t4, c) end - end - @timedtestset "Sylvester equation" begin - for T in (Float32, ComplexF64) - tA = rand(T, V1 ⊗ V2, V1 ⊗ V2) - tB = rand(T, (V3 ⊗ V4 ⊗ V5)', (V3 ⊗ V4 ⊗ V5)') - tA = 3 // 2 * left_polar(tA)[1] - tB = 1 // 5 * left_polar(tB)[1] - tC = rand(T, V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') - t = @constinferred sylvester(tA, tB, tC) - @test codomain(t) == V1 ⊗ V2 - @test domain(t) == (V3 ⊗ V4 ⊗ V5)' - @test norm(tA * t + t * tB + tC) < - (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - matrix(x) = reshape(convert(Array, x), dim(codomain(x)), dim(domain(x))) - @test matrix(t) ≈ sylvester(matrix(tA), matrix(tB), matrix(tC)) - end + @test @constinferred(removeunit(t4, 4)) == t + + t5 = @constinferred insertleftunit(t, 4; dual = true) + @test numin(t5) == numin(t) + 1 && numout(t5) == numout(t) + for (c, b) in blocks(t) + @test b == block(t5, c) end + @test @constinferred(removeunit(t5, 4)) == t end end -@testsuite :tensors "index manipulations" V -> begin +@testsuite :tensors "permutations via inner product invariance" V -> begin I = sectortype(first(V)) - hasbraiding = BraidingStyle(I) isa HasBraiding - symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding + BraidingStyle(I) isa SymmetricBraiding || return nothing V1, V2, V3, V4, V5 = V - @timedtestset "Trivial space insertion and removal" begin - W = V1 ⊗ V2 ← (V3 ⊗ V4 ⊗ V5)' - for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) - t2 = @constinferred insertleftunit(t) - @test t2 == @constinferred insertrightunit(t) - @test space(t2) == insertleftunit(space(t)) - @test @constinferred(removeunit(t2, $(numind(t2)))) == t - t3 = @constinferred insertleftunit(t; copy = true) - @test t3 == @constinferred insertrightunit(t; copy = true) - @test @constinferred(removeunit(t3, $(numind(t3)))) == t - - @test numind(t2) == numind(t) + 1 - @test scalartype(t2) === T - @test t.data === t2.data - - @test t.data !== t3.data - for (c, b) in blocks(t) - @test b == block(t3, c) - end - - t4 = @constinferred insertrightunit(t, 3; dual = true) - @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) - for (c, b) in blocks(t) - @test b == block(t4, c) - end - @test @constinferred(removeunit(t4, 4)) == t - - t5 = @constinferred insertleftunit(t, 4; dual = true) - @test numin(t5) == numin(t) + 1 && numout(t5) == numout(t) - for (c, b) in blocks(t) - @test b == block(t5, c) - end - @test @constinferred(removeunit(t5, 4)) == t + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + t = rand(ComplexF64, W) + t′ = randn!(similar(t)) + for k in 0:5 + for p in permutations(1:5) + p1 = ntuple(n -> p[n], k) + p2 = ntuple(n -> p[k + n], 5 - k) + t2 = @constinferred permute(t, (p1, p2)) + @test norm(t2) ≈ norm(t) + t2′ = permute(t′, (p1, p2)) + @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) end - end - symmetricbraiding && @timedtestset "Permutations: test via inner product invariance" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - t = rand(ComplexF64, W) - t′ = randn!(similar(t)) - for k in 0:5 - for p in permutations(1:5) - p1 = ntuple(n -> p[n], k) - p2 = ntuple(n -> p[k + n], 5 - k) - t2 = @constinferred permute(t, (p1, p2)) - @test norm(t2) ≈ norm(t) - t2′ = permute(t′, (p1, p2)) - @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) - end - t3 = @constinferred repartition(t, $k) - @test norm(t3) ≈ norm(t) - t3′ = @constinferred repartition!(similar(t3), t′) - @test norm(t3′) ≈ norm(t′) - @test dot(t′, t) ≈ dot(t3′, t3) - end + t3 = @constinferred repartition(t, $k) + @test norm(t3) ≈ norm(t) + t3′ = @constinferred repartition!(similar(t3), t′) + @test norm(t3′) ≈ norm(t′) + @test dot(t′, t) ≈ dot(t3′, t3) end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Permutations: test via conversion" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - t = rand(ComplexF64, W) - a = convert(Array, t) - for k in 0:5 - for p in permutations(1:5) - p1 = ntuple(n -> p[n], k) - p2 = ntuple(n -> p[k + n], 5 - k) - t2 = permute(t, (p1, p2)) - a2 = convert(Array, t2) - @test a2 ≈ permutedims(a, (p1..., p2...)) - @test convert(Array, transpose(t2)) ≈ - permutedims(a2, (5, 4, 3, 2, 1)) - end +end - t3 = repartition(t, k) - a3 = convert(Array, t3) - @test a3 ≈ permutedims( - a, (ntuple(identity, k)..., reverse(ntuple(i -> i + k, 5 - k))...) - ) - end - end - end - hasbraiding && @timedtestset "Index flipping: test flipping inverse" begin - t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)') - for i in 1:5 - @test t ≈ flip(flip(t, i), i; inv = true) - @test t ≈ flip(flip(t, i; inv = true), i) +@testsuite :tensors "permutations via conversion" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa Bosonic && hasfusiontensor(I) || return nothing + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + t = rand(ComplexF64, W) + a = convert(Array, t) + for k in 0:5 + for p in permutations(1:5) + p1 = ntuple(n -> p[n], k) + p2 = ntuple(n -> p[k + n], 5 - k) + t2 = permute(t, (p1, p2)) + a2 = convert(Array, t2) + @test a2 ≈ permutedims(a, (p1..., p2...)) + @test convert(Array, transpose(t2)) ≈ + permutedims(a2, (5, 4, 3, 2, 1)) end + + t3 = repartition(t, k) + a3 = convert(Array, t3) + @test a3 ≈ permutedims( + a, (ntuple(identity, k)..., reverse(ntuple(i -> i + k, 5 - k))...) + ) end - symmetricbraiding && @timedtestset "Index flipping: test via explicit flip" begin - t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) - F1 = unitary(flip(V1), V1) - - @tensor tf[a, b; c, d] := F1[a, a'] * t[a', b; c, d] - @test flip(t, 1) ≈ tf - @tensor tf[a, b; c, d] := conj(F1[b, b']) * t[a, b'; c, d] - @test twist!(flip(t, 2), 2) ≈ tf - @tensor tf[a, b; c, d] := F1[c, c'] * t[a, b; c', d] - @test flip(t, 3) ≈ tf - @tensor tf[a, b; c, d] := conj(F1[d, d']) * t[a, b; c, d'] - @test twist!(flip(t, 4), 4) ≈ tf - end - symmetricbraiding && @timedtestset "Index flipping: test via contraction" begin - t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V4) - t2 = rand(ComplexF64, V2' ⊗ V5 ← V4' ⊗ V1) - @tensor ta[a, b] := t1[x, y, a, z] * t2[y, b, z, x] - @tensor tb[a, b] := flip(t1, 1)[x, y, a, z] * flip(t2, 4)[y, b, z, x] - @test ta ≈ tb - @tensor tb[a, b] := flip(t1, (2, 4))[x, y, a, z] * flip(t2, (1, 3))[y, b, z, x] - @test ta ≈ tb - @tensor tb[a, b] := flip(t1, (1, 2, 4))[x, y, a, z] * flip(t2, (1, 3, 4))[y, b, z, x] - @tensor tb[a, b] := flip(t1, (1, 3))[x, y, a, z] * flip(t2, (2, 4))[y, b, z, x] - @test flip(ta, (1, 2)) ≈ tb - end - hasbraiding && !symmetricbraiding && @timedtestset "Braid AdjointTensorMap: adjoint identity" begin - t = rand(ComplexF64, V1 ⊗ V2 ← V3) - p = ((2,), (1, 3)) - levels = (1, 3, 2) - t1 = copy(braid(t', p, levels)) - t2 = braid(copy(t'), p, levels) - @test t1 ≈ t2 +end + +@testsuite :tensors "index flipping inverse" V -> begin # test flipping inverse + I = sectortype(first(V)) + BraidingStyle(I) isa HasBraiding || return nothing + V1, V2, V3, V4, V5 = V + t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← (V4 ⊗ V5)') + for i in 1:5 + @test t ≈ flip(flip(t, i), i; inv = true) + @test t ≈ flip(flip(t, i; inv = true), i) end end -@testsuite :tensors "braiding tensor" V -> begin +@testsuite :tensors "index flipping explicit" V -> begin # test flipping via explicit flip + I = sectortype(first(V)) + BraidingStyle(I) isa SymmetricBraiding || return nothing + V1, V2, V3, V4, V5 = V + t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) + F1 = unitary(flip(V1), V1) + + @tensor tf[a, b; c, d] := F1[a, a'] * t[a', b; c, d] + @test flip(t, 1) ≈ tf + @tensor tf[a, b; c, d] := conj(F1[b, b']) * t[a, b'; c, d] + @test twist!(flip(t, 2), 2) ≈ tf + @tensor tf[a, b; c, d] := F1[c, c'] * t[a, b; c', d] + @test flip(t, 3) ≈ tf + @tensor tf[a, b; c, d] := conj(F1[d, d']) * t[a, b; c, d'] + @test twist!(flip(t, 4), 4) ≈ tf +end + +@testsuite :tensors "index flipping via contraction" V -> begin # test flipping via contraction I = sectortype(first(V)) - BraidingStyle(I) isa NoBraiding && return nothing + BraidingStyle(I) isa SymmetricBraiding || return nothing + V1, V2, V3, V4, V5 = V + t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V4) + t2 = rand(ComplexF64, V2' ⊗ V5 ← V4' ⊗ V1) + @tensor ta[a, b] := t1[x, y, a, z] * t2[y, b, z, x] + @tensor tb[a, b] := flip(t1, 1)[x, y, a, z] * flip(t2, 4)[y, b, z, x] + @test ta ≈ tb + @tensor tb[a, b] := flip(t1, (2, 4))[x, y, a, z] * flip(t2, (1, 3))[y, b, z, x] + @test ta ≈ tb + @tensor tb[a, b] := flip(t1, (1, 2, 4))[x, y, a, z] * flip(t2, (1, 3, 4))[y, b, z, x] + @tensor tb[a, b] := flip(t1, (1, 3))[x, y, a, z] * flip(t2, (2, 4))[y, b, z, x] + @test flip(ta, (1, 2)) ≈ tb +end + +@testsuite :tensors "braid adjoint identity" V -> begin # Braid AdjointTensorMap: adjoint identity + I = sectortype(first(V)) + (BraidingStyle(I) isa HasBraiding && !(BraidingStyle(I) isa SymmetricBraiding)) || return nothing + V1, V2, V3, V4, V5 = V + t = rand(ComplexF64, V1 ⊗ V2 ← V3) + p = ((2,), (1, 3)) + levels = (1, 3, 2) + t1 = copy(braid(t', p, levels)) + t2 = braid(copy(t'), p, levels) + @test t1 ≈ t2 +end + +# braiding tensor +#---------------- + +function _braiding_tensor_setup(V::NTuple{5, GradedSpace{I, NTuple{N, Int}}}) where {I <: Sector, N} Vspace = first(V) - T = ComplexF64 - t = randn(T, Vspace ⊗ Vspace' ⊗ Vspace' ⊗ Vspace ← Vspace ⊗ Vspace') - @timedtestset "planaradd! with BraidingTensor" begin - b = BraidingTensor(Vspace, Vspace') - bb = TensorMap(b) - # Cyclic rotations of the planar leg cycle (cod1, cod2, dom2, dom1). - # Use transpose (F-symbols only) as reference, since permute requires SymmetricBraiding. - # rotation 0 (identity) - @planar t1[-1 -2; -3 -4] := b[-1 -2; -3 -4] - @test t1 ≈ bb - # rotation 1: single-tree cycle (4,1,2,3) → (p1=(2,4), p2=(1,3)) - @planar t2[-1 -2; -3 -4] := b[-3 -1; -4 -2] - @test t2 ≈ transpose(bb, ((2, 4), (1, 3))) - # rotation 2: single-tree cycle (3,4,1,2) → (p1=(4,3), p2=(2,1)) - @planar t3[-1 -2; -3 -4] := b[-4 -3; -2 -1] - @test t3 ≈ transpose(bb, ((4, 3), (2, 1))) - # rotation 3: single-tree cycle (2,3,4,1) → (p1=(3,1), p2=(4,2)) - @planar t4[-1 -2; -3 -4] := b[-2 -4; -1 -3] - @test t4 ≈ transpose(bb, ((3, 1), (4, 2))) - # adjoint BraidingTensor (rotation 0) - ba = b' - @planar t5[-1 -2; -3 -4] := ba[-1 -2; -3 -4] - @test t5 ≈ TensorMap(ba) - end + t = randn(ComplexF64, Vspace ⊗ Vspace' ⊗ Vspace' ⊗ Vspace ← Vspace ⊗ Vspace') + hasbraiding = BraidingStyle(I) isa HasBraiding + return hasbraiding, Vspace, t +end - @timedtestset "τ as left factor, all legs contracted" begin - # BraidingTensor(V, V') on leading codomain indices - ττ = TensorMap(BraidingTensor(Vspace, Vspace')) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -2 -1] * t[1 2 -3 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 2; -1 1] * t[1 2 -3 -4; -5 -6] - @test t1 ≈ braid(t, ((2, 1, 3, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V') on inner codomain indices - ττ = TensorMap(BraidingTensor(Vspace', Vspace')) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ braid(t, ((1, 3, 2, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V) on trailing codomain indices - ττ = TensorMap(BraidingTensor(Vspace', Vspace)) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -4 -3] * t[-1 -2 1 2; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-4 2; -3 1] * t[-1 -2 1 2; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end +@testsuite :tensors "braiding tensor planaradd" V -> begin + hasbraiding, Vspace, _ = _braiding_tensor_setup(V) + hasbraiding || return nothing + b = BraidingTensor(Vspace, Vspace') + bb = TensorMap(b) + # Cyclic rotations of the planar leg cycle (cod1, cod2, dom2, dom1). + # Use transpose (F-symbols only) as reference, since permute requires SymmetricBraiding. + # rotation 0 (identity) + @planar t1[-1 -2; -3 -4] := b[-1 -2; -3 -4] + @test t1 ≈ bb + # rotation 1: single-tree cycle (4,1,2,3) → (p1=(2,4), p2=(1,3)) + @planar t2[-1 -2; -3 -4] := b[-3 -1; -4 -2] + @test t2 ≈ transpose(bb, ((2, 4), (1, 3))) + # rotation 2: single-tree cycle (3,4,1,2) → (p1=(4,3), p2=(2,1)) + @planar t3[-1 -2; -3 -4] := b[-4 -3; -2 -1] + @test t3 ≈ transpose(bb, ((4, 3), (2, 1))) + # rotation 3: single-tree cycle (2,3,4,1) → (p1=(3,1), p2=(4,2)) + @planar t4[-1 -2; -3 -4] := b[-2 -4; -1 -3] + @test t4 ≈ transpose(bb, ((3, 1), (4, 2))) + # adjoint BraidingTensor (rotation 0) + ba = b' + @planar t5[-1 -2; -3 -4] := ba[-1 -2; -3 -4] + @test t5 ≈ TensorMap(ba) +end - @timedtestset "τ as left factor, mixed open legs" begin - # BraidingTensor(V', V) with mixed index pattern - ττ = TensorMap(BraidingTensor(Vspace', Vspace)) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V') with mixed index pattern (inverse of previous) - ττ = TensorMap(BraidingTensor(Vspace, Vspace')) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V) with mixed index pattern - ττ = TensorMap(BraidingTensor(Vspace, Vspace)) - @planar t1[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := ττ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] - @planar t4[-1 -2 -3 -4; -5 -6] := τ'[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end +@testsuite :tensors "braiding tensor left full contraction" V -> begin # τ as left factor, all legs contracted + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # BraidingTensor(V, V') on leading codomain indices + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-1 -2; 1 2] * t[1 2 -3 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -2 -1] * t[1 2 -3 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 2; -1 1] * t[1 2 -3 -4; -5 -6] + @test t1 ≈ braid(t, ((2, 1, 3, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V') on inner codomain indices + ττ = TensorMap(BraidingTensor(Vspace', Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ braid(t, ((1, 3, 2, 4), (5, 6)), (1, 2, 3, 4, 5, 6)) + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V) on trailing codomain indices + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 -4; 1 2] * t[-1 -2 1 2; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[2 1; -4 -3] * t[-1 -2 1 2; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-4 2; -3 1] * t[-1 -2 1 2; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 +end - @timedtestset "τ as right factor" begin - # BraidingTensor(V', V) on all domain indices - ττ = TensorMap(BraidingTensor(Vspace', Vspace)) - @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[1 2; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ[1 2; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[-6 -5; 2 1] - @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[2 -6; 1 -5] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V') adjoint on all domain indices - ττ = TensorMap(BraidingTensor(Vspace, Vspace')) - @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[1 2; -5 -6] - @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ'[1 2; -5 -6] - @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[-6 -5; 2 1] - @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[2 -6; 1 -5] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V) with mixed domain legs - ττ = TensorMap(BraidingTensor(Vspace, Vspace)) - @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[-4 -6; 1 2] - @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * ττ[-4 -6; 1 2] - @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[2 1; -6 -4] - @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ'[-6 2; -4 1] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end +@testsuite :tensors "braiding tensor left partial contraction" V -> begin # τ as left factor, mixed open legs + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # BraidingTensor(V', V) with mixed index pattern + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V') with mixed index pattern (inverse of previous) + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[-3 2; -2 1] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V) with mixed index pattern + ττ = TensorMap(BraidingTensor(Vspace, Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := τ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := ττ[2 1; -3 -2] * t[-1 1 2 -4; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := τ[-2 -3; 1 2] * t[-1 1 2 -4; -5 -6] + @planar t4[-1 -2 -3 -4; -5 -6] := τ'[1 -2; 2 -3] * t[-1 1 2 -4; -5 -6] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 +end - @timedtestset "τ with fully contracted output" begin - # scalar output - ττ = TensorMap(BraidingTensor(Vspace', Vspace)) - @planar t1[(); (-1, -2)] := τ[2 1; 3 4] * t[1 2 3 4; -1 -2] - @planar t2[(); (-1, -2)] := ττ[2 1; 3 4] * t[1 2 3 4; -1 -2] - @planar t3[(); (-1, -2)] := τ[4 3; 1 2] * t[1 2 3 4; -1 -2] - @planar t4[(); (-1, -2)] := τ'[1 4; 2 3] * t[1 2 3 4; -1 -2] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # rank-1 output - ττ = TensorMap(BraidingTensor(Vspace, Vspace)) - @planar t1[-1; -2] := τ[2 1; 3 4] * t[-1 1 2 3; -2 4] - @planar t2[-1; -2] := ττ[2 1; 3 4] * t[-1 1 2 3; -2 4] - @planar t3[-1; -2] := τ[4 3; 1 2] * t[-1 1 2 3; -2 4] - @planar t4[-1; -2] := τ'[1 4; 2 3] * t[-1 1 2 3; -2 4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # rank-2 output - ττ = TensorMap(BraidingTensor(Vspace, Vspace')) - @planar t1[-1 -2] := τ[2 1; 3 4] * t[-1 -2 1 2; 4 3] - @planar t2[-1 -2] := ττ[2 1; 3 4] * t[-1 -2 1 2; 4 3] - @planar t3[-1 -2] := τ[4 3; 1 2] * t[-1 -2 1 2; 4 3] - @planar t4[-1 -2] := τ'[1 4; 2 3] * t[-1 -2 1 2; 4 3] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end +@testsuite :tensors "braiding tensor right full contraction" V -> begin # τ as right factor + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # BraidingTensor(V', V) on all domain indices + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[1 2; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ[1 2; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[-6 -5; 2 1] + @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[2 -6; 1 -5] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V') adjoint on all domain indices + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[1 2; -5 -6] + @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * ττ'[1 2; -5 -6] + @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ'[-6 -5; 2 1] + @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 -4; 1 2] * τ[2 -6; 1 -5] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V) with mixed domain legs + ττ = TensorMap(BraidingTensor(Vspace, Vspace)) + @planar t1[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[-4 -6; 1 2] + @planar t2[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * ττ[-4 -6; 1 2] + @planar t3[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ[2 1; -6 -4] + @planar t4[-1 -2 -3 -4; -5 -6] := t[-1 -2 -3 1; -5 2] * τ'[-6 2; -4 1] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 +end - @timedtestset "τ with one open codomain leg" begin - # BraidingTensor(V, V') with one open codomain leg - ττ = TensorMap(BraidingTensor(Vspace, Vspace')) - @planar t1[-1 -2; -3 -4] := τ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] - @planar t2[-1 -2; -3 -4] := ττ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] - @planar t3[-1 -2; -3 -4] := τ[2 1; 3 -1] * t[1 2 3 -2; -3 -4] - @planar t4[-1 -2; -3 -4] := τ'[3 2; -1 1] * t[1 2 3 -2; -3 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V') adjoint with one open codomain leg - ττ = TensorMap(BraidingTensor(Vspace', Vspace')) - @planar t1[-1 -2; -3 -4] := τ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] - @planar t2[-1 -2; -3 -4] := ττ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] - @planar t3[-1 -2; -3 -4] := τ'[2 1; 3 -2] * t[-1 1 2 3; -3 -4] - @planar t4[-1 -2; -3 -4] := τ[3 2; -2 1] * t[-1 1 2 3; -3 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V', V) with one open codomain leg - ττ = TensorMap(BraidingTensor(Vspace', Vspace)) - @planar t1[-1 -2 -3; -4] := τ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] - @planar t2[-1 -2 -3; -4] := ττ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] - @planar t3[-1 -2 -3; -4] := τ[2 1; 3 -3] * t[-1 -2 1 2; -4 3] - @planar t4[-1 -2 -3; -4] := τ'[3 2; -3 1] * t[-1 -2 1 2; -4 3] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end +@testsuite :tensors "braiding tensor full contraction output" V -> begin + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # scalar output + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[(); (-1, -2)] := τ[2 1; 3 4] * t[1 2 3 4; -1 -2] + @planar t2[(); (-1, -2)] := ττ[2 1; 3 4] * t[1 2 3 4; -1 -2] + @planar t3[(); (-1, -2)] := τ[4 3; 1 2] * t[1 2 3 4; -1 -2] + @planar t4[(); (-1, -2)] := τ'[1 4; 2 3] * t[1 2 3 4; -1 -2] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # rank-1 output + ττ = TensorMap(BraidingTensor(Vspace, Vspace)) + @planar t1[-1; -2] := τ[2 1; 3 4] * t[-1 1 2 3; -2 4] + @planar t2[-1; -2] := ττ[2 1; 3 4] * t[-1 1 2 3; -2 4] + @planar t3[-1; -2] := τ[4 3; 1 2] * t[-1 1 2 3; -2 4] + @planar t4[-1; -2] := τ'[1 4; 2 3] * t[-1 1 2 3; -2 4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # rank-2 output + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2] := τ[2 1; 3 4] * t[-1 -2 1 2; 4 3] + @planar t2[-1 -2] := ττ[2 1; 3 4] * t[-1 -2 1 2; 4 3] + @planar t3[-1 -2] := τ[4 3; 1 2] * t[-1 -2 1 2; 4 3] + @planar t4[-1 -2] := τ'[1 4; 2 3] * t[-1 -2 1 2; 4 3] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 +end - @timedtestset "τ as right factor with open domain leg" begin - # BraidingTensor(V', V) as right factor with one open domain leg - ττ = TensorMap(BraidingTensor(Vspace', Vspace)) - @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[1 2; -4 3] - @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ[1 2; -4 3] - @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[3 -4; 2 1] - @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[2 3; 1 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - - # BraidingTensor(V, V') adjoint as right factor with one open domain leg - ττ = TensorMap(BraidingTensor(Vspace, Vspace')) - @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[1 2; -4 3] - @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ'[1 2; -4 3] - @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[3 -4; 2 1] - @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[2 3; 1 -4] - @test t1 ≈ t2 - @test t1 ≈ t3 - @test t1 ≈ t4 - end +@testsuite :tensors "braiding tensor open codomain leg" V -> begin # τ with one open codomain leg + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # BraidingTensor(V, V') with one open codomain leg + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2; -3 -4] := τ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] + @planar t2[-1 -2; -3 -4] := ττ[-1 3; 1 2] * t[1 2 3 -2; -3 -4] + @planar t3[-1 -2; -3 -4] := τ[2 1; 3 -1] * t[1 2 3 -2; -3 -4] + @planar t4[-1 -2; -3 -4] := τ'[3 2; -1 1] * t[1 2 3 -2; -3 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V') adjoint with one open codomain leg + ττ = TensorMap(BraidingTensor(Vspace', Vspace')) + @planar t1[-1 -2; -3 -4] := τ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] + @planar t2[-1 -2; -3 -4] := ττ'[-2 3; 1 2] * t[-1 1 2 3; -3 -4] + @planar t3[-1 -2; -3 -4] := τ'[2 1; 3 -2] * t[-1 1 2 3; -3 -4] + @planar t4[-1 -2; -3 -4] := τ[3 2; -2 1] * t[-1 1 2 3; -3 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V', V) with one open codomain leg + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3; -4] := τ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] + @planar t2[-1 -2 -3; -4] := ττ[-3 3; 1 2] * t[-1 -2 1 2; -4 3] + @planar t3[-1 -2 -3; -4] := τ[2 1; 3 -3] * t[-1 -2 1 2; -4 3] + @planar t4[-1 -2 -3; -4] := τ'[3 2; -3 1] * t[-1 -2 1 2; -4 3] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 +end - @timedtestset "BraidingTensor × BraidingTensor" begin - # b1 domain == b2 codomain == V⊗V', straight-through (planar) contraction - b1 = BraidingTensor(Vspace, Vspace') # space: V'⊗V ← V⊗V' - b2 = BraidingTensor(Vspace', Vspace) # space: V⊗V' ← V'⊗V - bb1 = TensorMap(b1) - bb2 = TensorMap(b2) - @planar t1[-1 -2; -3 -4] := b1[-1 -2; 1 2] * b2[1 2; -3 -4] - @planar t2[-1 -2; -3 -4] := bb1[-1 -2; 1 2] * bb2[1 2; -3 -4] - @test t1 ≈ t2 - end +@testsuite :tensors "braiding tensor open domain leg" V -> begin # τ as right factor with open domain leg + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # BraidingTensor(V', V) as right factor with one open domain leg + ττ = TensorMap(BraidingTensor(Vspace', Vspace)) + @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[1 2; -4 3] + @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ[1 2; -4 3] + @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[3 -4; 2 1] + @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[2 3; 1 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 + + # BraidingTensor(V, V') adjoint as right factor with one open domain leg + ττ = TensorMap(BraidingTensor(Vspace, Vspace')) + @planar t1[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[1 2; -4 3] + @planar t2[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * ττ'[1 2; -4 3] + @planar t3[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ'[3 -4; 2 1] + @planar t4[-1 -2 -3; -4] := t[-1 -2 -3 3; 1 2] * τ[2 3; 1 -4] + @test t1 ≈ t2 + @test t1 ≈ t3 + @test t1 ≈ t4 +end + +@testsuite :tensors "contraction between braiding tensors" V -> begin # BraidingTensor × BraidingTensor + hasbraiding, Vspace, t = _braiding_tensor_setup(V) + hasbraiding || return nothing + # b1 domain == b2 codomain == V⊗V', straight-through (planar) contraction + b1 = BraidingTensor(Vspace, Vspace') # space: V'⊗V ← V⊗V' + b2 = BraidingTensor(Vspace', Vspace) # space: V⊗V' ← V'⊗V + bb1 = TensorMap(b1) + bb2 = TensorMap(b2) + @planar t1[-1 -2; -3 -4] := b1[-1 -2; 1 2] * b2[1 2; -3 -4] + @planar t2[-1 -2; -3 -4] := bb1[-1 -2; 1 2] * bb2[1 2; -3 -4] + @test t1 ≈ t2 end +# hom space +#---------- @testsuite :tensors "hom space" V -> begin V1, V2, V3, V4, V5 = V W = HomSpace(V1 ⊗ V2, (V3 ⊗ V4 ⊗ V5)') From e1d0f334fa3117649605b1f3112d161d59bb2d91 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 8 Jul 2026 13:34:02 +0200 Subject: [PATCH 10/13] make diagonal tensor tests also an entry point --- test/runtests.jl | 1 + test/tensors/diagonal.jl | 234 ++------------------------- test/testsuite/TensorKitTestSuite.jl | 35 +++- test/testsuite/diagonal.jl | 227 ++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 225 deletions(-) create mode 100644 test/testsuite/diagonal.jl diff --git a/test/runtests.jl b/test/runtests.jl index 0d36a7004..8ae4f1e9c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,6 +9,7 @@ delete!(testsuite, "TensorKitTestSuite") # reusable test suite module, not a tes delete!(testsuite, "testsuite/fusiontrees") # only meant to be `include`d inside TensorKitTestSuite delete!(testsuite, "testsuite/spaces") delete!(testsuite, "testsuite/tensors") +delete!(testsuite, "testsuite/diagonal") # CUDA tests: only run if CUDA is functional using CUDA: CUDA diff --git a/test/tensors/diagonal.jl b/test/tensors/diagonal.jl index 8a4c46161..818808612 100644 --- a/test/tensors/diagonal.jl +++ b/test/tensors/diagonal.jl @@ -13,223 +13,21 @@ diagspacelist = ( Vect[IsingBimodule]((1, 1, 0) => 2, (1, 1, 1) => 3), ) -@testset "DiagonalTensor with domain $V" for V in diagspacelist - @timedtestset "Basic properties and algebra" begin - for T in (fast_tests ? (Float64, ComplexF64) : (Float32, Float64, ComplexF32, ComplexF64, BigFloat)) - # constructors - t = @constinferred DiagonalTensorMap{T}(undef, V) - t = @constinferred DiagonalTensorMap(rand(T, reduceddim(V)), V) - t2 = @constinferred DiagonalTensorMap{T}(undef, space(t)) - @test space(t2) == space(t) - @test_throws ArgumentError DiagonalTensorMap{T}(undef, V^2 ← V) - t2 = @constinferred DiagonalTensorMap{T}(undef, domain(t)) - @test space(t2) == space(t) - @test_throws ArgumentError DiagonalTensorMap{T}(undef, V^2) - # properties - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test codomain(t) == ProductSpace(V) - @test domain(t) == ProductSpace(V) - @test space(t) == (V ← V) - @test space(t') == (V ← V) - @test dim(t) == dim(space(t)) - # blocks - bs = @constinferred blocks(t) - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(V ← V)) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t, first(blocksectors(t))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t) - # basic linear algebra - @test isa(@constinferred(norm(t)), real(T)) - @test norm(t)^2 ≈ dot(t, t) - α = rand(T) - @test norm(α * t) ≈ abs(α) * norm(t) - @test norm(t + t, 2) ≈ 2 * norm(t, 2) - @test norm(t + t, 1) ≈ 2 * norm(t, 1) - @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) - p = 3 * rand(Float64) - @test norm(t + t, p) ≈ 2 * norm(t, p) - @test norm(t) ≈ norm(t') - - @test t == @constinferred(TensorMap(t)) - @test norm(t + TensorMap(t)) ≈ norm(TensorMap(t) + t) ≈ 2 * norm(t) - - @test norm(zerovector!(t)) == 0 - @test norm(one!(t)) ≈ sqrt(dim(V)) - @test one!(t) == id(V) - if T != BigFloat # seems broken for now - @test norm(one!(t) - id(V)) == 0 - end - - t2 = randn!(TensorMap(t)) - @test t2 + t ≈ t + t2 ≈ t2 + TensorMap(t) - - t1 = DiagonalTensorMap(rand(T, reduceddim(V)), V) - t2 = DiagonalTensorMap(rand(T, reduceddim(V)), V) - t3 = DiagonalTensorMap(rand(T, reduceddim(V)), V) - α = rand(T) - β = rand(T) - @test @constinferred(dot(t1, t2)) ≈ conj(dot(t2, t1)) - @test dot(t2, t1) ≈ conj(dot(t2', t1')) - @test dot(t3, α * t1 + β * t2) ≈ α * dot(t3, t1) + β * dot(t3, t2) - end - end - I = sectortype(V) - @timedtestset "Basic linear algebra: test via conversion" begin - for T in (Float32, ComplexF64) - t1 = DiagonalTensorMap(rand(T, reduceddim(V)), V) - t2 = DiagonalTensorMap(rand(T, reduceddim(V)), V) - @test norm(t1, 2) ≈ norm(convert(TensorMap, t1), 2) - @test dot(t2, t1) ≈ dot(convert(TensorMap, t2), convert(TensorMap, t1)) - α = rand(T) - @test convert(TensorMap, α * t1) ≈ α * convert(TensorMap, t1) - @test convert(TensorMap, t1') ≈ convert(TensorMap, t1)' - @test convert(TensorMap, t1 + t2) ≈ convert(TensorMap, t1) + convert(TensorMap, t2) - end - end - @timedtestset "Real and imaginary parts" begin - for T in (Float64, ComplexF64, ComplexF32) - t = DiagonalTensorMap(rand(T, reduceddim(V)), V) - - tr = @constinferred real(t) - @test scalartype(tr) <: Real - @test real(convert(TensorMap, t)) == convert(TensorMap, tr) - - ti = @constinferred imag(t) - @test scalartype(ti) <: Real - @test imag(convert(TensorMap, t)) == convert(TensorMap, ti) - - tc = @inferred complex(t) - @test scalartype(tc) <: Complex - @test complex(convert(TensorMap, t)) == convert(TensorMap, tc) - - tc2 = @inferred complex(tr, ti) - @test tc2 ≈ tc - end - end - @timedtestset "Tensor conversion" begin - t = @constinferred DiagonalTensorMap(undef, V) - rand!(t.data) - # element type conversion - tc = complex(t) - @test convert(typeof(tc), t) == tc - @test typeof(convert(typeof(tc), t)) == typeof(tc) - # to and from generic TensorMap - td = DiagonalTensorMap(TensorMap(t)) - @test t == td - @test typeof(td) == typeof(t) - end +for V in diagspacelist I = sectortype(V) - if BraidingStyle(I) isa SymmetricBraiding - @timedtestset "Permutations" begin - t = DiagonalTensorMap(randn(ComplexF64, reduceddim(V)), V) - t_tm = convert(TensorMap, t) - - # preserving diagonal - t1 = @constinferred permute(t, $(((2,), (1,)))) - @test t1 isa DiagonalTensorMap - @test convert(TensorMap, t1) == permute(t_tm, (((2,), (1,)))) - t1′ = @constinferred transpose(t) - @test t1′ isa DiagonalTensorMap - @test convert(TensorMap, t1′) == transpose(t_tm) - BraidingStyle(I) isa Bosonic && @test t1 ≈ t1′ - - # not preserving diagonal - t2 = @constinferred permute(t, $(((1, 2), ()))) - @test convert(TensorMap, t2) == permute(t_tm, (((1, 2), ()))) - t3 = @constinferred permute(t, $(((2, 1), ()))) - @test convert(TensorMap, t3) == permute(t_tm, (((2, 1), ()))) - t4 = @constinferred permute(t, $(((), (1, 2)))) - @test convert(TensorMap, t4) == permute(t_tm, (((), (1, 2)))) - t5 = @constinferred permute(t, $(((), (2, 1)))) - @test convert(TensorMap, t5) == permute(t_tm, (((), (2, 1)))) - end - end - @timedtestset "Trace, Multiplication and inverse" begin - t1 = DiagonalTensorMap(rand(Float64, reduceddim(V)), V) - t2 = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) - @test tr(TensorMap(t1)) == @constinferred tr(t1) - @test tr(TensorMap(t2)) == @constinferred tr(t2) - @test TensorMap(@constinferred t1 * t2) ≈ TensorMap(t1) * TensorMap(t2) - @test TensorMap(@constinferred t1 \ t2) ≈ TensorMap(t1) \ TensorMap(t2) - @test TensorMap(@constinferred t1 / t2) ≈ TensorMap(t1) / TensorMap(t2) - @test TensorMap(@constinferred inv(t1)) ≈ inv(TensorMap(t1)) - @test TensorMap(@constinferred pinv(t1)) ≈ pinv(TensorMap(t1)) - @test all( - Base.Fix2(isa, DiagonalTensorMap), (t1 * t2, t1 \ t2, t1 / t2, inv(t1), pinv(t1)) - ) - - u = randn(Float64, V * V' * V, V) - @test u * t1 ≈ u * TensorMap(t1) - @test u / t1 ≈ u / TensorMap(t1) - @test t1 * u' ≈ TensorMap(t1) * u' - @test t1 \ u' ≈ TensorMap(t1) \ u' - - t3 = rand(Float64, V ← V^2) - t4 = rand(ComplexF64, V ← V^2) - @test t1 * t3 ≈ lmul!(t1, copy(t3)) - @test t2 * t4 ≈ lmul!(t2, copy(t4)) - - t3 = rand(Float64, V^2 ← V) - t4 = rand(ComplexF64, V^2 ← V) - @test t3 * t1 ≈ rmul!(copy(t3), t1) - @test t4 * t2 ≈ rmul!(copy(t4), t2) - end - @timedtestset "Tensor contraction" begin - d = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) - t = TensorMap(d) - A = randn(ComplexF64, V ⊗ V' ⊗ V, V) - B = randn(ComplexF64, V ⊗ V' ⊗ V, V ⊗ V') - if BraidingStyle(I) isa SymmetricBraiding - @tensor C[a b c; d] := A[a b c; e] * d[e, d] - @test C ≈ A * d - @tensor D[a; b] := d[a, c] * d[c, b] - @test D ≈ d * d - @test D isa DiagonalTensorMap - end - @planar E1[-1 -2 -3; -4 -5] := B[-1 -2 -3; 1 -5] * d[1; -4] - @planar E2[-1 -2 -3; -4 -5] := B[-1 -2 -3; 1 -5] * t[1; -4] - @test E1 ≈ E2 - @planar E1[-1 -2 -3; -4 -5] = B[-1 -2 -3; -4 1] * d'[-5; 1] - @planar E2[-1 -2 -3; -4 -5] = B[-1 -2 -3; -4 1] * t'[-5; 1] - @test E1 ≈ E2 - @planar E1[-1 -2 -3; -4 -5] = B[1 -2 -3; -4 -5] * d[-1; 1] - @planar E2[-1 -2 -3; -4 -5] = B[1 -2 -3; -4 -5] * t[-1; 1] - @test E1 ≈ E2 - @planar E1[-1 -2 -3; -4 -5] = B[-1 1 -3; -4 -5] * d[1; -2] - @planar E2[-1 -2 -3; -4 -5] = B[-1 1 -3; -4 -5] * t[1; -2] - @test E1 ≈ E2 - @planar E1[-1 -2 -3; -4 -5] = B[-1 -2 1; -4 -5] * d'[-3; 1] - @planar E2[-1 -2 -3; -4 -5] = B[-1 -2 1; -4 -5] * t'[-3; 1] - @test E1 ≈ E2 - end - @timedtestset "Tensor functions" begin - for T in (Float64, ComplexF64) - d = DiagonalTensorMap(rand(T, reduceddim(V)), V) - # rand is important for positive numbers in the real case, for log and sqrt - t = TensorMap(d) - @test @constinferred exp(d) ≈ exp(t) - @test @constinferred log(d) ≈ log(t) - @test @constinferred sqrt(d) ≈ sqrt(t) - @test @constinferred sin(d) ≈ sin(t) - @test @constinferred cos(d) ≈ cos(t) - @test @constinferred tan(d) ≈ tan(t) - @test @constinferred cot(d) ≈ cot(t) - @test @constinferred sinh(d) ≈ sinh(t) - @test @constinferred cosh(d) ≈ cosh(t) - @test @constinferred tanh(d) ≈ tanh(t) - @test @constinferred coth(d) ≈ coth(t) - @test @constinferred asin(d) ≈ asin(t) - @test @constinferred acos(d) ≈ acos(t) - @test @constinferred atan(d) ≈ atan(t) - @test @constinferred acot(d) ≈ acot(t) - @test @constinferred asinh(d) ≈ asinh(t) - @test @constinferred acosh(one(d) + d) ≈ acosh(one(t) + t) - @test @constinferred atanh(d) ≈ atanh(t) - @test @constinferred acoth(one(t) + d) ≈ acoth(one(d) + t) - end - end + Istr = type_repr(I) + println("---------------------------------------") + println("DiagonalTensor with domain $V") + println("---------------------------------------") + @timedtestset "DiagonalTensor with symmetry: $Istr" verbose = true begin + TensorKitTestSuite.test_diagonal_tensors_basic_properties_and_algebra(V) + TensorKitTestSuite.test_diagonal_tensors_linear_algebra_conversion(V) + TensorKitTestSuite.test_diagonal_tensors_real_and_imaginary_parts(V) + TensorKitTestSuite.test_diagonal_tensors_tensor_conversion(V) + TensorKitTestSuite.test_diagonal_tensors_permutations(V) + TensorKitTestSuite.test_diagonal_tensors_trace_multiplication_and_inverse(V) + TensorKitTestSuite.test_diagonal_tensors_contraction(V) + TensorKitTestSuite.test_diagonal_tensors_tensor_functions(V) + end + TensorKit.empty_globalcaches!() end diff --git a/test/testsuite/TensorKitTestSuite.jl b/test/testsuite/TensorKitTestSuite.jl index c34851a74..7df711b05 100644 --- a/test/testsuite/TensorKitTestSuite.jl +++ b/test/testsuite/TensorKitTestSuite.jl @@ -19,9 +19,10 @@ TensorKitTestSuite.test_single_fusiontrees(MySector) TensorKitTestSuite.test_double_fusiontrees(MySector) TensorKitTestSuite.test_spaces(MySector) TensorKitTestSuite.test_tensors((V1, V2, V3, V4, V5)) # 5 mutually compatible spaces +TensorKitTestSuite.test_diagonal_tensors(V) # 1 space for diagonal tensors ``` -The four entry points above are independent and may be run selectively. +The entry points above are independent and may be run selectively. This module additionally exports: * [`force_planar`](@ref) * [`eval_show`](@ref) @@ -31,7 +32,7 @@ but deliberately *not* re-exported here. """ module TensorKitTestSuite -export test_single_fusiontrees, test_double_fusiontrees, test_spaces, test_tensors +export test_single_fusiontrees, test_double_fusiontrees, test_spaces, test_tensors, test_diagonal_tensors export force_planar, eval_show using Test @@ -45,7 +46,7 @@ using TensorOperations using MatrixAlgebraKit: left_polar, isunitary using TensorKit -using TensorKit: type_repr, FusionTreeBlock, ℙ, PlanarTrivial, hassector, HomSpace +using TensorKit: type_repr, FusionTreeBlock, ℙ, PlanarTrivial, hassector, HomSpace, check_spacetype import TensorKit as TK using TensorKitSectors using TensorKitSectors: × @@ -64,6 +65,7 @@ const testgroups = Dict{Symbol, Dict{String, Expr}}( :double_fusiontrees => Dict{String, Expr}(), :spaces => Dict{String, Expr}(), :tensors => Dict{String, Expr}(), + :diagonal_tensors => Dict{String, Expr}(), ) # cannot just esc() the body, because that would make it a closure, compile it at a fixed world age and break constprop=true @@ -80,9 +82,10 @@ end # test code here end -Register a testsuite entry under `testgroup` (one of `:single_fusiontrees`, `:double_fusiontrees`, `:spaces`,`:tensors`). +Register a testsuite entry under `testgroup` (one of `:single_fusiontrees`, `:double_fusiontrees`, `:spaces`,`:tensors`, `:diagonal_tensors`). The body is executed with a single argument: the concrete `Sector` type under test -(for `:single_fusiontrees`, `:double_fusiontrees` and `:spaces`), or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). +(for `:single_fusiontrees`, `:double_fusiontrees` and `:spaces`), a space (for `:diagonal_tensors`), +or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). Important: Whatever is passed as `name` becomes part of the generated function that must be called to run that body. In particular, a `safe_name` is made where `name`'s spaces are replaced by underscores, and everything becomes lowercase. @@ -148,13 +151,14 @@ function test_spaces(I::Type{<:Sector}) #TODO: change since it's just 1 testsuit end """ - test_tensors(V) + test_tensors(V::NTuple{5, ElementarySpace}) Runs the tensor operation test suite (construction, contractions, linear algebra, index manipulations, braiding, `HomSpace`) on `V`, a 5-tuple of mutually compatible `ElementarySpace`s. See `setup.jl` for space design considerations. """ -function test_tensors(V::NTuple{5, GradedSpace{I, NTuple{N, Int}}}) where {I <: Sector, N} +function test_tensors(V::NTuple{5, ElementarySpace}) + I = check_spacetype(V) return @testset "$(type_repr(I))" begin for (name, lambda) in testgroups[:tensors] @testset "$name" begin @@ -164,6 +168,23 @@ function test_tensors(V::NTuple{5, GradedSpace{I, NTuple{N, Int}}}) where {I <: end end +""" + test_diagonal_tensors(V::ElementarySpace) + +Runs the diagonal tensor operation test suite (construction, contractions, linear algebra, +index manipulations) on an `ElementarySpace` `V`. See `setup.jl` for space design considerations, +but now applied to just one space, since diagonal tensors are endomorphisms. +""" +function test_diagonal_tensors(V::ElementarySpace) + return @testset "$(type_repr(sectortype(V)))" begin + for (name, lambda) in testgroups[:diagonal_tensors] + @testset "$name" begin + _run_testsuite_entry(lambda, V) + end + end + end +end + # Sector utilities # ---------------- """ diff --git a/test/testsuite/diagonal.jl b/test/testsuite/diagonal.jl new file mode 100644 index 000000000..700fe900a --- /dev/null +++ b/test/testsuite/diagonal.jl @@ -0,0 +1,227 @@ +# DiagonalTensor operations +# ===================== + +@testsuite :diagonal_tensors "basic properties and algebra" V -> begin + for T in (fast_tests ? (Float64, ComplexF64) : (Float32, Float64, ComplexF32, ComplexF64, BigFloat)) + # constructors + t = @constinferred DiagonalTensorMap{T}(undef, V) + t = @constinferred DiagonalTensorMap(rand(T, reduceddim(V)), V) + t2 = @constinferred DiagonalTensorMap{T}(undef, space(t)) + @test space(t2) == space(t) + @test_throws ArgumentError DiagonalTensorMap{T}(undef, V^2 ← V) + t2 = @constinferred DiagonalTensorMap{T}(undef, domain(t)) + @test space(t2) == space(t) + @test_throws ArgumentError DiagonalTensorMap{T}(undef, V^2) + # properties + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test codomain(t) == ProductSpace(V) + @test domain(t) == ProductSpace(V) + @test space(t) == (V ← V) + @test space(t') == (V ← V) + @test dim(t) == dim(space(t)) + # blocks + bs = @constinferred blocks(t) + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(V ← V)) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t, first(blocksectors(t))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t) + # basic linear algebra + @test isa(@constinferred(norm(t)), real(T)) + @test norm(t)^2 ≈ dot(t, t) + α = rand(T) + @test norm(α * t) ≈ abs(α) * norm(t) + @test norm(t + t, 2) ≈ 2 * norm(t, 2) + @test norm(t + t, 1) ≈ 2 * norm(t, 1) + @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) + p = 3 * rand(Float64) + @test norm(t + t, p) ≈ 2 * norm(t, p) + @test norm(t) ≈ norm(t') + + @test t == @constinferred(TensorMap(t)) + @test norm(t + TensorMap(t)) ≈ norm(TensorMap(t) + t) ≈ 2 * norm(t) + + @test norm(zerovector!(t)) == 0 + @test norm(one!(t)) ≈ sqrt(dim(V)) + @test one!(t) == id(V) + if T != BigFloat # seems broken for now + @test norm(one!(t) - id(V)) == 0 + end + + t2 = randn!(TensorMap(t)) + @test t2 + t ≈ t + t2 ≈ t2 + TensorMap(t) + + t1 = DiagonalTensorMap(rand(T, reduceddim(V)), V) + t2 = DiagonalTensorMap(rand(T, reduceddim(V)), V) + t3 = DiagonalTensorMap(rand(T, reduceddim(V)), V) + α = rand(T) + β = rand(T) + @test @constinferred(dot(t1, t2)) ≈ conj(dot(t2, t1)) + @test dot(t2, t1) ≈ conj(dot(t2', t1')) + @test dot(t3, α * t1 + β * t2) ≈ α * dot(t3, t1) + β * dot(t3, t2) + end +end + +@testsuite :diagonal_tensors "linear algebra conversion" V -> begin + for T in (Float32, ComplexF64) + t1 = DiagonalTensorMap(rand(T, reduceddim(V)), V) + t2 = DiagonalTensorMap(rand(T, reduceddim(V)), V) + @test norm(t1, 2) ≈ norm(convert(TensorMap, t1), 2) + @test dot(t2, t1) ≈ dot(convert(TensorMap, t2), convert(TensorMap, t1)) + α = rand(T) + @test convert(TensorMap, α * t1) ≈ α * convert(TensorMap, t1) + @test convert(TensorMap, t1') ≈ convert(TensorMap, t1)' + @test convert(TensorMap, t1 + t2) ≈ convert(TensorMap, t1) + convert(TensorMap, t2) + end +end + +@testsuite :diagonal_tensors "real and imaginary parts" V -> begin + for T in (Float64, ComplexF64, ComplexF32) + t = DiagonalTensorMap(rand(T, reduceddim(V)), V) + + tr = @constinferred real(t) + @test scalartype(tr) <: Real + @test real(convert(TensorMap, t)) == convert(TensorMap, tr) + + ti = @constinferred imag(t) + @test scalartype(ti) <: Real + @test imag(convert(TensorMap, t)) == convert(TensorMap, ti) + + tc = @inferred complex(t) + @test scalartype(tc) <: Complex + @test complex(convert(TensorMap, t)) == convert(TensorMap, tc) + + tc2 = @inferred complex(tr, ti) + @test tc2 ≈ tc + end +end + +@testsuite :diagonal_tensors "tensor conversion" V -> begin + t = @constinferred DiagonalTensorMap(undef, V) + rand!(t.data) + # element type conversion + tc = complex(t) + @test convert(typeof(tc), t) == tc + @test typeof(convert(typeof(tc), t)) == typeof(tc) + # to and from generic TensorMap + td = DiagonalTensorMap(TensorMap(t)) + @test t == td + @test typeof(td) == typeof(t) +end + +@testsuite :diagonal_tensors "permutations" V -> begin + I = sectortype(V) + BraidingStyle(I) isa SymmetricBraiding || return nothing + t = DiagonalTensorMap(randn(ComplexF64, reduceddim(V)), V) + t_tm = convert(TensorMap, t) + + # preserving diagonal + t1 = @constinferred permute(t, $(((2,), (1,)))) + @test t1 isa DiagonalTensorMap + @test convert(TensorMap, t1) == permute(t_tm, (((2,), (1,)))) + t1′ = @constinferred transpose(t) + @test t1′ isa DiagonalTensorMap + @test convert(TensorMap, t1′) == transpose(t_tm) + BraidingStyle(I) isa Bosonic && @test t1 ≈ t1′ + + # not preserving diagonal + t2 = @constinferred permute(t, $(((1, 2), ()))) + @test convert(TensorMap, t2) == permute(t_tm, (((1, 2), ()))) + t3 = @constinferred permute(t, $(((2, 1), ()))) + @test convert(TensorMap, t3) == permute(t_tm, (((2, 1), ()))) + t4 = @constinferred permute(t, $(((), (1, 2)))) + @test convert(TensorMap, t4) == permute(t_tm, (((), (1, 2)))) + t5 = @constinferred permute(t, $(((), (2, 1)))) + @test convert(TensorMap, t5) == permute(t_tm, (((), (2, 1)))) +end + +@testsuite :diagonal_tensors "trace multiplication and inverse" V -> begin + t1 = DiagonalTensorMap(rand(Float64, reduceddim(V)), V) + t2 = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) + @test tr(TensorMap(t1)) == @constinferred tr(t1) + @test tr(TensorMap(t2)) == @constinferred tr(t2) + @test TensorMap(@constinferred t1 * t2) ≈ TensorMap(t1) * TensorMap(t2) + @test TensorMap(@constinferred t1 \ t2) ≈ TensorMap(t1) \ TensorMap(t2) + @test TensorMap(@constinferred t1 / t2) ≈ TensorMap(t1) / TensorMap(t2) + @test TensorMap(@constinferred inv(t1)) ≈ inv(TensorMap(t1)) + @test TensorMap(@constinferred pinv(t1)) ≈ pinv(TensorMap(t1)) + @test all( + Base.Fix2(isa, DiagonalTensorMap), (t1 * t2, t1 \ t2, t1 / t2, inv(t1), pinv(t1)) + ) + + u = randn(Float64, V * V' * V, V) + @test u * t1 ≈ u * TensorMap(t1) + @test u / t1 ≈ u / TensorMap(t1) + @test t1 * u' ≈ TensorMap(t1) * u' + @test t1 \ u' ≈ TensorMap(t1) \ u' + + t3 = rand(Float64, V ← V^2) + t4 = rand(ComplexF64, V ← V^2) + @test t1 * t3 ≈ lmul!(t1, copy(t3)) + @test t2 * t4 ≈ lmul!(t2, copy(t4)) + + t3 = rand(Float64, V^2 ← V) + t4 = rand(ComplexF64, V^2 ← V) + @test t3 * t1 ≈ rmul!(copy(t3), t1) + @test t4 * t2 ≈ rmul!(copy(t4), t2) +end + +@testsuite :diagonal_tensors "contraction" V -> begin + I = sectortype(V) + d = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) + t = TensorMap(d) + A = randn(ComplexF64, V ⊗ V' ⊗ V, V) + B = randn(ComplexF64, V ⊗ V' ⊗ V, V ⊗ V') + if BraidingStyle(I) isa SymmetricBraiding + @tensor C[a b c; d] := A[a b c; e] * d[e, d] + @test C ≈ A * d + @tensor D[a; b] := d[a, c] * d[c, b] + @test D ≈ d * d + @test D isa DiagonalTensorMap + end + @planar E1[-1 -2 -3; -4 -5] := B[-1 -2 -3; 1 -5] * d[1; -4] + @planar E2[-1 -2 -3; -4 -5] := B[-1 -2 -3; 1 -5] * t[1; -4] + @test E1 ≈ E2 + @planar E1[-1 -2 -3; -4 -5] = B[-1 -2 -3; -4 1] * d'[-5; 1] + @planar E2[-1 -2 -3; -4 -5] = B[-1 -2 -3; -4 1] * t'[-5; 1] + @test E1 ≈ E2 + @planar E1[-1 -2 -3; -4 -5] = B[1 -2 -3; -4 -5] * d[-1; 1] + @planar E2[-1 -2 -3; -4 -5] = B[1 -2 -3; -4 -5] * t[-1; 1] + @test E1 ≈ E2 + @planar E1[-1 -2 -3; -4 -5] = B[-1 1 -3; -4 -5] * d[1; -2] + @planar E2[-1 -2 -3; -4 -5] = B[-1 1 -3; -4 -5] * t[1; -2] + @test E1 ≈ E2 + @planar E1[-1 -2 -3; -4 -5] = B[-1 -2 1; -4 -5] * d'[-3; 1] + @planar E2[-1 -2 -3; -4 -5] = B[-1 -2 1; -4 -5] * t'[-3; 1] + @test E1 ≈ E2 +end + +@testsuite :diagonal_tensors "tensor functions" V -> begin + for T in (Float64, ComplexF64) + d = DiagonalTensorMap(rand(T, reduceddim(V)), V) + # rand is important for positive numbers in the real case, for log and sqrt + t = TensorMap(d) + @test @constinferred exp(d) ≈ exp(t) + @test @constinferred log(d) ≈ log(t) + @test @constinferred sqrt(d) ≈ sqrt(t) + @test @constinferred sin(d) ≈ sin(t) + @test @constinferred cos(d) ≈ cos(t) + @test @constinferred tan(d) ≈ tan(t) + @test @constinferred cot(d) ≈ cot(t) + @test @constinferred sinh(d) ≈ sinh(t) + @test @constinferred cosh(d) ≈ cosh(t) + @test @constinferred tanh(d) ≈ tanh(t) + @test @constinferred coth(d) ≈ coth(t) + @test @constinferred asin(d) ≈ asin(t) + @test @constinferred acos(d) ≈ acos(t) + @test @constinferred atan(d) ≈ atan(t) + @test @constinferred acot(d) ≈ acot(t) + @test @constinferred asinh(d) ≈ asinh(t) + @test @constinferred acosh(one(d) + d) ≈ acosh(one(t) + t) + @test @constinferred atanh(d) ≈ atanh(t) + @test @constinferred acoth(one(t) + d) ≈ acoth(one(d) + t) + end +end From cb8d0672eee9d343eeea9878e4b75b2f19f0ff86 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 8 Jul 2026 13:42:55 +0200 Subject: [PATCH 11/13] convert braiding tensor properties testset to testsuite --- test/tensors/planar.jl | 40 +++++------------------------------- test/testsuite/tensors.jl | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/test/tensors/planar.jl b/test/tensors/planar.jl index 8e8a3e778..7a1eee8e2 100644 --- a/test/tensors/planar.jl +++ b/test/tensors/planar.jl @@ -13,46 +13,16 @@ for V in spacelist Istr = type_repr(I) BraidingStyle(I) isa NoBraiding && continue @timedtestset "Braiding tensor with symmetry: $Istr" verbose = true begin - W = V[1] ⊗ V[2] ← V[2] ⊗ V[1] + TensorKitTestSuite.test_tensors_braiding_tensor_properties(V) + + # adapt tests + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ← V2 ⊗ V1 t1 = @constinferred BraidingTensor(W) - @test space(t1) == W - @test codomain(t1) == codomain(W) - @test domain(t1) == domain(W) - @test scalartype(t1) == (isreal(sectortype(W)) ? Float64 : ComplexF64) - @test storagetype(t1) == Vector{scalartype(t1)} t2 = @constinferred BraidingTensor{ComplexF64}(W) - @test scalartype(t2) == ComplexF64 - @test storagetype(t2) == Vector{ComplexF64} t3 = @testinferred adapt(storagetype(t2), t1) @test storagetype(t3) == storagetype(t2) @test t3 == t2 - - W2 = reverse(codomain(W)) ← domain(W) - @test_throws SpaceMismatch BraidingTensor(W2) - - @test adjoint(t1) isa BraidingTensor - @test complex(t1) isa BraidingTensor - @test scalartype(complex(t1)) <: Complex - - t3 = @inferred TensorMap(t2) - t4 = braid(id(storagetype(t2), domain(t2)), ((2, 1), (3, 4)), (1, 2, 3, 4)) - @test t1 ≈ t4 - for (c, b) in blocks(t1) - @test block(t1, c) ≈ b ≈ block(t3, c) - end - for (f1, f2) in fusiontrees(t1) - @test t1[f1, f2] ≈ t3[f1, f2] - end - - t5 = @inferred TensorMap(t2') - t6 = braid(id(storagetype(t2), domain(t2')), ((2, 1), (3, 4)), (4, 3, 2, 1)) - @test t5 ≈ t6 - for (c, b) in blocks(t1') - @test block(t1', c) ≈ b ≈ block(t5, c) - end - for (f1, f2) in fusiontrees(t1') - @test t1'[f1, f2] ≈ t5[f1, f2] - end end end diff --git a/test/testsuite/tensors.jl b/test/testsuite/tensors.jl index fa0668ab7..318dc31f2 100644 --- a/test/testsuite/tensors.jl +++ b/test/testsuite/tensors.jl @@ -902,6 +902,49 @@ end @test t1 ≈ t2 end +@testsuite :tensors "braiding tensor properties" V -> begin + I = sectortype(first(V)) + BraidingStyle(I) isa HasBraiding || return nothing + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ← V2 ⊗ V1 + t1 = @constinferred BraidingTensor(W) + @test space(t1) == W + @test codomain(t1) == codomain(W) + @test domain(t1) == domain(W) + @test scalartype(t1) == (isreal(sectortype(W)) ? Float64 : ComplexF64) + @test storagetype(t1) == Vector{scalartype(t1)} + t2 = @constinferred BraidingTensor{ComplexF64}(W) + @test scalartype(t2) == ComplexF64 + @test storagetype(t2) == Vector{ComplexF64} + + W2 = reverse(codomain(W)) ← domain(W) + @test_throws SpaceMismatch BraidingTensor(W2) + + @test adjoint(t1) isa BraidingTensor + @test complex(t1) isa BraidingTensor + @test scalartype(complex(t1)) <: Complex + + t3 = @inferred TensorMap(t2) + t4 = braid(id(storagetype(t2), domain(t2)), ((2, 1), (3, 4)), (1, 2, 3, 4)) + @test t1 ≈ t4 + for (c, b) in blocks(t1) + @test block(t1, c) ≈ b ≈ block(t3, c) + end + for (f1, f2) in fusiontrees(t1) + @test t1[f1, f2] ≈ t3[f1, f2] + end + + t5 = @inferred TensorMap(t2') + t6 = braid(id(storagetype(t2), domain(t2')), ((2, 1), (3, 4)), (4, 3, 2, 1)) + @test t5 ≈ t6 + for (c, b) in blocks(t1') + @test block(t1', c) ≈ b ≈ block(t5, c) + end + for (f1, f2) in fusiontrees(t1') + @test t1'[f1, f2] ≈ t5[f1, f2] + end +end + # hom space #---------- @testsuite :tensors "hom space" V -> begin From df01132409055b8718917bb9b9f37e7d365ec386 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 8 Jul 2026 16:21:23 +0200 Subject: [PATCH 12/13] have single access function to run testsuites + changes related to moving file location --- test/symmetries/doubletree.jl | 12 +-- test/symmetries/singletree.jl | 14 ++-- test/symmetries/spaces.jl | 2 +- test/tensors/braidingtensor.jl | 16 ++-- test/tensors/construction.jl | 10 +-- test/tensors/contractions.jl | 18 ++--- test/tensors/diagonal.jl | 17 +++-- test/tensors/indexmanipulations.jl | 14 ++-- test/tensors/linalg.jl | 16 ++-- test/tensors/planar.jl | 2 +- test/testsuite/TensorKitTestSuite.jl | 109 ++++----------------------- test/testsuite/diagonal.jl | 2 +- test/testsuite/tensors.jl | 2 +- 13 files changed, 79 insertions(+), 155 deletions(-) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index 483d5b26e..6d165f419 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -6,11 +6,11 @@ import TensorKit as TK using TensorKitSectors @timedtestset "Double fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - TensorKitTestSuite.test_double_fusiontrees_bending(I) - TensorKitTestSuite.test_double_fusiontrees_folding(I) - TensorKitTestSuite.test_double_fusiontrees_repartitioning(I) - TensorKitTestSuite.test_double_fusiontrees_transposition(I) - TensorKitTestSuite.test_double_fusiontrees_permutation_and_braiding(I) - TensorKitTestSuite.test_double_fusiontrees_planar_trace(I) + TensorKitTestSuite.run_testsuite(:double_fusiontrees, "bending", I) + TensorKitTestSuite.run_testsuite(:double_fusiontrees, "folding", I) + TensorKitTestSuite.run_testsuite(:double_fusiontrees, "repartitioning", I) + TensorKitTestSuite.run_testsuite(:double_fusiontrees, "transposition", I) + TensorKitTestSuite.run_testsuite(:double_fusiontrees, "permutation and braiding", I) + TensorKitTestSuite.run_testsuite(:double_fusiontrees, "planar trace", I) TK.empty_globalcaches!() end diff --git a/test/symmetries/singletree.jl b/test/symmetries/singletree.jl index faa2cd5e9..099d9a970 100644 --- a/test/symmetries/singletree.jl +++ b/test/symmetries/singletree.jl @@ -6,12 +6,12 @@ import TensorKit as TK using TensorKitSectors @timedtestset "Single fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - TensorKitTestSuite.test_single_fusiontrees_iterate_and_printing(I) - TensorKitTestSuite.test_single_fusiontrees_constructor_properties(I) - TensorKitTestSuite.test_single_fusiontrees_split_and_join(I) - TensorKitTestSuite.test_single_fusiontrees_multi_fmove(I) - TensorKitTestSuite.test_single_fusiontrees_insertat(I) - TensorKitTestSuite.test_single_fusiontrees_merging(I) - TensorKitTestSuite.test_single_fusiontrees_elementary_planar_trace(I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "iterate and printing", I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "constructor properties", I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "split and join", I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "multi fmove", I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "insertat", I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "merging", I) + TensorKitTestSuite.run_testsuite(:single_fusiontrees, "elementary planar trace", I) TK.empty_globalcaches!() end diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 8774bd449..475b0381c 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -176,7 +176,7 @@ end end @timedtestset "ElementarySpace: $(type_repr(Vect[I]))" verbose = true for I in sectorlist - TensorKitTestSuite.test_spaces_graded_space(I) + TensorKitTestSuite.run_testsuite(:spaces, "graded space", I) end @timedtestset "ProductSpace{ℂ}" begin diff --git a/test/tensors/braidingtensor.jl b/test/tensors/braidingtensor.jl index d11532b7c..274892159 100644 --- a/test/tensors/braidingtensor.jl +++ b/test/tensors/braidingtensor.jl @@ -12,14 +12,14 @@ for V in spacelist println("BraidingTensor planar contractions with symmetry: $Istr") println("---------------------------------------") @timedtestset "BraidingTensor planar contractions with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_braiding_tensor_planaradd(V) - TensorKitTestSuite.test_tensors_braiding_tensor_left_full_contraction(V) - TensorKitTestSuite.test_tensors_braiding_tensor_left_partial_contraction(V) - TensorKitTestSuite.test_tensors_braiding_tensor_right_full_contraction(V) - TensorKitTestSuite.test_tensors_braiding_tensor_full_contraction_output(V) - TensorKitTestSuite.test_tensors_braiding_tensor_open_codomain_leg(V) - TensorKitTestSuite.test_tensors_braiding_tensor_open_domain_leg(V) - TensorKitTestSuite.test_tensors_contraction_between_braiding_tensors(V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor planaradd!", V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor left full contraction", V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor left partial contraction", V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor right full contraction", V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor full contraction output", V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor open codomain leg", V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor open domain leg", V) + TensorKitTestSuite.run_testsuite(:tensors, "contraction between braiding tensors", V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/construction.jl b/test/tensors/construction.jl index a34b492ef..09e50d3bb 100644 --- a/test/tensors/construction.jl +++ b/test/tensors/construction.jl @@ -12,11 +12,11 @@ for V in spacelist println("Tensor constructions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor constructions with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_basic_properties(V) - TensorKitTestSuite.test_tensors_dict_conversion(V) - TensorKitTestSuite.test_tensors_array_conversion(V) - TensorKitTestSuite.test_tensors_real_and_imaginary_parts(V) - TensorKitTestSuite.test_tensors_tensor_conversion(V) + TensorKitTestSuite.run_testsuite(:tensors, "basic properties", V) + TensorKitTestSuite.run_testsuite(:tensors, "dict conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "array conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "real and imaginary parts", V) + TensorKitTestSuite.run_testsuite(:tensors, "tensor conversion", V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/contractions.jl b/test/tensors/contractions.jl index 627112bed..11bbbe2ea 100644 --- a/test/tensors/contractions.jl +++ b/test/tensors/contractions.jl @@ -12,15 +12,15 @@ for V in spacelist println("Tensor contractions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor contractions with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_full_trace(V) - TensorKitTestSuite.test_tensors_partial_trace(V) - TensorKitTestSuite.test_tensors_trace_via_conversion(V) - TensorKitTestSuite.test_tensors_trace_and_contraction(V) - TensorKitTestSuite.test_tensors_contraction_via_conversion(V) - TensorKitTestSuite.test_tensors_tensor_product_norm_preservation(V) - TensorKitTestSuite.test_tensors_tensor_product_via_conversion(V) - TensorKitTestSuite.test_tensors_tensor_product_via_contraction(V) - TensorKitTestSuite.test_tensors_absorption(V) + TensorKitTestSuite.run_testsuite(:tensors, "full trace", V) + TensorKitTestSuite.run_testsuite(:tensors, "partial trace", V) + TensorKitTestSuite.run_testsuite(:tensors, "trace via conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "trace and contraction", V) + TensorKitTestSuite.run_testsuite(:tensors, "contraction via conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "tensor product norm preservation", V) + TensorKitTestSuite.run_testsuite(:tensors, "tensor product via conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "tensor product via contraction", V) + TensorKitTestSuite.run_testsuite(:tensors, "absorption", V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/diagonal.jl b/test/tensors/diagonal.jl index 818808612..8c4855e79 100644 --- a/test/tensors/diagonal.jl +++ b/test/tensors/diagonal.jl @@ -20,14 +20,15 @@ for V in diagspacelist println("DiagonalTensor with domain $V") println("---------------------------------------") @timedtestset "DiagonalTensor with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_diagonal_tensors_basic_properties_and_algebra(V) - TensorKitTestSuite.test_diagonal_tensors_linear_algebra_conversion(V) - TensorKitTestSuite.test_diagonal_tensors_real_and_imaginary_parts(V) - TensorKitTestSuite.test_diagonal_tensors_tensor_conversion(V) - TensorKitTestSuite.test_diagonal_tensors_permutations(V) - TensorKitTestSuite.test_diagonal_tensors_trace_multiplication_and_inverse(V) - TensorKitTestSuite.test_diagonal_tensors_contraction(V) - TensorKitTestSuite.test_diagonal_tensors_tensor_functions(V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "basic properties and algebra", V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "linear algebra conversion", V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "real and imaginary parts", V) + + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "tensor conversion", V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "permutations", V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "trace, multiplication and inverse", V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "contraction", V) + TensorKitTestSuite.run_testsuite(:diagonal_tensors, "tensor functions", V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index 0693ab4f9..834904e50 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -12,13 +12,13 @@ for V in spacelist println("Tensor index manipulations with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor index manipulations with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_trivial_space_insertion_and_removal(V) - TensorKitTestSuite.test_tensors_permutations_via_inner_product_invariance(V) - TensorKitTestSuite.test_tensors_permutations_via_conversion(V) - TensorKitTestSuite.test_tensors_index_flipping_inverse(V) - TensorKitTestSuite.test_tensors_index_flipping_explicit(V) - TensorKitTestSuite.test_tensors_index_flipping_via_contraction(V) - TensorKitTestSuite.test_tensors_braid_adjoint_identity(V) + TensorKitTestSuite.run_testsuite(:tensors, "trivial space insertion and removal", V) + TensorKitTestSuite.run_testsuite(:tensors, "permutations via inner product invariance", V) + TensorKitTestSuite.run_testsuite(:tensors, "permutations via conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "index flipping inverse", V) + TensorKitTestSuite.run_testsuite(:tensors, "index flipping explicit", V) + TensorKitTestSuite.run_testsuite(:tensors, "index flipping via contraction", V) + TensorKitTestSuite.run_testsuite(:tensors, "braid adjoint identity", V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/linalg.jl b/test/tensors/linalg.jl index 5fdce8627..1d494fe5b 100644 --- a/test/tensors/linalg.jl +++ b/test/tensors/linalg.jl @@ -12,14 +12,14 @@ for V in spacelist println("Tensor linear algebra with symmetry: $Istr") println("---------------------------------------") @timedtestset "Tensor linear algebra with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_basic_linear_algebra(V) - TensorKitTestSuite.test_tensors_linear_algebra_conversion(V) - TensorKitTestSuite.test_tensors_multiplication_of_isometries(V) - TensorKitTestSuite.test_tensors_multiplication_and_inverse_compatibility(V) - TensorKitTestSuite.test_tensors_multiplication_and_inverse_conversion(V) - TensorKitTestSuite.test_tensors_diag_and_diagm(V) - TensorKitTestSuite.test_tensors_tensor_functions(V) - TensorKitTestSuite.test_tensors_sylvester_equation(V) + TensorKitTestSuite.run_testsuite(:tensors, "basic linear algebra", V) + TensorKitTestSuite.run_testsuite(:tensors, "linear algebra conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "multiplication of isometries", V) + TensorKitTestSuite.run_testsuite(:tensors, "multiplication and inverse compatibility", V) + TensorKitTestSuite.run_testsuite(:tensors, "multiplication and inverse conversion", V) + TensorKitTestSuite.run_testsuite(:tensors, "diag and diagm", V) + TensorKitTestSuite.run_testsuite(:tensors, "tensor functions", V) + TensorKitTestSuite.run_testsuite(:tensors, "sylvester equation", V) end TensorKit.empty_globalcaches!() end diff --git a/test/tensors/planar.jl b/test/tensors/planar.jl index 7a1eee8e2..aa8e6e576 100644 --- a/test/tensors/planar.jl +++ b/test/tensors/planar.jl @@ -13,7 +13,7 @@ for V in spacelist Istr = type_repr(I) BraidingStyle(I) isa NoBraiding && continue @timedtestset "Braiding tensor with symmetry: $Istr" verbose = true begin - TensorKitTestSuite.test_tensors_braiding_tensor_properties(V) + TensorKitTestSuite.run_testsuite(:tensors, "braiding tensor properties", V) # adapt tests V1, V2, V3, V4, V5 = V diff --git a/test/testsuite/TensorKitTestSuite.jl b/test/testsuite/TensorKitTestSuite.jl index 7df711b05..b7ca378f2 100644 --- a/test/testsuite/TensorKitTestSuite.jl +++ b/test/testsuite/TensorKitTestSuite.jl @@ -10,19 +10,20 @@ Downstream packages may include this test suite as follows: import TensorKit testsuite_path = joinpath( dirname(dirname(pathof(TensorKit))), # TensorKit root - "test", "TensorKitTestSuite.jl" + "test", "testsuite", "TensorKitTestSuite.jl" ) include(testsuite_path) using .TensorKitTestSuite -TensorKitTestSuite.test_single_fusiontrees(MySector) -TensorKitTestSuite.test_double_fusiontrees(MySector) -TensorKitTestSuite.test_spaces(MySector) -TensorKitTestSuite.test_tensors((V1, V2, V3, V4, V5)) # 5 mutually compatible spaces -TensorKitTestSuite.test_diagonal_tensors(V) # 1 space for diagonal tensors +TensorKitTestSuite.run_testsuite(:single_fusiontrees, "test", MySector) +TensorKitTestSuite.run_testsuite(:double_fusiontrees, "test", MySector) +TensorKitTestSuite.run_testsuite(:spaces, "test", MySector) +TensorKitTestSuite.run_testsuite(:tensors, "test", (V1, V2, V3, V4, V5)) # 5 mutually compatible spaces +TensorKitTestSuite.run_testsuite(:diagonal_tensors, "test", V) # 1 space for diagonal tensors ``` -The entry points above are independent and may be run selectively. +The entry points are denoted by the `Symbol`s above, with "test" being the name of the test suite. +These are independent and may be run selectively. See [`@testsuite`](@ref) for more information. This module additionally exports: * [`force_planar`](@ref) * [`eval_show`](@ref) @@ -32,7 +33,7 @@ but deliberately *not* re-exported here. """ module TensorKitTestSuite -export test_single_fusiontrees, test_double_fusiontrees, test_spaces, test_tensors, test_diagonal_tensors +export run_testsuite export force_planar, eval_show using Test @@ -87,103 +88,25 @@ The body is executed with a single argument: the concrete `Sector` type under te (for `:single_fusiontrees`, `:double_fusiontrees` and `:spaces`), a space (for `:diagonal_tensors`), or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). -Important: Whatever is passed as `name` becomes part of the generated function that must be called to run that body. -In particular, a `safe_name` is made where `name`'s spaces are replaced by underscores, and everything becomes lowercase. -One then calls `test__`. This way, individual entries can be invoked without running the whole test group. +Run a registered entry via `run_testsuite(testgroup, name, arg)`. """ macro testsuite(testgroup, name, ex) Meta.isexpr(ex, :(->)) || error("@testsuite requires an `arg -> body` expression") testgroupsym = testgroup isa QuoteNode ? testgroup.value : testgroup - safe_name = lowercase(replace(name, r"[^A-Za-z0-9]+" => "_")) - fn = Symbol("test_", testgroupsym, "_", safe_name) group = QuoteNode(testgroupsym) return quote @assert !haskey(testgroups[$group], $name) "duplicate testsuite name: $($name) ($($group))" testgroups[$group][$name] = $(QuoteNode(ex)) - $(esc(fn))(arg) = _run_testsuite_entry(testgroups[$group][$name], arg) nothing end end """ - test_single_fusiontrees(I::Type{<:Sector}) + run_testsuite(group::Symbol, name::String, arg) -Runs the single fusion-tree manipulation test suite on sector type `I`. +Run a single registered testsuite entry by its `group` and `name`. """ -function test_single_fusiontrees(I::Type{<:Sector}) - return @testset "$(type_repr(I))" begin - for (name, lambda) in testgroups[:single_fusiontrees] - @testset "$name" begin - _run_testsuite_entry(lambda, I) - end - end - end -end - -""" - test_double_fusiontrees(I::Type{<:Sector}) - -Runs the double fusion-tree manipulation test suite on sector type `I`. -""" -function test_double_fusiontrees(I::Type{<:Sector}) - return @testset "$(type_repr(I))" begin - for (name, lambda) in testgroups[:double_fusiontrees] - @testset "$name" begin - _run_testsuite_entry(lambda, I) - end - end - end -end - -""" - test_spaces(I::Type{<:Sector}) - -Runs the `GradedSpace` test suite on sector type `I`. -""" -function test_spaces(I::Type{<:Sector}) #TODO: change since it's just 1 testsuite, or change to get the hom-space tests in here - return @testset "$(type_repr(I))" begin - for (name, lambda) in testgroups[:spaces] - @testset "$name" begin - _run_testsuite_entry(lambda, I) - end - end - end -end - -""" - test_tensors(V::NTuple{5, ElementarySpace}) - -Runs the tensor operation test suite (construction, contractions, linear algebra, -index manipulations, braiding, `HomSpace`) on `V`, a 5-tuple of mutually -compatible `ElementarySpace`s. See `setup.jl` for space design considerations. -""" -function test_tensors(V::NTuple{5, ElementarySpace}) - I = check_spacetype(V) - return @testset "$(type_repr(I))" begin - for (name, lambda) in testgroups[:tensors] - @testset "$name" begin - _run_testsuite_entry(lambda, V) - end - end - end -end - -""" - test_diagonal_tensors(V::ElementarySpace) - -Runs the diagonal tensor operation test suite (construction, contractions, linear algebra, -index manipulations) on an `ElementarySpace` `V`. See `setup.jl` for space design considerations, -but now applied to just one space, since diagonal tensors are endomorphisms. -""" -function test_diagonal_tensors(V::ElementarySpace) - return @testset "$(type_repr(sectortype(V)))" begin - for (name, lambda) in testgroups[:diagonal_tensors] - @testset "$name" begin - _run_testsuite_entry(lambda, V) - end - end - end -end +run_testsuite(group::Symbol, name::String, arg) = _run_testsuite_entry(testgroups[group][name], arg) # Sector utilities # ---------------- @@ -248,8 +171,8 @@ function eval_show(x) return eval(ex) end -include("testsuite/fusiontrees.jl") -include("testsuite/spaces.jl") -include("testsuite/tensors.jl") +include("fusiontrees.jl") +include("spaces.jl") +include("tensors.jl") end # module TensorKitTestSuite diff --git a/test/testsuite/diagonal.jl b/test/testsuite/diagonal.jl index 700fe900a..a7423fbf3 100644 --- a/test/testsuite/diagonal.jl +++ b/test/testsuite/diagonal.jl @@ -138,7 +138,7 @@ end @test convert(TensorMap, t5) == permute(t_tm, (((), (2, 1)))) end -@testsuite :diagonal_tensors "trace multiplication and inverse" V -> begin +@testsuite :diagonal_tensors "trace, multiplication and inverse" V -> begin t1 = DiagonalTensorMap(rand(Float64, reduceddim(V)), V) t2 = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) @test tr(TensorMap(t1)) == @constinferred tr(t1) diff --git a/test/testsuite/tensors.jl b/test/testsuite/tensors.jl index 318dc31f2..76f762fd1 100644 --- a/test/testsuite/tensors.jl +++ b/test/testsuite/tensors.jl @@ -668,7 +668,7 @@ function _braiding_tensor_setup(V::NTuple{5, GradedSpace{I, NTuple{N, Int}}}) wh return hasbraiding, Vspace, t end -@testsuite :tensors "braiding tensor planaradd" V -> begin +@testsuite :tensors "braiding tensor planaradd!" V -> begin hasbraiding, Vspace, _ = _braiding_tensor_setup(V) hasbraiding || return nothing b = BraidingTensor(Vspace, Vspace') From a7e4b4aaf7a38bd499875ad72dd9ac1696d5b64a Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 8 Jul 2026 16:54:49 +0200 Subject: [PATCH 13/13] add factorizations to the testsuite --- test/factorizations/eig.jl | 89 +--- test/factorizations/ortho.jl | 188 +------- test/factorizations/projections.jl | 85 +--- test/factorizations/svd.jl | 284 +----------- test/runtests.jl | 1 + test/setup.jl | 2 +- test/testsuite/TensorKitTestSuite.jl | 8 +- test/testsuite/factorizations.jl | 654 +++++++++++++++++++++++++++ 8 files changed, 672 insertions(+), 639 deletions(-) create mode 100644 test/testsuite/factorizations.jl diff --git a/test/factorizations/eig.jl b/test/factorizations/eig.jl index 55ff622cd..267c7be5c 100644 --- a/test/factorizations/eig.jl +++ b/test/factorizations/eig.jl @@ -3,11 +3,8 @@ using TensorKit using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: DefaultAlgorithm, diagview - spacelist = factorization_spacelist(fast_tests) -eltypes = (Float32, ComplexF64) - for V in spacelist I = sectortype(first(V)) Istr = TensorKit.type_repr(I) @@ -15,90 +12,6 @@ for V in spacelist println("Eigenvalue decompositions with symmetry: $Istr") println("---------------------------------------") @timedtestset "Eigenvalue decompositions with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - W = V1 ⊗ V2 ⊗ V3 - Vd = fuse(V1 ⊗ V2) - - @testset "Eigenvalue decomposition" begin - for T in eltypes, - t in ( - rand(T, V1, V1), rand(T, W, W), rand(T, W, W)', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - d, v = @constinferred eig_full(t) - @test t * v ≈ v * d - - d, v = @constinferred eig_full(t, DefaultAlgorithm()) - @test t * v ≈ v * d - - d′ = @constinferred eig_vals(t) - @test d′ ≈ diagview(d) - @test d′ isa TensorKit.SectorVector - - d′ = @constinferred eig_vals(t, DefaultAlgorithm()) - @test d′ ≈ diagview(d) - @test d′ isa TensorKit.SectorVector - - d2 = @constinferred DiagonalTensorMap(d′) - @test d2 ≈ d - - vdv = project_hermitian!(v' * v) - @test @constinferred isposdef(vdv) - t isa DiagonalTensorMap || @test !isposdef(t) # unlikely for non-hermitian map - - nvals = round(Int, dim(domain(t)) / 2) - d, v = @constinferred eig_trunc(t; trunc = truncrank(nvals)) - @test t * v ≈ v * d - @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - - d, v = @constinferred eig_trunc(t, DefaultAlgorithm(; trunc = truncrank(nvals))) - @test t * v ≈ v * d - @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - - t2 = @constinferred project_hermitian(t) - D, V = eigen(t2) - @test isisometric(V) - D̃, Ṽ = @constinferred eigh_full(t2) - @test D ≈ D̃ - @test V ≈ Ṽ - - D̃, Ṽ = @constinferred eigh_full(t2, DefaultAlgorithm()) - @test D ≈ D̃ - @test V ≈ Ṽ - - λ = minimum(real, diagview(D)) - @test cond(Ṽ) ≈ one(real(T)) - @test isposdef(t2) == isposdef(λ) - @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) - @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) - - d, v = @constinferred eigh_full(t2) - @test t2 * v ≈ v * d - @test isunitary(v) - - d′ = @constinferred eigh_vals(t2) - @test d′ ≈ diagview(d) - @test d′ isa TensorKit.SectorVector - - d′ = @constinferred eigh_vals(t2, DefaultAlgorithm()) - @test d′ ≈ diagview(d) - @test d′ isa TensorKit.SectorVector - - λ = minimum(real, diagview(d)) - @test cond(v) ≈ one(real(T)) - @test isposdef(t2) == isposdef(λ) - @test isposdef(t2 - λ * one(t) + 0.1 * one(t2)) - @test !isposdef(t2 - λ * one(t) - 0.1 * one(t2)) - - d, v = @constinferred eigh_trunc(t2; trunc = truncrank(nvals)) - @test t2 * v ≈ v * d - @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - - d, v = @constinferred eigh_trunc(t2, DefaultAlgorithm(; trunc = truncrank(nvals))) - @test t2 * v ≈ v * d - @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - end - end + TensorKitTestSuite.run_testsuite(:factorizations, "eigenvalue decomposition", V) end end diff --git a/test/factorizations/ortho.jl b/test/factorizations/ortho.jl index 170d596a2..28121025d 100644 --- a/test/factorizations/ortho.jl +++ b/test/factorizations/ortho.jl @@ -3,11 +3,8 @@ using TensorKit using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: DefaultAlgorithm, diagview - spacelist = factorization_spacelist(fast_tests) -eltypes = (Float32, ComplexF64) - for V in spacelist I = sectortype(first(V)) Istr = TensorKit.type_repr(I) @@ -15,188 +12,7 @@ for V in spacelist println("QR and LQ decompositions with symmetry: $Istr") println("---------------------------------------") @timedtestset "QR and LQ decompositions with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - W = V1 ⊗ V2 ⊗ V3 - Vd = fuse(V1 ⊗ V2) - - @testset "QR decomposition" begin - for T in eltypes, - t in ( - rand(T, W, W), rand(T, W, W)', - rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)')', - rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5)), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - Q, R = @constinferred qr_full(t) - @test Q * R ≈ t - @test isunitary(Q) - - Q, R = @constinferred qr_full(t, DefaultAlgorithm()) - @test Q * R ≈ t - @test isunitary(Q) - - Q, R = @constinferred qr_compact(t) - @test Q * R ≈ t - @test isisometric(Q) - - Q, R = @constinferred qr_compact(t, DefaultAlgorithm()) - @test Q * R ≈ t - @test isisometric(Q) - - Q, R = @constinferred left_orth(t) - @test Q * R ≈ t - @test isisometric(Q) - - Q, R = @constinferred left_orth(t, DefaultAlgorithm()) - @test Q * R ≈ t - @test isisometric(Q) - - N = @constinferred qr_null(t) - @test isisometric(N) - @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) - - N = @constinferred qr_null(t, DefaultAlgorithm()) - @test isisometric(N) - @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) - - N = @constinferred left_null(t) - @test isisometric(N) - @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) - - N = @constinferred left_null(t, DefaultAlgorithm()) - @test isisometric(N) - @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) - end - - # empty tensor - for T in eltypes - t = rand(T, V1 ⊗ V2, zerospace(V1)) - - Q, R = @constinferred qr_full(t) - @test Q * R ≈ t - @test isunitary(Q) - @test dim(R) == dim(t) == 0 - - Q, R = @constinferred qr_full(t, DefaultAlgorithm()) - @test Q * R ≈ t - @test isunitary(Q) - @test dim(R) == dim(t) == 0 - - Q, R = @constinferred qr_compact(t) - @test Q * R ≈ t - @test isisometric(Q) - @test dim(Q) == dim(R) == dim(t) - - Q, R = @constinferred qr_compact(t, DefaultAlgorithm()) - @test Q * R ≈ t - @test isisometric(Q) - @test dim(Q) == dim(R) == dim(t) - - Q, R = @constinferred left_orth(t) - @test Q * R ≈ t - @test isisometric(Q) - @test dim(Q) == dim(R) == dim(t) - - Q, R = @constinferred left_orth(t, DefaultAlgorithm()) - @test Q * R ≈ t - @test isisometric(Q) - @test dim(Q) == dim(R) == dim(t) - - N = @constinferred qr_null(t) - @test isunitary(N) - @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) - - N = @constinferred qr_null(t, DefaultAlgorithm()) - @test isunitary(N) - @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) - end - end - - @testset "LQ decomposition" begin - for T in eltypes, - t in ( - rand(T, W, W), rand(T, W, W)', - rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)')', - rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5)), rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - L, Q = @constinferred lq_full(t) - @test L * Q ≈ t - @test isunitary(Q) - - L, Q = @constinferred lq_full(t, DefaultAlgorithm()) - @test L * Q ≈ t - @test isunitary(Q) - - L, Q = @constinferred lq_compact(t) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - - L, Q = @constinferred lq_compact(t, DefaultAlgorithm()) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - - L, Q = @constinferred right_orth(t) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - - L, Q = @constinferred right_orth(t, DefaultAlgorithm()) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - - Nᴴ = @constinferred lq_null(t) - @test isisometric(Nᴴ; side = :right) - @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) - - Nᴴ = @constinferred lq_null(t, DefaultAlgorithm()) - @test isisometric(Nᴴ; side = :right) - @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) - end - - for T in eltypes - # empty tensor - t = rand(T, zerospace(V1), V1 ⊗ V2) - - L, Q = @constinferred lq_full(t) - @test L * Q ≈ t - @test isunitary(Q) - @test dim(L) == dim(t) == 0 - - L, Q = @constinferred lq_full(t, DefaultAlgorithm()) - @test L * Q ≈ t - @test isunitary(Q) - @test dim(L) == dim(t) == 0 - - L, Q = @constinferred lq_compact(t) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - @test dim(Q) == dim(L) == dim(t) - - L, Q = @constinferred lq_compact(t, DefaultAlgorithm()) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - @test dim(Q) == dim(L) == dim(t) - - L, Q = @constinferred right_orth(t) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - @test dim(Q) == dim(L) == dim(t) - - L, Q = @constinferred right_orth(t, DefaultAlgorithm()) - @test L * Q ≈ t - @test isisometric(Q; side = :right) - @test dim(Q) == dim(L) == dim(t) - - Nᴴ = @constinferred lq_null(t) - @test isunitary(Nᴴ) - @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) - - Nᴴ = @constinferred lq_null(t, DefaultAlgorithm()) - @test isunitary(Nᴴ) - @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) - end - end + TensorKitTestSuite.run_testsuite(:factorizations, "QR decomposition", V) + TensorKitTestSuite.run_testsuite(:factorizations, "LQ decomposition", V) end end diff --git a/test/factorizations/projections.jl b/test/factorizations/projections.jl index fd32a804b..629b286e2 100644 --- a/test/factorizations/projections.jl +++ b/test/factorizations/projections.jl @@ -3,11 +3,8 @@ using TensorKit using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: DefaultAlgorithm, diagview - spacelist = factorization_spacelist(fast_tests) -eltypes = (Float32, ComplexF64) - for V in spacelist I = sectortype(first(V)) Istr = TensorKit.type_repr(I) @@ -15,85 +12,7 @@ for V in spacelist println("Projections with symmetry: $Istr") println("---------------------------------------") @timedtestset "Projections with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - W = V1 ⊗ V2 ⊗ V3 - Vd = fuse(V1 ⊗ V2) - - @testset "Hermitian projections" begin - for T in eltypes, - t in ( - rand(T, V1, V1), rand(T, W, W), rand(T, W, W)', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - normalize!(t) - noisefactor = eps(real(T))^(3 / 4) - - th = (t + t') / 2 - ta = (t - t') / 2 - tc = copy(t) - - th′ = @constinferred project_hermitian(t) - @test ishermitian(th′) - @test th′ ≈ th - - th′ = @constinferred project_hermitian(t, DefaultAlgorithm()) - @test ishermitian(th′) - @test th′ ≈ th - - @test t == tc - th_approx = th + noisefactor * ta - @test !ishermitian(th_approx) || (T <: Real && t isa DiagonalTensorMap) - @test ishermitian(th_approx; atol = 10 * noisefactor) - - ta′ = project_antihermitian(t) - @test isantihermitian(ta′) - @test ta′ ≈ ta - - ta′ = @constinferred project_antihermitian(t, DefaultAlgorithm()) - @test isantihermitian(ta′) - @test ta′ ≈ ta - - @test t == tc - ta_approx = ta + noisefactor * th - @test !isantihermitian(ta_approx) - @test isantihermitian(ta_approx; atol = 10 * noisefactor) || (T <: Real && t isa DiagonalTensorMap) - end - - @test_throws SpaceMismatch project_hermitian(rand(V1, V1^2)) - @test_throws SpaceMismatch project_antihermitian(rand(V1, V1^2)) - if V1 != V1' - @test_throws SpaceMismatch project_hermitian(rand(V1, V1')) - @test_throws SpaceMismatch project_antihermitian(rand(V1, V1')) - end - end - - @testset "Isometric projections" begin - for T in eltypes, - t in ( - rand(T, W, W), rand(T, W, W)', rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', - ) - t2 = project_isometric(t) - @test isisometric(t2) - t2′ = @constinferred project_isometric(t, DefaultAlgorithm()) - @test isisometric(t2′) - @test t2′ * ((t2′)' * t) ≈ t - - t3 = project_isometric(t2) - @test t3 ≈ t2 # stability of the projection - @test t2 * (t2' * t) ≈ t - - tc = similar(t) - t3 = @constinferred project_isometric!(copy!(tc, t), t2) - @test t3 === t2 - @test isisometric(t2) - - # test that t2 is closer to A then any other isometry - for k in 1:10 - δt = randn!(similar(t)) - t3 = project_isometric(t + δt / 100) - @test norm(t - t3) > norm(t - t2) - end - end - end + TensorKitTestSuite.run_testsuite(:factorizations, "hermitian projections", V) + TensorKitTestSuite.run_testsuite(:factorizations, "isometric projections", V) end end diff --git a/test/factorizations/svd.jl b/test/factorizations/svd.jl index e2eb19076..a000cb121 100644 --- a/test/factorizations/svd.jl +++ b/test/factorizations/svd.jl @@ -5,8 +5,6 @@ using MatrixAlgebraKit: DefaultAlgorithm, defaulttol, diagview spacelist = factorization_spacelist(fast_tests) -eltypes = (Float32, ComplexF64) - for V in spacelist I = sectortype(first(V)) Istr = TensorKit.type_repr(I) @@ -14,283 +12,9 @@ for V in spacelist println("Singular value and polar decompositions with symmetry: $Istr") println("---------------------------------------------------------------") @timedtestset "Singular value and polar decompositions with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - W = V1 ⊗ V2 ⊗ V3 - Vd = fuse(V1 ⊗ V2) - - @testset "Condition number and rank" begin - for T in eltypes, - t in ( - randn(T, W, W), randn(T, W, W)', - randn(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), randn(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', - randn(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), randn(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - d1, d2 = dim(codomain(t)), dim(domain(t)) - r = rank(t) - @test r ≈ min(d1, d2) - @test typeof(r) == typeof(d1) - M = left_null(t) - @test @constinferred(rank(M)) + r ≈ d1 - Mᴴ = right_null(t) - @test rank(Mᴴ) + r ≈ d2 - end - for T in eltypes - u = unitary(T, V1 ⊗ V2, V1 ⊗ V2) - @test @constinferred(cond(u)) ≈ one(real(T)) - @test @constinferred(rank(u)) ≈ dim(V1 ⊗ V2) - - t = rand(T, zerospace(V1), W) - @test rank(t) == 0 - t2 = rand(T, zerospace(V1) * zerospace(V2), zerospace(V1) * zerospace(V2)) - @test rank(t2) == 0 - @test cond(t2) == 0.0 - end - for T in eltypes, t in (randn(T, W, W), randn(T, W, W)') - project_hermitian!(t) - vals = @constinferred eigh_vals(t) - λmax = maximum(abs, vals) - λmin = minimum(abs, vals) - @test cond(t) ≈ λmax / λmin - end - end - - @testset "Polar decomposition" begin - for T in eltypes, - t in ( - rand(T, W, W), - rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), - rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - @assert domain(t) ≾ codomain(t) - w, p = @constinferred left_polar(t) - @test w * p ≈ t - @test isisometric(w) - @test isposdef(p) - - w′, p′ = @constinferred left_polar(t, DefaultAlgorithm()) - @test w ≈ w′ - @test p ≈ p′ - - w, p = @constinferred left_orth(t; alg = :polar) - @test w * p ≈ t - @test isisometric(w) - end - - for T in eltypes, - t in ( - rand(T, W, W), - rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), - rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - @assert codomain(t) ≾ domain(t) - p, wᴴ = @constinferred right_polar(t) - @test p * wᴴ ≈ t - @test isisometric(wᴴ; side = :right) - @test isposdef(p) - - p′, wᴴ′ = @constinferred right_polar(t, DefaultAlgorithm()) - @test p′ ≈ p - @test wᴴ′ ≈ wᴴ - - p, wᴴ = @constinferred right_orth(t; alg = :polar) - @test p * wᴴ ≈ t - @test isisometric(wᴴ; side = :right) - end - end - - @testset "SVD" begin - for T in eltypes, - t in ( - rand(T, W, W), rand(T, W, W)', - rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', - rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - u, s, vᴴ = @constinferred svd_full(t) - @test u * s * vᴴ ≈ t - @test isunitary(u) - @test isunitary(vᴴ) - - u′, s′, vᴴ′ = @constinferred svd_full(t, DefaultAlgorithm()) - @test u ≈ u′ - @test s ≈ s′ - @test vᴴ ≈ vᴴ′ - - u, s, vᴴ = @constinferred svd_compact(t) - @test u * s * vᴴ ≈ t - @test isisometric(u) - @test isposdef(s) - @test isisometric(vᴴ; side = :right) - - u′, s′, vᴴ′ = @constinferred svd_compact(t, DefaultAlgorithm()) - @test u ≈ u′ - @test s ≈ s′ - @test vᴴ ≈ vᴴ′ - - s′ = @constinferred svd_vals(t) - @test s′ ≈ diagview(s) - @test s′ isa TensorKit.SectorVector - - s′ = @constinferred svd_vals(t, DefaultAlgorithm()) - @test s′ ≈ diagview(s) - @test s′ isa TensorKit.SectorVector - - s2 = @constinferred DiagonalTensorMap(s′) - @test s2 ≈ s - - v, c = @constinferred left_orth(t; alg = :svd) - @test v * c ≈ t - @test isisometric(v) - - c, vᴴ = @constinferred right_orth(t; alg = :svd) - @test c * vᴴ ≈ t - @test isisometric(vᴴ; side = :right) - - atol = norm(t) * defaulttol(T) # tol used by `:svd` left_null/right_null - - N = @constinferred left_null(t; alg = :svd) - @test isisometric(N) - @test norm(N' * t) ≈ 0 atol = atol - - N = @constinferred left_null(t; trunc = (; atol = 6 * atol)) - @test isisometric(N) - @test norm(N' * t) ≈ 0 atol = 10 * atol - - Nᴴ = @constinferred right_null(t; alg = :svd) - @test isisometric(Nᴴ; side = :right) - @test norm(t * Nᴴ') ≈ 0 atol = atol - - Nᴴ = @constinferred right_null(t; trunc = (; atol = 6 * atol)) - @test isisometric(Nᴴ; side = :right) - @test norm(t * Nᴴ') ≈ 0 atol = 10 * atol - end - - # empty tensor - for T in eltypes, t in (rand(T, W, zerospace(V1)), rand(T, zerospace(V1), W)) - U, S, Vᴴ = @constinferred svd_full(t) - @test U * S * Vᴴ ≈ t - @test isunitary(U) - @test isunitary(Vᴴ) - - U, S, Vᴴ = @constinferred svd_full(t, DefaultAlgorithm()) - @test U * S * Vᴴ ≈ t - @test isunitary(U) - @test isunitary(Vᴴ) - - U, S, Vᴴ = @constinferred svd_compact(t) - @test U * S * Vᴴ ≈ t - @test dim(U) == dim(S) == dim(Vᴴ) == dim(t) == 0 - - U, S, Vᴴ = @constinferred svd_compact(t, DefaultAlgorithm()) - @test U * S * Vᴴ ≈ t - @test dim(U) == dim(S) == dim(Vᴴ) == dim(t) == 0 - end - end - - @testset "truncated SVD" begin - for T in eltypes, - t in ( - rand(T, W, W), rand(T, W, W)', - rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', - rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', - DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), - ) - - @constinferred normalize!(t) - - U, S, Vᴴ, ϵ = @constinferred svd_trunc(t; trunc = notrunc()) - @test U * S * Vᴴ ≈ t - @test ϵ ≈ 0 - @test isisometric(U) - @test isisometric(Vᴴ; side = :right) - - U, S, Vᴴ, ϵ = @constinferred svd_trunc(t, DefaultAlgorithm(; trunc = notrunc())) - @test U * S * Vᴴ ≈ t - @test ϵ ≈ 0 - @test isisometric(U) - @test isisometric(Vᴴ; side = :right) - - # when rank of t is already smaller than truncrank - t_rank = ceil(Int, min(dim(codomain(t)), dim(domain(t)))) - U, S, Vᴴ, ϵ = @constinferred svd_trunc(t; trunc = truncrank(t_rank + 1)) - @test U * S * Vᴴ ≈ t - @test ϵ ≈ 0 - @test isisometric(U) - @test isisometric(Vᴴ; side = :right) - - U, S, Vᴴ, ϵ = @constinferred svd_trunc(t, DefaultAlgorithm(; trunc = truncrank(t_rank + 1))) - @test U * S * Vᴴ ≈ t - @test ϵ ≈ 0 - @test isisometric(U) - @test isisometric(Vᴴ; side = :right) - - # dimension of S is a float for IsingBimodule - nvals = round(Int, dim(domain(S)) / 2) - trunc = truncrank(nvals) - U1, S1, Vᴴ1, ϵ1 = @constinferred svd_trunc(t; trunc) - @test t * Vᴴ1' ≈ U1 * S1 - @test isisometric(U1) - @test isisometric(Vᴴ1; side = :right) - @test norm(t - U1 * S1 * Vᴴ1) ≈ ϵ1 atol = eps(real(T))^(4 / 5) - @test abs(dim(domain(S1)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - - λ = minimum(diagview(S1)) - trunc = trunctol(; atol = λ - 10eps(λ)) - U2, S2, Vᴴ2, ϵ2 = @constinferred svd_trunc(t; trunc) - @test t * Vᴴ2' ≈ U2 * S2 - @test isisometric(U2) - @test isisometric(Vᴴ2; side = :right) - @test norm(t - U2 * S2 * Vᴴ2) ≈ ϵ2 atol = eps(real(T))^(4 / 5) - @test minimum(diagview(S1)) >= λ - @test U2 ≈ U1 - @test S2 ≈ S1 - @test Vᴴ2 ≈ Vᴴ1 - @test ϵ1 ≈ ϵ2 - - trunc = truncspace(space(S2, 1)) - @test spacetype(typeof(trunc)) == spacetype(W) - @test sectortype(trunc) == sectortype(W) - U3, S3, Vᴴ3, ϵ3 = @constinferred svd_trunc(t; trunc) - @test t * Vᴴ3' ≈ U3 * S3 - @test isisometric(U3) - @test isisometric(Vᴴ3; side = :right) - @test norm(t - U3 * S3 * Vᴴ3) ≈ ϵ3 atol = eps(real(T))^(4 / 5) - @test space(S3, 1) ≾ space(S2, 1) - - for trunc in (truncerror(; atol = ϵ2), truncerror(; rtol = ϵ2 / norm(t))) - U4, S4, Vᴴ4, ϵ4 = @constinferred svd_trunc(t; trunc) - @test t * Vᴴ4' ≈ U4 * S4 - @test isisometric(U4) - @test isisometric(Vᴴ4; side = :right) - @test norm(t - U4 * S4 * Vᴴ4) ≈ ϵ4 atol = eps(real(T))^(4 / 5) - @test ϵ4 ≤ ϵ2 - end - - trunc = truncrank(nvals) & trunctol(; atol = λ - 10eps(λ)) - U5, S5, Vᴴ5, ϵ5 = @constinferred svd_trunc(t; trunc) - @test t * Vᴴ5' ≈ U5 * S5 - @test isisometric(U5) - @test isisometric(Vᴴ5; side = :right) - @test norm(t - U5 * S5 * Vᴴ5) ≈ ϵ5 atol = eps(real(T))^(4 / 5) - @test minimum(diagview(S5)) >= λ - @test abs(dim(domain(S5)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - - trunc = truncrank(nvals) | trunctol(; atol = λ - 10eps(λ)) - U5, S5, Vᴴ5, ϵ5 = @constinferred svd_trunc(t; trunc) - @test t * Vᴴ5' ≈ U5 * S5 - @test isisometric(U5) - @test isisometric(Vᴴ5; side = :right) - @test norm(t - U5 * S5 * Vᴴ5) ≈ ϵ5 atol = eps(real(T))^(4 / 5) - @test minimum(diagview(S5)) >= λ - @test abs(dim(domain(S5)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) - end - end + TensorKitTestSuite.run_testsuite(:factorizations, "condition number and rank", V) + TensorKitTestSuite.run_testsuite(:factorizations, "polar decomposition", V) + TensorKitTestSuite.run_testsuite(:factorizations, "SVD", V) + TensorKitTestSuite.run_testsuite(:factorizations, "truncated SVD", V) end end diff --git a/test/runtests.jl b/test/runtests.jl index 8ae4f1e9c..cee02c4e5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,7 @@ delete!(testsuite, "testsuite/fusiontrees") # only meant to be `include`d inside delete!(testsuite, "testsuite/spaces") delete!(testsuite, "testsuite/tensors") delete!(testsuite, "testsuite/diagonal") +delete!(testsuite, "testsuite/factorizations") # CUDA tests: only run if CUDA is functional using CUDA: CUDA diff --git a/test/setup.jl b/test/setup.jl index 81a0926c5..a3cd80660 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -17,7 +17,7 @@ using TensorKit using TensorKitSectors using TensorOperations: IndexTuple, Index2Tuple using TupleTools -using MatrixAlgebraKit: MatrixAlgebraKit, diagview +using MatrixAlgebraKit: MatrixAlgebraKit, diagview, DefaultAlgorithm, defaulttol using ChainRulesCore: NoTangent using ChainRulesTestUtils: ChainRulesTestUtils, test_rrule using Zygote: Zygote, rrule_via_ad diff --git a/test/testsuite/TensorKitTestSuite.jl b/test/testsuite/TensorKitTestSuite.jl index b7ca378f2..2f6a6bef6 100644 --- a/test/testsuite/TensorKitTestSuite.jl +++ b/test/testsuite/TensorKitTestSuite.jl @@ -19,6 +19,7 @@ TensorKitTestSuite.run_testsuite(:single_fusiontrees, "test", MySector) TensorKitTestSuite.run_testsuite(:double_fusiontrees, "test", MySector) TensorKitTestSuite.run_testsuite(:spaces, "test", MySector) TensorKitTestSuite.run_testsuite(:tensors, "test", (V1, V2, V3, V4, V5)) # 5 mutually compatible spaces +TensorKitTestSuite.run_testsuite(:factorizations, "test", (V1, V2, V3, V4, V5)) # 5 mutually compatible spaces TensorKitTestSuite.run_testsuite(:diagonal_tensors, "test", V) # 1 space for diagonal tensors ``` @@ -67,6 +68,7 @@ const testgroups = Dict{Symbol, Dict{String, Expr}}( :spaces => Dict{String, Expr}(), :tensors => Dict{String, Expr}(), :diagonal_tensors => Dict{String, Expr}(), + :factorizations => Dict{String, Expr}(), ) # cannot just esc() the body, because that would make it a closure, compile it at a fixed world age and break constprop=true @@ -86,7 +88,9 @@ end Register a testsuite entry under `testgroup` (one of `:single_fusiontrees`, `:double_fusiontrees`, `:spaces`,`:tensors`, `:diagonal_tensors`). The body is executed with a single argument: the concrete `Sector` type under test (for `:single_fusiontrees`, `:double_fusiontrees` and `:spaces`), a space (for `:diagonal_tensors`), -or a 5-tuple/vector of mutually compatible spaces (for `:tensors`). +or a 5-tuple of mutually compatible spaces (for `:tensors` and `:factorizations`). + +For the test groups involving spaces, see `setup.jl` for the space design considerations. Run a registered entry via `run_testsuite(testgroup, name, arg)`. """ @@ -174,5 +178,7 @@ end include("fusiontrees.jl") include("spaces.jl") include("tensors.jl") +include("diagonal.jl") +include("factorizations.jl") end # module TensorKitTestSuite diff --git a/test/testsuite/factorizations.jl b/test/testsuite/factorizations.jl new file mode 100644 index 000000000..6f24ded7b --- /dev/null +++ b/test/testsuite/factorizations.jl @@ -0,0 +1,654 @@ +# Factorization tests +# =================== + +eltypes = (Float32, ComplexF64) + +# eigenvalue decompositions +#-------------------------- +@testsuite :factorizations "eigenvalue decomposition" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + + for T in eltypes, + t in ( + rand(T, V1, V1), rand(T, W, W), rand(T, W, W)', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + d, v = @constinferred eig_full(t) + @test t * v ≈ v * d + + d, v = @constinferred eig_full(t, DefaultAlgorithm()) + @test t * v ≈ v * d + + d′ = @constinferred eig_vals(t) + @test d′ ≈ diagview(d) + @test d′ isa TensorKit.SectorVector + + d′ = @constinferred eig_vals(t, DefaultAlgorithm()) + @test d′ ≈ diagview(d) + @test d′ isa TensorKit.SectorVector + + d2 = @constinferred DiagonalTensorMap(d′) + @test d2 ≈ d + + vdv = project_hermitian!(v' * v) + @test @constinferred isposdef(vdv) + t isa DiagonalTensorMap || @test !isposdef(t) # unlikely for non-hermitian map + + nvals = round(Int, dim(domain(t)) / 2) + d, v = @constinferred eig_trunc(t; trunc = truncrank(nvals)) + @test t * v ≈ v * d + @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + + d, v = @constinferred eig_trunc(t, DefaultAlgorithm(; trunc = truncrank(nvals))) + @test t * v ≈ v * d + @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + + t2 = @constinferred project_hermitian(t) + D, V = eigen(t2) + @test isisometric(V) + D̃, Ṽ = @constinferred eigh_full(t2) + @test D ≈ D̃ + @test V ≈ Ṽ + + D̃, Ṽ = @constinferred eigh_full(t2, DefaultAlgorithm()) + @test D ≈ D̃ + @test V ≈ Ṽ + + λ = minimum(real, diagview(D)) + @test cond(Ṽ) ≈ one(real(T)) + @test isposdef(t2) == isposdef(λ) + @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) + @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) + + d, v = @constinferred eigh_full(t2) + @test t2 * v ≈ v * d + @test isunitary(v) + + d′ = @constinferred eigh_vals(t2) + @test d′ ≈ diagview(d) + @test d′ isa TensorKit.SectorVector + + d′ = @constinferred eigh_vals(t2, DefaultAlgorithm()) + @test d′ ≈ diagview(d) + @test d′ isa TensorKit.SectorVector + + λ = minimum(real, diagview(d)) + @test cond(v) ≈ one(real(T)) + @test isposdef(t2) == isposdef(λ) + @test isposdef(t2 - λ * one(t) + 0.1 * one(t2)) + @test !isposdef(t2 - λ * one(t) - 0.1 * one(t2)) + + d, v = @constinferred eigh_trunc(t2; trunc = truncrank(nvals)) + @test t2 * v ≈ v * d + @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + + d, v = @constinferred eigh_trunc(t2, DefaultAlgorithm(; trunc = truncrank(nvals))) + @test t2 * v ≈ v * d + @test abs(dim(domain(d)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + end +end + +# QR and LQ decompositions +#-------------------------- +@testsuite :factorizations "QR decomposition" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', + rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)')', + rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5)), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + Q, R = @constinferred qr_full(t) + @test Q * R ≈ t + @test isunitary(Q) + + Q, R = @constinferred qr_full(t, DefaultAlgorithm()) + @test Q * R ≈ t + @test isunitary(Q) + + Q, R = @constinferred qr_compact(t) + @test Q * R ≈ t + @test isisometric(Q) + + Q, R = @constinferred qr_compact(t, DefaultAlgorithm()) + @test Q * R ≈ t + @test isisometric(Q) + + Q, R = @constinferred left_orth(t) + @test Q * R ≈ t + @test isisometric(Q) + + Q, R = @constinferred left_orth(t, DefaultAlgorithm()) + @test Q * R ≈ t + @test isisometric(Q) + + N = @constinferred qr_null(t) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + N = @constinferred qr_null(t, DefaultAlgorithm()) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + N = @constinferred left_null(t) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + N = @constinferred left_null(t, DefaultAlgorithm()) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + end + + # empty tensor + for T in eltypes + t = rand(T, V1 ⊗ V2, zerospace(V1)) + + Q, R = @constinferred qr_full(t) + @test Q * R ≈ t + @test isunitary(Q) + @test dim(R) == dim(t) == 0 + + Q, R = @constinferred qr_full(t, DefaultAlgorithm()) + @test Q * R ≈ t + @test isunitary(Q) + @test dim(R) == dim(t) == 0 + + Q, R = @constinferred qr_compact(t) + @test Q * R ≈ t + @test isisometric(Q) + @test dim(Q) == dim(R) == dim(t) + + Q, R = @constinferred qr_compact(t, DefaultAlgorithm()) + @test Q * R ≈ t + @test isisometric(Q) + @test dim(Q) == dim(R) == dim(t) + + Q, R = @constinferred left_orth(t) + @test Q * R ≈ t + @test isisometric(Q) + @test dim(Q) == dim(R) == dim(t) + + Q, R = @constinferred left_orth(t, DefaultAlgorithm()) + @test Q * R ≈ t + @test isisometric(Q) + @test dim(Q) == dim(R) == dim(t) + + N = @constinferred qr_null(t) + @test isunitary(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + N = @constinferred qr_null(t, DefaultAlgorithm()) + @test isunitary(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + end +end + +@testsuite :factorizations "LQ decomposition" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', + rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)')', + rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5)), rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + L, Q = @constinferred lq_full(t) + @test L * Q ≈ t + @test isunitary(Q) + + L, Q = @constinferred lq_full(t, DefaultAlgorithm()) + @test L * Q ≈ t + @test isunitary(Q) + + L, Q = @constinferred lq_compact(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + + L, Q = @constinferred lq_compact(t, DefaultAlgorithm()) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + + L, Q = @constinferred right_orth(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + + L, Q = @constinferred right_orth(t, DefaultAlgorithm()) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + + Nᴴ = @constinferred lq_null(t) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + + Nᴴ = @constinferred lq_null(t, DefaultAlgorithm()) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + end + + for T in eltypes + # empty tensor + t = rand(T, zerospace(V1), V1 ⊗ V2) + + L, Q = @constinferred lq_full(t) + @test L * Q ≈ t + @test isunitary(Q) + @test dim(L) == dim(t) == 0 + + L, Q = @constinferred lq_full(t, DefaultAlgorithm()) + @test L * Q ≈ t + @test isunitary(Q) + @test dim(L) == dim(t) == 0 + + L, Q = @constinferred lq_compact(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + @test dim(Q) == dim(L) == dim(t) + + L, Q = @constinferred lq_compact(t, DefaultAlgorithm()) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + @test dim(Q) == dim(L) == dim(t) + + L, Q = @constinferred right_orth(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + @test dim(Q) == dim(L) == dim(t) + + L, Q = @constinferred right_orth(t, DefaultAlgorithm()) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + @test dim(Q) == dim(L) == dim(t) + + Nᴴ = @constinferred lq_null(t) + @test isunitary(Nᴴ) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + + Nᴴ = @constinferred lq_null(t, DefaultAlgorithm()) + @test isunitary(Nᴴ) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + end +end + +# projections +#------------ +@testsuite :factorizations "hermitian projections" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, V1, V1), rand(T, W, W), rand(T, W, W)', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + normalize!(t) + noisefactor = eps(real(T))^(3 / 4) + + th = (t + t') / 2 + ta = (t - t') / 2 + tc = copy(t) + + th′ = @constinferred project_hermitian(t) + @test ishermitian(th′) + @test th′ ≈ th + + th′ = @constinferred project_hermitian(t, DefaultAlgorithm()) + @test ishermitian(th′) + @test th′ ≈ th + + @test t == tc + th_approx = th + noisefactor * ta + @test !ishermitian(th_approx) || (T <: Real && t isa DiagonalTensorMap) + @test ishermitian(th_approx; atol = 10 * noisefactor) + + ta′ = project_antihermitian(t) + @test isantihermitian(ta′) + @test ta′ ≈ ta + + ta′ = @constinferred project_antihermitian(t, DefaultAlgorithm()) + @test isantihermitian(ta′) + @test ta′ ≈ ta + + @test t == tc + ta_approx = ta + noisefactor * th + @test !isantihermitian(ta_approx) + @test isantihermitian(ta_approx; atol = 10 * noisefactor) || (T <: Real && t isa DiagonalTensorMap) + end + + @test_throws SpaceMismatch project_hermitian(rand(V1, V1^2)) + @test_throws SpaceMismatch project_antihermitian(rand(V1, V1^2)) + if V1 != V1' + @test_throws SpaceMismatch project_hermitian(rand(V1, V1')) + @test_throws SpaceMismatch project_antihermitian(rand(V1, V1')) + end +end + +@testsuite :factorizations "isometric projections" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', + ) + t2 = project_isometric(t) + @test isisometric(t2) + t2′ = @constinferred project_isometric(t, DefaultAlgorithm()) + @test isisometric(t2′) + @test t2′ * ((t2′)' * t) ≈ t + + t3 = project_isometric(t2) + @test t3 ≈ t2 # stability of the projection + @test t2 * (t2' * t) ≈ t + + tc = similar(t) + t3 = @constinferred project_isometric!(copy!(tc, t), t2) + @test t3 === t2 + @test isisometric(t2) + + # test that t2 is closer to A then any other isometry + for k in 1:10 + δt = randn!(similar(t)) + t3 = project_isometric(t + δt / 100) + @test norm(t - t3) > norm(t - t2) + end + end +end + +# singular value decompositions +#------------------------------ +@testsuite :factorizations "condition number and rank" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + randn(T, W, W), randn(T, W, W)', + randn(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), randn(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', + randn(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), randn(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + d1, d2 = dim(codomain(t)), dim(domain(t)) + r = rank(t) + @test r ≈ min(d1, d2) + @test typeof(r) == typeof(d1) + M = left_null(t) + @test @constinferred(rank(M)) + r ≈ d1 + Mᴴ = right_null(t) + @test rank(Mᴴ) + r ≈ d2 + end + for T in eltypes + u = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test @constinferred(cond(u)) ≈ one(real(T)) + @test @constinferred(rank(u)) ≈ dim(V1 ⊗ V2) + + t = rand(T, zerospace(V1), W) + @test rank(t) == 0 + t2 = rand(T, zerospace(V1) * zerospace(V2), zerospace(V1) * zerospace(V2)) + @test rank(t2) == 0 + @test cond(t2) == 0.0 + end + for T in eltypes, t in (randn(T, W, W), randn(T, W, W)') + project_hermitian!(t) + vals = @constinferred eigh_vals(t) + λmax = maximum(abs, vals) + λmin = minimum(abs, vals) + @test cond(t) ≈ λmax / λmin + end +end + +@testsuite :factorizations "polar decomposition" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, W, W), + rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), + rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + @assert domain(t) ≾ codomain(t) + w, p = @constinferred left_polar(t) + @test w * p ≈ t + @test isisometric(w) + @test isposdef(p) + + w′, p′ = @constinferred left_polar(t, DefaultAlgorithm()) + @test w ≈ w′ + @test p ≈ p′ + + w, p = @constinferred left_orth(t; alg = :polar) + @test w * p ≈ t + @test isisometric(w) + end + + for T in eltypes, + t in ( + rand(T, W, W), + rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), + rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + @assert codomain(t) ≾ domain(t) + p, wᴴ = @constinferred right_polar(t) + @test p * wᴴ ≈ t + @test isisometric(wᴴ; side = :right) + @test isposdef(p) + + p′, wᴴ′ = @constinferred right_polar(t, DefaultAlgorithm()) + @test p′ ≈ p + @test wᴴ′ ≈ wᴴ + + p, wᴴ = @constinferred right_orth(t; alg = :polar) + @test p * wᴴ ≈ t + @test isisometric(wᴴ; side = :right) + end +end + +@testsuite :factorizations "SVD" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', + rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', + rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + u, s, vᴴ = @constinferred svd_full(t) + @test u * s * vᴴ ≈ t + @test isunitary(u) + @test isunitary(vᴴ) + + u′, s′, vᴴ′ = @constinferred svd_full(t, DefaultAlgorithm()) + @test u ≈ u′ + @test s ≈ s′ + @test vᴴ ≈ vᴴ′ + + u, s, vᴴ = @constinferred svd_compact(t) + @test u * s * vᴴ ≈ t + @test isisometric(u) + @test isposdef(s) + @test isisometric(vᴴ; side = :right) + + u′, s′, vᴴ′ = @constinferred svd_compact(t, DefaultAlgorithm()) + @test u ≈ u′ + @test s ≈ s′ + @test vᴴ ≈ vᴴ′ + + s′ = @constinferred svd_vals(t) + @test s′ ≈ diagview(s) + @test s′ isa TensorKit.SectorVector + + s′ = @constinferred svd_vals(t, DefaultAlgorithm()) + @test s′ ≈ diagview(s) + @test s′ isa TensorKit.SectorVector + + s2 = @constinferred DiagonalTensorMap(s′) + @test s2 ≈ s + + v, c = @constinferred left_orth(t; alg = :svd) + @test v * c ≈ t + @test isisometric(v) + + c, vᴴ = @constinferred right_orth(t; alg = :svd) + @test c * vᴴ ≈ t + @test isisometric(vᴴ; side = :right) + + atol = norm(t) * defaulttol(T) # tol used by `:svd` left_null/right_null + + N = @constinferred left_null(t; alg = :svd) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = atol + + N = @constinferred left_null(t; trunc = (; atol = 6 * atol)) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 10 * atol + + Nᴴ = @constinferred right_null(t; alg = :svd) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = atol + + Nᴴ = @constinferred right_null(t; trunc = (; atol = 6 * atol)) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = 10 * atol + end + + # empty tensor + for T in eltypes, t in (rand(T, W, zerospace(V1)), rand(T, zerospace(V1), W)) + U, S, Vᴴ = @constinferred svd_full(t) + @test U * S * Vᴴ ≈ t + @test isunitary(U) + @test isunitary(Vᴴ) + + U, S, Vᴴ = @constinferred svd_full(t, DefaultAlgorithm()) + @test U * S * Vᴴ ≈ t + @test isunitary(U) + @test isunitary(Vᴴ) + + U, S, Vᴴ = @constinferred svd_compact(t) + @test U * S * Vᴴ ≈ t + @test dim(U) == dim(S) == dim(Vᴴ) == dim(t) == 0 + + U, S, Vᴴ = @constinferred svd_compact(t, DefaultAlgorithm()) + @test U * S * Vᴴ ≈ t + @test dim(U) == dim(S) == dim(Vᴴ) == dim(t) == 0 + end +end + +@testsuite :factorizations "truncated SVD" V -> begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 + Vd = fuse(V1 ⊗ V2) + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', + rand(T, (V1 ⊗ V2 ⊗ V3), (V4 ⊗ V5)'), rand(T, (V1 ⊗ V2)', (V3 ⊗ V4 ⊗ V5))', + rand(T, (V1 ⊗ V2), (V3 ⊗ V4 ⊗ V5)'), rand(T, (V1 ⊗ V2 ⊗ V3)', (V4 ⊗ V5))', + DiagonalTensorMap(rand(T, reduceddim(Vd)), Vd), + ) + + @constinferred normalize!(t) + + U, S, Vᴴ, ϵ = @constinferred svd_trunc(t; trunc = notrunc()) + @test U * S * Vᴴ ≈ t + @test ϵ ≈ 0 + @test isisometric(U) + @test isisometric(Vᴴ; side = :right) + + U, S, Vᴴ, ϵ = @constinferred svd_trunc(t, DefaultAlgorithm(; trunc = notrunc())) + @test U * S * Vᴴ ≈ t + @test ϵ ≈ 0 + @test isisometric(U) + @test isisometric(Vᴴ; side = :right) + + # when rank of t is already smaller than truncrank + t_rank = ceil(Int, min(dim(codomain(t)), dim(domain(t)))) + U, S, Vᴴ, ϵ = @constinferred svd_trunc(t; trunc = truncrank(t_rank + 1)) + @test U * S * Vᴴ ≈ t + @test ϵ ≈ 0 + @test isisometric(U) + @test isisometric(Vᴴ; side = :right) + + U, S, Vᴴ, ϵ = @constinferred svd_trunc(t, DefaultAlgorithm(; trunc = truncrank(t_rank + 1))) + @test U * S * Vᴴ ≈ t + @test ϵ ≈ 0 + @test isisometric(U) + @test isisometric(Vᴴ; side = :right) + + # dimension of S is a float for IsingBimodule + nvals = round(Int, dim(domain(S)) / 2) + trunc = truncrank(nvals) + U1, S1, Vᴴ1, ϵ1 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ1' ≈ U1 * S1 + @test isisometric(U1) + @test isisometric(Vᴴ1; side = :right) + @test norm(t - U1 * S1 * Vᴴ1) ≈ ϵ1 atol = eps(real(T))^(4 / 5) + @test abs(dim(domain(S1)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + + λ = minimum(diagview(S1)) + trunc = trunctol(; atol = λ - 10eps(λ)) + U2, S2, Vᴴ2, ϵ2 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ2' ≈ U2 * S2 + @test isisometric(U2) + @test isisometric(Vᴴ2; side = :right) + @test norm(t - U2 * S2 * Vᴴ2) ≈ ϵ2 atol = eps(real(T))^(4 / 5) + @test minimum(diagview(S1)) >= λ + @test U2 ≈ U1 + @test S2 ≈ S1 + @test Vᴴ2 ≈ Vᴴ1 + @test ϵ1 ≈ ϵ2 + + trunc = truncspace(space(S2, 1)) + @test spacetype(typeof(trunc)) == spacetype(W) + @test sectortype(trunc) == sectortype(W) + U3, S3, Vᴴ3, ϵ3 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ3' ≈ U3 * S3 + @test isisometric(U3) + @test isisometric(Vᴴ3; side = :right) + @test norm(t - U3 * S3 * Vᴴ3) ≈ ϵ3 atol = eps(real(T))^(4 / 5) + @test space(S3, 1) ≾ space(S2, 1) + + for trunc in (truncerror(; atol = ϵ2), truncerror(; rtol = ϵ2 / norm(t))) + U4, S4, Vᴴ4, ϵ4 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ4' ≈ U4 * S4 + @test isisometric(U4) + @test isisometric(Vᴴ4; side = :right) + @test norm(t - U4 * S4 * Vᴴ4) ≈ ϵ4 atol = eps(real(T))^(4 / 5) + @test ϵ4 ≤ ϵ2 + end + + trunc = truncrank(nvals) & trunctol(; atol = λ - 10eps(λ)) + U5, S5, Vᴴ5, ϵ5 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ5' ≈ U5 * S5 + @test isisometric(U5) + @test isisometric(Vᴴ5; side = :right) + @test norm(t - U5 * S5 * Vᴴ5) ≈ ϵ5 atol = eps(real(T))^(4 / 5) + @test minimum(diagview(S5)) >= λ + @test abs(dim(domain(S5)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + + trunc = truncrank(nvals) | trunctol(; atol = λ - 10eps(λ)) + U5, S5, Vᴴ5, ϵ5 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ5' ≈ U5 * S5 + @test isisometric(U5) + @test isisometric(Vᴴ5; side = :right) + @test norm(t - U5 * S5 * Vᴴ5) ≈ ϵ5 atol = eps(real(T))^(4 / 5) + @test minimum(diagview(S5)) >= λ + @test abs(dim(domain(S5)) - nvals) ≤ maximum(c -> blockdim(domain(t), c), blocksectors(t); init = 1) + end +end