From fb4693f2ecabc344d88bc925cbfbc2d2e174ad92 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 17 Mar 2025 17:58:28 +0100 Subject: [PATCH 01/86] add todo for function split --- src/fusiontrees/manipulations.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index fa9ccad39..6b5475aef 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -165,8 +165,9 @@ operation is the inverse of `insertat` in the sense that if f₂ = FusionTree{I}(f.uncoupled, f.coupled, isdual2, f.innerlines, f.vertices) return f₁, f₂ elseif M === 0 - f₁ = FusionTree{I}((), one(I), (), ()) - uncoupled2 = (one(I), f.uncoupled...) + # TODO: evaluate diagrams to see which unit is used here + f₁ = FusionTree{I}((), one(I), (), ()) + uncoupled2 = (one(I), f.uncoupled...) coupled2 = f.coupled isdual2 = (false, f.isdual...) innerlines2 = N >= 2 ? (f.uncoupled[1], f.innerlines...) : () From 13bcf823ff7a0095da09c06b03f17cf41b2a0163 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 17 Mar 2025 17:58:53 +0100 Subject: [PATCH 02/86] add comment for possible change in function merge --- src/fusiontrees/manipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 6b5475aef..8c02c1741 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -231,7 +231,7 @@ function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, return insertat(f, N₁ + 1, f₂) end function merge(f₁::FusionTree{I,0}, f₂::FusionTree{I,0}, c::I, μ) where {I} - isone(c) || + isone(c) || # I had this as Nsymbol(f₁.coupled, f₂.coupled, c) > 0, valid? throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) return fusiontreedict(I)(f₁ => Fsymbol(c, c, c, c, c, c)[1, 1, 1, 1]) end From 09467c63e1982b0e90f87a36f8b616001c213af7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 17 Mar 2025 17:59:16 +0100 Subject: [PATCH 03/86] change one to leftone in bendright --- src/fusiontrees/manipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 8c02c1741..218e2fa05 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -268,7 +268,7 @@ function bendright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I< # map final splitting vertex (a, b)<-c to fusion vertex a<-(c, dual(b)) @assert N₁ > 0 c = f₁.coupled - a = N₁ == 1 ? one(I) : (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) b = f₁.uncoupled[N₁] uncoupled1 = TupleTools.front(f₁.uncoupled) From 2878e082e671bc264552bb16d30c96adba061b0d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 17 Mar 2025 17:59:38 +0100 Subject: [PATCH 04/86] change one to leftone in foldright --- src/fusiontrees/manipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 218e2fa05..00a5d5027 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -339,7 +339,7 @@ function foldright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I< hasmultiplicities = FusionStyle(a) isa GenericFusion local newtrees if N₁ == 1 - cset = (one(c1),) + cset = (leftone(c1),) # or rightone(a) elseif N₁ == 2 cset = (f₁.uncoupled[2],) else From 10744b2d6540ce2039f3f071123ee6d9cc267d8b Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 17 Mar 2025 18:00:25 +0100 Subject: [PATCH 05/86] change one to f.coupled in elementary_trace --- src/fusiontrees/manipulations.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 00a5d5027..3fd29fd62 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -701,6 +701,7 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} T = sectorscalartype(I) F = fusiontreetype(I, N - 2) newtrees = FusionTreeDict{F,T}() + _one = f.coupled # otherwise ArgumentError above thrown j = mod1(i + 1, N) b = f.uncoupled[i] @@ -708,7 +709,7 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} # if trace is zero, return empty dict (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees if i < N - inner_extended = (one(I), f.uncoupled[1], f.innerlines..., f.coupled) + inner_extended = (_one, f.uncoupled[1], f.innerlines..., f.coupled) a = inner_extended[i] d = inner_extended[i + 2] a == d || return newtrees @@ -732,11 +733,11 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} if i > 1 c = f.innerlines[i - 1] if FusionStyle(I) isa MultiplicityFreeFusion - coeff *= Fsymbol(a, b, dual(b), a, c, one(I)) + coeff *= Fsymbol(a, b, dual(b), a, c, _one) else μ = f.vertices[i - 1] ν = f.vertices[i] - coeff *= Fsymbol(a, b, dual(b), a, c, one(I))[μ, ν, 1, 1] + coeff *= Fsymbol(a, b, dual(b), a, c, _one)[μ, ν, 1, 1] end end if f.isdual[i] @@ -746,7 +747,7 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} return newtrees else # i == N if N == 2 - f′ = FusionTree{I}((), one(I), (), (), ()) + f′ = FusionTree{I}((), _one, (), (), ()) coeff = sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) @@ -762,13 +763,13 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} vertices_ = TupleTools.front(f.vertices) f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) - for (f_′, coeff) in merge(fs, f_, one(I), 1) - f_′.innerlines[1] == one(I) || continue + for (f_′, coeff) in merge(fs, f_, _one, 1) + f_′.innerlines[1] == _one || continue uncoupled′ = Base.tail(Base.tail(f_′.uncoupled)) isdual′ = Base.tail(Base.tail(f_′.isdual)) inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) vertices′ = N <= 3 ? () : Base.tail(Base.tail(f_′.vertices)) - f′ = FusionTree(uncoupled′, one(I), isdual′, inner′, vertices′) + f′ = FusionTree(uncoupled′, _one, isdual′, inner′, vertices′) coeff *= sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) From 1ed1ed1bccc5fa107514605dff21bccbcfb4acf6 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 25 Mar 2025 14:10:46 +0100 Subject: [PATCH 06/86] progress on correct units in trace --- src/fusiontrees/manipulations.jl | 40 +++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 3fd29fd62..2cef01436 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -551,6 +551,9 @@ const TransposeKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, function _transpose((f₁, f₂, p1, p2)::TransposeKey{I,N₁,N₂}) where {I<:Sector,N₁,N₂} N = N₁ + N₂ p = linearizepermutation(p1, p2, length(f₁), length(f₂)) + @info "_transpose" + @show f₁ + @show f₂ newtrees = repartition(f₁, f₂, N₁) length(p) == 0 && return newtrees i1 = findfirst(==(1), p) @@ -619,8 +622,14 @@ function planar_trace(f₁::FusionTree{I}, f₂::FusionTree{I}, F₂ = fusiontreetype(I, N₂) newtrees = FusionTreeDict{Tuple{F₁,F₂},T}() for ((f₁′, f₂′), coeff′) in repartition(f₁, f₂, N) - for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) - for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) + for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) # errors in this planar_trace first + @info "planar_trace" + @show f₁ + @show f₂ + @show f₁′ + @show f₁′′ + @show f₂′ + for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) # for a different unit errors here coeff = coeff′ * coeff′′ * coeff′′′ if !iszero(coeff) newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff @@ -673,7 +682,7 @@ function planar_trace(f::FusionTree{I,N}, q2′ = let i = i, j = j map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q2, k)) end - for (f′, coeff′) in elementary_trace(f, i) + for (f′, coeff′) in elementary_trace(f, i) # errors then here for (f′′, coeff′′) in planar_trace(f′, q1′, q2′) coeff = coeff′ * coeff′′ if !iszero(coeff) @@ -702,6 +711,8 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} F = fusiontreetype(I, N - 2) newtrees = FusionTreeDict{F,T}() _one = f.coupled # otherwise ArgumentError above thrown + @info "elementary_trace" + @show f j = mod1(i + 1, N) b = f.uncoupled[i] @@ -709,7 +720,8 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} # if trace is zero, return empty dict (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees if i < N - inner_extended = (_one, f.uncoupled[1], f.innerlines..., f.coupled) + @info "elementary_trace: i < N" + inner_extended = (leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., f.coupled) a = inner_extended[i] d = inner_extended[i + 2] a == d || return newtrees @@ -733,21 +745,23 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} if i > 1 c = f.innerlines[i - 1] if FusionStyle(I) isa MultiplicityFreeFusion - coeff *= Fsymbol(a, b, dual(b), a, c, _one) + coeff *= Fsymbol(a, b, dual(b), a, c, rightone(a)) else μ = f.vertices[i - 1] ν = f.vertices[i] - coeff *= Fsymbol(a, b, dual(b), a, c, _one)[μ, ν, 1, 1] + coeff *= Fsymbol(a, b, dual(b), a, c, rightone(a))[μ, ν, 1, 1] end end if f.isdual[i] coeff *= frobeniusschur(b) end + @show f′ push!(newtrees, f′ => coeff) return newtrees else # i == N + @info "elementary_trace: i == N" if N == 2 - f′ = FusionTree{I}((), _one, (), (), ()) + f′ = FusionTree{I}((), _one, (), (), ()) # or leftone(f.uncoupled[1]) == rightone(f.uncoupled[2]) coeff = sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) @@ -758,18 +772,22 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} uncoupled_ = TupleTools.front(f.uncoupled) inner_ = TupleTools.front(f.innerlines) coupled_ = f.innerlines[end] - @assert coupled_ == dual(b) + @assert coupled_ == dual(b) # isn't this always true at this point? isdual_ = TupleTools.front(f.isdual) vertices_ = TupleTools.front(f.vertices) f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) - for (f_′, coeff) in merge(fs, f_, _one, 1) - f_′.innerlines[1] == _one || continue + @show f_ + @show fs + unit = leftone(fs.coupled) + for (f_′, coeff) in merge(fs, f_, unit, 1) # coloring gets reversed here, should be the other unit + f_′.innerlines[1] == unit || continue # is this one valid? uncoupled′ = Base.tail(Base.tail(f_′.uncoupled)) isdual′ = Base.tail(Base.tail(f_′.isdual)) inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) vertices′ = N <= 3 ? () : Base.tail(Base.tail(f_′.vertices)) - f′ = FusionTree(uncoupled′, _one, isdual′, inner′, vertices′) + f′ = FusionTree(uncoupled′, unit, isdual′, inner′, vertices′) # and this one? + @show f′ coeff *= sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) From c31b99c7a0cf80aa4e91afe97987f9fcabe82a67 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 25 Mar 2025 17:27:11 +0100 Subject: [PATCH 07/86] change dim of GradedSpace to not evaluate one --- src/spaces/gradedspace.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 00f97c962..8233a0833 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -90,9 +90,9 @@ Base.hash(V::GradedSpace, h::UInt) = hash(V.dual, hash(V.dims, h)) # properties field(::Type{<:GradedSpace}) = ℂ InnerProductStyle(::Type{<:GradedSpace}) = EuclideanInnerProduct() -function dim(V::GradedSpace) +function dim(V::GradedSpace{I}) where {I<:Sector} return reduce(+, dim(V, c) * dim(c) for c in sectors(V); - init=zero(dim(one(sectortype(V))))) + init=zero(TensorKitSectors._Fscalartype(I))) end function dim(V::GradedSpace{I,<:AbstractDict}, c::I) where {I<:Sector} return get(V.dims, isdual(V) ? dual(c) : c, 0) From a33d0287ae699c24dc3602c3141a87f3fb04f9e5 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 26 Mar 2025 09:27:25 +0100 Subject: [PATCH 08/86] use a function that's actually exported in dimension of `GradedSpace` --- src/spaces/gradedspace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 8233a0833..82c1a41c7 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -92,7 +92,7 @@ field(::Type{<:GradedSpace}) = ℂ InnerProductStyle(::Type{<:GradedSpace}) = EuclideanInnerProduct() function dim(V::GradedSpace{I}) where {I<:Sector} return reduce(+, dim(V, c) * dim(c) for c in sectors(V); - init=zero(TensorKitSectors._Fscalartype(I))) + init=zero(sectorscalartype(I))) end function dim(V::GradedSpace{I,<:AbstractDict}, c::I) where {I<:Sector} return get(V.dims, isdual(V) ? dual(c) : c, 0) From 0e3c5dfcb9f583cb216ddccafe1f83efb7ddd142 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 10 Apr 2025 13:47:13 -0400 Subject: [PATCH 09/86] update TensorOperations scalartype determination --- src/tensors/tensoroperations.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index cf977690f..6f59ed699 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -52,7 +52,8 @@ end function TO.tensoradd_type(TC, A::AbstractTensorMap, ::Index2Tuple{N₁,N₂}, ::Bool) where {N₁,N₂} - M = similarstoragetype(A, TC) + Tnew = VectorInterface.promote_add(TC, scalartype(A), sectorscalartype(sectortype(A))) + M = similarstoragetype(A, Tnew) return tensormaptype(spacetype(A), N₁, N₂, M) end @@ -113,9 +114,12 @@ function TO.tensorcontract_type(TC, A::AbstractTensorMap, ::Index2Tuple, ::Bool, B::AbstractTensorMap, ::Index2Tuple, ::Bool, ::Index2Tuple{N₁,N₂}) where {N₁,N₂} - M = similarstoragetype(A, TC) - M == similarstoragetype(B, TC) || - throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(similarstoragetype(B, TC))")) + Tnew = TensorOperations.promote_contract(TC, scalartype(A), + sectorscalartype(sectortype(A)), + scalartype(B), sectorscalartype(sectortype(B))) + M = similarstoragetype(A, Tnew) + M == similarstoragetype(B, Tnew) || + throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(similarstoragetype(B, Tnew))")) spacetype(A) == spacetype(B) || throw(SpaceMismatch("incompatible space types")) return tensormaptype(spacetype(A), N₁, N₂, M) end From 963536225a1e94a07a3a0c4f9980d9ca96ae9328 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 14 Apr 2025 17:11:39 +0200 Subject: [PATCH 10/86] Revert "use a function that's actually exported in dimension of `GradedSpace`" This reverts commit a33d0287ae699c24dc3602c3141a87f3fb04f9e5. --- src/spaces/gradedspace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 82c1a41c7..8233a0833 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -92,7 +92,7 @@ field(::Type{<:GradedSpace}) = ℂ InnerProductStyle(::Type{<:GradedSpace}) = EuclideanInnerProduct() function dim(V::GradedSpace{I}) where {I<:Sector} return reduce(+, dim(V, c) * dim(c) for c in sectors(V); - init=zero(sectorscalartype(I))) + init=zero(TensorKitSectors._Fscalartype(I))) end function dim(V::GradedSpace{I,<:AbstractDict}, c::I) where {I<:Sector} return get(V.dims, isdual(V) ? dual(c) : c, 0) From 4bc1e7c38b4b84661f409789e2ba62b3a28efe46 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 14 Apr 2025 17:11:46 +0200 Subject: [PATCH 11/86] Revert "change dim of GradedSpace to not evaluate one" This reverts commit c31b99c7a0cf80aa4e91afe97987f9fcabe82a67. --- src/spaces/gradedspace.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 8233a0833..00f97c962 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -90,9 +90,9 @@ Base.hash(V::GradedSpace, h::UInt) = hash(V.dual, hash(V.dims, h)) # properties field(::Type{<:GradedSpace}) = ℂ InnerProductStyle(::Type{<:GradedSpace}) = EuclideanInnerProduct() -function dim(V::GradedSpace{I}) where {I<:Sector} +function dim(V::GradedSpace) return reduce(+, dim(V, c) * dim(c) for c in sectors(V); - init=zero(TensorKitSectors._Fscalartype(I))) + init=zero(dim(one(sectortype(V))))) end function dim(V::GradedSpace{I,<:AbstractDict}, c::I) where {I<:Sector} return get(V.dims, isdual(V) ? dual(c) : c, 0) From 24be2ea1e8618c81600dc4c6fd990148c2d9e11d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 16 Apr 2025 16:57:32 +0200 Subject: [PATCH 12/86] irrelevant typos, but must be corrected --- src/spaces/gradedspace.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 00f97c962..cd43a4529 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -189,16 +189,16 @@ end function Base.show(io::IO, V::GradedSpace{I}) where {I<:Sector} print(io, type_repr(typeof(V)), "(") - seperator = "" + separator = "" comma = ", " io2 = IOContext(io, :typeinfo => I) for c in sectors(V) if isdual(V) - print(io2, seperator, dual(c), "=>", dim(V, c)) + print(io2, separator, dual(c), "=>", dim(V, c)) else - print(io2, seperator, c, "=>", dim(V, c)) + print(io2, separator, c, "=>", dim(V, c)) end - seperator = comma + separator = comma end print(io, ")") V.dual && print(io, "'") From 13af986e5beac9c5092120f6ad0143e632df7758 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 16 Apr 2025 17:00:37 +0200 Subject: [PATCH 13/86] another minor typo --- src/spaces/gradedspace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index cd43a4529..7aa746076 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -12,7 +12,7 @@ isomorphism classes of simple objects of a unitary and pivotal (pre-)fusion cate Here `dims` represents the degeneracy or multiplicity of every sector. -The data structure `D` of `dims` will depend on the result `Base.IteratorElsize(values(I))`; +The data structure `D` of `dims` will depend on the result `Base.IteratorSize(values(I))`; if the result is of type `HasLength` or `HasShape`, `dims` will be stored in a `NTuple{N,Int}` with `N = length(values(I))`. This requires that a sector `s::I` can be transformed into an index via `s == getindex(values(I), i)` and From 095dcda200596e68dea6e5893767d401e775cae0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 6 May 2025 14:54:03 +0200 Subject: [PATCH 14/86] minor typos --- docs/src/man/spaces.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/man/spaces.md b/docs/src/man/spaces.md index df59733ee..9091aaa0e 100644 --- a/docs/src/man/spaces.md +++ b/docs/src/man/spaces.md @@ -297,8 +297,8 @@ corresponding spaces, but in general none of those will be canonical. There are also a number of convenience functions to create isomorphic spaces. The function `fuse(V1, V2, ...)` or `fuse(V1 ⊗ V2 ⊗ ...)` returns an elementary space that is isomorphic to `V1 ⊗ V2 ⊗ ...`. The function `flip(V::ElementarySpace)` returns a space that is -isomorphic to `V` but has `isdual(flip(V)) == isdual(V')`, i.e. if `V` is a normal space -than `flip(V)` is a dual space. `flip(V)` is different from `dual(V)` in the case of +isomorphic to `V` but has `isdual(flip(V)) == isdual(V')`, i.e., if `V` is a normal space, +then `flip(V)` is a dual space. `flip(V)` is different from `dual(V)` in the case of [`GradedSpace`](@ref). It is useful to flip a tensor index from a ket to a bra (or vice versa), by contracting that index with a unitary map from `V1` to `flip(V1)`. We refer to the reference on [vector space methods](@ref s_spacemethods) for further information. From fbfb56418f9edd979b716a0e5cfa51c463d61c44 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 6 May 2025 15:37:54 +0200 Subject: [PATCH 15/86] then vs than is hard --- docs/src/man/sectors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/man/sectors.md b/docs/src/man/sectors.md index 81dbfd35d..b0a1bd27d 100644 --- a/docs/src/man/sectors.md +++ b/docs/src/man/sectors.md @@ -771,7 +771,7 @@ groups. Other methods for `ElementarySpace`, such as [`dual`](@ref), [`fuse`](@ref) and [`flip`](@ref) also work. In fact, `GradedSpace` is the reason `flip` exists, cause -in this case it is different then `dual`. The existence of flip originates from the +in this case it is different than `dual`. The existence of flip originates from the non-trivial isomorphism between ``R_{\overline{a}}`` and ``R_{a}^*``, i.e. the representation space of the dual ``\overline{a}`` of sector ``a`` and the dual of the representation space of sector ``a``. In order for `flip(V)` to be isomorphic to `V`, it is From d7cae4fc4e83be12c5a8ace099168980639126c2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 7 May 2025 11:14:53 +0200 Subject: [PATCH 16/86] remove debug elements --- src/fusiontrees/manipulations.jl | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 2cef01436..1a62c0a14 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -551,9 +551,6 @@ const TransposeKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, function _transpose((f₁, f₂, p1, p2)::TransposeKey{I,N₁,N₂}) where {I<:Sector,N₁,N₂} N = N₁ + N₂ p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - @info "_transpose" - @show f₁ - @show f₂ newtrees = repartition(f₁, f₂, N₁) length(p) == 0 && return newtrees i1 = findfirst(==(1), p) @@ -623,12 +620,6 @@ function planar_trace(f₁::FusionTree{I}, f₂::FusionTree{I}, newtrees = FusionTreeDict{Tuple{F₁,F₂},T}() for ((f₁′, f₂′), coeff′) in repartition(f₁, f₂, N) for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) # errors in this planar_trace first - @info "planar_trace" - @show f₁ - @show f₂ - @show f₁′ - @show f₁′′ - @show f₂′ for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) # for a different unit errors here coeff = coeff′ * coeff′′ * coeff′′′ if !iszero(coeff) @@ -711,8 +702,6 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} F = fusiontreetype(I, N - 2) newtrees = FusionTreeDict{F,T}() _one = f.coupled # otherwise ArgumentError above thrown - @info "elementary_trace" - @show f j = mod1(i + 1, N) b = f.uncoupled[i] @@ -720,7 +709,6 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} # if trace is zero, return empty dict (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees if i < N - @info "elementary_trace: i < N" inner_extended = (leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., f.coupled) a = inner_extended[i] d = inner_extended[i + 2] @@ -755,11 +743,9 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} if f.isdual[i] coeff *= frobeniusschur(b) end - @show f′ push!(newtrees, f′ => coeff) return newtrees else # i == N - @info "elementary_trace: i == N" if N == 2 f′ = FusionTree{I}((), _one, (), (), ()) # or leftone(f.uncoupled[1]) == rightone(f.uncoupled[2]) coeff = sqrtdim(b) @@ -777,8 +763,6 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} vertices_ = TupleTools.front(f.vertices) f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) - @show f_ - @show fs unit = leftone(fs.coupled) for (f_′, coeff) in merge(fs, f_, unit, 1) # coloring gets reversed here, should be the other unit f_′.innerlines[1] == unit || continue # is this one valid? @@ -787,7 +771,6 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) vertices′ = N <= 3 ? () : Base.tail(Base.tail(f_′.vertices)) f′ = FusionTree(uncoupled′, unit, isdual′, inner′, vertices′) # and this one? - @show f′ coeff *= sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) From 4fe82f967b30e8e418a952318e987a12c31c49f7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 8 May 2025 15:00:51 +0200 Subject: [PATCH 17/86] change TensorOperations scalartype promotion to base off field of numbers --- src/tensors/tensoroperations.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 6f59ed699..221213204 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -52,7 +52,8 @@ end function TO.tensoradd_type(TC, A::AbstractTensorMap, ::Index2Tuple{N₁,N₂}, ::Bool) where {N₁,N₂} - Tnew = VectorInterface.promote_add(TC, scalartype(A), sectorscalartype(sectortype(A))) + sst = sectorscalartype(sectortype(A)) + Tnew = sst <: Real ? TC : VectorInterface.promote_add(TC, scalartype(A), sst) M = similarstoragetype(A, Tnew) return tensormaptype(spacetype(A), N₁, N₂, M) end @@ -114,9 +115,10 @@ function TO.tensorcontract_type(TC, A::AbstractTensorMap, ::Index2Tuple, ::Bool, B::AbstractTensorMap, ::Index2Tuple, ::Bool, ::Index2Tuple{N₁,N₂}) where {N₁,N₂} - Tnew = TensorOperations.promote_contract(TC, scalartype(A), - sectorscalartype(sectortype(A)), - scalartype(B), sectorscalartype(sectortype(B))) + sst(x) = sectorscalartype(sectortype(x)) + Tnew = (sst(A) <: Real && sst(B) <: Real) ? TC : + TO.promote_contract(TC, scalartype(A), sst(A), + scalartype(B), sst(B)) M = similarstoragetype(A, Tnew) M == similarstoragetype(B, Tnew) || throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(similarstoragetype(B, Tnew))")) From 55a00c02284597ef66c23bd76d78d9d5669fee34 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 13 Jun 2025 15:08:47 +0200 Subject: [PATCH 18/86] typo in docs --- docs/src/man/categories.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/man/categories.md b/docs/src/man/categories.md index a6efcc85c..3515af5d4 100644 --- a/docs/src/man/categories.md +++ b/docs/src/man/categories.md @@ -225,7 +225,7 @@ morphism from ``I`` to ``V``. To map morphisms from ``\mathrm{Hom}(W,V)`` to ele ``V ⊗ W^*``, i.e. morphisms in ``\mathrm{Hom}(I, V ⊗ W^*)``, we use another morphism ``\mathrm{Hom}(I, W ⊗ W^*)`` which can be considered as the inverse of the evaluation map. -Hence, duality in a monoidal category is defined via an *exact paring*, i.e. two families +Hence, duality in a monoidal category is defined via an *exact pairing*, i.e. two families of non-degenerate morphisms, the evaluation (or co-unit) ``ϵ_V: {}^{∨}V ⊗ V → I`` and the coevaluation (or unit) ``η_V: I → V ⊗ {}^{∨}V`` which satisfy the "snake rules": From 0113f7396a6b9323066ca5480aca03473c1b0fb2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 13 Jun 2025 22:06:37 +0200 Subject: [PATCH 19/86] remove some debug comments --- src/fusiontrees/manipulations.jl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 44edca111..5016dbafe 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -166,8 +166,8 @@ operation is the inverse of `insertat` in the sense that if return f₁, f₂ elseif M === 0 # TODO: evaluate diagrams to see which unit is used here - f₁ = FusionTree{I}((), one(I), (), ()) - uncoupled2 = (one(I), f.uncoupled...) + f₁ = FusionTree{I}((), one(I), (), ()) + uncoupled2 = (one(I), f.uncoupled...) coupled2 = f.coupled isdual2 = (false, f.isdual...) innerlines2 = N >= 2 ? (f.uncoupled[1], f.innerlines...) : () @@ -290,7 +290,8 @@ function bendright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I< # map final splitting vertex (a, b)<-c to fusion vertex a<-(c, dual(b)) @assert N₁ > 0 c = f₁.coupled - a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : + (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) b = f₁.uncoupled[N₁] uncoupled1 = TupleTools.front(f₁.uncoupled) @@ -641,8 +642,8 @@ function planar_trace(f₁::FusionTree{I}, f₂::FusionTree{I}, F₂ = fusiontreetype(I, N₂) newtrees = FusionTreeDict{Tuple{F₁,F₂},T}() for ((f₁′, f₂′), coeff′) in repartition(f₁, f₂, N) - for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) # errors in this planar_trace first - for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) # for a different unit errors here + for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) + for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) coeff = coeff′ * coeff′′ * coeff′′′ if !iszero(coeff) newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff @@ -695,7 +696,7 @@ function planar_trace(f::FusionTree{I,N}, q2′ = let i = i, j = j map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q2, k)) end - for (f′, coeff′) in elementary_trace(f, i) # errors then here + for (f′, coeff′) in elementary_trace(f, i) for (f′′, coeff′′) in planar_trace(f′, q1′, q2′) coeff = coeff′ * coeff′′ if !iszero(coeff) @@ -723,7 +724,7 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} T = sectorscalartype(I) F = fusiontreetype(I, N - 2) newtrees = FusionTreeDict{F,T}() - _one = f.coupled # otherwise ArgumentError above thrown + _one = f.coupled j = mod1(i + 1, N) b = f.uncoupled[i] @@ -731,7 +732,8 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} # if trace is zero, return empty dict (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees if i < N - inner_extended = (leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., f.coupled) + inner_extended = (leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., + f.coupled) a = inner_extended[i] d = inner_extended[i + 2] a == d || return newtrees From 8e8b76087378e84be7f7a09073957a924ad0b21b Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 18 Jun 2025 10:25:02 +0200 Subject: [PATCH 20/86] more rigorous check in `merge` --- src/fusiontrees/manipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 5016dbafe..e5f2241ab 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -231,7 +231,7 @@ function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, return insertat(f, N₁ + 1, f₂) end function merge(f₁::FusionTree{I,0}, f₂::FusionTree{I,0}, c::I, μ) where {I} - isone(c) || # I had this as Nsymbol(f₁.coupled, f₂.coupled, c) > 0, valid? + Nsymbol(f₁.coupled, f₂.coupled, c) == μ == 1 || throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) return fusiontreedict(I)(f₁ => Fsymbol(c, c, c, c, c, c)[1, 1, 1, 1]) end From 78b135e318ff3b1dfd30fa0f343d45bbd7c0b1ce Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 18 Jun 2025 10:48:54 +0200 Subject: [PATCH 21/86] replace `one` evaluation in `split` with `leftone` --- src/fusiontrees/manipulations.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index e5f2241ab..2ad8a5252 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -165,9 +165,8 @@ operation is the inverse of `insertat` in the sense that if f₂ = FusionTree{I}(f.uncoupled, f.coupled, isdual2, f.innerlines, f.vertices) return f₁, f₂ elseif M === 0 - # TODO: evaluate diagrams to see which unit is used here - f₁ = FusionTree{I}((), one(I), (), ()) - uncoupled2 = (one(I), f.uncoupled...) + f₁ = FusionTree{I}((), leftone(f.uncoupled[1]), (), ()) + uncoupled2 = (leftone(f.uncoupled[1]), f.uncoupled...) coupled2 = f.coupled isdual2 = (false, f.isdual...) innerlines2 = N >= 2 ? (f.uncoupled[1], f.innerlines...) : () From 440b42b80cfb97f8603742434b8dda5e5aa3c62d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 18 Jun 2025 10:51:30 +0200 Subject: [PATCH 22/86] typos in docs --- docs/src/man/sectors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/man/sectors.md b/docs/src/man/sectors.md index b0a1bd27d..f771bc792 100644 --- a/docs/src/man/sectors.md +++ b/docs/src/man/sectors.md @@ -894,7 +894,7 @@ for the specific case ``N_1=4`` and ``N_2=3``. We can separate this tree into th part ``(b_1⊗b_2)⊗b_3 → c`` and the splitting part ``c→(((a_1⊗a_2)⊗a_3)⊗a_4)``. Given that the fusion tree can be considered to be the adjoint of a corresponding splitting tree ``c→(b_1⊗b_2)⊗b_3``, we now first consider splitting trees in isolation. A splitting tree -which goes from one coupled sectors ``c`` to ``N`` uncoupled sectors ``a_1``, ``a_2``, …, +which goes from one coupled sector ``c`` to ``N`` uncoupled sectors ``a_1``, ``a_2``, …, ``a_N`` needs ``N-2`` additional internal sector labels ``e_1``, …, ``e_{N-2}``, and, if `FusionStyle(I) isa GenericFusion`, ``N-1`` additional multiplicity labels ``μ_1``, …, ``μ_{N-1}``. We henceforth refer to them as vertex labels, as they are associated with @@ -908,7 +908,7 @@ the orthogonality condition which now forces all internal lines ``e_k`` and vertex labels ``μ_l`` to be the same. There is one subtle remark that we have so far ignored. Within the specific subtypes of -`Sector`, we do not explicitly distinguish between ``R_a^*`` (simply denoted as ``a`^*`` +`Sector`, we do not explicitly distinguish between ``R_a^*`` (simply denoted as ``a^*`` and graphically depicted as an upgoing arrow ``a``) and ``R_{\bar{a}}`` (simply denoted as ``\bar{a}`` and depicted with a downgoing arrow), i.e. between the dual space of ``R_a`` on which the conjugated irrep acts, or the irrep ``\bar{a}`` to which the complex conjugate of From 00825afad7717ec8dbe75296fa1e9aed00bda752 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 18 Jun 2025 16:47:19 +0200 Subject: [PATCH 23/86] typo in `_fusiontree_iterate` --- src/fusiontrees/iterator.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/iterator.jl b/src/fusiontrees/iterator.jl index a225b31b9..7e7106daf 100644 --- a/src/fusiontrees/iterator.jl +++ b/src/fusiontrees/iterator.jl @@ -157,7 +157,7 @@ function _fusiontree_iterate(uncoupledsectors::NTuple{N}, nextout = iterate(outiterN, outstateN) nextout === nothing && return nothing b, outstateN = nextout - vertexiterN = c ⊗ dual(b) + vertexiterN = coupled ⊗ dual(b) nextline = iterate(vertexiterN) end a, vertexstateN = nextline From aad8fc80740905b1547dddf64783c8842ce3e276 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 4 Jul 2025 18:06:22 +0200 Subject: [PATCH 24/86] apply suggestions related to tensor types --- src/tensors/tensoroperations.jl | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 221213204..aab2b67cf 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -52,9 +52,8 @@ end function TO.tensoradd_type(TC, A::AbstractTensorMap, ::Index2Tuple{N₁,N₂}, ::Bool) where {N₁,N₂} - sst = sectorscalartype(sectortype(A)) - Tnew = sst <: Real ? TC : VectorInterface.promote_add(TC, scalartype(A), sst) - M = similarstoragetype(A, Tnew) + I = sectortype(A) + M = similarstoragetype(A, sectorscalartype(I) <: Real ? TC : complex(TC)) return tensormaptype(spacetype(A), N₁, N₂, M) end @@ -115,14 +114,11 @@ function TO.tensorcontract_type(TC, A::AbstractTensorMap, ::Index2Tuple, ::Bool, B::AbstractTensorMap, ::Index2Tuple, ::Bool, ::Index2Tuple{N₁,N₂}) where {N₁,N₂} - sst(x) = sectorscalartype(sectortype(x)) - Tnew = (sst(A) <: Real && sst(B) <: Real) ? TC : - TO.promote_contract(TC, scalartype(A), sst(A), - scalartype(B), sst(B)) - M = similarstoragetype(A, Tnew) - M == similarstoragetype(B, Tnew) || - throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(similarstoragetype(B, Tnew))")) spacetype(A) == spacetype(B) || throw(SpaceMismatch("incompatible space types")) + I = sectortype(A) + M = similarstoragetype(A, sectorscalartype(I) <: Real ? TC : complex(TC)) + MB = similarstoragetype(B, sectorscalartype(I) <: Real ? TC : complex(TC)) + M == MB || throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(MB)")) return tensormaptype(spacetype(A), N₁, N₂, M) end From c0ef01ead2050e7be28c94b24c45e4df38db6826 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 4 Jul 2025 18:12:07 +0200 Subject: [PATCH 25/86] remove excess variable --- src/fusiontrees/manipulations.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 2ad8a5252..b53f60377 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -723,7 +723,6 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} T = sectorscalartype(I) F = fusiontreetype(I, N - 2) newtrees = FusionTreeDict{F,T}() - _one = f.coupled j = mod1(i + 1, N) b = f.uncoupled[i] @@ -770,7 +769,7 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} return newtrees else # i == N if N == 2 - f′ = FusionTree{I}((), _one, (), (), ()) # or leftone(f.uncoupled[1]) == rightone(f.uncoupled[2]) + f′ = FusionTree{I}((), f.coupled, (), (), ()) # or leftone(f.uncoupled[1]) == rightone(f.uncoupled[2]) coeff = sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) From c189284b609a8ff99c61dccd2f2042eb06e7ad4e Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 4 Jul 2025 18:13:37 +0200 Subject: [PATCH 26/86] apply `split` suggestion --- src/fusiontrees/manipulations.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index b53f60377..219a77789 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -165,8 +165,9 @@ operation is the inverse of `insertat` in the sense that if f₂ = FusionTree{I}(f.uncoupled, f.coupled, isdual2, f.innerlines, f.vertices) return f₁, f₂ elseif M === 0 - f₁ = FusionTree{I}((), leftone(f.uncoupled[1]), (), ()) - uncoupled2 = (leftone(f.uncoupled[1]), f.uncoupled...) + u = leftone(f.uncoupled[1]) + f₁ = FusionTree{I}((), u, (), ()) + uncoupled2 = (u, f.uncoupled...) coupled2 = f.coupled isdual2 = (false, f.isdual...) innerlines2 = N >= 2 ? (f.uncoupled[1], f.innerlines...) : () From bdbd2e61cb17ab767a212149f83a24b55e5bb556 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 7 Jul 2025 13:55:03 +0200 Subject: [PATCH 27/86] format --- src/tensors/tensoroperations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index aab2b67cf..6110093b6 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -52,7 +52,7 @@ end function TO.tensoradd_type(TC, A::AbstractTensorMap, ::Index2Tuple{N₁,N₂}, ::Bool) where {N₁,N₂} - I = sectortype(A) + I = sectortype(A) M = similarstoragetype(A, sectorscalartype(I) <: Real ? TC : complex(TC)) return tensormaptype(spacetype(A), N₁, N₂, M) end From e23e092c8e79921ddefc84def517fc1a7ad06559 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 15 Jul 2025 14:28:05 +0200 Subject: [PATCH 28/86] change units in `elementary_trace` --- src/fusiontrees/manipulations.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 219a77789..7f7fa28b2 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -769,8 +769,9 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} push!(newtrees, f′ => coeff) return newtrees else # i == N + unit = leftone(b) if N == 2 - f′ = FusionTree{I}((), f.coupled, (), (), ()) # or leftone(f.uncoupled[1]) == rightone(f.uncoupled[2]) + f′ = FusionTree{I}((), unit, (), (), ()) coeff = sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) @@ -781,14 +782,12 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} uncoupled_ = TupleTools.front(f.uncoupled) inner_ = TupleTools.front(f.innerlines) coupled_ = f.innerlines[end] - @assert coupled_ == dual(b) # isn't this always true at this point? isdual_ = TupleTools.front(f.isdual) vertices_ = TupleTools.front(f.vertices) f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) - unit = leftone(fs.coupled) for (f_′, coeff) in merge(fs, f_, unit, 1) # coloring gets reversed here, should be the other unit - f_′.innerlines[1] == unit || continue # is this one valid? + f_′.innerlines[1] == unit || continue uncoupled′ = Base.tail(Base.tail(f_′.uncoupled)) isdual′ = Base.tail(Base.tail(f_′.isdual)) inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) From 06671f3b70e0b39790c6a4c44528dc21d6e37817 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 16 Jul 2025 08:12:46 +0200 Subject: [PATCH 29/86] import IsingBimod + remove dupe exports --- src/TensorKit.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 06b074ba4..d4248638d 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -15,7 +15,7 @@ export BraidingStyle, SymmetricBraiding, Bosonic, Fermionic, Anyonic, NoBraiding export Trivial, Z2Irrep, Z3Irrep, Z4Irrep, ZNIrrep, U1Irrep, SU2Irrep, CU1Irrep export ProductSector export FermionParity, FermionNumber, FermionSpin -export FibonacciAnyon, IsingAnyon +export FibonacciAnyon, IsingAnyon, IsingBimod export VectorSpace, Field, ElementarySpace # abstract vector spaces export InnerProductStyle, NoInnerProduct, HasInnerProduct, EuclideanInnerProduct @@ -41,7 +41,6 @@ export infimum, supremum, isisomorphic, ismonomorphic, isepimorphic export sectortype, sectors, hassector, Nsymbol, Fsymbol, Rsymbol, Bsymbol, frobeniusschur, twist, otimes export fusiontrees, braid, permute, transpose -export ZNSpace, SU2Irrep, U1Irrep, CU1Irrep # other fusion tree manipulations, should not be exported: # export insertat, split, merge, repartition, artin_braid, # bendleft, bendright, foldleft, foldright, cycleclockwise, cycleanticlockwise From a18f73d8df748ffd2ec33a8f7d1e5e38402d0b81 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 16 Jul 2025 08:13:27 +0200 Subject: [PATCH 30/86] add first requirements for `IsingBimod`-graded vector spaces --- src/spaces/multifusionspace.jl | 84 ++++++++++++++++++++++++++++++++++ src/spaces/vectorspaces.jl | 4 ++ 2 files changed, 88 insertions(+) create mode 100644 src/spaces/multifusionspace.jl diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl new file mode 100644 index 000000000..337d7f3d2 --- /dev/null +++ b/src/spaces/multifusionspace.jl @@ -0,0 +1,84 @@ +# additional interface to deal with IsingBimod Sector +#------------------------------------------------------------------------------ + +# make this a separate module? + +function Base.oneunit(S::Vect[IsingBimod]) + allequal(a.i for a in sectors(S)) && allequal(a.j for a in sectors(S)) || + throw(ArgumentError("sectors of $S are not all equal")) + first(sectors(S)).i == first(sectors(S)).j || + throw(ArgumentError("sectors of $S are non-diagonal")) + sector = one(first(sectors(S))) + return spacetype(S)(sector => 1) +end + +function TensorKit.blocksectors(W::TensorMapSpace{S,N₁,N₂}) where + {S::Vect[IsingBimod],N₁,N₂} + codom = codomain(W) + dom = domain(W) + if N₁ == 0 && N₂ == 0 + return NTuple{2,IsingBimod}(IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) + elseif N₁ == 0 + @assert N₂ != 0 "one of Type IsingBimod doesn't exist" + return filter!(isone, collect(blocksectors(dom))) + elseif N₂ == 0 + @assert N₁ != 0 "one of Type IsingBimod doesn't exist" + return filter!(isone, collect(blocksectors(codom))) + elseif N₂ <= N₁ # keep intersection + return filter!(c -> hasblock(codom, c), collect(blocksectors(dom))) + else + return filter!(c -> hasblock(dom, c), collect(blocksectors(codom))) + end +end + +function rightoneunit(S::Vect[IsingBimod]) + allequal(a.j for a in sectors(S)) || + throw(ArgumentError("sectors of $S do not have the same rightone")) + + allequal(a.i for a in sectors(S)) || + throw(ArgumentError("sectors of $S are not all equal")) + + sector = rightone(first(sectors(S))) + return spacetype(S)(sector => 1) +end + +function leftoneunit(S::Vect[IsingBimod]) + allequal(a.i for a in sectors(S)) || + throw(ArgumentError("sectors of $S do not have the same leftone")) + + allequal(a.j for a in sectors(S)) || + throw(ArgumentError("sectors of $S are not all equal")) + + sector = leftone(first(sectors(S))) + return spacetype(S)(sector => 1) +end + +function TensorKit.insertrightunit(P::ProductSpace{V,N}, ::Val{i}; + conj::Bool=false, + dual::Bool=false) where {i,V::Vect[IsingBimod],N} + i > N && error("cannot insert a sensible right unit onto $P at index $(i+1)") + # possible change to rightone of correct space for N = 0 + u = N > 0 ? rightoneunit(P[i]) : error("no unit object in $P") + if dual + u = TensorKit.dual(u) + end + if conj + u = TensorKit.conj(u) + end + return ProductSpace(TupleTools.insertafter(P.spaces, i, (u,))) +end + +# possible TODO: overwrite defaults at level of HomSpace and TensorMap? +function TensorKit.insertleftunit(P::ProductSpace{V,N}, ::Val{i}; # want no defaults? + conj::Bool=false, + dual::Bool=false) where {i,V::Vect[IsingBimod],N} + i > N && error("cannot insert a sensible left unit onto $P at index $i") # do we want this to error in the diagonal case? + u = N > 0 ? leftoneunit(P[i]) : error("no unit object in $P") + if dual + u = TensorKit.dual(u) + end + if conj + u = TensorKit.conj(u) + end + return ProductSpace(TupleTools.insertafter(P.spaces, i - 1, (u,))) +end diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index cbe5d62b9..a2576e392 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -313,6 +313,10 @@ include("deligne.jl") #------------------------------ include("homspace.jl") +# Additional methods for IsingBimod Sector +# ---------------------------------------- +include("multifusionspace.jl") + # Partial order for vector spaces #--------------------------------- """ From 279a15f67009efc0e3324be15ee11a4ba2097099 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 17 Jul 2025 15:50:41 +0200 Subject: [PATCH 31/86] fix variable expression to not be variable because it's not variable --- src/spaces/multifusionspace.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 337d7f3d2..9a6a0cea6 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -12,8 +12,7 @@ function Base.oneunit(S::Vect[IsingBimod]) return spacetype(S)(sector => 1) end -function TensorKit.blocksectors(W::TensorMapSpace{S,N₁,N₂}) where - {S::Vect[IsingBimod],N₁,N₂} +function TensorKit.blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} codom = codomain(W) dom = domain(W) if N₁ == 0 && N₂ == 0 @@ -53,9 +52,9 @@ function leftoneunit(S::Vect[IsingBimod]) return spacetype(S)(sector => 1) end -function TensorKit.insertrightunit(P::ProductSpace{V,N}, ::Val{i}; +function TensorKit.insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; conj::Bool=false, - dual::Bool=false) where {i,V::Vect[IsingBimod],N} + dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible right unit onto $P at index $(i+1)") # possible change to rightone of correct space for N = 0 u = N > 0 ? rightoneunit(P[i]) : error("no unit object in $P") @@ -69,9 +68,9 @@ function TensorKit.insertrightunit(P::ProductSpace{V,N}, ::Val{i}; end # possible TODO: overwrite defaults at level of HomSpace and TensorMap? -function TensorKit.insertleftunit(P::ProductSpace{V,N}, ::Val{i}; # want no defaults? +function TensorKit.insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no defaults? conj::Bool=false, - dual::Bool=false) where {i,V::Vect[IsingBimod],N} + dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible left unit onto $P at index $i") # do we want this to error in the diagonal case? u = N > 0 ? leftoneunit(P[i]) : error("no unit object in $P") if dual From d7fb9f4324ab0aeddc3cd311c9c010431eacf221 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 18 Jul 2025 14:29:11 +0200 Subject: [PATCH 32/86] add `dim` for `Vect[IsingBimod]` + typos in fields --- src/spaces/multifusionspace.jl | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 9a6a0cea6..c4c78ff3d 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -3,20 +3,28 @@ # make this a separate module? +# possible TODO: zero GradedSpace? + +function dim(V::Vect[IsingBimod]) + T = Base.promote_op(*, Int, real(sectorscalartype(sectortype(V)))) + return reduce(+, dim(V, c) * dim(c) for c in sectors(V); + init=zero(T)) +end + function Base.oneunit(S::Vect[IsingBimod]) - allequal(a.i for a in sectors(S)) && allequal(a.j for a in sectors(S)) || + allequal(a.row for a in sectors(S)) && allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S are not all equal")) - first(sectors(S)).i == first(sectors(S)).j || + first(sectors(S)).row == first(sectors(S)).col || throw(ArgumentError("sectors of $S are non-diagonal")) sector = one(first(sectors(S))) return spacetype(S)(sector => 1) end -function TensorKit.blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} +function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} codom = codomain(W) dom = domain(W) if N₁ == 0 && N₂ == 0 - return NTuple{2,IsingBimod}(IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) + return (IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) elseif N₁ == 0 @assert N₂ != 0 "one of Type IsingBimod doesn't exist" return filter!(isone, collect(blocksectors(dom))) @@ -31,10 +39,10 @@ function TensorKit.blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) w end function rightoneunit(S::Vect[IsingBimod]) - allequal(a.j for a in sectors(S)) || + allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same rightone")) - allequal(a.i for a in sectors(S)) || + allequal(a.row for a in sectors(S)) || throw(ArgumentError("sectors of $S are not all equal")) sector = rightone(first(sectors(S))) @@ -42,17 +50,17 @@ function rightoneunit(S::Vect[IsingBimod]) end function leftoneunit(S::Vect[IsingBimod]) - allequal(a.i for a in sectors(S)) || + allequal(a.row for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same leftone")) - allequal(a.j for a in sectors(S)) || + allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S are not all equal")) sector = leftone(first(sectors(S))) return spacetype(S)(sector => 1) end -function TensorKit.insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; +function insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; conj::Bool=false, dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible right unit onto $P at index $(i+1)") @@ -68,7 +76,7 @@ function TensorKit.insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i} end # possible TODO: overwrite defaults at level of HomSpace and TensorMap? -function TensorKit.insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no defaults? +function insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no defaults? conj::Bool=false, dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible left unit onto $P at index $i") # do we want this to error in the diagonal case? From 95215743c5538bd4c24611bbcd8fc8244bf4deae Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 18 Jul 2025 14:29:24 +0200 Subject: [PATCH 33/86] start of multifusion tests --- test/multifusion.jl | 95 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 test/multifusion.jl diff --git a/test/multifusion.jl b/test/multifusion.jl new file mode 100644 index 000000000..842b1a120 --- /dev/null +++ b/test/multifusion.jl @@ -0,0 +1,95 @@ +I = IsingBimod +Istr = TensorKit.type_repr(I) + +println("------------------------------------") +println("Multifusion tests for $Istr") +println("------------------------------------") +ti = time() + +@timedtestset "ElementarySpace: $(TensorKit.type_repr(Vect[I]))" begin + gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) + + V = GradedSpace(gen) + @test eval(Meta.parse(TensorKit.type_repr(typeof(V)))) == typeof(V) + @test eval(Meta.parse(sprint(show, V))) == V + @test eval(Meta.parse(sprint(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)) + @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') + @test V == GradedSpace(reverse(collect(gen))...) + @test eval(Meta.parse(sprint(show, V))) == V + @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) + + # space with a single sector + # W = @constinferred GradedSpace(one(I) => 1) + Wleft = @constinferred Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 1) + Wright = @constinferred Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1) + WM = @constinferred Vect[I](I(1, 2, 0) => 1) + WMop = @constinferred Vect[I](I(2, 1, 0) => 1) + @test W == GradedSpace(one(I) => 1, randsector(I) => 0) + @test @constinferred(oneunit(V)) == W == oneunit(typeof(V)) + + # randsector never returns trivial sector, so this cannot error + @test_throws ArgumentError GradedSpace(one(I) => 1, randsector(I) => 0, one(I) => 3) + @test eval(Meta.parse(sprint(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(TensorKit.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)) + + @test @constinferred(⊕(V, zero(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, oneunit(V))) == + # Vect[I](c => isone(c) + dim(V, c) for c in sectors(V)) + # @test @constinferred(fuse(V, oneunit(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)) + # @test infimum(V, GradedSpace(one(I) => 3)) == GradedSpace(one(I) => 2) + @test_throws SpaceMismatch (⊕(V, V')) +end + + +tf = time() +printstyled("Finished multifusion tests in ", + string(round(tf - ti; sigdigits=3)), + " seconds."; bold=true, color=Base.info_color()) +println() \ No newline at end of file From 19ba22509cd1a789134cb655d443ccc201569720 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 23 Jul 2025 14:31:01 -0400 Subject: [PATCH 34/86] finish up multifusion space tests --- test/multifusion.jl | 56 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 842b1a120..21e66d41f 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -38,12 +38,15 @@ ti = time() Wright = @constinferred Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1) WM = @constinferred Vect[I](I(1, 2, 0) => 1) WMop = @constinferred Vect[I](I(2, 1, 0) => 1) - @test W == GradedSpace(one(I) => 1, randsector(I) => 0) - @test @constinferred(oneunit(V)) == W == oneunit(typeof(V)) - # randsector never returns trivial sector, so this cannot error - @test_throws ArgumentError GradedSpace(one(I) => 1, randsector(I) => 0, one(I) => 3) - @test eval(Meta.parse(sprint(show, W))) == W + @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) + @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) + @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) + @test @constinferred(rightoneunit(⊕(Wright, WMop))) == oneunit(Wright) + + @test_throws ArgumentError oneunit(I) + @test_throws ArgumentError oneunit(WM) + @test_throws ArgumentError oneunit(WMop) @test isa(V, VectorSpace) @test isa(V, ElementarySpace) @@ -52,7 +55,7 @@ ti = time() @test isa(V, GradedSpace) @test isa(V, GradedSpace{I}) @test @constinferred(dual(V)) == @constinferred(conj(V)) == - @constinferred(adjoint(V)) != V + @constinferred(adjoint(V)) != V @test @constinferred(field(V)) == ℂ @test @constinferred(sectortype(V)) == I slist = @constinferred sectors(V) @@ -64,9 +67,31 @@ ti = time() @test @constinferred(⊕(V, zero(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, oneunit(V))) == - # Vect[I](c => isone(c) + dim(V, c) for c in sectors(V)) - # @test @constinferred(fuse(V, oneunit(V))) == V + + for W in [Wleft, Wright] + @test @constinferred(⊕(W, oneunit(W))) == + Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) + @test @constinferred(fuse(W, oneunit(W))) == W + end + + # sensible direct sums and fuses + @test @constinferred(⊕(Wleft, WM)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(1, 1, 0)) + @test @constinferred(⊕(Wright, WMop)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(2, 2, 0)) + @test @constinferred(⊕(Wright, WM)) == + Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(2, 2, 0)) + @test @constinferred(⊕(Wleft, WMop)) == + Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(1, 1, 0)) + @test @constinferred(fuse(Wleft, WM)) == Vect[I](I(1, 2, 0) => 2) + @test @constinferred(fuse(Wright, WMop)) == Vect[I](I(2, 1, 0) => 2) + + # less sensible direct sums and fuses + @test @constinferred(⊕(Wleft, Wright)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == rightone(c)) + @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == + Vect[I](c => 0 for c in sectors(V)) + d = Dict{I,Int}() for a in sectors(V), b in sectors(V) for c in a ⊗ b @@ -75,7 +100,7 @@ ti = time() end @test @constinferred(fuse(V, V)) == GradedSpace(d) @test @constinferred(flip(V)) == - Vect[I](conj(c) => dim(V, c) for c in sectors(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 @@ -83,13 +108,18 @@ ti = time() @test V == @constinferred infimum(V, ⊕(V, V)) @test V ≺ ⊕(V, V) @test !(V ≻ ⊕(V, V)) - # @test infimum(V, GradedSpace(one(I) => 3)) == GradedSpace(one(I) => 2) + @test infimum(V, GradedSpace(I(1, 1, 0) => 3)) == + GradedSpace(I(1, 1, 0) => 2) + @test infimum(V, GradedSpace(I(1, 2, 0) => 6)) == + GradedSpace(I(1, 2, 0) => 5) + for W in [WM, WMop, Wright] + @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) + end @test_throws SpaceMismatch (⊕(V, V')) end - tf = time() printstyled("Finished multifusion tests in ", string(round(tf - ti; sigdigits=3)), " seconds."; bold=true, color=Base.info_color()) -println() \ No newline at end of file +println() From 5fef11eb2519342655f4a566bf94d3b13f70dd8f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 23 Jul 2025 14:32:32 -0400 Subject: [PATCH 35/86] remove equal sectors check in `left/rightoneunit` --- src/spaces/multifusionspace.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index c4c78ff3d..8d6b7711a 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -42,9 +42,6 @@ function rightoneunit(S::Vect[IsingBimod]) allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same rightone")) - allequal(a.row for a in sectors(S)) || - throw(ArgumentError("sectors of $S are not all equal")) - sector = rightone(first(sectors(S))) return spacetype(S)(sector => 1) end @@ -53,9 +50,6 @@ function leftoneunit(S::Vect[IsingBimod]) allequal(a.row for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same leftone")) - allequal(a.col for a in sectors(S)) || - throw(ArgumentError("sectors of $S are not all equal")) - sector = leftone(first(sectors(S))) return spacetype(S)(sector => 1) end From 2d9663b39667906f0f6d058c1eee521af51813f3 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 24 Jul 2025 16:36:44 -0400 Subject: [PATCH 36/86] add `left/rightoneunit` for any `GradedSpace` --- src/spaces/gradedspace.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 7aa746076..f7701083e 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -132,6 +132,23 @@ function Base.axes(V::GradedSpace{I}, c::I) where {I<:Sector} end Base.oneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 1) + +""" + leftoneunit(V::Type{<:GradedSpace{I}}) where {I<:Sector} -> GradedSpace{I} + +Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial +one-dimensional space consisting of the left unit of the underlying `Sector` `I`. +""" +leftoneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(leftone(I) => 1) + +""" + rightoneunit(V::Type{<:GradedSpace{I}}) where {I<:Sector} -> GradedSpace{I} + +Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial +one-dimensional space consisting of the right unit of the underlying `Sector` `I`. +""" +rightoneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(rightone(I) => 1) + Base.zero(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 0) # TODO: the following methods can probably be implemented more efficiently for From 00615884ebde245971946916c861028724861b9b Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 25 Jul 2025 14:31:52 -0400 Subject: [PATCH 37/86] start on `DiagonalTensor` tests --- test/multifusion.jl | 504 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 399 insertions(+), 105 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 21e66d41f..667688d27 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -6,118 +6,412 @@ println("Multifusion tests for $Istr") println("------------------------------------") ti = time() -@timedtestset "ElementarySpace: $(TensorKit.type_repr(Vect[I]))" begin - gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) - - V = GradedSpace(gen) - @test eval(Meta.parse(TensorKit.type_repr(typeof(V)))) == typeof(V) - @test eval(Meta.parse(sprint(show, V))) == V - @test eval(Meta.parse(sprint(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)) - @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') - @test V == GradedSpace(reverse(collect(gen))...) - @test eval(Meta.parse(sprint(show, V))) == V - @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) - - # space with a single sector - # W = @constinferred GradedSpace(one(I) => 1) - Wleft = @constinferred Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 1) - Wright = @constinferred Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1) - WM = @constinferred Vect[I](I(1, 2, 0) => 1) - WMop = @constinferred Vect[I](I(2, 1, 0) => 1) - - @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) - @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) - @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) - @test @constinferred(rightoneunit(⊕(Wright, WMop))) == oneunit(Wright) - - @test_throws ArgumentError oneunit(I) - @test_throws ArgumentError oneunit(WM) - @test_throws ArgumentError oneunit(WMop) - - @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(TensorKit.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)) - - @test @constinferred(⊕(V, zero(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)) - - for W in [Wleft, Wright] - @test @constinferred(⊕(W, oneunit(W))) == - Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) - @test @constinferred(fuse(W, oneunit(W))) == W +@timedtestset "Multifusion spaces " verbose = true begin + @timedtestset "GradedSpace: $(TensorKit.type_repr(Vect[I]))" begin + gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) + + V = GradedSpace(gen) + @test eval(Meta.parse(TensorKit.type_repr(typeof(V)))) == typeof(V) + @test eval(Meta.parse(sprint(show, V))) == V + @test eval(Meta.parse(sprint(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)) + @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') + @test V == GradedSpace(reverse(collect(gen))...) + @test eval(Meta.parse(sprint(show, V))) == V + @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) + + # space with a single sector + Wleft = @constinferred Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 1) + Wright = @constinferred Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1) + WM = @constinferred Vect[I](I(1, 2, 0) => 1) + WMop = @constinferred Vect[I](I(2, 1, 0) => 1) + + @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) + @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) + @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) + @test @constinferred(rightoneunit(⊕(Wright, WMop))) == oneunit(Wright) + + @test_throws ArgumentError oneunit(I) + @test_throws ArgumentError oneunit(WM) + @test_throws ArgumentError oneunit(WMop) + + @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(TensorKit.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)) + + @test @constinferred(⊕(V, zero(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)) + + for W in [Wleft, Wright] + @test @constinferred(⊕(W, oneunit(W))) == + Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) + @test @constinferred(fuse(W, oneunit(W))) == W + end + + # sensible direct sums and fuses + @test @constinferred(⊕(Wleft, WM)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(1, 1, 0)) + @test @constinferred(⊕(Wright, WMop)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(2, 2, 0)) + @test @constinferred(⊕(Wright, WM)) == + Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(2, 2, 0)) + @test @constinferred(⊕(Wleft, WMop)) == + Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(1, 1, 0)) + @test @constinferred(fuse(Wleft, WM)) == Vect[I](I(1, 2, 0) => 2) + @test @constinferred(fuse(Wright, WMop)) == Vect[I](I(2, 1, 0) => 2) + + # less sensible direct sums and fuses + @test @constinferred(⊕(Wleft, Wright)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == rightone(c)) + @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == + Vect[I](c => 0 for c in sectors(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)) + @test infimum(V, GradedSpace(I(1, 1, 0) => 3)) == + GradedSpace(I(1, 1, 0) => 2) + @test infimum(V, GradedSpace(I(1, 2, 0) => 6)) == + GradedSpace(I(1, 2, 0) => 5) + for W in [WM, WMop, Wright] + @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) + end + @test_throws SpaceMismatch (⊕(V, V')) + end + + @timedtestset "HomSpace" begin + # for (V1, V2, V3, V4, V5) in (Vtr, Vℤ₃, VSU₂) + # W = TensorKit.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 eval(Meta.parse(sprint(show, W))) == W + # @test eval(Meta.parse(sprint(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] == V3' + # @test W[4] == V4' + # @test W[5] == V5' + # @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') + # @test W == deepcopy(W) + # @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) + # @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') + # @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') + # @test (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ oneunit(V5)) == + # @constinferred(insertleftunit(W)) == + # @constinferred(insertrightunit(W)) + # @test @constinferred(removeunit(insertleftunit(W), $(numind(W) + 1))) == W + # @test (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ oneunit(V5)') == + # @constinferred(insertleftunit(W; conj=true)) == + # @constinferred(insertrightunit(W; conj=true)) + # @test (oneunit(V1) ⊗ V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5) == + # @constinferred(insertleftunit(W, 1)) == + # @constinferred(insertrightunit(W, 0)) + # @test (V1 ⊗ V2 ⊗ oneunit(V1) ← V3 ⊗ V4 ⊗ V5) == + # @constinferred(insertrightunit(W, 2)) + # @test (V1 ⊗ V2 ← oneunit(V1) ⊗ V3 ⊗ V4 ⊗ V5) == + # @constinferred(insertleftunit(W, 3)) + # @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W + # @test @constinferred(insertrightunit(one(V1) ← V1, 0)) == (oneunit(V1) ← V1) + # @test_throws BoundsError insertleftunit(one(V1) ← V1, 0) + # end + end +end + +@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true begin + +end + +multifusion_diagspacelist = (Vect[I](values(I)[k] => 1 for k in 1:length(values(I)))) +# TODO: more examples necessary? + +@testset "DiagonalTensor with domain $V" for V in multifusion_diagspacelist + @timedtestset "Basic properties and algebra" begin + for T in (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)) ≈ 2 * norm(t) + + @test norm(zerovector!(t)) == 0 + @test norm(one!(t)) ≈ sqrt(dim(V)) + @test one!(t) == id(V) + @test norm(one!(t) - id(V)) == 0 + + 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 + + @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 + @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) + # d = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) + t = TensorMap(d) + v = Vect[I](I(2,1,0)=>1) + A = randn(ComplexF64, v ⊗ v' ⊗ v, v) + B = randn(ComplexF64, v ⊗ v' ⊗ v, v ⊗ v') + # A = randn(ComplexF64, V ⊗ V' ⊗ V, V) + # B = randn(ComplexF64, V ⊗ V' ⊗ V, V ⊗ V') + + @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] # don't work for modules + @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 "Factorization" begin + for T in (Float32, ComplexF64) + t = DiagonalTensorMap(rand(T, reduceddim(V)), V) + @testset "eig" begin + D, W = @constinferred eig(t) + @test t * W ≈ W * D + t2 = t + t' + D2, V2 = @constinferred eigh(t2) + VdV2 = V2' * V2 + @test VdV2 ≈ one(VdV2) + @test t2 * V2 ≈ V2 * D2 + + @test rank(D) ≈ rank(t) + @test cond(D) ≈ cond(t) + @test all(((s, t),) -> isapprox(s, t), + zip(values(LinearAlgebra.eigvals(D)), + values(LinearAlgebra.eigvals(t)))) + end + @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QL()) + Q, R = @constinferred leftorth(t; alg=alg) + QdQ = Q' * Q + @test QdQ ≈ one(QdQ) + @test Q * R ≈ t + if alg isa Polar + @test isposdef(R) + end + end + @testset "rightorth with $alg" for alg in (TensorKit.RQ(), TensorKit.LQ()) + L, Q = @constinferred rightorth(t; alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ t + if alg isa Polar + @test isposdef(L) + end + end + @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + U, S, Vᴴ = @constinferred tsvd(t; alg=alg) + UdU = U' * U + @test UdU ≈ one(UdU) + VdV = Vᴴ * Vᴴ' + @test VdV ≈ one(VdV) + @test U * S * Vᴴ ≈ t - # sensible direct sums and fuses - @test @constinferred(⊕(Wleft, WM)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(1, 1, 0)) - @test @constinferred(⊕(Wright, WMop)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(2, 2, 0)) - @test @constinferred(⊕(Wright, WM)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(2, 2, 0)) - @test @constinferred(⊕(Wleft, WMop)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(1, 1, 0)) - @test @constinferred(fuse(Wleft, WM)) == Vect[I](I(1, 2, 0) => 2) - @test @constinferred(fuse(Wright, WMop)) == Vect[I](I(2, 1, 0) => 2) - - # less sensible direct sums and fuses - @test @constinferred(⊕(Wleft, Wright)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == rightone(c)) - @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == - Vect[I](c => 0 for c in sectors(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) + @test rank(S) ≈ rank(t) + @test cond(S) ≈ cond(t) + @test all(((s, t),) -> isapprox(s, t), + zip(values(LinearAlgebra.svdvals(S)), + values(LinearAlgebra.svdvals(t)))) + end 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)) - @test infimum(V, GradedSpace(I(1, 1, 0) => 3)) == - GradedSpace(I(1, 1, 0) => 2) - @test infimum(V, GradedSpace(I(1, 2, 0) => 6)) == - GradedSpace(I(1, 2, 0) => 5) - for W in [WM, WMop, Wright] - @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) + @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 - @test_throws SpaceMismatch (⊕(V, V')) end + + + + + +########## tf = time() printstyled("Finished multifusion tests in ", string(round(tf - ti; sigdigits=3)), From abb630154ff1d8f98f758cb8ac92f9199507608d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 25 Jul 2025 16:00:17 -0400 Subject: [PATCH 38/86] start of fusion tree tests --- test/multifusion.jl | 308 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 301 insertions(+), 7 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 667688d27..8dc40e5c5 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -160,13 +160,312 @@ ti = time() end @timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true begin + N = 6 + C0, C1, D0, D1, M, Mop = I(1,1,0), I(1,1,1), I(2,2,0), I(2,2,1), I(1,2,0), I(2,1,0) + out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? + isdual = ntuple(n -> rand(Bool), N) + in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out + numtrees = length(fusiontrees(out, in, isdual)) # will be 1 + @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) + + 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 $Istr: printing" begin + @test eval(Meta.parse(sprint(show, f))) == f + end + @testset "Fusion tree $Istr: constructor properties" for u in (C0, D0) + @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) + + # SimpleFusion + errstr = "fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`" + @test_throws errstr FusionTree((), u, ()) + @test_throws errstr FusionTree((u,), u, (false,)) + @test_throws errstr FusionTree((u, u), u, (false, false)) + @test_throws errstr FusionTree((u, u, u), u) + @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree required here + end + # CONTINUE HERE + + @testset "Fusion tree $Istr: insertat" begin + N = 4 + out2 = ntuple(n -> randsector(I), N) + in2 = rand(collect(⊗(out2...))) + isdual2 = ntuple(n -> rand(Bool), N) + f2 = rand(collect(fusiontrees(out2, in2, isdual2))) + for i in 1:N + out1 = ntuple(n -> randsector(I), N) + out1 = Base.setindex(out1, in2, i) + 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 + + f1a, f1b = @constinferred TK.split(f1, $i) + @test length(TK.insertat(f1b, 1, f1a)) == 1 + @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) + + levels = ntuple(identity, N) + function _reinsert_partial_tree(t, f) + (t′, c′) = first(TK.insertat(t, 1, f)) + @test c′ == one(c′) + return t′ + end + braid_i_to_1 = braid(f1, levels, (i, (1:(i - 1))..., ((i + 1):N)...)) + trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) + trees3 = empty(trees2) + p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) + levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) + for (t, coeff) in trees2 + for (t′, coeff′) in braid(t, levels, p) + trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ + end + end + for (t, coeff) in trees3 + coeff′ = get(trees, t, zero(coeff)) + @test isapprox(coeff′, coeff; atol=1e-12, rtol=1e-12) + end + end + end + # no planar trace tests + @testset "Fusion tree $Istr: elementy artin braid" begin + N = length(out) + isdual = ntuple(n -> rand(Bool), N) + for in in ⊗(out...) + for i in 1:(N - 1) + for f in fusiontrees(out, in, isdual) + d1 = @constinferred TK.artin_braid(f, i) + @test norm(values(d1)) ≈ 1 + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, i; inv=true) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + for (f2, coeff2) in d2 + if f2 == f + @test coeff2 ≈ 1 + else + @test isapprox(coeff2, 0; atol=1e-12, rtol=1e-12) + end + end + end + end + end + + f = rand(collect(it)) + d1 = TK.artin_braid(f, 2) + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, 3) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + d1 = d2 + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + d1 = d2 + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + d1 = d2 + for (f1, coeff1) in d1 + if f1 == f + @test coeff1 ≈ 1 + else + @test isapprox(coeff1, 0; atol=1e-12, rtol=1e-12) + end + end + end + @testset "Fusion tree $Istr: braiding and permuting" begin + f = rand(collect(fusiontrees(out, in, isdual))) + p = tuple(randperm(N)...) + ip = invperm(p) + + levels = ntuple(identity, N) + d = @constinferred braid(f, levels, p) + d2 = Dict{typeof(f),valtype(d)}() + levels2 = p + for (f2, coeff) in d + for (f1, coeff2) in braid(f2, levels2, ip) + d2[f1] = get(d2, f1, zero(coeff)) + coeff2 * coeff + end + end + for (f1, coeff2) in d2 + if f1 == f + @test coeff2 ≈ 1 + else + @test isapprox(coeff2, 0; atol=1e-12, rtol=1e-12) + end + end + end + + @testset "Fusion tree $Istr: merging" begin + N = 3 + out1 = ntuple(n -> randsector(I), N) + in1 = rand(collect(⊗(out1...))) + f1 = rand(collect(fusiontrees(out1, in1))) + out2 = ntuple(n -> randsector(I), N) + in2 = rand(collect(⊗(out2...))) + f2 = rand(collect(fusiontrees(out2, in2))) + + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) + if !(FusionStyle(I) isa GenericFusion) + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) + @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, μ)) + + for c in in1 ⊗ in2 + R = Rsymbol(in1, in2, c) + for μ in 1:Nsymbol(in1, in2, c) + trees1 = TK.merge(f1, f2, c, μ) + + # test merge and braid interplay + trees2 = Dict{keytype(trees1),complex(valtype(trees1))}() + trees3 = Dict{keytype(trees1),complex(valtype(trees1))}() + for ν in 1:Nsymbol(in2, in1, c) + for (t, coeff) in TK.merge(f2, f1, c, ν) + trees2[t] = get(trees2, t, zero(valtype(trees2))) + coeff * R[μ, ν] + end + end + perm = ((N .+ (1:N))..., (1:N)...) + levels = ntuple(identity, 2 * N) + for (t, coeff) in trees1 + for (t′, coeff′) in braid(t, levels, perm) + trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ + end + end + for (t, coeff) in trees3 + coeff′ = get(trees2, t, zero(coeff)) + @test isapprox(coeff, coeff′; atol=1e-12, rtol=1e-12) + end + end + end + end + + N = 4 + out = ntuple(n -> randsector(I), N) + numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) + while !(0 < numtrees < 100) + out = ntuple(n -> randsector(I), 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)))) + + @testset "Double fusion tree $Istr: repartioning" begin + for n in 0:(2 * N) + d = @constinferred TK.repartition(f1, f2, $n) + @test dim(incoming) ≈ + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)),valtype(d)}() + for ((f1′, f2′), coeff) in d + for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) + d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff + end + end + for ((f1′, f2′), coeff2) in d2 + if f1 == f1′ && f2 == f2′ + @test coeff2 ≈ 1 + else + @test isapprox(coeff2, 0; atol=1e-12, rtol=1e-12) + end + end + end + end + # no double fusion tree permutation tests + @testset "Double fusion tree $Istr: 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)] + + d = @constinferred transpose(f1, f2, p1, p2) + @test dim(incoming) ≈ + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)),valtype(d)}() + for ((f1′, f2′), coeff) in d + d′ = transpose(f1′, f2′, ip1, ip2) + for ((f1′′, f2′′), coeff2) in d′ + d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff + end + end + for ((f1′, f2′), coeff2) in d2 + if f1 == f1′ && f2 == f2′ + @test coeff2 ≈ 1 + else + @test abs(coeff2) < 1e-12 + end + end + + end + end + @testset "Double fusion tree $Istr: planar trace" begin + d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) + f1front, = TK.split(f1, N - 1) + T = typeof(Fsymbol(one(I), one(I), one(I), one(I), one(I), one(I))[1, 1, 1, 1]) + 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) < 1e-12 + end + end + end + TensorKit.empty_globalcaches!() end -multifusion_diagspacelist = (Vect[I](values(I)[k] => 1 for k in 1:length(values(I)))) +V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) # TODO: more examples necessary? -@testset "DiagonalTensor with domain $V" for V in multifusion_diagspacelist +@testset "DiagonalTensor with domain $V" begin @timedtestset "Basic properties and algebra" begin for T in (Float32, Float64, ComplexF32, ComplexF64, BigFloat) # constructors @@ -406,11 +705,6 @@ multifusion_diagspacelist = (Vect[I](values(I)[k] => 1 for k in 1:length(values( end end - - - - - ########## tf = time() printstyled("Finished multifusion tests in ", From 64982d625719fcc3b2d2aa328173d206cd0315a0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 25 Jul 2025 16:00:36 -0400 Subject: [PATCH 39/86] custom `FusionTree` constructor for only uncoupled provided --- src/spaces/multifusionspace.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 8d6b7711a..6b64318fd 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -83,3 +83,15 @@ function insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no end return ProductSpace(TupleTools.insertafter(P.spaces, i - 1, (u,))) end + +# is this even necessary? can let it error at fusiontrees.jl:93 from the one(IsingBimod) call +# but these errors are maybe more informative +function FusionTree(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) + coupled = collect(⊗(uncoupled...)) + @show coupled + if length(coupled) == 0 # illegal fusion somewhere + throw(ArgumentError("Forbidden fusion with uncoupled sectors $uncoupled")) + else # allowed fusions require inner lines + error("fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`") + end +end \ No newline at end of file From 520f81d292b225029b85473b26912d31d2673829 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 16:13:06 +0200 Subject: [PATCH 40/86] finish fusion tree and `DiagonalTensor` tests --- test/multifusion.jl | 369 +++++++++++++++++++------------------------- 1 file changed, 160 insertions(+), 209 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 8dc40e5c5..24e9389ee 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -14,19 +14,19 @@ ti = time() @test eval(Meta.parse(TensorKit.type_repr(typeof(V)))) == typeof(V) @test eval(Meta.parse(sprint(show, V))) == V @test eval(Meta.parse(sprint(show, V'))) == V' - @test V' == GradedSpace(gen; dual=true) + @test V' == GradedSpace(gen; dual = true) @test V == @constinferred GradedSpace(gen...) - @test V' == @constinferred GradedSpace(gen...; dual=true) + @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(tuple(gen...); dual = true) @test V == @constinferred GradedSpace(Dict(gen)) - @test V' == @constinferred GradedSpace(Dict(gen); dual=true) + @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; dual = true) @test V == @constinferred Vect[I](gen...) - @test V' == @constinferred Vect[I](gen...; dual=true) + @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 Vect[I](Dict(gen); dual = true) @test V == @constinferred typeof(V)(c => dim(V, c) for c in sectors(V)) @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') @test V == GradedSpace(reverse(collect(gen))...) @@ -42,7 +42,9 @@ ti = time() @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) - @test @constinferred(rightoneunit(⊕(Wright, WMop))) == oneunit(Wright) + @test @constinferred(leftoneunit(⊕(Wright, WMop))) == oneunit(Wright) + @test @constinferred(rightoneunit(⊕(Wright, WM))) == oneunit(Wright) + @test @constinferred(rightoneunit(⊕(Wleft, WMop))) == oneunit(Wleft) @test_throws ArgumentError oneunit(I) @test_throws ArgumentError oneunit(WM) @@ -92,7 +94,7 @@ ti = time() @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == Vect[I](c => 0 for c in sectors(V)) - d = Dict{I,Int}() + 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) @@ -161,7 +163,7 @@ end @timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true begin N = 6 - C0, C1, D0, D1, M, Mop = I(1,1,0), I(1,1,1), I(2,2,0), I(2,2,1), I(1,2,0), I(2,1,0) + C0, C1, D0, D1, M, Mop = I(1, 1, 0), I(1, 1, 1), I(2, 2, 0), I(2, 2, 1), I(1, 2, 0), I(2, 1, 0) out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? isdual = ntuple(n -> rand(Bool), N) in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out @@ -181,13 +183,19 @@ end @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)) + @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), (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)) @@ -202,21 +210,45 @@ end @test_throws errstr FusionTree((u,), u, (false,)) @test_throws errstr FusionTree((u, u), u, (false, false)) @test_throws errstr FusionTree((u, u, u), u) - @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree required here + @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree constructor required here end - + # CONTINUE HERE @testset "Fusion tree $Istr: insertat" begin N = 4 - out2 = ntuple(n -> randsector(I), N) - in2 = rand(collect(⊗(out2...))) + in2 = nothing # attempt at not hard-coding + out2 = nothing + while in2 === nothing + out2 = ntuple(n -> randsector(I), N) + try + in2 = rand(collect(⊗(out2...))) + catch e + if isa(e, ArgumentError) + in2 = nothing + else + rethrow(e) + end + end + end isdual2 = ntuple(n -> rand(Bool), N) f2 = rand(collect(fusiontrees(out2, in2, isdual2))) for i in 1:N - out1 = ntuple(n -> randsector(I), N) - out1 = Base.setindex(out1, in2, i) - in1 = rand(collect(⊗(out1...))) + in1 = nothing + out1 = nothing + while in1 === nothing + try + out1 = ntuple(n -> randsector(I), N) + out1 = Base.setindex(out1, in2, i) + in1 = rand(collect(⊗(out1...))) + catch e + if isa(e, ArgumentError) + in1 = nothing + else + rethrow(e) + end + end + end isdual1 = ntuple(n -> rand(Bool), N) isdual1 = Base.setindex(isdual1, false, i) f1 = rand(collect(fusiontrees(out1, in1, isdual1))) @@ -228,56 +260,18 @@ end @test length(TK.insertat(f1b, 1, f1a)) == 1 @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) - levels = ntuple(identity, N) - function _reinsert_partial_tree(t, f) - (t′, c′) = first(TK.insertat(t, 1, f)) - @test c′ == one(c′) - return t′ - end - braid_i_to_1 = braid(f1, levels, (i, (1:(i - 1))..., ((i + 1):N)...)) - trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) - trees3 = empty(trees2) - p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) - levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) - for (t, coeff) in trees2 - for (t′, coeff′) in braid(t, levels, p) - trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ - end - end - for (t, coeff) in trees3 - coeff′ = get(trees, t, zero(coeff)) - @test isapprox(coeff′, coeff; atol=1e-12, rtol=1e-12) - end + # no braid tests for non-hardcoded example end end # no planar trace tests - @testset "Fusion tree $Istr: elementy artin braid" begin + @testset "Fusion tree $Istr: elementary artin braid" begin N = length(out) isdual = ntuple(n -> rand(Bool), N) - for in in ⊗(out...) - for i in 1:(N - 1) - for f in fusiontrees(out, in, isdual) - d1 = @constinferred TK.artin_braid(f, i) - @test norm(values(d1)) ≈ 1 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, i; inv=true) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - for (f2, coeff2) in d2 - if f2 == f - @test coeff2 ≈ 1 - else - @test isapprox(coeff2, 0; atol=1e-12, rtol=1e-12) - end - end - end - end - end + # no general artin braid test - f = rand(collect(it)) - d1 = TK.artin_braid(f, 2) + # not sure how useful this test is, it does the trivial braiding + f = rand(collect(it)) # in this case the 1 tree + d1 = TK.artin_braid(f, 2) # takes a unit C0 d2 = empty(d1) for (f1, coeff1) in d1 for (f2, coeff2) in TK.artin_braid(f1, 3) @@ -287,14 +281,14 @@ end d1 = d2 d2 = empty(d1) for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) + for (f2, coeff2) in TK.artin_braid(f1, 3; inv = true) d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 end end d1 = d2 d2 = empty(d1) for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) + for (f2, coeff2) in TK.artin_braid(f1, 2; inv = true) d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 end end @@ -303,96 +297,57 @@ end if f1 == f @test coeff1 ≈ 1 else - @test isapprox(coeff1, 0; atol=1e-12, rtol=1e-12) + @test isapprox(coeff1, 0; atol = 1.0e-12, rtol = 1.0e-12) end end end - @testset "Fusion tree $Istr: braiding and permuting" begin - f = rand(collect(fusiontrees(out, in, isdual))) - p = tuple(randperm(N)...) - ip = invperm(p) - - levels = ntuple(identity, N) - d = @constinferred braid(f, levels, p) - d2 = Dict{typeof(f),valtype(d)}() - levels2 = p - for (f2, coeff) in d - for (f1, coeff2) in braid(f2, levels2, ip) - d2[f1] = get(d2, f1, zero(coeff)) + coeff2 * coeff - end - end - for (f1, coeff2) in d2 - if f1 == f - @test coeff2 ≈ 1 - else - @test isapprox(coeff2, 0; atol=1e-12, rtol=1e-12) + # no braiding and permuting test + @testset "Fusion tree $Istr: merging" begin + N = 3 + out1, in1, out2, in2 = nothing, nothing, nothing, nothing + while (in1 === nothing && in2 === nothing) || isempty(in1 ⊗ in2) + try + out1 = ntuple(n -> randsector(I), N) + in1 = rand(collect(⊗(out1...))) + out2 = ntuple(n -> randsector(I), N) + in2 = rand(collect(⊗(out2...))) + catch e + if isa(e, ArgumentError) + in1, in2 = nothing, nothing + else + rethrow(e) + end end end - end - @testset "Fusion tree $Istr: merging" begin - N = 3 - out1 = ntuple(n -> randsector(I), N) - in1 = rand(collect(⊗(out1...))) f1 = rand(collect(fusiontrees(out1, in1))) - out2 = ntuple(n -> randsector(I), N) - in2 = rand(collect(⊗(out2...))) f2 = rand(collect(fusiontrees(out2, in2))) @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) - if !(FusionStyle(I) isa GenericFusion) - @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) - @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, μ)) - - for c in in1 ⊗ in2 - R = Rsymbol(in1, in2, c) - for μ in 1:Nsymbol(in1, in2, c) - trees1 = TK.merge(f1, f2, c, μ) - - # test merge and braid interplay - trees2 = Dict{keytype(trees1),complex(valtype(trees1))}() - trees3 = Dict{keytype(trees1),complex(valtype(trees1))}() - for ν in 1:Nsymbol(in2, in1, c) - for (t, coeff) in TK.merge(f2, f1, c, ν) - trees2[t] = get(trees2, t, zero(valtype(trees2))) + coeff * R[μ, ν] - end - end - perm = ((N .+ (1:N))..., (1:N)...) - levels = ntuple(identity, 2 * N) - for (t, coeff) in trees1 - for (t′, coeff′) in braid(t, levels, perm) - trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ - end - end - for (t, coeff) in trees3 - coeff′ = get(trees2, t, zero(coeff)) - @test isapprox(coeff, coeff′; atol=1e-12, rtol=1e-12) - end - end - end + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) + + @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, μ) + ) + # no merge and braid interplay tests end - N = 4 - out = ntuple(n -> randsector(I), N) - numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) - while !(0 < numtrees < 100) - out = ntuple(n -> randsector(I), N) - numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) - end - incoming = rand(collect(⊗(out...))) + # hardcoded double fusion tree tests + N = 6 + out = (Mop, C0, C1, M, D0, D1) # same as above + out2 = (D0, D1, Mop, C0, C1, M) # different order that still fuses to D0 or D1 + incoming = rand(collect(⊗(out...))) # will be D0 or D1 f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) @testset "Double fusion tree $Istr: repartioning" begin for n in 0:(2 * N) d = @constinferred TK.repartition(f1, f2, $n) @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)),valtype(d)}() + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)), valtype(d)}() for ((f1′, f2′), coeff) in d for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff @@ -402,7 +357,7 @@ end if f1 == f1′ && f2 == f2′ @test coeff2 ≈ 1 else - @test isapprox(coeff2, 0; atol=1e-12, rtol=1e-12) + @test isapprox(coeff2, 0; atol = 1.0e-12, rtol = 1.0e-12) end end end @@ -420,8 +375,8 @@ end d = @constinferred transpose(f1, f2, p1, p2) @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)),valtype(d)}() + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)), valtype(d)}() for ((f1′, f2′), coeff) in d d′ = transpose(f1′, f2′, ip1, ip2) for ((f1′′, f2′′), coeff2) in d′ @@ -432,21 +387,22 @@ end if f1 == f1′ && f2 == f2′ @test coeff2 ≈ 1 else - @test abs(coeff2) < 1e-12 + @test abs(coeff2) < 1.0e-12 end end - end end @testset "Double fusion tree $Istr: planar trace" begin d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) f1front, = TK.split(f1, N - 1) - T = typeof(Fsymbol(one(I), one(I), one(I), one(I), one(I), one(I))[1, 1, 1, 1]) - d2 = Dict{typeof((f1front, f1front)),T}() + 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,)) + 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 @@ -455,7 +411,7 @@ end if (f1_, f2_) == (f1front, f1front) @test coeff ≈ dim(f1.coupled) / dim(f1front.coupled) else - @test abs(coeff) < 1e-12 + @test abs(coeff) < 1.0e-12 end end end @@ -463,9 +419,8 @@ end end V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) -# TODO: more examples necessary? -@testset "DiagonalTensor with domain $V" begin +@timedtestset "DiagonalTensor with domain $V" begin @timedtestset "Basic properties and algebra" begin for T in (Float32, Float64, ComplexF32, ComplexF64, BigFloat) # constructors @@ -492,7 +447,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) 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 eltype(bs) === Pair{typeof(c), typeof(b1)} @test typeof(b1) === TensorKit.blocktype(t) # basic linear algebra @test isa(@constinferred(norm(t)), real(T)) @@ -535,7 +490,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @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) + convert(TensorMap, t1) + convert(TensorMap, t2) end end @timedtestset "Real and imaginary parts" begin @@ -580,50 +535,36 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @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) + @test all( + Base.Fix2(isa, DiagonalTensorMap), + (t1 * t2, t1 \ t2, t1 / t2, inv(t1), pinv(t1)) + ) + # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden end @timedtestset "Tensor contraction" begin - d = DiagonalTensorMap(rand(ComplexF64, reduceddim(v)), v) - # d = DiagonalTensorMap(rand(ComplexF64, reduceddim(V)), V) - t = TensorMap(d) - v = Vect[I](I(2,1,0)=>1) - A = randn(ComplexF64, v ⊗ v' ⊗ v, v) - B = randn(ComplexF64, v ⊗ v' ⊗ v, v ⊗ v') - # A = randn(ComplexF64, V ⊗ V' ⊗ V, V) - # B = randn(ComplexF64, V ⊗ V' ⊗ V, V ⊗ V') - - @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] # don't work for modules - @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 + #TODO: fix this part + for W in (Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 3), Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 3)) + d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) + t = TensorMap(d) + A = randn(ComplexF64, W ⊗ W' ⊗ W, W) + B = randn(ComplexF64, W ⊗ W' ⊗ W, W ⊗ W') # empty for modules so untested + + @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 end @timedtestset "Factorization" begin for T in (Float32, ComplexF64) @@ -639,12 +580,16 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test rank(D) ≈ rank(t) @test cond(D) ≈ cond(t) - @test all(((s, t),) -> isapprox(s, t), - zip(values(LinearAlgebra.eigvals(D)), - values(LinearAlgebra.eigvals(t)))) + @test all( + ((s, t),) -> isapprox(s, t), + zip( + values(LinearAlgebra.eigvals(D)), + values(LinearAlgebra.eigvals(t)) + ) + ) end @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QL()) - Q, R = @constinferred leftorth(t; alg=alg) + Q, R = @constinferred leftorth(t; alg = alg) QdQ = Q' * Q @test QdQ ≈ one(QdQ) @test Q * R ≈ t @@ -653,7 +598,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end @testset "rightorth with $alg" for alg in (TensorKit.RQ(), TensorKit.LQ()) - L, Q = @constinferred rightorth(t; alg=alg) + L, Q = @constinferred rightorth(t; alg = alg) QQd = Q * Q' @test QQd ≈ one(QQd) @test L * Q ≈ t @@ -662,7 +607,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, Vᴴ = @constinferred tsvd(t; alg=alg) + U, S, Vᴴ = @constinferred tsvd(t; alg = alg) UdU = U' * U @test UdU ≈ one(UdU) VdV = Vᴴ * Vᴴ' @@ -671,9 +616,13 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test rank(S) ≈ rank(t) @test cond(S) ≈ cond(t) - @test all(((s, t),) -> isapprox(s, t), - zip(values(LinearAlgebra.svdvals(S)), - values(LinearAlgebra.svdvals(t)))) + @test all( + ((s, t),) -> isapprox(s, t), + zip( + values(LinearAlgebra.svdvals(S)), + values(LinearAlgebra.svdvals(t)) + ) + ) end end end @@ -707,7 +656,9 @@ end ########## tf = time() -printstyled("Finished multifusion tests in ", - string(round(tf - ti; sigdigits=3)), - " seconds."; bold=true, color=Base.info_color()) +printstyled( + "Finished multifusion tests in ", + string(round(tf - ti; sigdigits = 3)), + " seconds."; bold = true, color = Base.info_color() +) println() From 60bc1fc109764f4a8c4fc4e826611fa4d2acef7d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 16:15:13 +0200 Subject: [PATCH 41/86] minor changes --- test/fusiontrees.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fusiontrees.jl b/test/fusiontrees.jl index d758f9a83..67fdc5ae1 100644 --- a/test/fusiontrees.jl +++ b/test/fusiontrees.jl @@ -211,7 +211,7 @@ ti = time() end end end - @testset "Fusion tree $Istr: elementy artin braid" begin + @testset "Fusion tree $Istr: elementary artin braid" begin N = length(out) isdual = ntuple(n -> rand(Bool), N) for in in ⊗(out...) @@ -268,7 +268,7 @@ ti = time() end end @testset "Fusion tree $Istr: braiding and permuting" begin - f = rand(collect(fusiontrees(out, in, isdual))) + f = rand(collect(it)) p = tuple(randperm(N)...) ip = invperm(p) @@ -548,7 +548,7 @@ ti = time() @testset "Double fusion tree $Istr: planar trace" begin d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) f1front, = TK.split(f1, N - 1) - T = typeof(Fsymbol(one(I), one(I), one(I), one(I), one(I), one(I))[1, 1, 1, 1]) + T = sectorscalartype(I) d2 = Dict{typeof((f1front, f1front)),T}() for ((f1′, f2′), coeff′) in d1 for ((f1′′, f2′′), coeff′′) in From 490aa4fc39a29f33db3e087debd0a2fd384c20c1 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 16:15:40 +0200 Subject: [PATCH 42/86] export `left/rightoneunit` --- src/TensorKit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 7da0d9001..0003bfddf 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -32,7 +32,7 @@ export SpaceMismatch, SectorMismatch, IndexError # error types # general vector space methods export space, field, dual, dim, reduceddim, dims, fuse, flip, isdual, oplus, - insertleftunit, insertrightunit, removeunit + leftoneunit, rightoneunit, insertleftunit, insertrightunit, removeunit # partial order for vector spaces export infimum, supremum, isisomorphic, ismonomorphic, isepimorphic From 8ab417d9665c1e5b85e48f0ede785cc3585e196f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 16:18:26 +0200 Subject: [PATCH 43/86] make `artin_braid` excessively multifusion-compatible --- src/fusiontrees/manipulations.jl | 266 +++++++++++++++++++------------ 1 file changed, 160 insertions(+), 106 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 5bb021cf5..c32760d70 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -13,7 +13,7 @@ Attach a fusion tree `f₂` to the uncoupled leg `i` of the fusion tree `f₁` a into a linear combination of fusion trees in standard form. This requires that `f₂.coupled == f₁.uncoupled[i]` and `f₁.isdual[i] == false`. """ -function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I,0}) where {I} +function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I, 0}) where {I} # this actually removes uncoupled line i, which should be trivial (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) @@ -35,7 +35,7 @@ function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I,0}) where {I} f = FusionTree(uncoupled, coupled, isdual, inner, vertices) return fusiontreedict(I)(f => coeff) end -function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I,1}) where {I} +function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 1}) where {I} # identity operation (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) @@ -44,7 +44,7 @@ function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I,1}) where {I} f = FusionTree{I}(f₁.uncoupled, f₁.coupled, isdual′, f₁.innerlines, f₁.vertices) return fusiontreedict(I)(f => coeff) end -function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I,2}) where {I} +function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 2}) where {I} # elementary building block, (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) @@ -106,14 +106,14 @@ function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I,2}) where {I} return newtrees end end -function insertat(f₁::FusionTree{I,N₁}, i, f₂::FusionTree{I,N₂}) where {I,N₁,N₂} +function insertat(f₁::FusionTree{I, N₁}, i, f₂::FusionTree{I, N₂}) where {I, N₁, N₂} F = fusiontreetype(I, N₁ + N₂ - 1) (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) T = sectorscalartype(I) coeff = one(T) if length(f₁) == 1 - return fusiontreedict(I){F,T}(f₂ => coeff) + return fusiontreedict(I){F, T}(f₂ => coeff) end if i == 1 uncoupled = (f₂.uncoupled..., tail(f₁.uncoupled)...) @@ -122,18 +122,18 @@ function insertat(f₁::FusionTree{I,N₁}, i, f₂::FusionTree{I,N₂}) where { vertices = (f₂.vertices..., f₁.vertices...) coupled = f₁.coupled f′ = FusionTree(uncoupled, coupled, isdual, inner, vertices) - return fusiontreedict(I){F,T}(f′ => coeff) + return fusiontreedict(I){F, T}(f′ => coeff) else # recursive definition N2 = length(f₂) f₂′, f₂′′ = split(f₂, N2 - 1) - local newtrees::fusiontreedict(I){F,T} + local newtrees::fusiontreedict(I){F, T} for (f, coeff) in insertat(f₁, i, f₂′′) for (f′, coeff′) in insertat(f, i, f₂′) if @isdefined newtrees coeff′′ = coeff * coeff′ newtrees[f′] = get(newtrees, f′, zero(coeff′′)) + coeff′′ else - newtrees = fusiontreedict(I){F,T}(f′ => coeff * coeff′) + newtrees = fusiontreedict(I){F, T}(f′ => coeff * coeff′) end end end @@ -153,7 +153,7 @@ remaining `N-M` uncoupled sectors of `f`. It couples to the same sector as `f`. operation is the inverse of `insertat` in the sense that if `f₁, f₂ = split(t, M) ⇒ f == insertat(f₂, 1, f₁)`. """ -@inline function split(f::FusionTree{I,N}, M::Int) where {I,N} +@inline function split(f::FusionTree{I, N}, M::Int) where {I, N} if M > N || M < 0 throw(ArgumentError("M should be between 0 and N = $N")) elseif M === N @@ -210,15 +210,19 @@ sectors are those of `f₁` followed by those of `f₂`, and where the two coupl `FusionStyle(I) == GenericFusion()`, also a degeneracy label `μ` for the fusion of the coupled sectors of `f₁` and `f₂` to `c` needs to be specified. """ -function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, - c::I) where {I,N₁,N₂} +function merge( + f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, + c::I + ) where {I, N₁, N₂} if FusionStyle(I) isa GenericFusion throw(ArgumentError("vertex label for merging required")) end return merge(f₁, f₂, c, 1) end -function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, - c::I, μ) where {I,N₁,N₂} +function merge( + f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, + c::I, μ + ) where {I, N₁, N₂} if !(c in f₁.coupled ⊗ f₂.coupled) throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) end @@ -230,7 +234,7 @@ function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, @assert coeff == one(coeff) return insertat(f, N₁ + 1, f₂) end -function merge(f₁::FusionTree{I,0}, f₂::FusionTree{I,0}, c::I, μ) where {I} +function merge(f₁::FusionTree{I, 0}, f₂::FusionTree{I, 0}, c::I, μ) where {I} Nsymbol(f₁.coupled, f₂.coupled, c) == μ == 1 || throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) return fusiontreedict(I)(f₁ => Fsymbol(c, c, c, c, c, c)[1, 1, 1, 1]) @@ -244,8 +248,10 @@ end # -> A-move (foldleft, foldright) is complicated, needs to be reexpressed in standard form # flip a duality flag of a fusion tree -function flip(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, i::Int; - inv::Bool=false) where {I<:Sector,N₁,N₂} +function flip( + f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, i::Int; + inv::Bool = false + ) where {I <: Sector, N₁, N₂} @assert 0 < i ≤ N₁ + N₂ if i ≤ N₁ a = f₁.uncoupled[i] @@ -274,8 +280,10 @@ function flip(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, i::Int; return SingletonDict((f₁, f₂′) => factor) end end -function flip(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, ind; - inv::Bool=false) where {I<:Sector,N₁,N₂} +function flip( + f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, ind; + inv::Bool = false + ) where {I <: Sector, N₁, N₂} f₁′, f₂′ = f₁, f₂ factor = one(sectorscalartype(I)) for i in ind @@ -286,7 +294,7 @@ function flip(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, ind; end # change to N₁ - 1, N₂ + 1 -function bendright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I<:Sector,N₁,N₂} +function bendright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where {I <: Sector, N₁, N₂} # map final splitting vertex (a, b)<-c to fusion vertex a<-(c, dual(b)) @assert N₁ > 0 c = f₁.coupled @@ -334,13 +342,15 @@ end # change to N₁ + 1, N₂ - 1 function bendleft(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I} # map final fusion vertex c<-(a, b) to splitting vertex (c, dual(b))<-a - return fusiontreedict(I)((f₁′, f₂′) => conj(coeff) - for - ((f₂′, f₁′), coeff) in bendright(f₂, f₁)) + return fusiontreedict(I)( + (f₁′, f₂′) => conj(coeff) + for + ((f₂′, f₁′), coeff) in bendright(f₂, f₁) + ) end # change to N₁ - 1, N₂ + 1 -function foldright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I<:Sector,N₁,N₂} +function foldright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where {I <: Sector, N₁, N₂} # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) @assert N₁ > 0 a = f₁.uncoupled[1] @@ -384,7 +394,7 @@ function foldright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I< coeff = factor * coeff1 * conj(coeff2) if (@isdefined newtrees) newtrees[(fl, fr)] = get(newtrees, (fl, fr), zero(coeff)) + - coeff + coeff else newtrees = fusiontreedict(I)((fl, fr) => coeff) end @@ -398,9 +408,11 @@ end # change to N₁ + 1, N₂ - 1 function foldleft(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I} # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b - return fusiontreedict(I)((f₁′, f₂′) => conj(coeff) - for - ((f₂′, f₁′), coeff) in foldright(f₂, f₁)) + return fusiontreedict(I)( + (f₁′, f₂′) => conj(coeff) + for + ((f₂′, f₁′), coeff) in foldright(f₂, f₁) + ) end # COMPOSITE DUALITY MANIPULATIONS PART 1: Repartition and transpose @@ -423,7 +435,7 @@ function iscyclicpermutation(v1, v2) end # clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft -function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I<:Sector} +function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Sector} local newtrees if length(f₁) > 0 for ((f1a, f2a), coeffa) in foldright(f₁, f₂) @@ -452,7 +464,7 @@ function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I<:Sect end # anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright -function cycleanticlockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I<:Sector} +function cycleanticlockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Sector} local newtrees if length(f₂) > 0 for ((f1a, f2a), coeffa) in foldleft(f₁, f₂) @@ -492,17 +504,21 @@ outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical cou repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have `N` outgoing sectors. """ -@inline function repartition(f₁::FusionTree{I,N₁}, - f₂::FusionTree{I,N₂}, - N::Int) where {I<:Sector,N₁,N₂} +@inline function repartition( + f₁::FusionTree{I, N₁}, + f₂::FusionTree{I, N₂}, + N::Int + ) where {I <: Sector, N₁, N₂} f₁.coupled == f₂.coupled || throw(SectorMismatch()) @assert 0 <= N <= N₁ + N₂ return _recursive_repartition(f₁, f₂, Val(N)) end -function _recursive_repartition(f₁::FusionTree{I,N₁}, - f₂::FusionTree{I,N₂}, - ::Val{N}) where {I<:Sector,N₁,N₂,N} +function _recursive_repartition( + f₁::FusionTree{I, N₁}, + f₂::FusionTree{I, N₂}, + ::Val{N} + ) where {I <: Sector, N₁, N₂, N} # recursive definition is only way to get correct number of loops for # GenericFusion, but is too complex for type inference to handle, so we # precompute the parameters of the return type @@ -511,16 +527,18 @@ function _recursive_repartition(f₁::FusionTree{I,N₁}, T = sectorscalartype(I) coeff = one(T) if N == N₁ - return fusiontreedict(I){Tuple{F₁,F₂},T}((f₁, f₂) => coeff) + return fusiontreedict(I){Tuple{F₁, F₂}, T}((f₁, f₂) => coeff) else - local newtrees::fusiontreedict(I){Tuple{F₁,F₂},T} + local newtrees::fusiontreedict(I){Tuple{F₁, F₂}, T} for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright(f₁, f₂) : bendleft(f₁, f₂)) for ((f₁′′, f₂′′), coeff2) in _recursive_repartition(f₁′, f₂′, Val(N)) if (@isdefined newtrees) push!(newtrees, (f₁′′, f₂′′) => coeff1 * coeff2) else - newtrees = fusiontreedict(I){Tuple{F₁,F₂},T}((f₁′′, f₂′′) => coeff1 * - coeff2) + newtrees = fusiontreedict(I){Tuple{F₁, F₂}, T}( + (f₁′′, f₂′′) => coeff1 * + coeff2 + ) end end end @@ -540,8 +558,10 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function Base.transpose(f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector,N₁,N₂} +function Base.transpose( + f₁::FusionTree{I}, f₂::FusionTree{I}, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂} + ) where {I <: Sector, N₁, N₂} N = N₁ + N₂ @assert length(f₁) + length(f₂) == N p = linearizepermutation(p1, p2, length(f₁), length(f₂)) @@ -549,20 +569,26 @@ function Base.transpose(f₁::FusionTree{I}, f₂::FusionTree{I}, return fstranspose((f₁, f₂, p1, p2)) end -const FSTransposeKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, - IndexTuple{N₁},IndexTuple{N₂}} +const FSTransposeKey{I <: Sector, N₁, N₂} = Tuple{ + <:FusionTree{I}, <:FusionTree{I}, + IndexTuple{N₁}, IndexTuple{N₂}, +} function _fsdicttype(I, N₁, N₂) F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) T = sectorscalartype(I) - return fusiontreedict(I){Tuple{F₁,F₂},T} + return fusiontreedict(I){Tuple{F₁, F₂}, T} end -@cached function fstranspose(key::FSTransposeKey{I,N₁,N₂})::_fsdicttype(I, N₁, - N₂) where {I<:Sector, - N₁, - N₂} +@cached function fstranspose(key::FSTransposeKey{I, N₁, N₂})::_fsdicttype( + I, N₁, + N₂ + ) where { + I <: Sector, + N₁, + N₂, + } f₁, f₂, p1, p2 = key N = N₁ + N₂ p = linearizepermutation(p1, p2, length(f₁), length(f₂)) @@ -605,7 +631,7 @@ end return newtrees end -function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I<:Sector} +function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I <: Sector} if FusionStyle(I) isa UniqueFusion return NoCache() else @@ -618,29 +644,35 @@ end # -> composite manipulations that depend on the duality (rigidity) and pivotal structure # -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) -function planar_trace(f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}, - q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N₁,N₂,N₃} +function planar_trace( + f₁::FusionTree{I}, f₂::FusionTree{I}, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂}, + q1::IndexTuple{N₃}, q2::IndexTuple{N₃} + ) where {I <: Sector, N₁, N₂, N₃} N = N₁ + N₂ + 2N₃ @assert length(f₁) + length(f₂) == N if N₃ == 0 return transpose(f₁, f₂, p1, p2) end - linearindex = (ntuple(identity, Val(length(f₁)))..., - reverse(length(f₁) .+ ntuple(identity, Val(length(f₂))))...) + linearindex = ( + ntuple(identity, Val(length(f₁)))..., + reverse(length(f₁) .+ ntuple(identity, Val(length(f₂))))..., + ) q1′ = TupleTools.getindices(linearindex, q1) q2′ = TupleTools.getindices(linearindex, q2) p1′, p2′ = let q′ = (q1′..., q2′...) - (map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p1)), - map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p2))) + ( + map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p1)), + map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p2)), + ) end T = sectorscalartype(I) F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) - newtrees = FusionTreeDict{Tuple{F₁,F₂},T}() + newtrees = FusionTreeDict{Tuple{F₁, F₂}, T}() for ((f₁′, f₂′), coeff′) in repartition(f₁, f₂, N) for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) @@ -662,11 +694,13 @@ Perform a planar trace of the uncoupled indices of the fusion tree `f` at `q1` w `q2`, where `q1[i]` is connected to `q2[i]` for all `i`. The result is returned as a dictionary of output trees and corresponding coefficients. """ -function planar_trace(f::FusionTree{I,N}, - q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N,N₃} +function planar_trace( + f::FusionTree{I, N}, + q1::IndexTuple{N₃}, q2::IndexTuple{N₃} + ) where {I <: Sector, N, N₃} T = sectorscalartype(I) F = fusiontreetype(I, N - 2 * N₃) - newtrees = FusionTreeDict{F,T}() + newtrees = FusionTreeDict{F, T}() N₃ === 0 && return push!(newtrees, f => one(T)) for (i, j) in zip(q1, q2) @@ -715,7 +749,7 @@ Perform an elementary trace of neighbouring uncoupled indices `i` and `i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and corresponding coefficients. """ -function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} +function elementary_trace(f::FusionTree{I, N}, i) where {I <: Sector, N} (N > 1 && 1 <= i <= N) || throw(ArgumentError("Cannot trace outputs i=$i and i+1 out of only $N outputs")) i < N || isone(f.coupled) || @@ -723,7 +757,7 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} T = sectorscalartype(I) F = fusiontreetype(I, N - 2) - newtrees = FusionTreeDict{F,T}() + newtrees = FusionTreeDict{F, T}() j = mod1(i + 1, N) b = f.uncoupled[i] @@ -731,8 +765,10 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} # if trace is zero, return empty dict (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees if i < N - inner_extended = (leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., - f.coupled) + inner_extended = ( + leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., + f.coupled, + ) a = inner_extended[i] d = inner_extended[i + 2] a == d || return newtrees @@ -743,13 +779,13 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} inner′ = () else inner′ = i <= 2 ? Base.tail(Base.tail(f.innerlines)) : - TupleTools.deleteat(TupleTools.deleteat(f.innerlines, i - 1), i - 2) + TupleTools.deleteat(TupleTools.deleteat(f.innerlines, i - 1), i - 2) end if N <= 3 vertices′ = () else vertices′ = i <= 2 ? Base.tail(Base.tail(f.vertices)) : - TupleTools.deleteat(TupleTools.deleteat(f.vertices, i), i - 1) + TupleTools.deleteat(TupleTools.deleteat(f.vertices, i), i - 1) end f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) coeff = sqrtdim(b) @@ -786,13 +822,13 @@ function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} vertices_ = TupleTools.front(f.vertices) f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) - for (f_′, coeff) in merge(fs, f_, unit, 1) # coloring gets reversed here, should be the other unit + for (f_′, coeff) in merge(fs, f_, unit, 1) f_′.innerlines[1] == unit || continue uncoupled′ = Base.tail(Base.tail(f_′.uncoupled)) isdual′ = Base.tail(Base.tail(f_′.isdual)) inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) vertices′ = N <= 3 ? () : Base.tail(Base.tail(f_′.vertices)) - f′ = FusionTree(uncoupled′, unit, isdual′, inner′, vertices′) # and this one? + f′ = FusionTree(uncoupled′, unit, isdual′, inner′, vertices′) coeff *= sqrtdim(b) if !(f.isdual[N]) coeff *= conj(frobeniusschur(b)) @@ -820,7 +856,7 @@ applying `artin_braid(f′, i; inv = true)` to all the outputs `f′` of tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword has no effect if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ -function artin_braid(f::FusionTree{I,N}, i; inv::Bool=false) where {I<:Sector,N} +function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I <: Sector, N} 1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) uncoupled = f.uncoupled @@ -835,14 +871,18 @@ function artin_braid(f::FusionTree{I,N}, i; inv::Bool=false) where {I<:Sector,N} vertices = f.vertices oneT = one(sectorscalartype(I)) - if isone(uncoupled[i]) || isone(uncoupled[i + 1]) + diaga = a == leftone(a) == rightone(a) # might be excessive because multifusion doesn't support braiding currently + diagb = b == leftone(b) == rightone(b) + if diaga || diagb # braiding with trivial sector: simple and always possible inner′ = inner vertices′ = vertices if i > 1 # we also need to alter innerlines and vertices - inner′ = TupleTools.setindex(inner, - inner_extended[isone(a) ? (i + 1) : (i - 1)], - i - 1) + inner′ = TupleTools.setindex( + inner, + inner_extended[diaga ? (i + 1) : (i - 1)], + i - 1 + ) vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) end @@ -885,28 +925,32 @@ function artin_braid(f::FusionTree{I,N}, i; inv::Bool=false) where {I<:Sector,N} e = inner_extended[i + 1] if FusionStyle(I) isa UniqueFusion c′ = first(a ⊗ d) - coeff = oftype(oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * - Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * - conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * + Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * + conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) return fusiontreedict(I)(f′ => coeff) elseif FusionStyle(I) isa SimpleFusion local newtrees for c′ in intersect(a ⊗ d, e ⊗ conj(b)) - coeff = oftype(oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * - Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * - conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * + Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * + conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) iszero(coeff) && continue inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) @@ -963,9 +1007,11 @@ that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` an ``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid(f::FusionTree{I,N}, - levels::NTuple{N,Int}, - p::NTuple{N,Int}) where {I<:Sector,N} +function braid( + f::FusionTree{I, N}, + levels::NTuple{N, Int}, + p::NTuple{N, Int} + ) where {I <: Sector, N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding coeff = one(sectorscalartype(I)) @@ -990,7 +1036,7 @@ function braid(f::FusionTree{I,N}, for s in permutation2swaps(p) inv = levels[s] > levels[s + 1] for (f, c) in trees - for (f′, c′) in artin_braid(f, s; inv=inv) + for (f′, c′) in artin_braid(f, s; inv = inv) newtrees[f′] = get(newtrees, f′, zero(coeff)) + c * c′ end end @@ -1012,7 +1058,7 @@ Perform a permutation of the uncoupled indices of the fusion tree `f` and return as a `<:AbstractDict` of output trees and corresponding coefficients; this requires that `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ -function permute(f::FusionTree{I,N}, p::NTuple{N,Int}) where {I<:Sector,N} +function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I <: Sector, N} @assert BraidingStyle(I) isa SymmetricBraiding return braid(f, ntuple(identity, Val(N)), p) end @@ -1036,20 +1082,26 @@ respectively, which determines how indices braid. In particular, if `i` and `j` levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid(f₁::FusionTree{I}, f₂::FusionTree{I}, - levels1::IndexTuple, levels2::IndexTuple, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector,N₁,N₂} +function braid( + f₁::FusionTree{I}, f₂::FusionTree{I}, + levels1::IndexTuple, levels2::IndexTuple, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂} + ) where {I <: Sector, N₁, N₂} @assert length(f₁) + length(f₂) == N₁ + N₂ @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) return fsbraid((f₁, f₂, levels1, levels2, p1, p2)) end -const FSBraidKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, - IndexTuple,IndexTuple, - IndexTuple{N₁},IndexTuple{N₂}} +const FSBraidKey{I <: Sector, N₁, N₂} = Tuple{ + <:FusionTree{I}, <:FusionTree{I}, + IndexTuple, IndexTuple, + IndexTuple{N₁}, IndexTuple{N₂}, +} -@cached function fsbraid(key::FSBraidKey{I,N₁,N₂})::_fsdicttype(I, N₁, - N₂) where {I<:Sector,N₁,N₂} +@cached function fsbraid(key::FSBraidKey{I, N₁, N₂})::_fsdicttype( + I, N₁, + N₂ + ) where {I <: Sector, N₁, N₂} (f₁, f₂, l1, l2, p1, p2) = key p = linearizepermutation(p1, p2, length(f₁), length(f₂)) levels = (l1..., reverse(l2)...) @@ -1059,7 +1111,7 @@ const FSBraidKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, for ((f₁′, f₂′), coeff3) in repartition(f′, f0, N₁) if @isdefined newtrees newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + - coeff1 * coeff2 * coeff3 + coeff1 * coeff2 * coeff3 else newtrees = fusiontreedict(I)((f₁′, f₂′) => coeff1 * coeff2 * coeff3) end @@ -1069,7 +1121,7 @@ const FSBraidKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, return newtrees end -function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I<:Sector} +function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I <: Sector} if FusionStyle(I) isa UniqueFusion return NoCache() else @@ -1089,8 +1141,10 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function permute(f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector,N₁,N₂} +function permute( + f₁::FusionTree{I}, f₂::FusionTree{I}, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂} + ) where {I <: Sector, N₁, N₂} @assert BraidingStyle(I) isa SymmetricBraiding levels1 = ntuple(identity, length(f₁)) levels2 = length(f₁) .+ ntuple(identity, length(f₂)) From 63488150b5d8d050c9909e1152d3919b5600058d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 16:18:58 +0200 Subject: [PATCH 44/86] add `zero` and `fusiontrees` constructor for `IsingBimod` --- src/spaces/multifusionspace.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 6b64318fd..890282069 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -20,6 +20,8 @@ function Base.oneunit(S::Vect[IsingBimod]) return spacetype(S)(sector => 1) end +Base.zero(S::Type{Vect[IsingBimod]}) = Vect[IsingBimod]() + function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} codom = codomain(W) dom = domain(W) @@ -88,10 +90,14 @@ end # but these errors are maybe more informative function FusionTree(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) coupled = collect(⊗(uncoupled...)) - @show coupled if length(coupled) == 0 # illegal fusion somewhere throw(ArgumentError("Forbidden fusion with uncoupled sectors $uncoupled")) else # allowed fusions require inner lines error("fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`") end +end + +# this one might also be overkill, since `FusionTreeIterator`s don't check whether the fusion is allowed +function fusiontrees(uncoupled::Tuple{IsingBimod, Vararg{IsingBimod}}) + return throw(ArgumentError("coupled sector must be provided for IsingBimod fusion")) end \ No newline at end of file From 2a7d871059bbab1c8ec13bb6323322ce28e80d4f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 17:28:49 +0200 Subject: [PATCH 45/86] start of `HomSpace` tests --- test/multifusion.jl | 94 +++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 24e9389ee..d6e2b996c 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -61,7 +61,7 @@ ti = time() @test @constinferred(field(V)) == ℂ @test @constinferred(sectortype(V)) == I slist = @constinferred sectors(V) - @test @constinferred(TensorKit.hassector(V, first(slist))) + @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)) @@ -120,44 +120,57 @@ ti = time() @test_throws SpaceMismatch (⊕(V, V')) end - @timedtestset "HomSpace" begin - # for (V1, V2, V3, V4, V5) in (Vtr, Vℤ₃, VSU₂) - # W = TensorKit.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 eval(Meta.parse(sprint(show, W))) == W - # @test eval(Meta.parse(sprint(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] == V3' - # @test W[4] == V4' - # @test W[5] == V5' - # @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') - # @test W == deepcopy(W) - # @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) - # @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') - # @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') - # @test (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ oneunit(V5)) == - # @constinferred(insertleftunit(W)) == - # @constinferred(insertrightunit(W)) - # @test @constinferred(removeunit(insertleftunit(W), $(numind(W) + 1))) == W - # @test (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ oneunit(V5)') == - # @constinferred(insertleftunit(W; conj=true)) == - # @constinferred(insertrightunit(W; conj=true)) - # @test (oneunit(V1) ⊗ V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5) == - # @constinferred(insertleftunit(W, 1)) == - # @constinferred(insertrightunit(W, 0)) - # @test (V1 ⊗ V2 ⊗ oneunit(V1) ← V3 ⊗ V4 ⊗ V5) == - # @constinferred(insertrightunit(W, 2)) - # @test (V1 ⊗ V2 ← oneunit(V1) ⊗ V3 ⊗ V4 ⊗ V5) == - # @constinferred(insertleftunit(W, 3)) - # @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W - # @test @constinferred(insertrightunit(one(V1) ← V1, 0)) == (oneunit(V1) ← V1) - # @test_throws BoundsError insertleftunit(one(V1) ← V1, 0) - # end + VIB1 = ( + Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 1), + Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 2), + Vect[I](I(1, 1, 0) => 3, I(1, 1, 1) => 2), + Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 3), + Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 5), + ) + + VIB2 = ( + Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1), + Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 2), + Vect[I](I(2, 2, 0) => 3, I(2, 2, 1) => 2), + Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 3), + Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 5), + ) + + @timedtestset "HomSpace with $(TensorKit.type_repr(Vect[I])) " begin + for (V1, V2, V3, V4, V5) in (VIB1, VIB2) #TODO: examples with module spaces + 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 eval(Meta.parse(sprint(show, W))) == W + @test eval(Meta.parse(sprint(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] == V3' + @test W[4] == V4' + @test W[5] == V5' + + @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') + @test W == deepcopy(W) + @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) + @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') + @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') + + @test_throws ErrorException insertleftunit(W) + @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ oneunit(V1)) + @test_throws ErrorException insertrightunit(W, 6) + @test_throws ErrorException insertleftunit(W, 6) + + @test (V1 ⊗ V2 ⊗ oneunit(V1) ← V3 ⊗ V4 ⊗ V5) == + @constinferred(insertrightunit(W, 2)) + @test (V1 ⊗ V2 ← oneunit(V1) ⊗ V3 ⊗ V4 ⊗ V5) == + @constinferred(insertleftunit(W, 3)) + @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W + @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? + @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) + end end end @@ -415,7 +428,6 @@ end end end end - TensorKit.empty_globalcaches!() end V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @@ -542,7 +554,6 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden end @timedtestset "Tensor contraction" begin - #TODO: fix this part for W in (Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 3), Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 3)) d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) t = TensorMap(d) @@ -654,6 +665,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end +TensorKit.empty_globalcaches!() ########## tf = time() printstyled( From a6bf085bbc4b102e5d12c4bd6a1c570f9675d0f0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 18:00:27 +0200 Subject: [PATCH 46/86] start of tensor tests --- test/multifusion.jl | 540 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) diff --git a/test/multifusion.jl b/test/multifusion.jl index d6e2b996c..5a7669240 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -665,6 +665,546 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end +# whatever V will be +V1, V2, V3, V4, V5 = V +@assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests +@assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests + +@timedtestset "Tensors with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + @timedtestset "Basic tensor properties" begin # passes for diagonal sectors + 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}} + # blocks + 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) + 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 + # no tensor array conversion tests + @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') + + i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) + 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)) + + w = @constinferred(isometry(T, V1 ⊗ (oneunit(V1) ⊕ oneunit(V1)), # works for diagonal + V1)) + @test dim(w) == 2 * dim(V1 ← V1) + @test w' * w == id(Vector{T}, V1) + @test w * w' == (w * w')^2 + end + end + @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) + + # CONTINUE HERE + + @test t2 == @constinferred insertrightunit(t) + @test numind(t2) == numind(t) + 1 + @test space(t2) == insertleftunit(space(t)) + @test scalartype(t2) === T + @test t.data === t2.data + @test @constinferred(removeunit(t2, $(numind(t2)))) == t + t3 = @constinferred insertleftunit(t; copy=true) + @test t3 == @constinferred insertrightunit(t; copy=true) + @test t.data !== t3.data + for (c, b) in blocks(t) + @test b == block(t3, c) + end + @test @constinferred(removeunit(t3, $(numind(t3)))) == t + t4 = @constinferred insertrightunit(t, 3; dual=true) + @test numin(t4) == numin(t) && numout(t4) == numout(t) + 1 + 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 + 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 + @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 + @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 = VERSION < v"1.7" ? repartition(t, k) : + @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 + # no permutations test via conversion + @timedtestset "Full trace: test self-consistency" begin + 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 + @timedtestset "Partial trace: test self-consistency" begin + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') + @tensor t2[a, b] := t[c, d, b, d, c, a] + @tensor t4[a, b, c, d] := t[d, e, b, e, c, a] + @tensor t5[a, b] := t4[a, b, c, c] + @test t2 ≈ t5 + end + # no trace test via conversion + @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 + # no tensor contraction test via contraction + @timedtestset "Index flipping: test flipping inverse" begin + t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) + for i in 1:4 + @test t ≈ flip(flip(t, i), i; inv=true) + @test t ≈ flip(flip(t, i; inv=true), i) + end + end + @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 + @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 + @timedtestset "Multiplication of isometries: test properties" begin + W2 = V4 ⊗ V5 + W1 = W2 ⊗ (oneunit(V1) ⊕ oneunit(V1)) + for T in (Float64, ComplexF64) + t1 = randisometry(T, W1, W2) + t2 = randisometry(T, W2 ← W2) + @test t1' * t1 ≈ one(t2) + @test t2' * t2 ≈ one(t2) + @test t2 * t2' ≈ one(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 + # no multiplication and inverse test via conversion + @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 + @timedtestset "Factorization" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + for T in (Float32, ComplexF64) + # Test both a normal tensor and an adjoint one. + ts = (rand(T, W), rand(T, W)') + for t in ts + @testset "leftorth with $alg" for alg in + (TensorKit.QR(), TensorKit.QRpos(), + TensorKit.QL(), TensorKit.QLpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + Q, R = @constinferred leftorth(t, ((3, 4, 2), (1, 5)); alg=alg) + QdQ = Q' * Q + @test QdQ ≈ one(QdQ) + @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) + if alg isa Polar + @test isposdef(R) + @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' + end + end + @testset "leftnull with $alg" for alg in + (TensorKit.QR(), TensorKit.SVD(), + TensorKit.SDD()) + N = @constinferred leftnull(t, ((3, 4, 2), (1, 5)); alg=alg) + NdN = N' * N + @test NdN ≈ one(NdN) + @test norm(N' * permute(t, ((3, 4, 2), (1, 5)))) < + 100 * eps(norm(t)) + end + @testset "rightorth with $alg" for alg in + (TensorKit.RQ(), TensorKit.RQpos(), + TensorKit.LQ(), TensorKit.LQpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) + if alg isa Polar + @test isposdef(L) + @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) + end + end + @testset "rightnull with $alg" for alg in + (TensorKit.LQ(), TensorKit.SVD(), + TensorKit.SDD()) + M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) + MMd = M * M' + @test MMd ≈ one(MMd) + @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < + 100 * eps(norm(t)) + end + @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) + UdU = U' * U + @test UdU ≈ one(UdU) + VVd = V * V' + @test VVd ≈ one(VVd) + t2 = permute(t, ((3, 4, 2), (1, 5))) + @test U * S * V ≈ t2 + + s = LinearAlgebra.svdvals(t2) + s′ = LinearAlgebra.diag(S) + for (c, b) in s + @test b ≈ s′[c] + end + end + @testset "cond and rank" begin + t2 = permute(t, ((3, 4, 2), (1, 5))) + d1 = dim(codomain(t2)) + d2 = dim(domain(t2)) + @test rank(t2) == min(d1, d2) + M = leftnull(t2) + @test rank(M) == max(d1, d2) - min(d1, d2) + t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test cond(t3) ≈ one(real(T)) + @test rank(t3) == dim(V1 ⊗ V2) + t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) + t4 = (t4 + t4') / 2 + vals = LinearAlgebra.eigvals(t4) + λmax = maximum(s -> maximum(abs, s), values(vals)) + λmin = minimum(s -> minimum(abs, s), values(vals)) + @test cond(t4) ≈ λmax / λmin + end + end + @testset "empty tensor" begin + t = randn(T, V1 ⊗ V2, zero(V1)) + @testset "leftorth with $alg" for alg in + (TensorKit.QR(), TensorKit.QRpos(), + TensorKit.QL(), TensorKit.QLpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + Q, R = @constinferred leftorth(t; alg=alg) + @test Q == t + @test dim(Q) == dim(R) == 0 + end + @testset "leftnull with $alg" for alg in + (TensorKit.QR(), TensorKit.SVD(), + TensorKit.SDD()) + N = @constinferred leftnull(t; alg=alg) + @test N' * N ≈ id(domain(N)) + @test N * N' ≈ id(codomain(N)) + end + @testset "rightorth with $alg" for alg in + (TensorKit.RQ(), TensorKit.RQpos(), + TensorKit.LQ(), TensorKit.LQpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + L, Q = @constinferred rightorth(copy(t'); alg=alg) + @test Q == t' + @test dim(Q) == dim(L) == 0 + end + @testset "rightnull with $alg" for alg in + (TensorKit.LQ(), TensorKit.SVD(), + TensorKit.SDD()) + M = @constinferred rightnull(copy(t'); alg=alg) + @test M * M' ≈ id(codomain(M)) + @test M' * M ≈ id(domain(M)) + end + @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + U, S, V = @constinferred tsvd(t; alg=alg) + @test U == t + @test dim(U) == dim(S) == dim(V) + end + @testset "cond and rank" begin + @test rank(t) == 0 + W2 = zero(V1) * zero(V2) + t2 = rand(W2, W2) + @test rank(t2) == 0 + @test cond(t2) == 0.0 + end + end + t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') + @testset "eig and isposdef" begin + D, V = eigen(t, ((1, 3), (2, 4))) + t2 = permute(t, ((1, 3), (2, 4))) + @test t2 * V ≈ V * D + + d = LinearAlgebra.eigvals(t2; sortby=nothing) + d′ = LinearAlgebra.diag(D) + for (c, b) in d + @test b ≈ d′[c] + end + + # Somehow moving these test before the previous one gives rise to errors + # with T=Float32 on x86 platforms. Is this an OpenBLAS issue? + VdV = V' * V + VdV = (VdV + VdV') / 2 + @test isposdef(VdV) + + @test !isposdef(t2) # unlikely for non-hermitian map + t2 = (t2 + t2') + D, V = eigen(t2) + VdV = V' * V + @test VdV ≈ one(VdV) + D̃, Ṽ = @constinferred eigh(t2) + @test D ≈ D̃ + @test V ≈ Ṽ + λ = minimum(minimum(real(LinearAlgebra.diag(b))) + for (c, b) in blocks(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)) + end + end + end + @timedtestset "Tensor truncation" begin + for T in (Float32, ComplexF64) + for p in (1, 2, 3, Inf) + # Test both a normal tensor and an adjoint one. + ts = (randn(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5), + randn(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)') + for t in ts + U₀, S₀, V₀, = tsvd(t) + t = rmul!(t, 1 / norm(S₀, p)) + U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) + # @show p, ϵ + # @show domain(S) + # @test min(space(S,1), space(S₀,1)) != space(S₀,1) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), + p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently + U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) + # @show p, ϵ + # @show domain(S) + # @test min(space(S,1), space(S₀,1)) != space(S₀,1) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + end + end + end + end + # no tensor functions tests + @timedtestset "Sylvester equation" begin + for T in (Float32, ComplexF64) + tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) + tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) + tA = 3 // 2 * leftorth(tA; alg=Polar())[1] + tB = 1 // 5 * leftorth(tB; alg=Polar())[1] + tC = rand(T, V1 ⊗ V3, V2 ⊗ V4) + t = @constinferred sylvester(tA, tB, tC) + @test codomain(t) == V1 ⊗ V3 + @test domain(t) == V2 ⊗ V4 + @test norm(tA * t + t * tB + tC) < + (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) + # no reshape test + end + end + @timedtestset "Tensor product: test via norm preservation" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1 ⊗ V2) + t2 = rand(T, V2 ⊗ V1 ⊗ V3, V1 ⊗ V1) + t = @constinferred (t1 ⊗ t2) + @test norm(t) ≈ norm(t1) * norm(t2) + end + end + # no tensor product test via conversion + @timedtestset "Tensor product: test via tensor contraction" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V2 ⊗ V3 ⊗ V1) + t2 = rand(T, V2 ⊗ V1 ⊗ V3) + t = @constinferred (t1 ⊗ t2) + @tensor t′[1, 2, 3, 4, 5, 6] := t1[1, 2, 3] * t2[4, 5, 6] + @test t ≈ t′ + end + end +end + +# TODO: add AD tests? + TensorKit.empty_globalcaches!() ########## tf = time() From 159143e07401aa52a4229dcdd2006c1f1f1ce776 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 18:00:39 +0200 Subject: [PATCH 47/86] remove todo --- src/spaces/multifusionspace.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 890282069..2913812a7 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -3,8 +3,6 @@ # make this a separate module? -# possible TODO: zero GradedSpace? - function dim(V::Vect[IsingBimod]) T = Base.promote_op(*, Int, real(sectorscalartype(sectortype(V)))) return reduce(+, dim(V, c) * dim(c) for c in sectors(V); From 42fd12b64c832f294b34204fb1e168fdc28dace7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 30 Jul 2025 18:01:47 +0200 Subject: [PATCH 48/86] remove version check --- test/tensors.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/tensors.jl b/test/tensors.jl index 25bc157b9..59c8b97ef 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -235,8 +235,7 @@ for V in spacelist @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) end - t3 = VERSION < v"1.7" ? repartition(t, k) : - @constinferred repartition(t, $k) + t3 = @constinferred repartition(t, $k) @test norm(t3) ≈ norm(t) t3′ = @constinferred repartition!(similar(t3), t′) @test norm(t3′) ≈ norm(t′) From 5393d87b289124bc14997eaba75fb978af6493a5 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 31 Jul 2025 14:04:19 +0200 Subject: [PATCH 49/86] remove tests that will inherently fail + comments on future changes --- test/multifusion.jl | 191 +++++++++++++------------------------------- 1 file changed, 54 insertions(+), 137 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 5a7669240..923c2fe79 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -665,7 +665,9 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end -# whatever V will be +# whatever V will be, for now VIB1 +#TODO: test with non-diagonal sectors +# needs to be 1 dimensional stuff for the isomorphism test in removeunit V1, V2, V3, V4, V5 = V @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests @@ -703,7 +705,7 @@ V1, V2, V3, V4, V5 = V @test t == convert(TensorMap, d) end end - # no tensor array conversion tests + # no tensor array conversion tests: no fusion tensor @timedtestset "Basic linear algebra" begin W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 for T in (Float32, ComplexF64) @@ -756,26 +758,25 @@ V1, V2, V3, V4, V5 = V end end @timedtestset "Trivial space insertion and removal" begin - W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 # passes for diagonal sectors for T in (Float32, ComplexF64) t = @constinferred rand(T, W) - # t2 = @constinferred insertleftunit(t) - - # CONTINUE HERE + t2 = @constinferred insertleftunit(t, 5) # default errors - @test t2 == @constinferred insertrightunit(t) + @test t2 == @constinferred insertrightunit(t, 4) # default doesn't error bc i==N then @test numind(t2) == numind(t) + 1 - @test space(t2) == insertleftunit(space(t)) + @test space(t2) == insertleftunit(space(t), 5) @test scalartype(t2) === T @test t.data === t2.data - @test @constinferred(removeunit(t2, $(numind(t2)))) == t - t3 = @constinferred insertleftunit(t; copy=true) - @test t3 == @constinferred insertrightunit(t; copy=true) + @test @constinferred(removeunit(t2, $(numind(t2)) - 1)) == t # -1 required + + t3 = @constinferred insertleftunit(t, 5; copy=true) # same here + @test t3 == @constinferred insertrightunit(t, 4; copy=true) @test t.data !== t3.data for (c, b) in blocks(t) @test b == block(t3, c) end - @test @constinferred(removeunit(t3, $(numind(t3)))) == t + @test @constinferred(removeunit(t3, $(numind(t3)) - 1)) == t t4 = @constinferred insertrightunit(t, 3; dual=true) @test numin(t4) == numin(t) && numout(t4) == numout(t) + 1 for (c, b) in blocks(t) @@ -790,41 +791,7 @@ V1, V2, V3, V4, V5 = V @test @constinferred(removeunit(t5, 4)) == t 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 - @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 + # no basic linear algebra tests via conversion: no fusion tensor @timedtestset "Tensor conversion" begin W = V1 ⊗ V2 t = @constinferred randn(W ← W) @@ -836,101 +803,46 @@ V1, V2, V3, V4, V5 = V @test Base.promote_typeof(t, tc) == typeof(tc) @test Base.promote_typeof(tc, t) == typeof(tc + t) end - @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 = VERSION < v"1.7" ? repartition(t, k) : - @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 - # no permutations test via conversion + # no permutations test via inner product invariance: NoBraiding + # no permutations test via conversion: NoBraiding and no fusion tensor @timedtestset "Full trace: test self-consistency" begin - 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) + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') + t2 = permute(t, ((1, 2), (4, 3))) #TODO: rewrite to not permute + 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 - 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 - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') - @tensor t2[a, b] := t[c, d, b, d, c, a] + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') #TODO: fix by removing need to permute + @tensor t2[a, b] := t[c, d, b, d, c, a] # change @tensor to @planar @tensor t4[a, b, c, d] := t[d, e, b, e, c, a] @tensor t5[a, b] := t4[a, b, c, c] @test t2 ≈ t5 end - # no trace test via conversion + # no trace test via conversion: NoBraiding and no fusion tensor @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 ta[a, b] := t1[x, y, a] * t2[y, b, x] #TODO: fix by making planar @tensor tb[a, b] := t3[x, y, a, y, b, x] @test ta ≈ tb end - # no tensor contraction test via contraction - @timedtestset "Index flipping: test flipping inverse" begin - t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) - for i in 1:4 - @test t ≈ flip(flip(t, i), i; inv=true) - @test t ≈ flip(flip(t, i; inv=true), i) - end - end - @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 - @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 + # no tensor contraction test via conversion: NoBraiding and no fusion tensor + # no index flipping tests: NoBraiding @timedtestset "Multiplication of isometries: test properties" begin - W2 = V4 ⊗ V5 + W2 = V4 ⊗ V5 # works for diagonal sectors W1 = W2 ⊗ (oneunit(V1) ⊕ oneunit(V1)) for T in (Float64, ComplexF64) t1 = randisometry(T, W1, W2) @@ -943,7 +855,7 @@ V1, V2, V3, V4, V5 = V end end @timedtestset "Multiplication and inverse: test compatibility" begin - W1 = V1 ⊗ V2 ⊗ V3 + W1 = V1 ⊗ V2 ⊗ V3 # works for diagonal sectors W2 = V4 ⊗ V5 for T in (Float64, ComplexF64) t1 = rand(T, W1, W1) @@ -960,9 +872,9 @@ V1, V2, V3, V4, V5 = V @test tp ≈ tp * tp end end - # no multiplication and inverse test via conversion + # no multiplication and inverse test via conversion: NoBraiding and no fusion tensor @timedtestset "diag/diagm" begin - W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 # works for diagonal sectors t = randn(ComplexF64, W) d = LinearAlgebra.diag(t) D = LinearAlgebra.diagm(codomain(t), domain(t), d) @@ -972,6 +884,9 @@ V1, V2, V3, V4, V5 = V @timedtestset "Factorization" begin W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 for T in (Float32, ComplexF64) + # no left/rightorth, left/rightnull, tsvd, cond and rank tests for filled tensor + #TODO: rewrite these tests without permuting + # Test both a normal tensor and an adjoint one. ts = (rand(T, W), rand(T, W)') for t in ts @@ -1054,7 +969,9 @@ V1, V2, V3, V4, V5 = V @test cond(t4) ≈ λmax / λmin end end - @testset "empty tensor" begin + + # how useful is this test? + @testset "empty tensor" begin # passes for diagonal sectors t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QRpos(), @@ -1136,7 +1053,7 @@ V1, V2, V3, V4, V5 = V end end end - @timedtestset "Tensor truncation" begin + @timedtestset "Tensor truncation" begin # works for diagonal case for T in (Float32, ComplexF64) for p in (1, 2, 3, Inf) # Test both a normal tensor and an adjoint one. @@ -1167,8 +1084,8 @@ V1, V2, V3, V4, V5 = V end end end - # no tensor functions tests - @timedtestset "Sylvester equation" begin + # no tensor functions tests: NoBraiding and no fusion tensor + @timedtestset "Sylvester equation" begin # works for diagonal case for T in (Float32, ComplexF64) tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) @@ -1183,7 +1100,7 @@ V1, V2, V3, V4, V5 = V # no reshape test end end - @timedtestset "Tensor product: test via norm preservation" begin + @timedtestset "Tensor product: test via norm preservation" begin # works for diagonal case for T in (Float32, ComplexF64) t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1 ⊗ V2) t2 = rand(T, V2 ⊗ V1 ⊗ V3, V1 ⊗ V1) @@ -1191,8 +1108,8 @@ V1, V2, V3, V4, V5 = V @test norm(t) ≈ norm(t1) * norm(t2) end end - # no tensor product test via conversion - @timedtestset "Tensor product: test via tensor contraction" begin + # no tensor product test via conversion: NoBraiding and no fusion tensor + @timedtestset "Tensor product: test via tensor contraction" begin # works for diagonal case for T in (Float32, ComplexF64) t1 = rand(T, V2 ⊗ V3 ⊗ V1) t2 = rand(T, V2 ⊗ V1 ⊗ V3) From 5898bc0041fbff4ae2a96bcf98c34d8129200305 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 31 Jul 2025 15:40:35 +0200 Subject: [PATCH 50/86] rewrite tensor factorisation tests to not use `permute` --- test/multifusion.jl | 147 +++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 83 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 923c2fe79..da5fdd952 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -599,7 +599,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) ) ) end - @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QL()) + @testset "leftorth with $alg" for alg in (QR(), QL()) Q, R = @constinferred leftorth(t; alg = alg) QdQ = Q' * Q @test QdQ ≈ one(QdQ) @@ -608,7 +608,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test isposdef(R) end end - @testset "rightorth with $alg" for alg in (TensorKit.RQ(), TensorKit.LQ()) + @testset "rightorth with $alg" for alg in (RQ(), LQ()) L, Q = @constinferred rightorth(t; alg = alg) QQd = Q * Q' @test QQd ≈ one(QQd) @@ -617,7 +617,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test isposdef(L) end end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + @testset "tsvd with $alg" for alg in (SVD(), SDD()) U, S, Vᴴ = @constinferred tsvd(t; alg = alg) UdU = U' * U @test UdU ≈ one(UdU) @@ -882,91 +882,81 @@ V1, V2, V3, V4, V5 = V @test LinearAlgebra.diag(D) == d end @timedtestset "Factorization" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + WL = V3 ⊗ V4 ⊗ V2 ← V1' ⊗ V5' # old left permute resulted in this space + WR = V3 ⊗ V4 ← V2' ⊗ V1' ⊗ V5' # old right permute for T in (Float32, ComplexF64) - # no left/rightorth, left/rightnull, tsvd, cond and rank tests for filled tensor - #TODO: rewrite these tests without permuting - # Test both a normal tensor and an adjoint one. - ts = (rand(T, W), rand(T, W)') - for t in ts + # adjoint takes other space for shape of matrix in RQ(pos) + for t in (rand(T, WR), rand(T, WL)') + @testset "rightorth with $alg" for alg in + (RQ(), RQpos(), LQ(), LQpos(), + Polar(), SVD(), SDD()) + L, Q = @constinferred rightorth(t; alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ t + if alg isa Polar + @test isposdef(L) + @test domain(L) == codomain(L) == space(t, 1) ⊗ space(t, 2) + end + end + @testset "rightnull with $alg" for alg in (LQ(), SVD(), SDD()) + M = @constinferred rightnull(t; alg=alg) + MMd = M * M' + @test MMd ≈ one(MMd) + @test norm(t * M') < 100 * eps(norm(t)) + end + end + # adjoints take other space for shape of matrix in QL(pos) + for t in (rand(T, WL), rand(T, WR)') @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - Q, R = @constinferred leftorth(t, ((3, 4, 2), (1, 5)); alg=alg) + (QR(), QRpos(), QL(), QLpos(), + Polar(), SVD(), SDD()) + Q, R = @constinferred leftorth(t; alg=alg) QdQ = Q' * Q @test QdQ ≈ one(QdQ) - @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) + @test Q * R ≈ t if alg isa Polar @test isposdef(R) - @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' + @test domain(R) == codomain(R) == space(t, 4)' ⊗ space(t, 5)' end end @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) - N = @constinferred leftnull(t, ((3, 4, 2), (1, 5)); alg=alg) + (QR(), SVD(), SDD()) + N = @constinferred leftnull(t; alg=alg) NdN = N' * N @test NdN ≈ one(NdN) - @test norm(N' * permute(t, ((3, 4, 2), (1, 5)))) < - 100 * eps(norm(t)) + @test norm(N' * t) < 100 * eps(norm(t)) end - @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) - QQd = Q * Q' - @test QQd ≈ one(QQd) - @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) - if alg isa Polar - @test isposdef(L) - @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) - end - end - @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) - M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) - MMd = M * M' - @test MMd ≈ one(MMd) - @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < - 100 * eps(norm(t)) - end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) + @testset "tsvd with $alg" for alg in (SVD(), SDD()) + U, S, V = @constinferred tsvd(t; alg=alg) UdU = U' * U @test UdU ≈ one(UdU) VVd = V * V' @test VVd ≈ one(VVd) - t2 = permute(t, ((3, 4, 2), (1, 5))) - @test U * S * V ≈ t2 + @test U * S * V ≈ t - s = LinearAlgebra.svdvals(t2) + s = LinearAlgebra.svdvals(t) s′ = LinearAlgebra.diag(S) for (c, b) in s @test b ≈ s′[c] end end @testset "cond and rank" begin - t2 = permute(t, ((3, 4, 2), (1, 5))) - d1 = dim(codomain(t2)) - d2 = dim(domain(t2)) - @test rank(t2) == min(d1, d2) - M = leftnull(t2) + d1 = dim(codomain(t)) + d2 = dim(domain(t)) + @test rank(t) == min(d1, d2) + M = leftnull(t) @test rank(M) == max(d1, d2) - min(d1, d2) - t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) - @test cond(t3) ≈ one(real(T)) - @test rank(t3) == dim(V1 ⊗ V2) - t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) - t4 = (t4 + t4') / 2 - vals = LinearAlgebra.eigvals(t4) + t2 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test cond(t2) ≈ one(real(T)) + @test rank(t2) == dim(V1 ⊗ V2) + t3 = randn(T, V1 ⊗ V2, V1 ⊗ V2) + t3 = (t3 + t3') / 2 + vals = LinearAlgebra.eigvals(t3) λmax = maximum(s -> maximum(abs, s), values(vals)) λmin = minimum(s -> minimum(abs, s), values(vals)) - @test cond(t4) ≈ λmax / λmin + @test cond(t3) ≈ λmax / λmin end end @@ -974,38 +964,30 @@ V1, V2, V3, V4, V5 = V @testset "empty tensor" begin # passes for diagonal sectors t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) + (QR(), QRpos(), QL(), QLpos(), + Polar(), SVD(), SDD()) Q, R = @constinferred leftorth(t; alg=alg) @test Q == t @test dim(Q) == dim(R) == 0 end - @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) + @testset "leftnull with $alg" for alg in (QR(), SVD(), SDD()) N = @constinferred leftnull(t; alg=alg) @test N' * N ≈ id(domain(N)) @test N * N' ≈ id(codomain(N)) end @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) + (RQ(), RQpos(), LQ(), LQpos(), + Polar(), SVD(), SDD()) L, Q = @constinferred rightorth(copy(t'); alg=alg) @test Q == t' @test dim(Q) == dim(L) == 0 end - @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) + @testset "rightnull with $alg" for alg in (LQ(), SVD(), SDD()) M = @constinferred rightnull(copy(t'); alg=alg) @test M * M' ≈ id(codomain(M)) @test M' * M ≈ id(domain(M)) end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + @testset "tsvd with $alg" for alg in (SVD(), SDD()) U, S, V = @constinferred tsvd(t; alg=alg) @test U == t @test dim(U) == dim(S) == dim(V) @@ -1018,13 +1000,12 @@ V1, V2, V3, V4, V5 = V @test cond(t2) == 0.0 end end - t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') + t = rand(T, V1 ⊗ V2 ← V1 ⊗ V2) @testset "eig and isposdef" begin - D, V = eigen(t, ((1, 3), (2, 4))) - t2 = permute(t, ((1, 3), (2, 4))) - @test t2 * V ≈ V * D + D, V = eigen(t) + @test t * V ≈ V * D - d = LinearAlgebra.eigvals(t2; sortby=nothing) + d = LinearAlgebra.eigvals(t; sortby=nothing) d′ = LinearAlgebra.diag(D) for (c, b) in d @test b ≈ d′[c] @@ -1036,8 +1017,8 @@ V1, V2, V3, V4, V5 = V VdV = (VdV + VdV') / 2 @test isposdef(VdV) - @test !isposdef(t2) # unlikely for non-hermitian map - t2 = (t2 + t2') + @test !isposdef(t) # unlikely for non-hermitian map + t2 = (t + t') D, V = eigen(t2) VdV = V' * V @test VdV ≈ one(VdV) From 6351cf0752a7d4bc48c6ba41f81e5fb69e26ccb6 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 31 Jul 2025 15:55:43 +0200 Subject: [PATCH 51/86] rewrite tensor product test via contraction to not permute + be planar --- test/multifusion.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index da5fdd952..3c19e5379 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -1078,7 +1078,7 @@ V1, V2, V3, V4, V5 = V @test domain(t) == V2 ⊗ V4 @test norm(tA * t + t * tB + tC) < (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) - # no reshape test + # no reshape test: NoBraiding and no fusion tensor end end @timedtestset "Tensor product: test via norm preservation" begin # works for diagonal case @@ -1092,10 +1092,10 @@ V1, V2, V3, V4, V5 = V # no tensor product test via conversion: NoBraiding and no fusion tensor @timedtestset "Tensor product: test via tensor contraction" begin # works for diagonal case for T in (Float32, ComplexF64) - t1 = rand(T, V2 ⊗ V3 ⊗ V1) - t2 = rand(T, V2 ⊗ V1 ⊗ V3) + t1 = rand(T, V2 ⊗ V3, V1) + t2 = rand(T, V2, V1 ⊗ V3) t = @constinferred (t1 ⊗ t2) - @tensor t′[1, 2, 3, 4, 5, 6] := t1[1, 2, 3] * t2[4, 5, 6] + @planar t′[1 2 4; 3 5 6] := t1[1 2; 3] * t2[4; 5 6] @test t ≈ t′ end end From 2d107a52776822c7b5f609301921203210ea874f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 31 Jul 2025 17:15:39 +0200 Subject: [PATCH 52/86] rewrite partial and full trace tests to not use `permute` and friends --- test/multifusion.jl | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 3c19e5379..756bee0b8 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -806,28 +806,20 @@ V1, V2, V3, V4, V5 = V # no permutations test via inner product invariance: NoBraiding # no permutations test via conversion: NoBraiding and no fusion tensor @timedtestset "Full trace: test self-consistency" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') - t2 = permute(t, ((1, 2), (4, 3))) #TODO: rewrite to not permute - 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) + s = @constinferred tr(t) + @test conj(s) ≈ tr(t') + @planar s2 = t[a b; a b] # no twist needed bc permute avoided + @planar t3[a; b] := t[a c; b c] + @planar s3 = t3[a; a] + @test s ≈ s2 + @test s ≈ s3 + end @timedtestset "Partial trace: test self-consistency" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') #TODO: fix by removing need to permute - @tensor t2[a, b] := t[c, d, b, d, c, a] # change @tensor to @planar - @tensor t4[a, b, c, d] := t[d, e, b, e, c, a] - @tensor t5[a, b] := t4[a, b, c, c] + t= rand(ComplexF64, V1 ⊗ V3 ⊗ V2 ← V1 ⊗ V3 ⊗ V2) + @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 # no trace test via conversion: NoBraiding and no fusion tensor From 1d3c1709ff8a76f3e8ecac1aaf009f9dc76091b2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 31 Jul 2025 17:55:49 +0200 Subject: [PATCH 53/86] give all objects of `IsingBimod` a name + attempt at non-diagonal spaces --- test/multifusion.jl | 89 +++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 756bee0b8..f8802f4a6 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -6,6 +6,30 @@ println("Multifusion tests for $Istr") println("------------------------------------") ti = time() +C0, C1, D0, D1, M, Mop = I(1, 1, 0), I(1, 1, 1), I(2, 2, 0), I(2, 2, 1), I(1, 2, 0), I(2, 1, 0) +VIB1 = ( + Vect[I](C0 => 1, C1 => 1), + Vect[I](C0 => 1, C1 => 2), + Vect[I](C0 => 3, C1 => 2), + Vect[I](C0 => 2, C1 => 3), + Vect[I](C0 => 2, C1 => 5), +) + +VIB2 = ( + Vect[I](D0 => 1, D1 => 1), + Vect[I](D0 => 1, D1 => 2), + Vect[I](D0 => 3, D1 => 2), + Vect[I](D0 => 2, D1 => 3), + Vect[I](D0 => 2, D1 => 5), +) + +VIB3 = (Vect[I](I(1,2,0) => 2), + Vect[I](I(2, 2, 0) => 1, I(2,2,1) => 2), # MD ← CMD + Vect[I](I(1, 1, 0) => 2, I(1,1,1) => 3), + Vect[I](I(1,2,0) => 4), + Vect[I](I(2, 2, 0) => 3, I(2,2,1) => 5) + ) + @timedtestset "Multifusion spaces " verbose = true begin @timedtestset "GradedSpace: $(TensorKit.type_repr(Vect[I]))" begin gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) @@ -34,10 +58,10 @@ ti = time() @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) # space with a single sector - Wleft = @constinferred Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 1) - Wright = @constinferred Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1) - WM = @constinferred Vect[I](I(1, 2, 0) => 1) - WMop = @constinferred Vect[I](I(2, 1, 0) => 1) + Wleft = @constinferred Vect[I](C0 => 1, C1 => 1) + Wright = @constinferred Vect[I](D0 => 1, D1 => 1) + WM = @constinferred Vect[I](M => 1) + WMop = @constinferred Vect[I](Mop => 1) @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) @@ -78,15 +102,15 @@ ti = time() # sensible direct sums and fuses @test @constinferred(⊕(Wleft, WM)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(1, 1, 0)) + Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) @test @constinferred(⊕(Wright, WMop)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == I(2, 2, 0)) + Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) @test @constinferred(⊕(Wright, WM)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(2, 2, 0)) + Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) @test @constinferred(⊕(Wleft, WMop)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == I(1, 1, 0)) - @test @constinferred(fuse(Wleft, WM)) == Vect[I](I(1, 2, 0) => 2) - @test @constinferred(fuse(Wright, WMop)) == Vect[I](I(2, 1, 0) => 2) + Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) + @test @constinferred(fuse(Wleft, WM)) == Vect[I](M => 2) + @test @constinferred(fuse(Wright, WMop)) == Vect[I](Mop => 2) # less sensible direct sums and fuses @test @constinferred(⊕(Wleft, Wright)) == @@ -110,32 +134,16 @@ ti = time() @test V == @constinferred infimum(V, ⊕(V, V)) @test V ≺ ⊕(V, V) @test !(V ≻ ⊕(V, V)) - @test infimum(V, GradedSpace(I(1, 1, 0) => 3)) == - GradedSpace(I(1, 1, 0) => 2) - @test infimum(V, GradedSpace(I(1, 2, 0) => 6)) == - GradedSpace(I(1, 2, 0) => 5) + @test infimum(V, GradedSpace(C0 => 3)) == + GradedSpace(C0 => 2) + @test infimum(V, GradedSpace(M => 6)) == + GradedSpace(M => 5) for W in [WM, WMop, Wright] @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) end @test_throws SpaceMismatch (⊕(V, V')) end - VIB1 = ( - Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 1), - Vect[I](I(1, 1, 0) => 1, I(1, 1, 1) => 2), - Vect[I](I(1, 1, 0) => 3, I(1, 1, 1) => 2), - Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 3), - Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 5), - ) - - VIB2 = ( - Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 1), - Vect[I](I(2, 2, 0) => 1, I(2, 2, 1) => 2), - Vect[I](I(2, 2, 0) => 3, I(2, 2, 1) => 2), - Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 3), - Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 5), - ) - @timedtestset "HomSpace with $(TensorKit.type_repr(Vect[I])) " begin for (V1, V2, V3, V4, V5) in (VIB1, VIB2) #TODO: examples with module spaces W = HomSpace(V1 ⊗ V2, V3 ⊗ V4 ⊗ V5) @@ -176,7 +184,6 @@ end @timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true begin N = 6 - C0, C1, D0, D1, M, Mop = I(1, 1, 0), I(1, 1, 1), I(2, 2, 0), I(2, 2, 1), I(1, 2, 0), I(2, 1, 0) out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? isdual = ntuple(n -> rand(Bool), N) in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out @@ -554,7 +561,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden end @timedtestset "Tensor contraction" begin - for W in (Vect[I](I(1, 1, 0) => 2, I(1, 1, 1) => 3), Vect[I](I(2, 2, 0) => 2, I(2, 2, 1) => 3)) + for W in (Vect[I](C0 => 2, C1 => 3), Vect[I](D0 => 2, D1 => 3)) d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) t = TensorMap(d) A = randn(ComplexF64, W ⊗ W' ⊗ W, W) @@ -668,11 +675,15 @@ end # whatever V will be, for now VIB1 #TODO: test with non-diagonal sectors # needs to be 1 dimensional stuff for the isomorphism test in removeunit -V1, V2, V3, V4, V5 = V -@assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests -@assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests +# is ^ still true? + +for V in (VIB1, VIB2) + V1, V2, V3, V4, V5 = V + @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests + @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests +end -@timedtestset "Tensors with symmetry: $Istr" verbose = true begin +@timedtestset "Tensors with symmetry: $Istr" verbose = true for V in (VIB1, VIB2) V1, V2, V3, V4, V5 = V @timedtestset "Basic tensor properties" begin # passes for diagonal sectors W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 @@ -687,10 +698,10 @@ V1, V2, V3, V4, V5 = V @test typeof(t) == TensorMap{T,spacetype(t),5,0,Vector{T}} # blocks bs = @constinferred blocks(t) - (c, b1), state = @constinferred Nothing iterate(bs) + (c, b1), state = @constinferred Nothing iterate(bs) # fusion matters here @test c == first(blocksectors(W)) next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t, first(blocksectors(t))) + b2 = @constinferred block(t, first(blocksectors(t))) # and here @test b1 == b2 @test eltype(bs) === Pair{typeof(c),typeof(b1)} @test typeof(b1) === TensorKit.blocktype(t) @@ -702,7 +713,7 @@ V1, V2, V3, V4, V5 = V for T in (Int, Float32, ComplexF64) t = @constinferred rand(T, W) d = convert(Dict, t) - @test t == convert(TensorMap, d) + @test t == convert(TensorMap, d) # fusion matters here end end # no tensor array conversion tests: no fusion tensor From bae646a504cf69eee202115fff91ba8607f3ca21 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 11:13:43 +0200 Subject: [PATCH 54/86] add off-diagonal spaces + make (most) tests compatible with off-diagonal spaces + bunch of comments --- test/multifusion.jl | 280 +++++++++++++++++++++++++++----------------- 1 file changed, 174 insertions(+), 106 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index f8802f4a6..cf3dbf174 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -7,7 +7,7 @@ println("------------------------------------") ti = time() C0, C1, D0, D1, M, Mop = I(1, 1, 0), I(1, 1, 1), I(2, 2, 0), I(2, 2, 1), I(1, 2, 0), I(2, 1, 0) -VIB1 = ( +VIBC = ( Vect[I](C0 => 1, C1 => 1), Vect[I](C0 => 1, C1 => 2), Vect[I](C0 => 3, C1 => 2), @@ -15,7 +15,7 @@ VIB1 = ( Vect[I](C0 => 2, C1 => 5), ) -VIB2 = ( +VIBD = ( Vect[I](D0 => 1, D1 => 1), Vect[I](D0 => 1, D1 => 2), Vect[I](D0 => 3, D1 => 2), @@ -23,12 +23,33 @@ VIB2 = ( Vect[I](D0 => 2, D1 => 5), ) -VIB3 = (Vect[I](I(1,2,0) => 2), - Vect[I](I(2, 2, 0) => 1, I(2,2,1) => 2), # MD ← CMD - Vect[I](I(1, 1, 0) => 2, I(1,1,1) => 3), - Vect[I](I(1,2,0) => 4), - Vect[I](I(2, 2, 0) => 3, I(2,2,1) => 5) - ) +VIBM1 = (Vect[I](C0 => 1, C1 => 2), # not a random ordering! designed to make V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 work + Vect[I](M => 3), + Vect[I](C0 => 2, C1 => 3), + Vect[I](M => 4), + Vect[I](D0 => 3, D1 => 4) +) + +VIBM2 = (Vect[I](M => 2), Vect[I](D0 =>1, D1 => 1), + Vect[I](C0 => 1, C1 => 2), + Vect[I](M => 4), + Vect[I](D0 => 3, D1 => 4) +) + +VIBMop1 = (Vect[I](Mop => 2), + Vect[I](C0 => 1, C1 => 2), + Vect[I](D0 => 3, D1 => 4), + Vect[I](Mop => 4), + Vect[I](C0 => 3, C1 => 4)) + +VIBMop2 = (Vect[I](D0 => 1, D1 => 1), + Vect[I](Mop => 2), + Vect[I](D0 => 3, D1 => 4), + Vect[I](Mop => 4), + Vect[I](C0 => 3, C1 => 4) +) + +#TODO: MxMop or MopxM fusion needed? @timedtestset "Multifusion spaces " verbose = true begin @timedtestset "GradedSpace: $(TensorKit.type_repr(Vect[I]))" begin @@ -114,7 +135,7 @@ VIB3 = (Vect[I](I(1,2,0) => 2), # less sensible direct sums and fuses @test @constinferred(⊕(Wleft, Wright)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == rightone(c)) + Vect[I](c => 1 for c in sectors(V) if c.row == c.col) @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == Vect[I](c => 0 for c in sectors(V)) @@ -145,7 +166,7 @@ VIB3 = (Vect[I](I(1,2,0) => 2), end @timedtestset "HomSpace with $(TensorKit.type_repr(Vect[I])) " begin - for (V1, V2, V3, V4, V5) in (VIB1, VIB2) #TODO: examples with module spaces + for (V1, V2, V3, V4, V5) in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) W = HomSpace(V1 ⊗ V2, V3 ⊗ V4 ⊗ V5) @test W == (V3 ⊗ V4 ⊗ V5 → V1 ⊗ V2) @test W == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5) @@ -167,15 +188,15 @@ VIB3 = (Vect[I](I(1,2,0) => 2), @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') @test_throws ErrorException insertleftunit(W) - @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ oneunit(V1)) + @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) # works for VIBM1, VIBM2 @test_throws ErrorException insertrightunit(W, 6) @test_throws ErrorException insertleftunit(W, 6) - @test (V1 ⊗ V2 ⊗ oneunit(V1) ← V3 ⊗ V4 ⊗ V5) == - @constinferred(insertrightunit(W, 2)) - @test (V1 ⊗ V2 ← oneunit(V1) ⊗ V3 ⊗ V4 ⊗ V5) == - @constinferred(insertleftunit(W, 3)) - @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W + @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == + @constinferred(insertrightunit(W, 2)) # works for VIBM + @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == + @constinferred(insertleftunit(W, 3)) # same + @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W # same @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) end @@ -672,55 +693,60 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end -# whatever V will be, for now VIB1 #TODO: test with non-diagonal sectors # needs to be 1 dimensional stuff for the isomorphism test in removeunit # is ^ still true? -for V in (VIB1, VIB2) +for V in (VIBC, VIBD) V1, V2, V3, V4, V5 = V @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests - @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests + @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests -> this condition makes it hard to test non-diagonal sectors end -@timedtestset "Tensors with symmetry: $Istr" verbose = true for V in (VIB1, VIB2) +@timedtestset "Tensors with symmetry: $Istr" verbose = true for V in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) V1, V2, V3, V4, V5 = V @timedtestset "Basic tensor properties" begin # passes for diagonal sectors - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 # fusion matters for T in (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat) t = @constinferred zeros(T, W) + if isempty(t.data) # non-diagonal sector fuses poorly + W = V3 ⊗ V4 ⊗ V5 + t = @constinferred zeros(T, W) # also empty because M isn't diagonal so can't fuse to empty space + end @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}} + @test typeof(t) == TensorMap{T,spacetype(t),length(W),0,Vector{T}} # blocks bs = @constinferred blocks(t) - (c, b1), state = @constinferred Nothing iterate(bs) # fusion matters here - @test c == first(blocksectors(W)) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t, first(blocksectors(t))) # and here - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c),typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t) - @test typeof(c) === sectortype(t) + if !isempty(bs) + (c, b1), state = @constinferred Nothing iterate(bs) # errors if fusion gives empty data + @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 + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 # rewritten to be compatible with module fusion for T in (Int, Float32, ComplexF64) t = @constinferred rand(T, W) d = convert(Dict, t) - @test t == convert(TensorMap, d) # fusion matters here + @test t == convert(TensorMap, d) end end # no tensor array conversion tests: no fusion tensor @timedtestset "Basic linear algebra" begin - W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) + t = @constinferred rand(T, W) # fusion matters here @test scalartype(t) == T @test space(t) == W @test space(t') == W' @@ -756,22 +782,27 @@ end @test dot(t2, t) ≈ conj(dot(t2', t')) @test dot(t2, t) ≈ dot(t', t2') - i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) - 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)) - - w = @constinferred(isometry(T, V1 ⊗ (oneunit(V1) ⊕ oneunit(V1)), # works for diagonal - V1)) - @test dim(w) == 2 * dim(V1 ← V1) - @test w' * w == id(Vector{T}, V1) - @test w * w' == (w * w')^2 + if all(a.row == a.col for a in blocksectors(W)) # can't reverse fusion for these + i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) + 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 + for v in (V1, V2, V3, V4, V5) + wl = @constinferred(isometry(T, (leftoneunit(v) ⊕ leftoneunit(v)) ⊗ v, v)) + wr = @constinferred(isometry(T, v ⊗ (rightoneunit(v) ⊕ rightoneunit(v)), v)) + for w in (wl, wr) + @test dim(w) == 2 * dim(v ← v) + @test w' * w == id(Vector{T}, v) + @test w * w' == (w * w')^2 + end + end end end @timedtestset "Trivial space insertion and removal" begin - W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 # passes for diagonal sectors + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) + t = @constinferred rand(T, W) # fusion matters here t2 = @constinferred insertleftunit(t, 5) # default errors @test t2 == @constinferred insertrightunit(t, 4) # default doesn't error bc i==N then @@ -789,7 +820,7 @@ end end @test @constinferred(removeunit(t3, $(numind(t3)) - 1)) == t t4 = @constinferred insertrightunit(t, 3; dual=true) - @test numin(t4) == numin(t) && numout(t4) == numout(t) + 1 + @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) for (c, b) in blocks(t) @test b == block(t4, c) end @@ -805,7 +836,7 @@ end # no basic linear algebra tests via conversion: no fusion tensor @timedtestset "Tensor conversion" begin W = V1 ⊗ V2 - t = @constinferred randn(W ← W) + t = @constinferred randn(W ← W) # fusion matters here @test typeof(convert(TensorMap, t')) == typeof(t) tc = complex(t) @test convert(typeof(tc), t) == tc @@ -820,46 +851,64 @@ end t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) s = @constinferred tr(t) @test conj(s) ≈ tr(t') - @planar s2 = t[a b; a b] # no twist needed bc permute avoided - @planar t3[a; b] := t[a c; b c] - @planar s3 = t3[a; a] - @test s ≈ s2 - @test s ≈ s3 + try # needed for module cases: certain transposes with module legs will result in different colorings + @planar s2 = t[a b; a b] # no twist needed bc permute avoided + @test s ≈ s2 #FIXME: currently this trace gives zero for VIBD, but not for VIBC + catch e + @test isa(e, SectorMismatch) + end + + try # TODO?: skip module traces + @planar t3[a; b] := t[a c; b c] + @planar s3 = t3[a; a] # this contraction order gives zero for VIBMop1 and VIBMop2 because it traces out the module legs + @test s ≈ s3 + catch e + @test isa(e, SectorMismatch) + end + end @timedtestset "Partial trace: test self-consistency" begin - t= rand(ComplexF64, V1 ⊗ V3 ⊗ V2 ← V1 ⊗ V3 ⊗ V2) + t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # rewritten to be 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 # no trace test via conversion: NoBraiding and no fusion tensor - @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] #TODO: fix by making planar - @tensor tb[a, b] := t3[x, y, a, y, b, x] - @test ta ≈ tb + @timedtestset "Trace and contraction" begin #TODO: find some version of this that works for off-diagonal case + t1 = rand(ComplexF64, V3 ⊗ V4 ⊗ V5) + t2 = rand(ComplexF64, V3 ⊗ V4 ⊗ V5) + t3 = t1 ⊗ t2' + # if all(a.row != a.col for a in blocksectors(t3)) + # replace!(x -> rand(ComplexF64), t3.data) # otherwise full of zeros in off-diagonal case + # end + if all(a.row == a.col for a in blocksectors(t3)) + @planar ta[b; a] := conj(t2[x, a, y]) * t1[x, b, y] # works for diagonal case + @planar tb[a; b] := t3[x a y; x b y] + @test ta ≈ tb + end end # no tensor contraction test via conversion: NoBraiding and no fusion tensor # no index flipping tests: NoBraiding @timedtestset "Multiplication of isometries: test properties" begin - W2 = V4 ⊗ V5 # works for diagonal sectors - W1 = W2 ⊗ (oneunit(V1) ⊕ oneunit(V1)) - for T in (Float64, ComplexF64) - t1 = randisometry(T, W1, W2) - t2 = randisometry(T, W2 ← W2) - @test t1' * t1 ≈ one(t2) - @test t2' * t2 ≈ one(t2) - @test t2 * t2' ≈ one(t2) - P = t1 * t1' - @test P * P ≈ P + W2 = V4 ⊗ V5 + W1 = W2 ⊗ (rightoneunit(V5) ⊕ rightoneunit(V5)) + W3 = (leftoneunit(V4) ⊕ leftoneunit(V4)) ⊗ W2 + for W in (W1, W3) + for T in (Float64, ComplexF64) + t1 = @constinferred randisometry(T, W, W2) + t2 = randisometry(T, W2 ← W2) + @test t1' * t1 ≈ one(t2) + @test t2' * t2 ≈ one(t2) + @test t2 * t2' ≈ one(t2) + P = t1 * t1' + @test P * P ≈ P + end end end @timedtestset "Multiplication and inverse: test compatibility" begin - W1 = V1 ⊗ V2 ⊗ V3 # works for diagonal sectors - W2 = V4 ⊗ V5 + W1 = V1 ⊗ V2 + W2 = V3 ⊗ V4 ⊗ V5 for T in (Float64, ComplexF64) t1 = rand(T, W1, W1) t2 = rand(T, W2 ← W2) @@ -877,7 +926,7 @@ end end # no multiplication and inverse test via conversion: NoBraiding and no fusion tensor @timedtestset "diag/diagm" begin - W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 # works for diagonal sectors + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 t = randn(ComplexF64, W) d = LinearAlgebra.diag(t) D = LinearAlgebra.diagm(codomain(t), domain(t), d) @@ -887,10 +936,17 @@ end @timedtestset "Factorization" begin WL = V3 ⊗ V4 ⊗ V2 ← V1' ⊗ V5' # old left permute resulted in this space WR = V3 ⊗ V4 ← V2' ⊗ V1' ⊗ V5' # old right permute + WmodR = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 # new fusion order for right + WmodL = V1 ⊗ V2 ⊗ V5' ← V3 ⊗ V4 # new fusion order for left + + isdiag = all(c.row == c.col for c in blocksectors(WmodR)) # this blocksectors call should always work + + tsR = isdiag ? (rand(T, WR), rand(T, WL)') : (rand(T, WmodR), rand(T, WmodL)') # shape of matrices require different spaces for left/right + tsL = isdiag ? (rand(T, WL), rand(T, WR)') : (rand(T, WmodR), rand(T, WmodL)') for T in (Float32, ComplexF64) # Test both a normal tensor and an adjoint one. # adjoint takes other space for shape of matrix in RQ(pos) - for t in (rand(T, WR), rand(T, WL)') + for t in tsR @testset "rightorth with $alg" for alg in (RQ(), RQpos(), LQ(), LQpos(), Polar(), SVD(), SDD()) @@ -911,21 +967,26 @@ end end end # adjoints take other space for shape of matrix in QL(pos) - for t in (rand(T, WL), rand(T, WR)') + for t in tsL @testset "leftorth with $alg" for alg in (QR(), QRpos(), QL(), QLpos(), Polar(), SVD(), SDD()) + # skip QL because the monomorphism condition is hard to satisfy for off-diagonal case + # have to skip Polar as well as all tests fail with modules + (alg isa QL || alg isa QLpos || alg isa Polar) && !isdiag && continue Q, R = @constinferred leftorth(t; alg=alg) - QdQ = Q' * Q + QdQ = Q' * Q @test QdQ ≈ one(QdQ) @test Q * R ≈ t if alg isa Polar - @test isposdef(R) - @test domain(R) == codomain(R) == space(t, 4)' ⊗ space(t, 5)' + @test isposdef(R) # this fails with modules + @test domain(R) == codomain(R) == space(t, 4)' ⊗ space(t, 5)' # this as well end end @testset "leftnull with $alg" for alg in (QR(), SVD(), SDD()) + # less rows than columns so either fails or no data in off-diagonal case + !isdiag && continue N = @constinferred leftnull(t; alg=alg) NdN = N' * N @test NdN ≈ one(NdN) @@ -949,8 +1010,10 @@ end d1 = dim(codomain(t)) d2 = dim(domain(t)) @test rank(t) == min(d1, d2) - M = leftnull(t) - @test rank(M) == max(d1, d2) - min(d1, d2) + if isdiag # leftnull doesn't work for off-diagonal case + M = leftnull(t) + @test rank(M) == max(d1, d2) - min(d1, d2) + end t2 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) @test cond(t2) ≈ one(real(T)) @test rank(t2) == dim(V1 ⊗ V2) @@ -963,8 +1026,8 @@ end end end - # how useful is this test? - @testset "empty tensor" begin # passes for diagonal sectors + # how useful is this test? everything just works regardless of the space + @testset "empty tensor" begin t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in (QR(), QRpos(), QL(), QLpos(), @@ -1037,19 +1100,15 @@ end end end end - @timedtestset "Tensor truncation" begin # works for diagonal case + @timedtestset "Tensor truncation" begin for T in (Float32, ComplexF64) + # Test both a normal tensor and an adjoint one. + ts = (randn(T, V1 ⊗ V2, V3 ⊗ V4 ⊗ V5), randn(T, V4 ⊗ V5, V3 ⊗ V1 ⊗ V2)') # rewritten for modules for p in (1, 2, 3, Inf) - # Test both a normal tensor and an adjoint one. - ts = (randn(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5), - randn(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)') for t in ts U₀, S₀, V₀, = tsvd(t) t = rmul!(t, 1 / norm(S₀, p)) U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) - # @show p, ϵ - # @show domain(S) - # @test min(space(S,1), space(S₀,1)) != space(S₀,1) U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), @@ -1059,9 +1118,6 @@ end @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) - # @show p, ϵ - # @show domain(S) - # @test min(space(S,1), space(S₀,1)) != space(S₀,1) U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) end @@ -1069,36 +1125,48 @@ end end end # no tensor functions tests: NoBraiding and no fusion tensor - @timedtestset "Sylvester equation" begin # works for diagonal case + @timedtestset "Sylvester equation" begin for T in (Float32, ComplexF64) - tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) - tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) + tA = rand(T, V1 ⊗ V2, V1 ⊗ V2) # rewritten for modules + tB = rand(T, V4 ⊗ V5, V4 ⊗ V5) tA = 3 // 2 * leftorth(tA; alg=Polar())[1] tB = 1 // 5 * leftorth(tB; alg=Polar())[1] - tC = rand(T, V1 ⊗ V3, V2 ⊗ V4) + tC = rand(T, V1 ⊗ V2, V4 ⊗ V5) t = @constinferred sylvester(tA, tB, tC) - @test codomain(t) == V1 ⊗ V3 - @test domain(t) == V2 ⊗ V4 + @test codomain(t) == V1 ⊗ V2 + @test domain(t) == V4 ⊗ V5 @test norm(tA * t + t * tB + tC) < (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) # no reshape test: NoBraiding and no fusion tensor end end - @timedtestset "Tensor product: test via norm preservation" begin # works for diagonal case + @timedtestset "Tensor product: test via norm preservation" begin for T in (Float32, ComplexF64) - t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1 ⊗ V2) - t2 = rand(T, V2 ⊗ V1 ⊗ V3, V1 ⊗ V1) - t = @constinferred (t1 ⊗ t2) + t1 = rand(T, V3 ⊗ V4 ⊗ V5 ← V1 ⊗ V2) + if all(a.row != a.col for a in blocksectors(t1)) + t2 = rand(T, V5'⊗ V4' ⊗ V3', V2' ⊗ V1') + else + t2 = rand(T, V3' ⊗ V1, V4 ⊗ V5 ⊗ V2') # keep a non-trivial permutation in diagonal case + end + t = @constinferred (t1 ⊗ t2); @test norm(t) ≈ norm(t1) * norm(t2) end end # no tensor product test via conversion: NoBraiding and no fusion tensor @timedtestset "Tensor product: test via tensor contraction" begin # works for diagonal case + W = V3 ⊗ V4 ⊗ V5 ← V1 ⊗ V2 + isdiag = all(a.row == a.col for a in blocksectors(W)) for T in (Float32, ComplexF64) - t1 = rand(T, V2 ⊗ V3, V1) - t2 = rand(T, V2, V1 ⊗ V3) + if !isdiag + t1 = rand(T, W) + t2 = rand(T, V5'⊗ V4' ⊗ V3', V2' ⊗ V1') # same as previous test + @planar t′[1 2 3 6 7 8; 4 5 9 10] := t1[1 2 3; 4 5] * t2[6 7 8; 9 10] + else + t1 = rand(T, V2 ⊗ V3, V1) + t2 = rand(T, V2, V1 ⊗ V3) + @planar t′[1 2 4; 3 5 6] := t1[1 2; 3] * t2[4; 5 6] + end t = @constinferred (t1 ⊗ t2) - @planar t′[1 2 4; 3 5 6] := t1[1 2; 3] * t2[4; 5 6] @test t ≈ t′ end end From 99b8d1cfe7acd494c03d814879cb4c6496e7400c Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 11:14:28 +0200 Subject: [PATCH 55/86] change `blocksectors` to not evaluate `isone` (see comment!) --- src/spaces/multifusionspace.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 2913812a7..8cd5d89c3 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -27,10 +27,10 @@ function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁ return (IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) elseif N₁ == 0 @assert N₂ != 0 "one of Type IsingBimod doesn't exist" - return filter!(isone, collect(blocksectors(dom))) - elseif N₂ == 0 + return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(dom))) # is this what we want? doesn't allow M/Mop to end at empty space + elseif N₂ == 0 # also causes traces over module legs to vanish @assert N₁ != 0 "one of Type IsingBimod doesn't exist" - return filter!(isone, collect(blocksectors(codom))) + return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(codom))) elseif N₂ <= N₁ # keep intersection return filter!(c -> hasblock(codom, c), collect(blocksectors(dom))) else @@ -69,7 +69,7 @@ function insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; return ProductSpace(TupleTools.insertafter(P.spaces, i, (u,))) end -# possible TODO: overwrite defaults at level of HomSpace and TensorMap? +# TODO?: overwrite defaults at level of HomSpace and TensorMap? function insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no defaults? conj::Bool=false, dual::Bool=false) where {i,N} From a8e8b81d790cca573c4a773c2e5117d9209942fd Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 11:24:11 +0200 Subject: [PATCH 56/86] import `leftone` and `rightone` to tests + remove `pentagon/hexagon_equation` import --- test/runtests.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9e44b8a3a..9ab15584f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,7 +3,9 @@ using TestExtras using Random using TensorKit using Combinatorics -using TensorKit: ProductSector, fusiontensor, pentagon_equation, hexagon_equation +using TensorKit: ProductSector, fusiontensor +using TensorKitSectors +using TensorKitSectors: leftone, rightone using TensorOperations using Base.Iterators: take, product # using SUNRepresentations: SUNIrrep @@ -29,7 +31,7 @@ end function randsector(::Type{I}) where {I<:Sector} s = collect(smallset(I)) a = rand(s) - while a == one(a) # don't use trivial label + while a == leftone(a) == rightone(a) # don't use trivial label a = rand(s) end return a From 45ff5591dbd1911fea5bcd22881563e0c2f1cd26 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 12:27:02 +0200 Subject: [PATCH 57/86] respecify TensorKit method for factorisation algorithms in multifusion --- test/multifusion.jl | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index cf3dbf174..342423475 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -1,5 +1,5 @@ I = IsingBimod -Istr = TensorKit.type_repr(I) +Istr = TK.type_repr(I) println("------------------------------------") println("Multifusion tests for $Istr") @@ -52,11 +52,11 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), #TODO: MxMop or MopxM fusion needed? @timedtestset "Multifusion spaces " verbose = true begin - @timedtestset "GradedSpace: $(TensorKit.type_repr(Vect[I]))" begin + @timedtestset "GradedSpace: $(TK.type_repr(Vect[I]))" begin gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) V = GradedSpace(gen) - @test eval(Meta.parse(TensorKit.type_repr(typeof(V)))) == typeof(V) + @test eval(Meta.parse(TK.type_repr(typeof(V)))) == typeof(V) @test eval(Meta.parse(sprint(show, V))) == V @test eval(Meta.parse(sprint(show, V'))) == V' @test V' == GradedSpace(gen; dual = true) @@ -165,7 +165,7 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test_throws SpaceMismatch (⊕(V, V')) end - @timedtestset "HomSpace with $(TensorKit.type_repr(Vect[I])) " begin + @timedtestset "HomSpace with $(TK.type_repr(Vect[I])) " begin for (V1, V2, V3, V4, V5) in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) W = HomSpace(V1 ⊗ V2, V3 ⊗ V4 ⊗ V5) @test W == (V3 ⊗ V4 ⊗ V5 → V1 ⊗ V2) @@ -185,7 +185,7 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test W == deepcopy(W) @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') - @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') + @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TK.compose(W, W') @test_throws ErrorException insertleftunit(W) @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) # works for VIBM1, VIBM2 @@ -203,7 +203,7 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), end end -@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true begin +@timedtestset "Fusion trees for $(TK.type_repr(I))" verbose = true begin N = 6 out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? isdual = ntuple(n -> rand(Bool), N) @@ -488,7 +488,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) 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(b1) === TK.blocktype(t) # basic linear algebra @test isa(@constinferred(norm(t)), real(T)) @test norm(t)^2 ≈ dot(t, t) @@ -627,7 +627,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) ) ) end - @testset "leftorth with $alg" for alg in (QR(), QL()) + @testset "leftorth with $alg" for alg in (TK.QR(), TK.QL()) Q, R = @constinferred leftorth(t; alg = alg) QdQ = Q' * Q @test QdQ ≈ one(QdQ) @@ -636,7 +636,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test isposdef(R) end end - @testset "rightorth with $alg" for alg in (RQ(), LQ()) + @testset "rightorth with $alg" for alg in (TK.RQ(), TK.LQ()) L, Q = @constinferred rightorth(t; alg = alg) QQd = Q * Q' @test QQd ≈ one(QQd) @@ -645,7 +645,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test isposdef(L) end end - @testset "tsvd with $alg" for alg in (SVD(), SDD()) + @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) U, S, Vᴴ = @constinferred tsvd(t; alg = alg) UdU = U' * U @test UdU ≈ one(UdU) @@ -729,7 +729,7 @@ end 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(b1) === TK.blocktype(t) @test typeof(c) === sectortype(t) end end @@ -761,7 +761,7 @@ end 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(b1) === TK.blocktype(t') @test typeof(c) === sectortype(t) # linear algebra @test isa(@constinferred(norm(t)), real(T)) @@ -948,8 +948,8 @@ end # adjoint takes other space for shape of matrix in RQ(pos) for t in tsR @testset "rightorth with $alg" for alg in - (RQ(), RQpos(), LQ(), LQpos(), - Polar(), SVD(), SDD()) + (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(t; alg=alg) QQd = Q * Q' @test QQd ≈ one(QQd) @@ -959,7 +959,7 @@ end @test domain(L) == codomain(L) == space(t, 1) ⊗ space(t, 2) end end - @testset "rightnull with $alg" for alg in (LQ(), SVD(), SDD()) + @testset "rightnull with $alg" for alg in (TK.LQ(), TK.SVD(), TK.SDD()) M = @constinferred rightnull(t; alg=alg) MMd = M * M' @test MMd ≈ one(MMd) @@ -969,8 +969,8 @@ end # adjoints take other space for shape of matrix in QL(pos) for t in tsL @testset "leftorth with $alg" for alg in - (QR(), QRpos(), QL(), QLpos(), - Polar(), SVD(), SDD()) + (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + TK.Polar(), TK.SVD(), TK.SDD()) # skip QL because the monomorphism condition is hard to satisfy for off-diagonal case # have to skip Polar as well as all tests fail with modules (alg isa QL || alg isa QLpos || alg isa Polar) && !isdiag && continue @@ -984,7 +984,7 @@ end end end @testset "leftnull with $alg" for alg in - (QR(), SVD(), SDD()) + (TK.QR(), TK.SVD(), TK.SDD()) # less rows than columns so either fails or no data in off-diagonal case !isdiag && continue N = @constinferred leftnull(t; alg=alg) @@ -992,7 +992,7 @@ end @test NdN ≈ one(NdN) @test norm(N' * t) < 100 * eps(norm(t)) end - @testset "tsvd with $alg" for alg in (SVD(), SDD()) + @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) U, S, V = @constinferred tsvd(t; alg=alg) UdU = U' * U @test UdU ≈ one(UdU) @@ -1030,30 +1030,30 @@ end @testset "empty tensor" begin t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in - (QR(), QRpos(), QL(), QLpos(), - Polar(), SVD(), SDD()) + (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + TK.Polar(), TK.SVD(), TK.SDD()) Q, R = @constinferred leftorth(t; alg=alg) @test Q == t @test dim(Q) == dim(R) == 0 end - @testset "leftnull with $alg" for alg in (QR(), SVD(), SDD()) + @testset "leftnull with $alg" for alg in (TK.QR(), TK.SVD(), TK.SDD()) N = @constinferred leftnull(t; alg=alg) @test N' * N ≈ id(domain(N)) @test N * N' ≈ id(codomain(N)) end @testset "rightorth with $alg" for alg in - (RQ(), RQpos(), LQ(), LQpos(), - Polar(), SVD(), SDD()) + (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(copy(t'); alg=alg) @test Q == t' @test dim(Q) == dim(L) == 0 end - @testset "rightnull with $alg" for alg in (LQ(), SVD(), SDD()) + @testset "rightnull with $alg" for alg in (TK.LQ(), TK.SVD(), TK.SDD()) M = @constinferred rightnull(copy(t'); alg=alg) @test M * M' ≈ id(codomain(M)) @test M' * M ≈ id(domain(M)) end - @testset "tsvd with $alg" for alg in (SVD(), SDD()) + @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) U, S, V = @constinferred tsvd(t; alg=alg) @test U == t @test dim(U) == dim(S) == dim(V) @@ -1129,8 +1129,8 @@ end for T in (Float32, ComplexF64) tA = rand(T, V1 ⊗ V2, V1 ⊗ V2) # rewritten for modules tB = rand(T, V4 ⊗ V5, V4 ⊗ V5) - tA = 3 // 2 * leftorth(tA; alg=Polar())[1] - tB = 1 // 5 * leftorth(tB; alg=Polar())[1] + tA = 3 // 2 * leftorth(tA; alg=TK.Polar())[1] + tB = 1 // 5 * leftorth(tB; alg=TK.Polar())[1] tC = rand(T, V1 ⊗ V2, V4 ⊗ V5) t = @constinferred sylvester(tA, tB, tC) @test codomain(t) == V1 ⊗ V2 @@ -1174,7 +1174,7 @@ end # TODO: add AD tests? -TensorKit.empty_globalcaches!() +TK.empty_globalcaches!() ########## tf = time() printstyled( From 260ef9232e26ef8b872cfb4b854bc040ca1b9870 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 12:45:12 +0200 Subject: [PATCH 58/86] rewrite trace and factorisation tests to not use permute + assert symmetric braiding where it's needed --- test/tensors.jl | 182 +++++++++++++++++++++++------------------------- 1 file changed, 89 insertions(+), 93 deletions(-) diff --git a/test/tensors.jl b/test/tensors.jl index 59c8b97ef..9338d17a4 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -21,9 +21,13 @@ catch (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁)#, VSU₃) end +#TODO?: divide tests into those requiring symmetric braiding and those not +# this allows testing the anyonic sectors + for V in spacelist I = sectortype(first(V)) - Istr = TensorKit.type_repr(I) + Istr = TK.type_repr(I) + symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding println("---------------------------------------") println("Tensors with symmetry: $Istr") println("---------------------------------------") @@ -48,7 +52,7 @@ for V in spacelist 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(b1) === TK.blocktype(t) @test typeof(c) === sectortype(t) end end @@ -110,7 +114,7 @@ for V in spacelist 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(b1) === TK.blocktype(t') @test typeof(c) === sectortype(t) # linear algebra @test isa(@constinferred(norm(t)), real(T)) @@ -222,6 +226,7 @@ for V in spacelist @test Base.promote_typeof(tc, t) == typeof(tc + t) end @timedtestset "Permutations: test via inner product invariance" begin + @assert symmetricbraiding W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 t = rand(ComplexF64, W) t′ = randn!(similar(t)) @@ -267,28 +272,27 @@ for V in spacelist end end @timedtestset "Full trace: test self-consistency" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') - t2 = permute(t, ((1, 2), (4, 3))) - s = @constinferred tr(t2) - @test conj(s) ≈ tr(t2') + t = rand(ComplexF64, V1 ⊗ V2' ← V1 ⊗ V2') + s = @constinferred tr(t) + @test conj(s) ≈ tr(t') if !isdual(V1) - t2 = twist!(t2, 1) + t2 = twist!(t, 1) end if isdual(V2) - t2 = twist!(t2, 2) + t2 = twist!(t, 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] + @tensor s2 = t[a b; a b] + @tensor t2[a; b] := t[a c; b c] + @tensor s3 = t2[a; a] @test ss ≈ s2 @test ss ≈ s3 end @timedtestset "Partial trace: test self-consistency" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') - @tensor t2[a, b] := t[c, d, b, d, c, a] - @tensor t4[a, b, c, d] := t[d, e, b, e, c, a] - @tensor t5[a, b] := t4[a, b, c, c] + t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) + @planar t2[a; b] := t[c d b; c d a] + @planar t4[a b; c d] := t[e d c; e b a] + @planar t5[a; b] := t4[a c; b c] @test t2 ≈ t5 end if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) @@ -328,6 +332,7 @@ for V in spacelist end end @timedtestset "Index flipping: test flipping inverse" begin + @assert BraidingStyle(I) isa SymmetricBraiding t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) for i in 1:4 @test t ≈ flip(flip(t, i), i; inv=true) @@ -335,6 +340,7 @@ for V in spacelist end end @timedtestset "Index flipping: test via explicit flip" begin + @assert BraidingStyle(I) isa SymmetricBraiding t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) F1 = unitary(flip(V1), V1) @@ -348,6 +354,7 @@ for V in spacelist @test twist!(flip(t, 4), 4) ≈ tf end @timedtestset "Index flipping: test via contraction" begin + @assert BraidingStyle(I) isa SymmetricBraiding 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] @@ -439,125 +446,114 @@ for V in spacelist @test LinearAlgebra.diag(D) == d end @timedtestset "Factorization" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + WL = V3 ⊗ V4 ⊗ V2 ← V1' ⊗ V5' + WR = V3 ⊗ V4 ← V2' ⊗ V1' ⊗ V5' for T in (Float32, ComplexF64) # Test both a normal tensor and an adjoint one. - ts = (rand(T, W), rand(T, W)') - for t in ts + tsL = (rand(T, WL), rand(T, WR)') + tsR = (rand(T, WR), rand(T, WL)') # can also have one space by taking adjoints in rightorth/rightnull + # but this avoids taking copies + + for t in tsR + @testset "rightorth with $alg" for alg in + (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + TK.Polar(), TK.SVD(), TK.SDD()) + L, Q = @constinferred rightorth(t; alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ t + if alg isa Polar + @test isposdef(L) + @test domain(L) == codomain(L) == space(t, 1) ⊗ space(t, 2) + end + end + @testset "rightnull with $alg" for alg in (TK.LQ(), TK.SVD(), TK.SDD()) + M = @constinferred rightnull(t; alg=alg) + MMd = M * M' + @test MMd ≈ one(MMd) + @test norm(t * M') < 100 * eps(norm(t)) + end + end + + for t in tsL @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - Q, R = @constinferred leftorth(t, ((3, 4, 2), (1, 5)); alg=alg) + (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + TK.Polar(), TK.SVD(), TK.SDD()) + Q, R = @constinferred leftorth(t; alg=alg) QdQ = Q' * Q @test QdQ ≈ one(QdQ) - @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) + @test Q * R ≈ t if alg isa Polar @test isposdef(R) - @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' + @test domain(R) == codomain(R) == space(t, 4)' ⊗ space(t, 5)' end end @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) - N = @constinferred leftnull(t, ((3, 4, 2), (1, 5)); alg=alg) + (TK.QR(), TK.SVD(), TK.SDD()) + N = @constinferred leftnull(t; alg=alg) NdN = N' * N @test NdN ≈ one(NdN) - @test norm(N' * permute(t, ((3, 4, 2), (1, 5)))) < - 100 * eps(norm(t)) + @test norm(N' * t) < 100 * eps(norm(t)) end - @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) - QQd = Q * Q' - @test QQd ≈ one(QQd) - @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) - if alg isa Polar - @test isposdef(L) - @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) - end - end - @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) - M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) - MMd = M * M' - @test MMd ≈ one(MMd) - @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < - 100 * eps(norm(t)) - end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) + @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) + U, S, V = @constinferred tsvd(t; alg=alg) UdU = U' * U @test UdU ≈ one(UdU) VVd = V * V' @test VVd ≈ one(VVd) - t2 = permute(t, ((3, 4, 2), (1, 5))) - @test U * S * V ≈ t2 + @test U * S * V ≈ t - s = LinearAlgebra.svdvals(t2) + s = LinearAlgebra.svdvals(t) s′ = LinearAlgebra.diag(S) for (c, b) in s @test b ≈ s′[c] end end @testset "cond and rank" begin - t2 = permute(t, ((3, 4, 2), (1, 5))) - d1 = dim(codomain(t2)) - d2 = dim(domain(t2)) - @test rank(t2) == min(d1, d2) - M = leftnull(t2) + d1 = dim(codomain(t)) + d2 = dim(domain(t)) + @test rank(t) == min(d1, d2) + M = leftnull(t) @test rank(M) == max(d1, d2) - min(d1, d2) - t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) - @test cond(t3) ≈ one(real(T)) - @test rank(t3) == dim(V1 ⊗ V2) - t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) - t4 = (t4 + t4') / 2 - vals = LinearAlgebra.eigvals(t4) + t2 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test cond(t2) ≈ one(real(T)) + @test rank(t2) == dim(V1 ⊗ V2) + t3 = randn(T, V1 ⊗ V2, V1 ⊗ V2) + t3 = (t3 + t3') / 2 + vals = LinearAlgebra.eigvals(t3) λmax = maximum(s -> maximum(abs, s), values(vals)) λmin = minimum(s -> minimum(abs, s), values(vals)) - @test cond(t4) ≈ λmax / λmin + @test cond(t3) ≈ λmax / λmin end end @testset "empty tensor" begin t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) + (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + TK.Polar(), TK.SVD(), TK.SDD()) Q, R = @constinferred leftorth(t; alg=alg) @test Q == t @test dim(Q) == dim(R) == 0 end - @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) + @testset "leftnull with $alg" for alg in (TK.QR(), TK.SVD(), TK.SDD()) N = @constinferred leftnull(t; alg=alg) @test N' * N ≈ id(domain(N)) @test N * N' ≈ id(codomain(N)) end @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) + (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(copy(t'); alg=alg) @test Q == t' @test dim(Q) == dim(L) == 0 end @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) + (TK.LQ(), TK.SVD(), TK.SDD()) M = @constinferred rightnull(copy(t'); alg=alg) @test M * M' ≈ id(codomain(M)) @test M' * M ≈ id(domain(M)) end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) U, S, V = @constinferred tsvd(t; alg=alg) @test U == t @test dim(U) == dim(S) == dim(V) @@ -570,13 +566,12 @@ for V in spacelist @test cond(t2) == 0.0 end end - t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') + t = rand(T, V1 ⊗ V2 ← V1 ⊗ V2) @testset "eig and isposdef" begin - D, V = eigen(t, ((1, 3), (2, 4))) - t2 = permute(t, ((1, 3), (2, 4))) - @test t2 * V ≈ V * D + D, V = eigen(t) + @test t * V ≈ V * D - d = LinearAlgebra.eigvals(t2; sortby=nothing) + d = LinearAlgebra.eigvals(t; sortby=nothing) d′ = LinearAlgebra.diag(D) for (c, b) in d @test b ≈ d′[c] @@ -588,8 +583,8 @@ for V in spacelist VdV = (VdV + VdV') / 2 @test isposdef(VdV) - @test !isposdef(t2) # unlikely for non-hermitian map - t2 = (t2 + t2') + @test !isposdef(t) # unlikely for non-hermitian map + t2 = (t + t') D, V = eigen(t2) VdV = V' * V @test VdV ≈ one(VdV) @@ -730,6 +725,7 @@ for V in spacelist end end @timedtestset "Tensor product: test via tensor contraction" begin + @assert symmetricbraiding for T in (Float32, ComplexF64) t1 = rand(T, V2 ⊗ V3 ⊗ V1) t2 = rand(T, V2 ⊗ V1 ⊗ V3) @@ -739,7 +735,7 @@ for V in spacelist end end end - TensorKit.empty_globalcaches!() + TK.empty_globalcaches!() end @timedtestset "Deligne tensor product: test via conversion" begin From cd8e37b75511bf828d183ece0b0b05ee93e2f225 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 14:37:41 +0200 Subject: [PATCH 59/86] format with v1 --- src/fusiontrees/manipulations.jl | 258 ++++++++++++------------------- src/spaces/multifusionspace.jl | 12 +- test/multifusion.jl | 237 +++++++++++++--------------- test/tensors.jl | 16 +- 4 files changed, 225 insertions(+), 298 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index c32760d70..3bbcf71ec 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -13,7 +13,7 @@ Attach a fusion tree `f₂` to the uncoupled leg `i` of the fusion tree `f₁` a into a linear combination of fusion trees in standard form. This requires that `f₂.coupled == f₁.uncoupled[i]` and `f₁.isdual[i] == false`. """ -function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I, 0}) where {I} +function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I,0}) where {I} # this actually removes uncoupled line i, which should be trivial (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) @@ -35,7 +35,7 @@ function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I, 0}) where {I} f = FusionTree(uncoupled, coupled, isdual, inner, vertices) return fusiontreedict(I)(f => coeff) end -function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 1}) where {I} +function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I,1}) where {I} # identity operation (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) @@ -44,7 +44,7 @@ function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 1}) where {I} f = FusionTree{I}(f₁.uncoupled, f₁.coupled, isdual′, f₁.innerlines, f₁.vertices) return fusiontreedict(I)(f => coeff) end -function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 2}) where {I} +function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I,2}) where {I} # elementary building block, (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) @@ -106,14 +106,14 @@ function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 2}) where {I} return newtrees end end -function insertat(f₁::FusionTree{I, N₁}, i, f₂::FusionTree{I, N₂}) where {I, N₁, N₂} +function insertat(f₁::FusionTree{I,N₁}, i, f₂::FusionTree{I,N₂}) where {I,N₁,N₂} F = fusiontreetype(I, N₁ + N₂ - 1) (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) T = sectorscalartype(I) coeff = one(T) if length(f₁) == 1 - return fusiontreedict(I){F, T}(f₂ => coeff) + return fusiontreedict(I){F,T}(f₂ => coeff) end if i == 1 uncoupled = (f₂.uncoupled..., tail(f₁.uncoupled)...) @@ -122,18 +122,18 @@ function insertat(f₁::FusionTree{I, N₁}, i, f₂::FusionTree{I, N₂}) where vertices = (f₂.vertices..., f₁.vertices...) coupled = f₁.coupled f′ = FusionTree(uncoupled, coupled, isdual, inner, vertices) - return fusiontreedict(I){F, T}(f′ => coeff) + return fusiontreedict(I){F,T}(f′ => coeff) else # recursive definition N2 = length(f₂) f₂′, f₂′′ = split(f₂, N2 - 1) - local newtrees::fusiontreedict(I){F, T} + local newtrees::fusiontreedict(I){F,T} for (f, coeff) in insertat(f₁, i, f₂′′) for (f′, coeff′) in insertat(f, i, f₂′) if @isdefined newtrees coeff′′ = coeff * coeff′ newtrees[f′] = get(newtrees, f′, zero(coeff′′)) + coeff′′ else - newtrees = fusiontreedict(I){F, T}(f′ => coeff * coeff′) + newtrees = fusiontreedict(I){F,T}(f′ => coeff * coeff′) end end end @@ -153,7 +153,7 @@ remaining `N-M` uncoupled sectors of `f`. It couples to the same sector as `f`. operation is the inverse of `insertat` in the sense that if `f₁, f₂ = split(t, M) ⇒ f == insertat(f₂, 1, f₁)`. """ -@inline function split(f::FusionTree{I, N}, M::Int) where {I, N} +@inline function split(f::FusionTree{I,N}, M::Int) where {I,N} if M > N || M < 0 throw(ArgumentError("M should be between 0 and N = $N")) elseif M === N @@ -210,19 +210,15 @@ sectors are those of `f₁` followed by those of `f₂`, and where the two coupl `FusionStyle(I) == GenericFusion()`, also a degeneracy label `μ` for the fusion of the coupled sectors of `f₁` and `f₂` to `c` needs to be specified. """ -function merge( - f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, - c::I - ) where {I, N₁, N₂} +function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, + c::I) where {I,N₁,N₂} if FusionStyle(I) isa GenericFusion throw(ArgumentError("vertex label for merging required")) end return merge(f₁, f₂, c, 1) end -function merge( - f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, - c::I, μ - ) where {I, N₁, N₂} +function merge(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, + c::I, μ) where {I,N₁,N₂} if !(c in f₁.coupled ⊗ f₂.coupled) throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) end @@ -234,7 +230,7 @@ function merge( @assert coeff == one(coeff) return insertat(f, N₁ + 1, f₂) end -function merge(f₁::FusionTree{I, 0}, f₂::FusionTree{I, 0}, c::I, μ) where {I} +function merge(f₁::FusionTree{I,0}, f₂::FusionTree{I,0}, c::I, μ) where {I} Nsymbol(f₁.coupled, f₂.coupled, c) == μ == 1 || throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) return fusiontreedict(I)(f₁ => Fsymbol(c, c, c, c, c, c)[1, 1, 1, 1]) @@ -248,10 +244,8 @@ end # -> A-move (foldleft, foldright) is complicated, needs to be reexpressed in standard form # flip a duality flag of a fusion tree -function flip( - f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, i::Int; - inv::Bool = false - ) where {I <: Sector, N₁, N₂} +function flip(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, i::Int; + inv::Bool=false) where {I<:Sector,N₁,N₂} @assert 0 < i ≤ N₁ + N₂ if i ≤ N₁ a = f₁.uncoupled[i] @@ -280,10 +274,8 @@ function flip( return SingletonDict((f₁, f₂′) => factor) end end -function flip( - f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, ind; - inv::Bool = false - ) where {I <: Sector, N₁, N₂} +function flip(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}, ind; + inv::Bool=false) where {I<:Sector,N₁,N₂} f₁′, f₂′ = f₁, f₂ factor = one(sectorscalartype(I)) for i in ind @@ -294,7 +286,7 @@ function flip( end # change to N₁ - 1, N₂ + 1 -function bendright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where {I <: Sector, N₁, N₂} +function bendright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I<:Sector,N₁,N₂} # map final splitting vertex (a, b)<-c to fusion vertex a<-(c, dual(b)) @assert N₁ > 0 c = f₁.coupled @@ -342,15 +334,13 @@ end # change to N₁ + 1, N₂ - 1 function bendleft(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I} # map final fusion vertex c<-(a, b) to splitting vertex (c, dual(b))<-a - return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) - for - ((f₂′, f₁′), coeff) in bendright(f₂, f₁) - ) + return fusiontreedict(I)((f₁′, f₂′) => conj(coeff) + for + ((f₂′, f₁′), coeff) in bendright(f₂, f₁)) end # change to N₁ - 1, N₂ + 1 -function foldright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where {I <: Sector, N₁, N₂} +function foldright(f₁::FusionTree{I,N₁}, f₂::FusionTree{I,N₂}) where {I<:Sector,N₁,N₂} # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) @assert N₁ > 0 a = f₁.uncoupled[1] @@ -394,7 +384,7 @@ function foldright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where { coeff = factor * coeff1 * conj(coeff2) if (@isdefined newtrees) newtrees[(fl, fr)] = get(newtrees, (fl, fr), zero(coeff)) + - coeff + coeff else newtrees = fusiontreedict(I)((fl, fr) => coeff) end @@ -408,11 +398,9 @@ end # change to N₁ + 1, N₂ - 1 function foldleft(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I} # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b - return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) - for - ((f₂′, f₁′), coeff) in foldright(f₂, f₁) - ) + return fusiontreedict(I)((f₁′, f₂′) => conj(coeff) + for + ((f₂′, f₁′), coeff) in foldright(f₂, f₁)) end # COMPOSITE DUALITY MANIPULATIONS PART 1: Repartition and transpose @@ -435,7 +423,7 @@ function iscyclicpermutation(v1, v2) end # clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft -function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Sector} +function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I<:Sector} local newtrees if length(f₁) > 0 for ((f1a, f2a), coeffa) in foldright(f₁, f₂) @@ -464,7 +452,7 @@ function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Se end # anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright -function cycleanticlockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Sector} +function cycleanticlockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I<:Sector} local newtrees if length(f₂) > 0 for ((f1a, f2a), coeffa) in foldleft(f₁, f₂) @@ -504,21 +492,17 @@ outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical cou repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have `N` outgoing sectors. """ -@inline function repartition( - f₁::FusionTree{I, N₁}, - f₂::FusionTree{I, N₂}, - N::Int - ) where {I <: Sector, N₁, N₂} +@inline function repartition(f₁::FusionTree{I,N₁}, + f₂::FusionTree{I,N₂}, + N::Int) where {I<:Sector,N₁,N₂} f₁.coupled == f₂.coupled || throw(SectorMismatch()) @assert 0 <= N <= N₁ + N₂ return _recursive_repartition(f₁, f₂, Val(N)) end -function _recursive_repartition( - f₁::FusionTree{I, N₁}, - f₂::FusionTree{I, N₂}, - ::Val{N} - ) where {I <: Sector, N₁, N₂, N} +function _recursive_repartition(f₁::FusionTree{I,N₁}, + f₂::FusionTree{I,N₂}, + ::Val{N}) where {I<:Sector,N₁,N₂,N} # recursive definition is only way to get correct number of loops for # GenericFusion, but is too complex for type inference to handle, so we # precompute the parameters of the return type @@ -527,18 +511,16 @@ function _recursive_repartition( T = sectorscalartype(I) coeff = one(T) if N == N₁ - return fusiontreedict(I){Tuple{F₁, F₂}, T}((f₁, f₂) => coeff) + return fusiontreedict(I){Tuple{F₁,F₂},T}((f₁, f₂) => coeff) else - local newtrees::fusiontreedict(I){Tuple{F₁, F₂}, T} + local newtrees::fusiontreedict(I){Tuple{F₁,F₂},T} for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright(f₁, f₂) : bendleft(f₁, f₂)) for ((f₁′′, f₂′′), coeff2) in _recursive_repartition(f₁′, f₂′, Val(N)) if (@isdefined newtrees) push!(newtrees, (f₁′′, f₂′′) => coeff1 * coeff2) else - newtrees = fusiontreedict(I){Tuple{F₁, F₂}, T}( - (f₁′′, f₂′′) => coeff1 * - coeff2 - ) + newtrees = fusiontreedict(I){Tuple{F₁,F₂},T}((f₁′′, f₂′′) => coeff1 * + coeff2) end end end @@ -558,10 +540,8 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function Base.transpose( - f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂} - ) where {I <: Sector, N₁, N₂} +function Base.transpose(f₁::FusionTree{I}, f₂::FusionTree{I}, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector,N₁,N₂} N = N₁ + N₂ @assert length(f₁) + length(f₂) == N p = linearizepermutation(p1, p2, length(f₁), length(f₂)) @@ -569,26 +549,20 @@ function Base.transpose( return fstranspose((f₁, f₂, p1, p2)) end -const FSTransposeKey{I <: Sector, N₁, N₂} = Tuple{ - <:FusionTree{I}, <:FusionTree{I}, - IndexTuple{N₁}, IndexTuple{N₂}, -} +const FSTransposeKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, + IndexTuple{N₁},IndexTuple{N₂}} function _fsdicttype(I, N₁, N₂) F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) T = sectorscalartype(I) - return fusiontreedict(I){Tuple{F₁, F₂}, T} + return fusiontreedict(I){Tuple{F₁,F₂},T} end -@cached function fstranspose(key::FSTransposeKey{I, N₁, N₂})::_fsdicttype( - I, N₁, - N₂ - ) where { - I <: Sector, - N₁, - N₂, - } +@cached function fstranspose(key::FSTransposeKey{I,N₁,N₂})::_fsdicttype(I, N₁, + N₂) where {I<:Sector, + N₁, + N₂} f₁, f₂, p1, p2 = key N = N₁ + N₂ p = linearizepermutation(p1, p2, length(f₁), length(f₂)) @@ -631,7 +605,7 @@ end return newtrees end -function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I <: Sector} +function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I<:Sector} if FusionStyle(I) isa UniqueFusion return NoCache() else @@ -644,35 +618,29 @@ end # -> composite manipulations that depend on the duality (rigidity) and pivotal structure # -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) -function planar_trace( - f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}, - q1::IndexTuple{N₃}, q2::IndexTuple{N₃} - ) where {I <: Sector, N₁, N₂, N₃} +function planar_trace(f₁::FusionTree{I}, f₂::FusionTree{I}, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂}, + q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N₁,N₂,N₃} N = N₁ + N₂ + 2N₃ @assert length(f₁) + length(f₂) == N if N₃ == 0 return transpose(f₁, f₂, p1, p2) end - linearindex = ( - ntuple(identity, Val(length(f₁)))..., - reverse(length(f₁) .+ ntuple(identity, Val(length(f₂))))..., - ) + linearindex = (ntuple(identity, Val(length(f₁)))..., + reverse(length(f₁) .+ ntuple(identity, Val(length(f₂))))...) q1′ = TupleTools.getindices(linearindex, q1) q2′ = TupleTools.getindices(linearindex, q2) p1′, p2′ = let q′ = (q1′..., q2′...) - ( - map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p1)), - map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p2)), - ) + (map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p1)), + map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p2))) end T = sectorscalartype(I) F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) - newtrees = FusionTreeDict{Tuple{F₁, F₂}, T}() + newtrees = FusionTreeDict{Tuple{F₁,F₂},T}() for ((f₁′, f₂′), coeff′) in repartition(f₁, f₂, N) for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) @@ -694,13 +662,11 @@ Perform a planar trace of the uncoupled indices of the fusion tree `f` at `q1` w `q2`, where `q1[i]` is connected to `q2[i]` for all `i`. The result is returned as a dictionary of output trees and corresponding coefficients. """ -function planar_trace( - f::FusionTree{I, N}, - q1::IndexTuple{N₃}, q2::IndexTuple{N₃} - ) where {I <: Sector, N, N₃} +function planar_trace(f::FusionTree{I,N}, + q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N,N₃} T = sectorscalartype(I) F = fusiontreetype(I, N - 2 * N₃) - newtrees = FusionTreeDict{F, T}() + newtrees = FusionTreeDict{F,T}() N₃ === 0 && return push!(newtrees, f => one(T)) for (i, j) in zip(q1, q2) @@ -749,7 +715,7 @@ Perform an elementary trace of neighbouring uncoupled indices `i` and `i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and corresponding coefficients. """ -function elementary_trace(f::FusionTree{I, N}, i) where {I <: Sector, N} +function elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} (N > 1 && 1 <= i <= N) || throw(ArgumentError("Cannot trace outputs i=$i and i+1 out of only $N outputs")) i < N || isone(f.coupled) || @@ -757,7 +723,7 @@ function elementary_trace(f::FusionTree{I, N}, i) where {I <: Sector, N} T = sectorscalartype(I) F = fusiontreetype(I, N - 2) - newtrees = FusionTreeDict{F, T}() + newtrees = FusionTreeDict{F,T}() j = mod1(i + 1, N) b = f.uncoupled[i] @@ -765,10 +731,8 @@ function elementary_trace(f::FusionTree{I, N}, i) where {I <: Sector, N} # if trace is zero, return empty dict (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees if i < N - inner_extended = ( - leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., - f.coupled, - ) + inner_extended = (leftone(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., + f.coupled) a = inner_extended[i] d = inner_extended[i + 2] a == d || return newtrees @@ -779,13 +743,13 @@ function elementary_trace(f::FusionTree{I, N}, i) where {I <: Sector, N} inner′ = () else inner′ = i <= 2 ? Base.tail(Base.tail(f.innerlines)) : - TupleTools.deleteat(TupleTools.deleteat(f.innerlines, i - 1), i - 2) + TupleTools.deleteat(TupleTools.deleteat(f.innerlines, i - 1), i - 2) end if N <= 3 vertices′ = () else vertices′ = i <= 2 ? Base.tail(Base.tail(f.vertices)) : - TupleTools.deleteat(TupleTools.deleteat(f.vertices, i), i - 1) + TupleTools.deleteat(TupleTools.deleteat(f.vertices, i), i - 1) end f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) coeff = sqrtdim(b) @@ -856,7 +820,7 @@ applying `artin_braid(f′, i; inv = true)` to all the outputs `f′` of tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword has no effect if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ -function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I <: Sector, N} +function artin_braid(f::FusionTree{I,N}, i; inv::Bool=false) where {I<:Sector,N} 1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) uncoupled = f.uncoupled @@ -878,11 +842,9 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I <: Sect inner′ = inner vertices′ = vertices if i > 1 # we also need to alter innerlines and vertices - inner′ = TupleTools.setindex( - inner, - inner_extended[diaga ? (i + 1) : (i - 1)], - i - 1 - ) + inner′ = TupleTools.setindex(inner, + inner_extended[diaga ? (i + 1) : (i - 1)], + i - 1) vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) end @@ -925,32 +887,28 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I <: Sect e = inner_extended[i + 1] if FusionStyle(I) isa UniqueFusion c′ = first(a ⊗ d) - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * - Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * - conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) + coeff = oftype(oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * + Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * + conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end) inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) return fusiontreedict(I)(f′ => coeff) elseif FusionStyle(I) isa SimpleFusion local newtrees for c′ in intersect(a ⊗ d, e ⊗ conj(b)) - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * - Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * - conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) + coeff = oftype(oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * + Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * + conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end) iszero(coeff) && continue inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) @@ -1007,11 +965,9 @@ that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` an ``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid( - f::FusionTree{I, N}, - levels::NTuple{N, Int}, - p::NTuple{N, Int} - ) where {I <: Sector, N} +function braid(f::FusionTree{I,N}, + levels::NTuple{N,Int}, + p::NTuple{N,Int}) where {I<:Sector,N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding coeff = one(sectorscalartype(I)) @@ -1036,7 +992,7 @@ function braid( for s in permutation2swaps(p) inv = levels[s] > levels[s + 1] for (f, c) in trees - for (f′, c′) in artin_braid(f, s; inv = inv) + for (f′, c′) in artin_braid(f, s; inv=inv) newtrees[f′] = get(newtrees, f′, zero(coeff)) + c * c′ end end @@ -1058,7 +1014,7 @@ Perform a permutation of the uncoupled indices of the fusion tree `f` and return as a `<:AbstractDict` of output trees and corresponding coefficients; this requires that `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ -function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I <: Sector, N} +function permute(f::FusionTree{I,N}, p::NTuple{N,Int}) where {I<:Sector,N} @assert BraidingStyle(I) isa SymmetricBraiding return braid(f, ntuple(identity, Val(N)), p) end @@ -1082,26 +1038,20 @@ respectively, which determines how indices braid. In particular, if `i` and `j` levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid( - f₁::FusionTree{I}, f₂::FusionTree{I}, - levels1::IndexTuple, levels2::IndexTuple, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂} - ) where {I <: Sector, N₁, N₂} +function braid(f₁::FusionTree{I}, f₂::FusionTree{I}, + levels1::IndexTuple, levels2::IndexTuple, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector,N₁,N₂} @assert length(f₁) + length(f₂) == N₁ + N₂ @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) return fsbraid((f₁, f₂, levels1, levels2, p1, p2)) end -const FSBraidKey{I <: Sector, N₁, N₂} = Tuple{ - <:FusionTree{I}, <:FusionTree{I}, - IndexTuple, IndexTuple, - IndexTuple{N₁}, IndexTuple{N₂}, -} +const FSBraidKey{I<:Sector,N₁,N₂} = Tuple{<:FusionTree{I},<:FusionTree{I}, + IndexTuple,IndexTuple, + IndexTuple{N₁},IndexTuple{N₂}} -@cached function fsbraid(key::FSBraidKey{I, N₁, N₂})::_fsdicttype( - I, N₁, - N₂ - ) where {I <: Sector, N₁, N₂} +@cached function fsbraid(key::FSBraidKey{I,N₁,N₂})::_fsdicttype(I, N₁, + N₂) where {I<:Sector,N₁,N₂} (f₁, f₂, l1, l2, p1, p2) = key p = linearizepermutation(p1, p2, length(f₁), length(f₂)) levels = (l1..., reverse(l2)...) @@ -1111,7 +1061,7 @@ const FSBraidKey{I <: Sector, N₁, N₂} = Tuple{ for ((f₁′, f₂′), coeff3) in repartition(f′, f0, N₁) if @isdefined newtrees newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + - coeff1 * coeff2 * coeff3 + coeff1 * coeff2 * coeff3 else newtrees = fusiontreedict(I)((f₁′, f₂′) => coeff1 * coeff2 * coeff3) end @@ -1121,7 +1071,7 @@ const FSBraidKey{I <: Sector, N₁, N₂} = Tuple{ return newtrees end -function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I <: Sector} +function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I<:Sector} if FusionStyle(I) isa UniqueFusion return NoCache() else @@ -1141,10 +1091,8 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function permute( - f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂} - ) where {I <: Sector, N₁, N₂} +function permute(f₁::FusionTree{I}, f₂::FusionTree{I}, + p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector,N₁,N₂} @assert BraidingStyle(I) isa SymmetricBraiding levels1 = ntuple(identity, length(f₁)) levels2 = length(f₁) .+ ntuple(identity, length(f₂)) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 8cd5d89c3..dddf09fa6 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -55,8 +55,8 @@ function leftoneunit(S::Vect[IsingBimod]) end function insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; - conj::Bool=false, - dual::Bool=false) where {i,N} + conj::Bool=false, + dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible right unit onto $P at index $(i+1)") # possible change to rightone of correct space for N = 0 u = N > 0 ? rightoneunit(P[i]) : error("no unit object in $P") @@ -71,8 +71,8 @@ end # TODO?: overwrite defaults at level of HomSpace and TensorMap? function insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no defaults? - conj::Bool=false, - dual::Bool=false) where {i,N} + conj::Bool=false, + dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible left unit onto $P at index $i") # do we want this to error in the diagonal case? u = N > 0 ? leftoneunit(P[i]) : error("no unit object in $P") if dual @@ -96,6 +96,6 @@ function FusionTree(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) end # this one might also be overkill, since `FusionTreeIterator`s don't check whether the fusion is allowed -function fusiontrees(uncoupled::Tuple{IsingBimod, Vararg{IsingBimod}}) +function fusiontrees(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) return throw(ArgumentError("coupled sector must be provided for IsingBimod fusion")) -end \ No newline at end of file +end diff --git a/test/multifusion.jl b/test/multifusion.jl index 342423475..fc65d4e46 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -6,48 +6,42 @@ println("Multifusion tests for $Istr") println("------------------------------------") ti = time() -C0, C1, D0, D1, M, Mop = I(1, 1, 0), I(1, 1, 1), I(2, 2, 0), I(2, 2, 1), I(1, 2, 0), I(2, 1, 0) -VIBC = ( - Vect[I](C0 => 1, C1 => 1), - Vect[I](C0 => 1, C1 => 2), - Vect[I](C0 => 3, C1 => 2), - Vect[I](C0 => 2, C1 => 3), - Vect[I](C0 => 2, C1 => 5), -) - -VIBD = ( - Vect[I](D0 => 1, D1 => 1), - Vect[I](D0 => 1, D1 => 2), - Vect[I](D0 => 3, D1 => 2), - Vect[I](D0 => 2, D1 => 3), - Vect[I](D0 => 2, D1 => 5), -) +C0, C1, D0, D1, M, Mop = I(1, 1, 0), I(1, 1, 1), I(2, 2, 0), I(2, 2, 1), I(1, 2, 0), + I(2, 1, 0) +VIBC = (Vect[I](C0 => 1, C1 => 1), + Vect[I](C0 => 1, C1 => 2), + Vect[I](C0 => 3, C1 => 2), + Vect[I](C0 => 2, C1 => 3), + Vect[I](C0 => 2, C1 => 5)) + +VIBD = (Vect[I](D0 => 1, D1 => 1), + Vect[I](D0 => 1, D1 => 2), + Vect[I](D0 => 3, D1 => 2), + Vect[I](D0 => 2, D1 => 3), + Vect[I](D0 => 2, D1 => 5)) VIBM1 = (Vect[I](C0 => 1, C1 => 2), # not a random ordering! designed to make V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 work - Vect[I](M => 3), - Vect[I](C0 => 2, C1 => 3), - Vect[I](M => 4), - Vect[I](D0 => 3, D1 => 4) -) + Vect[I](M => 3), + Vect[I](C0 => 2, C1 => 3), + Vect[I](M => 4), + Vect[I](D0 => 3, D1 => 4)) -VIBM2 = (Vect[I](M => 2), Vect[I](D0 =>1, D1 => 1), - Vect[I](C0 => 1, C1 => 2), - Vect[I](M => 4), - Vect[I](D0 => 3, D1 => 4) -) +VIBM2 = (Vect[I](M => 2), Vect[I](D0 => 1, D1 => 1), + Vect[I](C0 => 1, C1 => 2), + Vect[I](M => 4), + Vect[I](D0 => 3, D1 => 4)) VIBMop1 = (Vect[I](Mop => 2), - Vect[I](C0 => 1, C1 => 2), - Vect[I](D0 => 3, D1 => 4), - Vect[I](Mop => 4), - Vect[I](C0 => 3, C1 => 4)) + Vect[I](C0 => 1, C1 => 2), + Vect[I](D0 => 3, D1 => 4), + Vect[I](Mop => 4), + Vect[I](C0 => 3, C1 => 4)) VIBMop2 = (Vect[I](D0 => 1, D1 => 1), - Vect[I](Mop => 2), - Vect[I](D0 => 3, D1 => 4), - Vect[I](Mop => 4), - Vect[I](C0 => 3, C1 => 4) -) + Vect[I](Mop => 2), + Vect[I](D0 => 3, D1 => 4), + Vect[I](Mop => 4), + Vect[I](C0 => 3, C1 => 4)) #TODO: MxMop or MopxM fusion needed? @@ -59,19 +53,19 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test eval(Meta.parse(TK.type_repr(typeof(V)))) == typeof(V) @test eval(Meta.parse(sprint(show, V))) == V @test eval(Meta.parse(sprint(show, V'))) == V' - @test V' == GradedSpace(gen; dual = true) + @test V' == GradedSpace(gen; dual=true) @test V == @constinferred GradedSpace(gen...) - @test V' == @constinferred GradedSpace(gen...; dual = true) + @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(tuple(gen...); dual=true) @test V == @constinferred GradedSpace(Dict(gen)) - @test V' == @constinferred GradedSpace(Dict(gen); dual = true) + @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; dual=true) @test V == @constinferred Vect[I](gen...) - @test V' == @constinferred Vect[I](gen...; dual = true) + @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 Vect[I](Dict(gen); dual=true) @test V == @constinferred typeof(V)(c => dim(V, c) for c in sectors(V)) @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') @test V == GradedSpace(reverse(collect(gen))...) @@ -102,7 +96,7 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test isa(V, GradedSpace) @test isa(V, GradedSpace{I}) @test @constinferred(dual(V)) == @constinferred(conj(V)) == - @constinferred(adjoint(V)) != V + @constinferred(adjoint(V)) != V @test @constinferred(field(V)) == ℂ @test @constinferred(sectortype(V)) == I slist = @constinferred sectors(V) @@ -117,29 +111,29 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), for W in [Wleft, Wright] @test @constinferred(⊕(W, oneunit(W))) == - Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) + Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) @test @constinferred(fuse(W, oneunit(W))) == W end # sensible direct sums and fuses @test @constinferred(⊕(Wleft, WM)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) + Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) @test @constinferred(⊕(Wright, WMop)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) + Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) @test @constinferred(⊕(Wright, WM)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) + Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) @test @constinferred(⊕(Wleft, WMop)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) + Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) @test @constinferred(fuse(Wleft, WM)) == Vect[I](M => 2) @test @constinferred(fuse(Wright, WMop)) == Vect[I](Mop => 2) # less sensible direct sums and fuses @test @constinferred(⊕(Wleft, Wright)) == - Vect[I](c => 1 for c in sectors(V) if c.row == c.col) + Vect[I](c => 1 for c in sectors(V) if c.row == c.col) @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == - Vect[I](c => 0 for c in sectors(V)) + Vect[I](c => 0 for c in sectors(V)) - d = Dict{I, Int}() + 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) @@ -147,7 +141,7 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), end @test @constinferred(fuse(V, V)) == GradedSpace(d) @test @constinferred(flip(V)) == - Vect[I](conj(c) => dim(V, c) for c in sectors(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 @@ -156,9 +150,9 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test V ≺ ⊕(V, V) @test !(V ≻ ⊕(V, V)) @test infimum(V, GradedSpace(C0 => 3)) == - GradedSpace(C0 => 2) + GradedSpace(C0 => 2) @test infimum(V, GradedSpace(M => 6)) == - GradedSpace(M => 5) + GradedSpace(M => 5) for W in [WM, WMop, Wright] @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) end @@ -193,9 +187,9 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test_throws ErrorException insertleftunit(W, 6) @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == - @constinferred(insertrightunit(W, 2)) # works for VIBM + @constinferred(insertrightunit(W, 2)) # works for VIBM @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == - @constinferred(insertleftunit(W, 3)) # same + @constinferred(insertleftunit(W, 3)) # same @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W # same @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) @@ -224,19 +218,13 @@ end @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) - ) + @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), (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)) @@ -322,14 +310,14 @@ end d1 = d2 d2 = empty(d1) for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 3; inv = true) + for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 end end d1 = d2 d2 = empty(d1) for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 2; inv = true) + for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 end end @@ -338,7 +326,7 @@ end if f1 == f @test coeff1 ≈ 1 else - @test isapprox(coeff1, 0; atol = 1.0e-12, rtol = 1.0e-12) + @test isapprox(coeff1, 0; atol=1.0e-12, rtol=1.0e-12) end end end @@ -367,11 +355,9 @@ end @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) - @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, μ) - ) + @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, μ)) # no merge and braid interplay tests end @@ -387,8 +373,8 @@ end for n in 0:(2 * N) d = @constinferred TK.repartition(f1, f2, $n) @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)), valtype(d)}() + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)),valtype(d)}() for ((f1′, f2′), coeff) in d for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff @@ -398,7 +384,7 @@ end if f1 == f1′ && f2 == f2′ @test coeff2 ≈ 1 else - @test isapprox(coeff2, 0; atol = 1.0e-12, rtol = 1.0e-12) + @test isapprox(coeff2, 0; atol=1.0e-12, rtol=1.0e-12) end end end @@ -416,8 +402,8 @@ end d = @constinferred transpose(f1, f2, p1, p2) @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)), valtype(d)}() + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)),valtype(d)}() for ((f1′, f2′), coeff) in d d′ = transpose(f1′, f2′, ip1, ip2) for ((f1′′, f2′′), coeff2) in d′ @@ -437,13 +423,11 @@ end d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) f1front, = TK.split(f1, N - 1) T = sectorscalartype(I) - d2 = Dict{typeof((f1front, f1front)), T}() + 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,) - ) + 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 @@ -487,7 +471,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) 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 eltype(bs) === Pair{typeof(c),typeof(b1)} @test typeof(b1) === TK.blocktype(t) # basic linear algebra @test isa(@constinferred(norm(t)), real(T)) @@ -530,7 +514,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @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) + convert(TensorMap, t1) + convert(TensorMap, t2) end end @timedtestset "Real and imaginary parts" begin @@ -575,10 +559,8 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @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)) - ) + @test all(Base.Fix2(isa, DiagonalTensorMap), + (t1 * t2, t1 \ t2, t1 / t2, inv(t1), pinv(t1))) # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden end @timedtestset "Tensor contraction" begin @@ -619,16 +601,12 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test rank(D) ≈ rank(t) @test cond(D) ≈ cond(t) - @test all( - ((s, t),) -> isapprox(s, t), - zip( - values(LinearAlgebra.eigvals(D)), - values(LinearAlgebra.eigvals(t)) - ) - ) + @test all(((s, t),) -> isapprox(s, t), + zip(values(LinearAlgebra.eigvals(D)), + values(LinearAlgebra.eigvals(t)))) end @testset "leftorth with $alg" for alg in (TK.QR(), TK.QL()) - Q, R = @constinferred leftorth(t; alg = alg) + Q, R = @constinferred leftorth(t; alg=alg) QdQ = Q' * Q @test QdQ ≈ one(QdQ) @test Q * R ≈ t @@ -637,7 +615,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end @testset "rightorth with $alg" for alg in (TK.RQ(), TK.LQ()) - L, Q = @constinferred rightorth(t; alg = alg) + L, Q = @constinferred rightorth(t; alg=alg) QQd = Q * Q' @test QQd ≈ one(QQd) @test L * Q ≈ t @@ -646,7 +624,7 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) - U, S, Vᴴ = @constinferred tsvd(t; alg = alg) + U, S, Vᴴ = @constinferred tsvd(t; alg=alg) UdU = U' * U @test UdU ≈ one(UdU) VdV = Vᴴ * Vᴴ' @@ -655,13 +633,9 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) @test rank(S) ≈ rank(t) @test cond(S) ≈ cond(t) - @test all( - ((s, t),) -> isapprox(s, t), - zip( - values(LinearAlgebra.svdvals(S)), - values(LinearAlgebra.svdvals(t)) - ) - ) + @test all(((s, t),) -> isapprox(s, t), + zip(values(LinearAlgebra.svdvals(S)), + values(LinearAlgebra.svdvals(t)))) end end end @@ -703,7 +677,9 @@ for V in (VIBC, VIBD) @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests -> this condition makes it hard to test non-diagonal sectors end -@timedtestset "Tensors with symmetry: $Istr" verbose = true for V in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) +@timedtestset "Tensors with symmetry: $Istr" verbose = true for V in + (VIBC, VIBD, VIBM1, VIBM2, + VIBMop1, VIBMop2) V1, V2, V3, V4, V5 = V @timedtestset "Basic tensor properties" begin # passes for diagonal sectors W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 # fusion matters @@ -865,7 +841,6 @@ end catch e @test isa(e, SectorMismatch) end - end @timedtestset "Partial trace: test self-consistency" begin t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # rewritten to be compatible with module fusion @@ -948,8 +923,9 @@ end # adjoint takes other space for shape of matrix in RQ(pos) for t in tsR @testset "rightorth with $alg" for alg in - (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), - TK.Polar(), TK.SVD(), TK.SDD()) + (TK.RQ(), TK.RQpos(), TK.LQ(), + TK.LQpos(), + TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(t; alg=alg) QQd = Q * Q' @test QQd ≈ one(QQd) @@ -969,13 +945,13 @@ end # adjoints take other space for shape of matrix in QL(pos) for t in tsL @testset "leftorth with $alg" for alg in - (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), - TK.Polar(), TK.SVD(), TK.SDD()) + (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + TK.Polar(), TK.SVD(), TK.SDD()) # skip QL because the monomorphism condition is hard to satisfy for off-diagonal case # have to skip Polar as well as all tests fail with modules (alg isa QL || alg isa QLpos || alg isa Polar) && !isdiag && continue Q, R = @constinferred leftorth(t; alg=alg) - QdQ = Q' * Q + QdQ = Q' * Q @test QdQ ≈ one(QdQ) @test Q * R ≈ t if alg isa Polar @@ -984,7 +960,7 @@ end end end @testset "leftnull with $alg" for alg in - (TK.QR(), TK.SVD(), TK.SDD()) + (TK.QR(), TK.SVD(), TK.SDD()) # less rows than columns so either fails or no data in off-diagonal case !isdiag && continue N = @constinferred leftnull(t; alg=alg) @@ -1030,8 +1006,8 @@ end @testset "empty tensor" begin t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in - (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), - TK.Polar(), TK.SVD(), TK.SDD()) + (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + TK.Polar(), TK.SVD(), TK.SDD()) Q, R = @constinferred leftorth(t; alg=alg) @test Q == t @test dim(Q) == dim(R) == 0 @@ -1042,7 +1018,8 @@ end @test N * N' ≈ id(codomain(N)) end @testset "rightorth with $alg" for alg in - (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + (TK.RQ(), TK.RQpos(), TK.LQ(), + TK.LQpos(), TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(copy(t'); alg=alg) @test Q == t' @@ -1106,13 +1083,13 @@ end ts = (randn(T, V1 ⊗ V2, V3 ⊗ V4 ⊗ V5), randn(T, V4 ⊗ V5, V3 ⊗ V1 ⊗ V2)') # rewritten for modules for p in (1, 2, 3, Inf) for t in ts - U₀, S₀, V₀, = tsvd(t) + U₀, S₀, V₀ = tsvd(t) t = rmul!(t, 1 / norm(S₀, p)) U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), - p=p) + p=p) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) @@ -1136,7 +1113,7 @@ end @test codomain(t) == V1 ⊗ V2 @test domain(t) == V4 ⊗ V5 @test norm(tA * t + t * tB + tC) < - (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) + (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) # no reshape test: NoBraiding and no fusion tensor end end @@ -1144,11 +1121,11 @@ end for T in (Float32, ComplexF64) t1 = rand(T, V3 ⊗ V4 ⊗ V5 ← V1 ⊗ V2) if all(a.row != a.col for a in blocksectors(t1)) - t2 = rand(T, V5'⊗ V4' ⊗ V3', V2' ⊗ V1') + t2 = rand(T, V5' ⊗ V4' ⊗ V3', V2' ⊗ V1') else t2 = rand(T, V3' ⊗ V1, V4 ⊗ V5 ⊗ V2') # keep a non-trivial permutation in diagonal case end - t = @constinferred (t1 ⊗ t2); + t = @constinferred (t1 ⊗ t2) @test norm(t) ≈ norm(t1) * norm(t2) end end @@ -1159,7 +1136,7 @@ end for T in (Float32, ComplexF64) if !isdiag t1 = rand(T, W) - t2 = rand(T, V5'⊗ V4' ⊗ V3', V2' ⊗ V1') # same as previous test + t2 = rand(T, V5' ⊗ V4' ⊗ V3', V2' ⊗ V1') # same as previous test @planar t′[1 2 3 6 7 8; 4 5 9 10] := t1[1 2 3; 4 5] * t2[6 7 8; 9 10] else t1 = rand(T, V2 ⊗ V3, V1) @@ -1177,9 +1154,7 @@ end TK.empty_globalcaches!() ########## tf = time() -printstyled( - "Finished multifusion tests in ", - string(round(tf - ti; sigdigits = 3)), - " seconds."; bold = true, color = Base.info_color() -) +printstyled("Finished multifusion tests in ", + string(round(tf - ti; sigdigits=3)), + " seconds."; bold=true, color=Base.info_color()) println() diff --git a/test/tensors.jl b/test/tensors.jl index 9338d17a4..573cda07d 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -452,11 +452,12 @@ for V in spacelist # Test both a normal tensor and an adjoint one. tsL = (rand(T, WL), rand(T, WR)') tsR = (rand(T, WR), rand(T, WL)') # can also have one space by taking adjoints in rightorth/rightnull - # but this avoids taking copies + # but this avoids taking copies for t in tsR @testset "rightorth with $alg" for alg in - (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + (TK.RQ(), TK.RQpos(), TK.LQ(), + TK.LQpos(), TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(t; alg=alg) QQd = Q * Q' @@ -477,7 +478,8 @@ for V in spacelist for t in tsL @testset "leftorth with $alg" for alg in - (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + (TK.QR(), TK.QRpos(), TK.QL(), + TK.QLpos(), TK.Polar(), TK.SVD(), TK.SDD()) Q, R = @constinferred leftorth(t; alg=alg) QdQ = Q' * Q @@ -529,7 +531,8 @@ for V in spacelist @testset "empty tensor" begin t = randn(T, V1 ⊗ V2, zero(V1)) @testset "leftorth with $alg" for alg in - (TK.QR(), TK.QRpos(), TK.QL(), TK.QLpos(), + (TK.QR(), TK.QRpos(), TK.QL(), + TK.QLpos(), TK.Polar(), TK.SVD(), TK.SDD()) Q, R = @constinferred leftorth(t; alg=alg) @test Q == t @@ -541,7 +544,8 @@ for V in spacelist @test N * N' ≈ id(codomain(N)) end @testset "rightorth with $alg" for alg in - (TK.RQ(), TK.RQpos(), TK.LQ(), TK.LQpos(), + (TK.RQ(), TK.RQpos(), TK.LQ(), + TK.LQpos(), TK.Polar(), TK.SVD(), TK.SDD()) L, Q = @constinferred rightorth(copy(t'); alg=alg) @test Q == t' @@ -607,7 +611,7 @@ for V in spacelist ts = (randn(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5), randn(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)') for t in ts - U₀, S₀, V₀, = tsvd(t) + U₀, S₀, V₀ = tsvd(t) t = rmul!(t, 1 / norm(S₀, p)) U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) # @show p, ϵ From 772452fe5da790f2bdc539e60edc2f0e5e32b05e Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 14:38:09 +0200 Subject: [PATCH 60/86] add TensorKitSectors to extras for tests --- Project.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index b063816f7..dd65d28a1 100644 --- a/Project.toml +++ b/Project.toml @@ -49,10 +49,11 @@ ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [targets] -test = ["Aqua", "Combinatorics", "LinearAlgebra", "TensorOperations", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "FiniteDifferences", "Zygote"] +test = ["Aqua", "Combinatorics", "LinearAlgebra", "TensorKitSectors", "TensorOperations", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "FiniteDifferences", "Zygote"] From fd55983a82a1387023d51830b6777a788f4e72f7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 15:13:56 +0200 Subject: [PATCH 61/86] define `left/rightoneunit` correctly --- src/spaces/gradedspace.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index f7701083e..3bbcac4af 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -139,7 +139,10 @@ Base.oneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 1) Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial one-dimensional space consisting of the left unit of the underlying `Sector` `I`. """ -leftoneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(leftone(I) => 1) +function leftoneunit(S::GradedSpace{I}) where {I<:Sector} + sector = leftone(first(sectors(S))) + return spacetype(S)(sector => 1) +end """ rightoneunit(V::Type{<:GradedSpace{I}}) where {I<:Sector} -> GradedSpace{I} @@ -147,7 +150,10 @@ leftoneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(leftone(I) => 1) Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial one-dimensional space consisting of the right unit of the underlying `Sector` `I`. """ -rightoneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(rightone(I) => 1) +function rightoneunit(S::GradedSpace{I}) where {I<:Sector} + sector = rightone(first(sectors(S))) + return spacetype(S)(sector => 1) +end Base.zero(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 0) From 04d6162c55baa9c13356c2f17243848d15c60d19 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 15:20:03 +0200 Subject: [PATCH 62/86] remove some comments --- test/multifusion.jl | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index fc65d4e46..121178a56 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -182,15 +182,15 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TK.compose(W, W') @test_throws ErrorException insertleftunit(W) - @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) # works for VIBM1, VIBM2 + @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) @test_throws ErrorException insertrightunit(W, 6) @test_throws ErrorException insertleftunit(W, 6) @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == - @constinferred(insertrightunit(W, 2)) # works for VIBM + @constinferred(insertrightunit(W, 2)) @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == - @constinferred(insertleftunit(W, 3)) # same - @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W # same + @constinferred(insertleftunit(W, 3)) + @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) end @@ -242,8 +242,6 @@ end @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree constructor required here end - # CONTINUE HERE - @testset "Fusion tree $Istr: insertat" begin N = 4 in2 = nothing # attempt at not hard-coding @@ -667,10 +665,6 @@ V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) end end -#TODO: test with non-diagonal sectors -# needs to be 1 dimensional stuff for the isomorphism test in removeunit -# is ^ still true? - for V in (VIBC, VIBD) V1, V2, V3, V4, V5 = V @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests @@ -681,7 +675,7 @@ end (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) V1, V2, V3, V4, V5 = V - @timedtestset "Basic tensor properties" begin # passes for diagonal sectors + @timedtestset "Basic tensor properties" begin W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 # fusion matters for T in (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat) t = @constinferred zeros(T, W) @@ -1149,8 +1143,6 @@ end end end -# TODO: add AD tests? - TK.empty_globalcaches!() ########## tf = time() From 5d34b60306dd09f01da7b3c447129ce07999b763 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 15:29:46 +0200 Subject: [PATCH 63/86] fix docstring --- src/spaces/gradedspace.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 3bbcac4af..7c8b4cbea 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -134,10 +134,10 @@ end Base.oneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 1) """ - leftoneunit(V::Type{<:GradedSpace{I}}) where {I<:Sector} -> GradedSpace{I} + leftoneunit(S::GradedSpace{I}) where {I<:Sector} -> GradedSpace{I} Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial -one-dimensional space consisting of the left unit of the underlying `Sector` `I`. +one-dimensional space consisting of the left unit of the objects in `Sector` `I`. """ function leftoneunit(S::GradedSpace{I}) where {I<:Sector} sector = leftone(first(sectors(S))) @@ -145,10 +145,10 @@ function leftoneunit(S::GradedSpace{I}) where {I<:Sector} end """ - rightoneunit(V::Type{<:GradedSpace{I}}) where {I<:Sector} -> GradedSpace{I} + rightoneunit(S::GradedSpace{I}) where {I<:Sector} -> GradedSpace{I} Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial -one-dimensional space consisting of the right unit of the underlying `Sector` `I`. +one-dimensional space consisting of the right unit of the objects in `Sector` `I`. """ function rightoneunit(S::GradedSpace{I}) where {I<:Sector} sector = rightone(first(sectors(S))) From d25510a868e2416823e774229e858dfcb70554ac Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 15:32:25 +0200 Subject: [PATCH 64/86] actually include the multifusion tests --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index 9ab15584f..f2ef0a36d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -120,6 +120,7 @@ include("spaces.jl") include("tensors.jl") include("diagonal.jl") include("planar.jl") +include("multifusion.jl") # TODO: remove once we know AD is slow on macOS CI if !(Sys.isapple() && get(ENV, "CI", "false") == "true") && isempty(VERSION.prerelease) include("ad.jl") From a3857daad181b0a5c2ded966acf81b6fd107606d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 16:03:20 +0200 Subject: [PATCH 65/86] fix fermion parity full trace test --- test/tensors.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tensors.jl b/test/tensors.jl index 573cda07d..1dd991bd0 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -282,9 +282,9 @@ for V in spacelist t2 = twist!(t, 2) end ss = tr(t2) - @tensor s2 = t[a b; a b] - @tensor t2[a; b] := t[a c; b c] - @tensor s3 = t2[a; a] + @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 From de2bcaeb09e87ed4775ae13bda01318cff627e55 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 4 Aug 2025 18:19:49 +0200 Subject: [PATCH 66/86] fix quote escape in @constinferred + move misplaced tensormaps --- test/multifusion.jl | 1247 +++++++++++++++++++++---------------------- 1 file changed, 623 insertions(+), 624 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 121178a56..732724194 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -45,625 +45,625 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), #TODO: MxMop or MopxM fusion needed? -@timedtestset "Multifusion spaces " verbose = true begin - @timedtestset "GradedSpace: $(TK.type_repr(Vect[I]))" begin - gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) - - V = GradedSpace(gen) - @test eval(Meta.parse(TK.type_repr(typeof(V)))) == typeof(V) - @test eval(Meta.parse(sprint(show, V))) == V - @test eval(Meta.parse(sprint(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)) - @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') - @test V == GradedSpace(reverse(collect(gen))...) - @test eval(Meta.parse(sprint(show, V))) == V - @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) - - # space with a single sector - Wleft = @constinferred Vect[I](C0 => 1, C1 => 1) - Wright = @constinferred Vect[I](D0 => 1, D1 => 1) - WM = @constinferred Vect[I](M => 1) - WMop = @constinferred Vect[I](Mop => 1) - - @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) - @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) - @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) - @test @constinferred(leftoneunit(⊕(Wright, WMop))) == oneunit(Wright) - @test @constinferred(rightoneunit(⊕(Wright, WM))) == oneunit(Wright) - @test @constinferred(rightoneunit(⊕(Wleft, WMop))) == oneunit(Wleft) - - @test_throws ArgumentError oneunit(I) - @test_throws ArgumentError oneunit(WM) - @test_throws ArgumentError oneunit(WMop) - - @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)) - - @test @constinferred(⊕(V, zero(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)) - - for W in [Wleft, Wright] - @test @constinferred(⊕(W, oneunit(W))) == - Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) - @test @constinferred(fuse(W, oneunit(W))) == W - end - - # sensible direct sums and fuses - @test @constinferred(⊕(Wleft, WM)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) - @test @constinferred(⊕(Wright, WMop)) == - Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) - @test @constinferred(⊕(Wright, WM)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) - @test @constinferred(⊕(Wleft, WMop)) == - Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) - @test @constinferred(fuse(Wleft, WM)) == Vect[I](M => 2) - @test @constinferred(fuse(Wright, WMop)) == Vect[I](Mop => 2) - - # less sensible direct sums and fuses - @test @constinferred(⊕(Wleft, Wright)) == - Vect[I](c => 1 for c in sectors(V) if c.row == c.col) - @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == - Vect[I](c => 0 for c in sectors(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)) - @test infimum(V, GradedSpace(C0 => 3)) == - GradedSpace(C0 => 2) - @test infimum(V, GradedSpace(M => 6)) == - GradedSpace(M => 5) - for W in [WM, WMop, Wright] - @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) - end - @test_throws SpaceMismatch (⊕(V, V')) - end - - @timedtestset "HomSpace with $(TK.type_repr(Vect[I])) " begin - for (V1, V2, V3, V4, V5) in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) - 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 eval(Meta.parse(sprint(show, W))) == W - @test eval(Meta.parse(sprint(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] == V3' - @test W[4] == V4' - @test W[5] == V5' - - @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') - @test W == deepcopy(W) - @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) - @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') - @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TK.compose(W, W') - - @test_throws ErrorException insertleftunit(W) - @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) - @test_throws ErrorException insertrightunit(W, 6) - @test_throws ErrorException insertleftunit(W, 6) - - @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == - @constinferred(insertrightunit(W, 2)) - @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == - @constinferred(insertleftunit(W, 3)) - @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W - @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? - @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) - end - end -end - -@timedtestset "Fusion trees for $(TK.type_repr(I))" verbose = true begin - N = 6 - out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? - isdual = ntuple(n -> rand(Bool), N) - in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out - numtrees = length(fusiontrees(out, in, isdual)) # will be 1 - @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - - 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 $Istr: printing" begin - @test eval(Meta.parse(sprint(show, f))) == f - end - @testset "Fusion tree $Istr: constructor properties" for u in (C0, D0) - @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) - - # SimpleFusion - errstr = "fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`" - @test_throws errstr FusionTree((), u, ()) - @test_throws errstr FusionTree((u,), u, (false,)) - @test_throws errstr FusionTree((u, u), u, (false, false)) - @test_throws errstr FusionTree((u, u, u), u) - @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree constructor required here - end - - @testset "Fusion tree $Istr: insertat" begin - N = 4 - in2 = nothing # attempt at not hard-coding - out2 = nothing - while in2 === nothing - out2 = ntuple(n -> randsector(I), N) - try - in2 = rand(collect(⊗(out2...))) - catch e - if isa(e, ArgumentError) - in2 = nothing - else - rethrow(e) - end - end - end - isdual2 = ntuple(n -> rand(Bool), N) - f2 = rand(collect(fusiontrees(out2, in2, isdual2))) - for i in 1:N - in1 = nothing - out1 = nothing - while in1 === nothing - try - out1 = ntuple(n -> randsector(I), N) - out1 = Base.setindex(out1, in2, i) - in1 = rand(collect(⊗(out1...))) - catch e - if isa(e, ArgumentError) - in1 = nothing - else - rethrow(e) - end - end - end - 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 - - f1a, f1b = @constinferred TK.split(f1, $i) - @test length(TK.insertat(f1b, 1, f1a)) == 1 - @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) - - # no braid tests for non-hardcoded example - end - end - # no planar trace tests - @testset "Fusion tree $Istr: elementary artin braid" begin - N = length(out) - isdual = ntuple(n -> rand(Bool), N) - # no general artin braid test - - # not sure how useful this test is, it does the trivial braiding - f = rand(collect(it)) # in this case the 1 tree - d1 = TK.artin_braid(f, 2) # takes a unit C0 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 3) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - d1 = d2 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - d1 = d2 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - d1 = d2 - for (f1, coeff1) in d1 - if f1 == f - @test coeff1 ≈ 1 - else - @test isapprox(coeff1, 0; atol=1.0e-12, rtol=1.0e-12) - end - end - end - # no braiding and permuting test - @testset "Fusion tree $Istr: merging" begin - N = 3 - out1, in1, out2, in2 = nothing, nothing, nothing, nothing - while (in1 === nothing && in2 === nothing) || isempty(in1 ⊗ in2) - try - out1 = ntuple(n -> randsector(I), N) - in1 = rand(collect(⊗(out1...))) - out2 = ntuple(n -> randsector(I), N) - in2 = rand(collect(⊗(out2...))) - catch e - if isa(e, ArgumentError) - in1, in2 = nothing, nothing - else - rethrow(e) - end - end - end - - f1 = rand(collect(fusiontrees(out1, in1))) - f2 = rand(collect(fusiontrees(out2, in2))) - - @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) - @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) - - @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, μ)) - # no merge and braid interplay tests - end - - # hardcoded double fusion tree tests - N = 6 - out = (Mop, C0, C1, M, D0, D1) # same as above - out2 = (D0, D1, Mop, C0, C1, M) # different order that still fuses to D0 or D1 - incoming = rand(collect(⊗(out...))) # will be D0 or D1 - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) - - @testset "Double fusion tree $Istr: repartioning" begin - for n in 0:(2 * N) - d = @constinferred TK.repartition(f1, f2, $n) - @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)),valtype(d)}() - for ((f1′, f2′), coeff) in d - for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) - d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff - end - end - for ((f1′, f2′), coeff2) in d2 - if f1 == f1′ && f2 == f2′ - @test coeff2 ≈ 1 - else - @test isapprox(coeff2, 0; atol=1.0e-12, rtol=1.0e-12) - end - end - end - end - # no double fusion tree permutation tests - @testset "Double fusion tree $Istr: 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)] - - d = @constinferred transpose(f1, f2, p1, p2) - @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)),valtype(d)}() - for ((f1′, f2′), coeff) in d - d′ = transpose(f1′, f2′, ip1, ip2) - for ((f1′′, f2′′), coeff2) in d′ - d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff - end - end - for ((f1′, f2′), coeff2) in d2 - if f1 == f1′ && f2 == f2′ - @test coeff2 ≈ 1 - else - @test abs(coeff2) < 1.0e-12 - end - end - end - end - @testset "Double fusion tree $Istr: planar trace" begin - d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) - 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 -end - -V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) - -@timedtestset "DiagonalTensor with domain $V" begin - @timedtestset "Basic properties and algebra" begin - for T in (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) === TK.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)) ≈ 2 * norm(t) - - @test norm(zerovector!(t)) == 0 - @test norm(one!(t)) ≈ sqrt(dim(V)) - @test one!(t) == id(V) - @test norm(one!(t) - id(V)) == 0 - - 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 - - @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 - @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))) - # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden - end - @timedtestset "Tensor contraction" begin - for W in (Vect[I](C0 => 2, C1 => 3), Vect[I](D0 => 2, D1 => 3)) - d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) - t = TensorMap(d) - A = randn(ComplexF64, W ⊗ W' ⊗ W, W) - B = randn(ComplexF64, W ⊗ W' ⊗ W, W ⊗ W') # empty for modules so untested - - @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 - end - @timedtestset "Factorization" begin - for T in (Float32, ComplexF64) - t = DiagonalTensorMap(rand(T, reduceddim(V)), V) - @testset "eig" begin - D, W = @constinferred eig(t) - @test t * W ≈ W * D - t2 = t + t' - D2, V2 = @constinferred eigh(t2) - VdV2 = V2' * V2 - @test VdV2 ≈ one(VdV2) - @test t2 * V2 ≈ V2 * D2 - - @test rank(D) ≈ rank(t) - @test cond(D) ≈ cond(t) - @test all(((s, t),) -> isapprox(s, t), - zip(values(LinearAlgebra.eigvals(D)), - values(LinearAlgebra.eigvals(t)))) - end - @testset "leftorth with $alg" for alg in (TK.QR(), TK.QL()) - Q, R = @constinferred leftorth(t; alg=alg) - QdQ = Q' * Q - @test QdQ ≈ one(QdQ) - @test Q * R ≈ t - if alg isa Polar - @test isposdef(R) - end - end - @testset "rightorth with $alg" for alg in (TK.RQ(), TK.LQ()) - L, Q = @constinferred rightorth(t; alg=alg) - QQd = Q * Q' - @test QQd ≈ one(QQd) - @test L * Q ≈ t - if alg isa Polar - @test isposdef(L) - end - end - @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) - U, S, Vᴴ = @constinferred tsvd(t; alg=alg) - UdU = U' * U - @test UdU ≈ one(UdU) - VdV = Vᴴ * Vᴴ' - @test VdV ≈ one(VdV) - @test U * S * Vᴴ ≈ t - - @test rank(S) ≈ rank(t) - @test cond(S) ≈ cond(t) - @test all(((s, t),) -> isapprox(s, t), - zip(values(LinearAlgebra.svdvals(S)), - values(LinearAlgebra.svdvals(t)))) - end - end - 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 -end +# @timedtestset "Multifusion spaces " verbose = true begin +# @timedtestset "GradedSpace: $(TK.type_repr(Vect[I]))" begin +# gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) + +# V = GradedSpace(gen) +# @test eval(Meta.parse(TK.type_repr(typeof(V)))) == typeof(V) +# @test eval(Meta.parse(sprint(show, V))) == V +# @test eval(Meta.parse(sprint(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)) +# @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') +# @test V == GradedSpace(reverse(collect(gen))...) +# @test eval(Meta.parse(sprint(show, V))) == V +# @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) + +# # space with a single sector +# Wleft = @constinferred Vect[I](C0 => 1, C1 => 1) +# Wright = @constinferred Vect[I](D0 => 1, D1 => 1) +# WM = @constinferred Vect[I](M => 1) +# WMop = @constinferred Vect[I](Mop => 1) + +# @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) +# @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) +# @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) +# @test @constinferred(leftoneunit(⊕(Wright, WMop))) == oneunit(Wright) +# @test @constinferred(rightoneunit(⊕(Wright, WM))) == oneunit(Wright) +# @test @constinferred(rightoneunit(⊕(Wleft, WMop))) == oneunit(Wleft) + +# @test_throws ArgumentError oneunit(I) +# @test_throws ArgumentError oneunit(WM) +# @test_throws ArgumentError oneunit(WMop) + +# @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)) + +# @test @constinferred(⊕(V, zero(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)) + +# for W in [Wleft, Wright] +# @test @constinferred(⊕(W, oneunit(W))) == +# Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) +# @test @constinferred(fuse(W, oneunit(W))) == W +# end + +# # sensible direct sums and fuses +# @test @constinferred(⊕(Wleft, WM)) == +# Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) +# @test @constinferred(⊕(Wright, WMop)) == +# Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) +# @test @constinferred(⊕(Wright, WM)) == +# Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) +# @test @constinferred(⊕(Wleft, WMop)) == +# Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) +# @test @constinferred(fuse(Wleft, WM)) == Vect[I](M => 2) +# @test @constinferred(fuse(Wright, WMop)) == Vect[I](Mop => 2) + +# # less sensible direct sums and fuses +# @test @constinferred(⊕(Wleft, Wright)) == +# Vect[I](c => 1 for c in sectors(V) if c.row == c.col) +# @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == +# Vect[I](c => 0 for c in sectors(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)) +# @test infimum(V, GradedSpace(C0 => 3)) == +# GradedSpace(C0 => 2) +# @test infimum(V, GradedSpace(M => 6)) == +# GradedSpace(M => 5) +# for W in [WM, WMop, Wright] +# @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) +# end +# @test_throws SpaceMismatch (⊕(V, V')) +# end + +# @timedtestset "HomSpace with $(TK.type_repr(Vect[I])) " begin +# for (V1, V2, V3, V4, V5) in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) +# 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 eval(Meta.parse(sprint(show, W))) == W +# @test eval(Meta.parse(sprint(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] == V3' +# @test W[4] == V4' +# @test W[5] == V5' + +# @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') +# @test W == deepcopy(W) +# @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) +# @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') +# @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TK.compose(W, W') + +# @test_throws ErrorException insertleftunit(W) +# @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) +# @test_throws ErrorException insertrightunit(W, 6) +# @test_throws ErrorException insertleftunit(W, 6) + +# @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == +# @constinferred(insertrightunit(W, 2)) +# @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == +# @constinferred(insertleftunit(W, 3)) +# @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W +# @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? +# @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) +# end +# end +# end + +# @timedtestset "Fusion trees for $(TK.type_repr(I))" verbose = true begin +# N = 6 +# out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? +# isdual = ntuple(n -> rand(Bool), N) +# in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out +# numtrees = length(fusiontrees(out, in, isdual)) # will be 1 +# @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) + +# 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 $Istr: printing" begin +# @test eval(Meta.parse(sprint(show, f))) == f +# end +# @testset "Fusion tree $Istr: constructor properties" for u in (C0, D0) +# @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) + +# # SimpleFusion +# errstr = "fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`" +# @test_throws errstr FusionTree((), u, ()) +# @test_throws errstr FusionTree((u,), u, (false,)) +# @test_throws errstr FusionTree((u, u), u, (false, false)) +# @test_throws errstr FusionTree((u, u, u), u) +# @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree constructor required here +# end + +# @testset "Fusion tree $Istr: insertat" begin +# N = 4 +# in2 = nothing # attempt at not hard-coding +# out2 = nothing +# while in2 === nothing +# out2 = ntuple(n -> randsector(I), N) +# try +# in2 = rand(collect(⊗(out2...))) +# catch e +# if isa(e, ArgumentError) +# in2 = nothing +# else +# rethrow(e) +# end +# end +# end +# isdual2 = ntuple(n -> rand(Bool), N) +# f2 = rand(collect(fusiontrees(out2, in2, isdual2))) +# for i in 1:N +# in1 = nothing +# out1 = nothing +# while in1 === nothing +# try +# out1 = ntuple(n -> randsector(I), N) +# out1 = Base.setindex(out1, in2, i) +# in1 = rand(collect(⊗(out1...))) +# catch e +# if isa(e, ArgumentError) +# in1 = nothing +# else +# rethrow(e) +# end +# end +# end +# 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 + +# f1a, f1b = @constinferred TK.split(f1, $i) +# @test length(TK.insertat(f1b, 1, f1a)) == 1 +# @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) + +# # no braid tests for non-hardcoded example +# end +# end +# # no planar trace tests +# @testset "Fusion tree $Istr: elementary artin braid" begin +# N = length(out) +# isdual = ntuple(n -> rand(Bool), N) +# # no general artin braid test + +# # not sure how useful this test is, it does the trivial braiding +# f = rand(collect(it)) # in this case the 1 tree +# d1 = TK.artin_braid(f, 2) # takes a unit C0 +# d2 = empty(d1) +# for (f1, coeff1) in d1 +# for (f2, coeff2) in TK.artin_braid(f1, 3) +# d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 +# end +# end +# d1 = d2 +# d2 = empty(d1) +# for (f1, coeff1) in d1 +# for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) +# d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 +# end +# end +# d1 = d2 +# d2 = empty(d1) +# for (f1, coeff1) in d1 +# for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) +# d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 +# end +# end +# d1 = d2 +# for (f1, coeff1) in d1 +# if f1 == f +# @test coeff1 ≈ 1 +# else +# @test isapprox(coeff1, 0; atol=1.0e-12, rtol=1.0e-12) +# end +# end +# end +# # no braiding and permuting test +# @testset "Fusion tree $Istr: merging" begin +# N = 3 +# out1, in1, out2, in2 = nothing, nothing, nothing, nothing +# while (in1 === nothing && in2 === nothing) || isempty(in1 ⊗ in2) +# try +# out1 = ntuple(n -> randsector(I), N) +# in1 = rand(collect(⊗(out1...))) +# out2 = ntuple(n -> randsector(I), N) +# in2 = rand(collect(⊗(out2...))) +# catch e +# if isa(e, ArgumentError) +# in1, in2 = nothing, nothing +# else +# rethrow(e) +# end +# end +# end + +# f1 = rand(collect(fusiontrees(out1, in1))) +# f2 = rand(collect(fusiontrees(out2, in2))) + +# @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) +# @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) + +# @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, μ)) +# # no merge and braid interplay tests +# end + +# # hardcoded double fusion tree tests +# N = 6 +# out = (Mop, C0, C1, M, D0, D1) # same as above +# out2 = (D0, D1, Mop, C0, C1, M) # different order that still fuses to D0 or D1 +# incoming = rand(collect(⊗(out...))) # will be D0 or D1 +# f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) +# f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) + +# @testset "Double fusion tree $Istr: repartioning" begin +# for n in 0:(2 * N) +# d = @constinferred TK.repartition(f1, f2, $n) +# @test dim(incoming) ≈ +# sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) +# d2 = Dict{typeof((f1, f2)),valtype(d)}() +# for ((f1′, f2′), coeff) in d +# for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) +# d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff +# end +# end +# for ((f1′, f2′), coeff2) in d2 +# if f1 == f1′ && f2 == f2′ +# @test coeff2 ≈ 1 +# else +# @test isapprox(coeff2, 0; atol=1.0e-12, rtol=1.0e-12) +# end +# end +# end +# end +# # no double fusion tree permutation tests +# @testset "Double fusion tree $Istr: 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)] + +# d = @constinferred transpose(f1, f2, p1, p2) +# @test dim(incoming) ≈ +# sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) +# d2 = Dict{typeof((f1, f2)),valtype(d)}() +# for ((f1′, f2′), coeff) in d +# d′ = transpose(f1′, f2′, ip1, ip2) +# for ((f1′′, f2′′), coeff2) in d′ +# d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff +# end +# end +# for ((f1′, f2′), coeff2) in d2 +# if f1 == f1′ && f2 == f2′ +# @test coeff2 ≈ 1 +# else +# @test abs(coeff2) < 1.0e-12 +# end +# end +# end +# end +# @testset "Double fusion tree $Istr: planar trace" begin +# d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) +# 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 +# end + +# V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) + +# @timedtestset "DiagonalTensor with domain $V" begin +# @timedtestset "Basic properties and algebra" begin +# for T in (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) === TK.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)) ≈ 2 * norm(t) + +# @test norm(zerovector!(t)) == 0 +# @test norm(one!(t)) ≈ sqrt(dim(V)) +# @test one!(t) == id(V) +# @test norm(one!(t) - id(V)) == 0 + +# 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 + +# @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 +# @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))) +# # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden +# end +# @timedtestset "Tensor contraction" begin +# for W in (Vect[I](C0 => 2, C1 => 3), Vect[I](D0 => 2, D1 => 3)) +# d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) +# t = TensorMap(d) +# A = randn(ComplexF64, W ⊗ W' ⊗ W, W) +# B = randn(ComplexF64, W ⊗ W' ⊗ W, W ⊗ W') # empty for modules so untested + +# @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 +# end +# @timedtestset "Factorization" begin +# for T in (Float32, ComplexF64) +# t = DiagonalTensorMap(rand(T, reduceddim(V)), V) +# @testset "eig" begin +# D, W = @constinferred eig(t) +# @test t * W ≈ W * D +# t2 = t + t' +# D2, V2 = @constinferred eigh(t2) +# VdV2 = V2' * V2 +# @test VdV2 ≈ one(VdV2) +# @test t2 * V2 ≈ V2 * D2 + +# @test rank(D) ≈ rank(t) +# @test cond(D) ≈ cond(t) +# @test all(((s, t),) -> isapprox(s, t), +# zip(values(LinearAlgebra.eigvals(D)), +# values(LinearAlgebra.eigvals(t)))) +# end +# @testset "leftorth with $alg" for alg in (TK.QR(), TK.QL()) +# Q, R = @constinferred leftorth(t; alg=alg) +# QdQ = Q' * Q +# @test QdQ ≈ one(QdQ) +# @test Q * R ≈ t +# if alg isa Polar +# @test isposdef(R) +# end +# end +# @testset "rightorth with $alg" for alg in (TK.RQ(), TK.LQ()) +# L, Q = @constinferred rightorth(t; alg=alg) +# QQd = Q * Q' +# @test QQd ≈ one(QQd) +# @test L * Q ≈ t +# if alg isa Polar +# @test isposdef(L) +# end +# end +# @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) +# U, S, Vᴴ = @constinferred tsvd(t; alg=alg) +# UdU = U' * U +# @test UdU ≈ one(UdU) +# VdV = Vᴴ * Vᴴ' +# @test VdV ≈ one(VdV) +# @test U * S * Vᴴ ≈ t + +# @test rank(S) ≈ rank(t) +# @test cond(S) ≈ cond(t) +# @test all(((s, t),) -> isapprox(s, t), +# zip(values(LinearAlgebra.svdvals(S)), +# values(LinearAlgebra.svdvals(t)))) +# end +# end +# 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 +# end for V in (VIBC, VIBD) V1, V2, V3, V4, V5 = V @@ -780,7 +780,7 @@ end @test space(t2) == insertleftunit(space(t), 5) @test scalartype(t2) === T @test t.data === t2.data - @test @constinferred(removeunit(t2, $(numind(t2)) - 1)) == t # -1 required + @test @constinferred(removeunit(t2, $(numind(t2) - 1))) == t # -1 required t3 = @constinferred insertleftunit(t, 5; copy=true) # same here @test t3 == @constinferred insertrightunit(t, 4; copy=true) @@ -788,7 +788,7 @@ end for (c, b) in blocks(t) @test b == block(t3, c) end - @test @constinferred(removeunit(t3, $(numind(t3)) - 1)) == t + @test @constinferred(removeunit(t3, $(numind(t3) - 1))) == t t4 = @constinferred insertrightunit(t, 3; dual=true) @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) for (c, b) in blocks(t) @@ -909,12 +909,11 @@ end WmodL = V1 ⊗ V2 ⊗ V5' ← V3 ⊗ V4 # new fusion order for left isdiag = all(c.row == c.col for c in blocksectors(WmodR)) # this blocksectors call should always work - - tsR = isdiag ? (rand(T, WR), rand(T, WL)') : (rand(T, WmodR), rand(T, WmodL)') # shape of matrices require different spaces for left/right - tsL = isdiag ? (rand(T, WL), rand(T, WR)') : (rand(T, WmodR), rand(T, WmodL)') for T in (Float32, ComplexF64) # Test both a normal tensor and an adjoint one. # adjoint takes other space for shape of matrix in RQ(pos) + tsR = isdiag ? (rand(T, WR), rand(T, WL)') : (rand(T, WmodR), rand(T, WmodL)') # shape of matrices require different spaces for left/right + tsL = isdiag ? (rand(T, WL), rand(T, WR)') : (rand(T, WmodR), rand(T, WmodL)') for t in tsR @testset "rightorth with $alg" for alg in (TK.RQ(), TK.RQpos(), TK.LQ(), From 3ea52930b2f952c4a0c4acb170dccad2d777c6fd Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 6 Aug 2025 12:35:14 +0200 Subject: [PATCH 67/86] uncomment tests --- test/multifusion.jl | 1238 +++++++++++++++++++++---------------------- 1 file changed, 619 insertions(+), 619 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 732724194..dc0e16dfc 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -45,625 +45,625 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), #TODO: MxMop or MopxM fusion needed? -# @timedtestset "Multifusion spaces " verbose = true begin -# @timedtestset "GradedSpace: $(TK.type_repr(Vect[I]))" begin -# gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) - -# V = GradedSpace(gen) -# @test eval(Meta.parse(TK.type_repr(typeof(V)))) == typeof(V) -# @test eval(Meta.parse(sprint(show, V))) == V -# @test eval(Meta.parse(sprint(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)) -# @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') -# @test V == GradedSpace(reverse(collect(gen))...) -# @test eval(Meta.parse(sprint(show, V))) == V -# @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) - -# # space with a single sector -# Wleft = @constinferred Vect[I](C0 => 1, C1 => 1) -# Wright = @constinferred Vect[I](D0 => 1, D1 => 1) -# WM = @constinferred Vect[I](M => 1) -# WMop = @constinferred Vect[I](Mop => 1) - -# @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) -# @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) -# @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) -# @test @constinferred(leftoneunit(⊕(Wright, WMop))) == oneunit(Wright) -# @test @constinferred(rightoneunit(⊕(Wright, WM))) == oneunit(Wright) -# @test @constinferred(rightoneunit(⊕(Wleft, WMop))) == oneunit(Wleft) - -# @test_throws ArgumentError oneunit(I) -# @test_throws ArgumentError oneunit(WM) -# @test_throws ArgumentError oneunit(WMop) - -# @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)) - -# @test @constinferred(⊕(V, zero(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)) - -# for W in [Wleft, Wright] -# @test @constinferred(⊕(W, oneunit(W))) == -# Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) -# @test @constinferred(fuse(W, oneunit(W))) == W -# end - -# # sensible direct sums and fuses -# @test @constinferred(⊕(Wleft, WM)) == -# Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) -# @test @constinferred(⊕(Wright, WMop)) == -# Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) -# @test @constinferred(⊕(Wright, WM)) == -# Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) -# @test @constinferred(⊕(Wleft, WMop)) == -# Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) -# @test @constinferred(fuse(Wleft, WM)) == Vect[I](M => 2) -# @test @constinferred(fuse(Wright, WMop)) == Vect[I](Mop => 2) - -# # less sensible direct sums and fuses -# @test @constinferred(⊕(Wleft, Wright)) == -# Vect[I](c => 1 for c in sectors(V) if c.row == c.col) -# @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == -# Vect[I](c => 0 for c in sectors(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)) -# @test infimum(V, GradedSpace(C0 => 3)) == -# GradedSpace(C0 => 2) -# @test infimum(V, GradedSpace(M => 6)) == -# GradedSpace(M => 5) -# for W in [WM, WMop, Wright] -# @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) -# end -# @test_throws SpaceMismatch (⊕(V, V')) -# end - -# @timedtestset "HomSpace with $(TK.type_repr(Vect[I])) " begin -# for (V1, V2, V3, V4, V5) in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) -# 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 eval(Meta.parse(sprint(show, W))) == W -# @test eval(Meta.parse(sprint(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] == V3' -# @test W[4] == V4' -# @test W[5] == V5' - -# @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') -# @test W == deepcopy(W) -# @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) -# @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') -# @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TK.compose(W, W') - -# @test_throws ErrorException insertleftunit(W) -# @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) -# @test_throws ErrorException insertrightunit(W, 6) -# @test_throws ErrorException insertleftunit(W, 6) - -# @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == -# @constinferred(insertrightunit(W, 2)) -# @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == -# @constinferred(insertleftunit(W, 3)) -# @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W -# @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? -# @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) -# end -# end -# end - -# @timedtestset "Fusion trees for $(TK.type_repr(I))" verbose = true begin -# N = 6 -# out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? -# isdual = ntuple(n -> rand(Bool), N) -# in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out -# numtrees = length(fusiontrees(out, in, isdual)) # will be 1 -# @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - -# 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 $Istr: printing" begin -# @test eval(Meta.parse(sprint(show, f))) == f -# end -# @testset "Fusion tree $Istr: constructor properties" for u in (C0, D0) -# @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) - -# # SimpleFusion -# errstr = "fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`" -# @test_throws errstr FusionTree((), u, ()) -# @test_throws errstr FusionTree((u,), u, (false,)) -# @test_throws errstr FusionTree((u, u), u, (false, false)) -# @test_throws errstr FusionTree((u, u, u), u) -# @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree constructor required here -# end - -# @testset "Fusion tree $Istr: insertat" begin -# N = 4 -# in2 = nothing # attempt at not hard-coding -# out2 = nothing -# while in2 === nothing -# out2 = ntuple(n -> randsector(I), N) -# try -# in2 = rand(collect(⊗(out2...))) -# catch e -# if isa(e, ArgumentError) -# in2 = nothing -# else -# rethrow(e) -# end -# end -# end -# isdual2 = ntuple(n -> rand(Bool), N) -# f2 = rand(collect(fusiontrees(out2, in2, isdual2))) -# for i in 1:N -# in1 = nothing -# out1 = nothing -# while in1 === nothing -# try -# out1 = ntuple(n -> randsector(I), N) -# out1 = Base.setindex(out1, in2, i) -# in1 = rand(collect(⊗(out1...))) -# catch e -# if isa(e, ArgumentError) -# in1 = nothing -# else -# rethrow(e) -# end -# end -# end -# 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 - -# f1a, f1b = @constinferred TK.split(f1, $i) -# @test length(TK.insertat(f1b, 1, f1a)) == 1 -# @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) - -# # no braid tests for non-hardcoded example -# end -# end -# # no planar trace tests -# @testset "Fusion tree $Istr: elementary artin braid" begin -# N = length(out) -# isdual = ntuple(n -> rand(Bool), N) -# # no general artin braid test - -# # not sure how useful this test is, it does the trivial braiding -# f = rand(collect(it)) # in this case the 1 tree -# d1 = TK.artin_braid(f, 2) # takes a unit C0 -# d2 = empty(d1) -# for (f1, coeff1) in d1 -# for (f2, coeff2) in TK.artin_braid(f1, 3) -# d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 -# end -# end -# d1 = d2 -# d2 = empty(d1) -# for (f1, coeff1) in d1 -# for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) -# d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 -# end -# end -# d1 = d2 -# d2 = empty(d1) -# for (f1, coeff1) in d1 -# for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) -# d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 -# end -# end -# d1 = d2 -# for (f1, coeff1) in d1 -# if f1 == f -# @test coeff1 ≈ 1 -# else -# @test isapprox(coeff1, 0; atol=1.0e-12, rtol=1.0e-12) -# end -# end -# end -# # no braiding and permuting test -# @testset "Fusion tree $Istr: merging" begin -# N = 3 -# out1, in1, out2, in2 = nothing, nothing, nothing, nothing -# while (in1 === nothing && in2 === nothing) || isempty(in1 ⊗ in2) -# try -# out1 = ntuple(n -> randsector(I), N) -# in1 = rand(collect(⊗(out1...))) -# out2 = ntuple(n -> randsector(I), N) -# in2 = rand(collect(⊗(out2...))) -# catch e -# if isa(e, ArgumentError) -# in1, in2 = nothing, nothing -# else -# rethrow(e) -# end -# end -# end - -# f1 = rand(collect(fusiontrees(out1, in1))) -# f2 = rand(collect(fusiontrees(out2, in2))) - -# @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) -# @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) - -# @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, μ)) -# # no merge and braid interplay tests -# end - -# # hardcoded double fusion tree tests -# N = 6 -# out = (Mop, C0, C1, M, D0, D1) # same as above -# out2 = (D0, D1, Mop, C0, C1, M) # different order that still fuses to D0 or D1 -# incoming = rand(collect(⊗(out...))) # will be D0 or D1 -# f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) -# f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) - -# @testset "Double fusion tree $Istr: repartioning" begin -# for n in 0:(2 * N) -# d = @constinferred TK.repartition(f1, f2, $n) -# @test dim(incoming) ≈ -# sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) -# d2 = Dict{typeof((f1, f2)),valtype(d)}() -# for ((f1′, f2′), coeff) in d -# for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) -# d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff -# end -# end -# for ((f1′, f2′), coeff2) in d2 -# if f1 == f1′ && f2 == f2′ -# @test coeff2 ≈ 1 -# else -# @test isapprox(coeff2, 0; atol=1.0e-12, rtol=1.0e-12) -# end -# end -# end -# end -# # no double fusion tree permutation tests -# @testset "Double fusion tree $Istr: 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)] - -# d = @constinferred transpose(f1, f2, p1, p2) -# @test dim(incoming) ≈ -# sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) -# d2 = Dict{typeof((f1, f2)),valtype(d)}() -# for ((f1′, f2′), coeff) in d -# d′ = transpose(f1′, f2′, ip1, ip2) -# for ((f1′′, f2′′), coeff2) in d′ -# d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff -# end -# end -# for ((f1′, f2′), coeff2) in d2 -# if f1 == f1′ && f2 == f2′ -# @test coeff2 ≈ 1 -# else -# @test abs(coeff2) < 1.0e-12 -# end -# end -# end -# end -# @testset "Double fusion tree $Istr: planar trace" begin -# d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) -# 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 -# end - -# V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) - -# @timedtestset "DiagonalTensor with domain $V" begin -# @timedtestset "Basic properties and algebra" begin -# for T in (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) === TK.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)) ≈ 2 * norm(t) - -# @test norm(zerovector!(t)) == 0 -# @test norm(one!(t)) ≈ sqrt(dim(V)) -# @test one!(t) == id(V) -# @test norm(one!(t) - id(V)) == 0 - -# 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 - -# @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 -# @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))) -# # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden -# end -# @timedtestset "Tensor contraction" begin -# for W in (Vect[I](C0 => 2, C1 => 3), Vect[I](D0 => 2, D1 => 3)) -# d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) -# t = TensorMap(d) -# A = randn(ComplexF64, W ⊗ W' ⊗ W, W) -# B = randn(ComplexF64, W ⊗ W' ⊗ W, W ⊗ W') # empty for modules so untested - -# @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 -# end -# @timedtestset "Factorization" begin -# for T in (Float32, ComplexF64) -# t = DiagonalTensorMap(rand(T, reduceddim(V)), V) -# @testset "eig" begin -# D, W = @constinferred eig(t) -# @test t * W ≈ W * D -# t2 = t + t' -# D2, V2 = @constinferred eigh(t2) -# VdV2 = V2' * V2 -# @test VdV2 ≈ one(VdV2) -# @test t2 * V2 ≈ V2 * D2 - -# @test rank(D) ≈ rank(t) -# @test cond(D) ≈ cond(t) -# @test all(((s, t),) -> isapprox(s, t), -# zip(values(LinearAlgebra.eigvals(D)), -# values(LinearAlgebra.eigvals(t)))) -# end -# @testset "leftorth with $alg" for alg in (TK.QR(), TK.QL()) -# Q, R = @constinferred leftorth(t; alg=alg) -# QdQ = Q' * Q -# @test QdQ ≈ one(QdQ) -# @test Q * R ≈ t -# if alg isa Polar -# @test isposdef(R) -# end -# end -# @testset "rightorth with $alg" for alg in (TK.RQ(), TK.LQ()) -# L, Q = @constinferred rightorth(t; alg=alg) -# QQd = Q * Q' -# @test QQd ≈ one(QQd) -# @test L * Q ≈ t -# if alg isa Polar -# @test isposdef(L) -# end -# end -# @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) -# U, S, Vᴴ = @constinferred tsvd(t; alg=alg) -# UdU = U' * U -# @test UdU ≈ one(UdU) -# VdV = Vᴴ * Vᴴ' -# @test VdV ≈ one(VdV) -# @test U * S * Vᴴ ≈ t - -# @test rank(S) ≈ rank(t) -# @test cond(S) ≈ cond(t) -# @test all(((s, t),) -> isapprox(s, t), -# zip(values(LinearAlgebra.svdvals(S)), -# values(LinearAlgebra.svdvals(t)))) -# end -# end -# 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 -# end +@timedtestset "Multifusion spaces " verbose = true begin + @timedtestset "GradedSpace: $(TK.type_repr(Vect[I]))" begin + gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) + + V = GradedSpace(gen) + @test eval(Meta.parse(TK.type_repr(typeof(V)))) == typeof(V) + @test eval(Meta.parse(sprint(show, V))) == V + @test eval(Meta.parse(sprint(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)) + @test @constinferred(hash(V)) == hash(deepcopy(V)) != hash(V') + @test V == GradedSpace(reverse(collect(gen))...) + @test eval(Meta.parse(sprint(show, V))) == V + @test eval(Meta.parse(sprint(show, typeof(V)))) == typeof(V) + + # space with a single sector + Wleft = @constinferred Vect[I](C0 => 1, C1 => 1) + Wright = @constinferred Vect[I](D0 => 1, D1 => 1) + WM = @constinferred Vect[I](M => 1) + WMop = @constinferred Vect[I](Mop => 1) + + @test @constinferred(oneunit(Wleft)) == leftoneunit(Wleft) == rightoneunit(Wleft) + @test @constinferred(oneunit(Wright)) == leftoneunit(Wright) == rightoneunit(Wright) + @test @constinferred(leftoneunit(⊕(Wleft, WM))) == oneunit(Wleft) + @test @constinferred(leftoneunit(⊕(Wright, WMop))) == oneunit(Wright) + @test @constinferred(rightoneunit(⊕(Wright, WM))) == oneunit(Wright) + @test @constinferred(rightoneunit(⊕(Wleft, WMop))) == oneunit(Wleft) + + @test_throws ArgumentError oneunit(I) + @test_throws ArgumentError oneunit(WM) + @test_throws ArgumentError oneunit(WMop) + + @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)) + + @test @constinferred(⊕(V, zero(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)) + + for W in [Wleft, Wright] + @test @constinferred(⊕(W, oneunit(W))) == + Vect[I](c => isone(c) + dim(W, c) for c in sectors(W)) + @test @constinferred(fuse(W, oneunit(W))) == W + end + + # sensible direct sums and fuses + @test @constinferred(⊕(Wleft, WM)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == C0) + @test @constinferred(⊕(Wright, WMop)) == + Vect[I](c => 1 for c in sectors(V) if leftone(c) == D0) + @test @constinferred(⊕(Wright, WM)) == + Vect[I](c => 1 for c in sectors(V) if rightone(c) == D0) + @test @constinferred(⊕(Wleft, WMop)) == + Vect[I](c => 1 for c in sectors(V) if rightone(c) == C0) + @test @constinferred(fuse(Wleft, WM)) == Vect[I](M => 2) + @test @constinferred(fuse(Wright, WMop)) == Vect[I](Mop => 2) + + # less sensible direct sums and fuses + @test @constinferred(⊕(Wleft, Wright)) == + Vect[I](c => 1 for c in sectors(V) if c.row == c.col) + @test @constinferred(fuse(Wleft, WMop)) == fuse(Wright, WM) == + Vect[I](c => 0 for c in sectors(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)) + @test infimum(V, GradedSpace(C0 => 3)) == + GradedSpace(C0 => 2) + @test infimum(V, GradedSpace(M => 6)) == + GradedSpace(M => 5) + for W in [WM, WMop, Wright] + @test infimum(Wleft, W) == Vect[I](c => 0 for c in sectors(V)) + end + @test_throws SpaceMismatch (⊕(V, V')) + end + + @timedtestset "HomSpace with $(TK.type_repr(Vect[I])) " begin + for (V1, V2, V3, V4, V5) in (VIBC, VIBD, VIBM1, VIBM2, VIBMop1, VIBMop2) + 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 eval(Meta.parse(sprint(show, W))) == W + @test eval(Meta.parse(sprint(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] == V3' + @test W[4] == V4' + @test W[5] == V5' + + @test @constinferred(hash(W)) == hash(deepcopy(W)) != hash(W') + @test W == deepcopy(W) + @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) + @test permute(W, ((2, 4, 5), (3, 1))) == (V2 ⊗ V4' ⊗ V5' ← V3 ⊗ V1') + @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TK.compose(W, W') + + @test_throws ErrorException insertleftunit(W) + @test insertrightunit(W) == (V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 ⊗ rightoneunit(V5)) + @test_throws ErrorException insertrightunit(W, 6) + @test_throws ErrorException insertleftunit(W, 6) + + @test (V1 ⊗ V2 ⊗ rightoneunit(V2) ← V3 ⊗ V4 ⊗ V5) == + @constinferred(insertrightunit(W, 2)) + @test (V1 ⊗ V2 ← leftoneunit(V3) ⊗ V3 ⊗ V4 ⊗ V5) == + @constinferred(insertleftunit(W, 3)) + @test @constinferred(removeunit(insertleftunit(W, 3), 3)) == W + @test_throws ErrorException @constinferred(insertrightunit(one(V1) ← V1, 0)) # should I specify it's the other error? + @test_throws ErrorException insertleftunit(one(V1) ← V1, 0) + end + end +end + +@timedtestset "Fusion trees for $(TK.type_repr(I))" verbose = true begin + N = 6 + out = (Mop, C0, C1, M, D0, D1) # should I try to make a non-hardcoded example? + isdual = ntuple(n -> rand(Bool), N) + in = rand(collect(⊗(out...))) # will be D0 or D1 in this choice of out + numtrees = length(fusiontrees(out, in, isdual)) # will be 1 + @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) + + 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 $Istr: printing" begin + @test eval(Meta.parse(sprint(show, f))) == f + end + @testset "Fusion tree $Istr: constructor properties" for u in (C0, D0) + @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) + + # SimpleFusion + errstr = "fusion tree requires inner lines if `FusionStyle(I) <: MultipleFusion`" + @test_throws errstr FusionTree((), u, ()) + @test_throws errstr FusionTree((u,), u, (false,)) + @test_throws errstr FusionTree((u, u), u, (false, false)) + @test_throws errstr FusionTree((u, u, u), u) + @test_throws errstr FusionTree((u, u, u, u)) # custom FusionTree constructor required here + end + + @testset "Fusion tree $Istr: insertat" begin + N = 4 + in2 = nothing # attempt at not hard-coding + out2 = nothing + while in2 === nothing + out2 = ntuple(n -> randsector(I), N) + try + in2 = rand(collect(⊗(out2...))) + catch e + if isa(e, ArgumentError) + in2 = nothing + else + rethrow(e) + end + end + end + isdual2 = ntuple(n -> rand(Bool), N) + f2 = rand(collect(fusiontrees(out2, in2, isdual2))) + for i in 1:N + in1 = nothing + out1 = nothing + while in1 === nothing + try + out1 = ntuple(n -> randsector(I), N) + out1 = Base.setindex(out1, in2, i) + in1 = rand(collect(⊗(out1...))) + catch e + if isa(e, ArgumentError) + in1 = nothing + else + rethrow(e) + end + end + end + 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 + + f1a, f1b = @constinferred TK.split(f1, $i) + @test length(TK.insertat(f1b, 1, f1a)) == 1 + @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) + + # no braid tests for non-hardcoded example + end + end + # no planar trace tests + @testset "Fusion tree $Istr: elementary artin braid" begin + N = length(out) + isdual = ntuple(n -> rand(Bool), N) + # no general artin braid test + + # not sure how useful this test is, it does the trivial braiding + f = rand(collect(it)) # in this case the 1 tree + d1 = TK.artin_braid(f, 2) # takes a unit C0 + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, 3) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + d1 = d2 + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, 3; inv=true) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + d1 = d2 + d2 = empty(d1) + for (f1, coeff1) in d1 + for (f2, coeff2) in TK.artin_braid(f1, 2; inv=true) + d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + end + end + d1 = d2 + for (f1, coeff1) in d1 + if f1 == f + @test coeff1 ≈ 1 + else + @test isapprox(coeff1, 0; atol=1.0e-12, rtol=1.0e-12) + end + end + end + # no braiding and permuting test + @testset "Fusion tree $Istr: merging" begin + N = 3 + out1, in1, out2, in2 = nothing, nothing, nothing, nothing + while (in1 === nothing && in2 === nothing) || isempty(in1 ⊗ in2) + try + out1 = ntuple(n -> randsector(I), N) + in1 = rand(collect(⊗(out1...))) + out2 = ntuple(n -> randsector(I), N) + in2 = rand(collect(⊗(out2...))) + catch e + if isa(e, ArgumentError) + in1, in2 = nothing, nothing + else + rethrow(e) + end + end + end + + f1 = rand(collect(fusiontrees(out1, in1))) + f2 = rand(collect(fusiontrees(out2, in2))) + + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) + + @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, μ)) + # no merge and braid interplay tests + end + + # hardcoded double fusion tree tests + N = 6 + out = (Mop, C0, C1, M, D0, D1) # same as above + out2 = (D0, D1, Mop, C0, C1, M) # different order that still fuses to D0 or D1 + incoming = rand(collect(⊗(out...))) # will be D0 or D1 + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) + + @testset "Double fusion tree $Istr: repartioning" begin + for n in 0:(2 * N) + d = @constinferred TK.repartition(f1, f2, $n) + @test dim(incoming) ≈ + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)),valtype(d)}() + for ((f1′, f2′), coeff) in d + for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) + d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff + end + end + for ((f1′, f2′), coeff2) in d2 + if f1 == f1′ && f2 == f2′ + @test coeff2 ≈ 1 + else + @test isapprox(coeff2, 0; atol=1.0e-12, rtol=1.0e-12) + end + end + end + end + # no double fusion tree permutation tests + @testset "Double fusion tree $Istr: 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)] + + d = @constinferred transpose(f1, f2, p1, p2) + @test dim(incoming) ≈ + sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) + d2 = Dict{typeof((f1, f2)),valtype(d)}() + for ((f1′, f2′), coeff) in d + d′ = transpose(f1′, f2′, ip1, ip2) + for ((f1′′, f2′′), coeff2) in d′ + d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff + end + end + for ((f1′, f2′), coeff2) in d2 + if f1 == f1′ && f2 == f2′ + @test coeff2 ≈ 1 + else + @test abs(coeff2) < 1.0e-12 + end + end + end + end + @testset "Double fusion tree $Istr: planar trace" begin + d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) + 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 +end + +V = Vect[I](values(I)[k] => 1 for k in 1:length(values(I))) + +@timedtestset "DiagonalTensor with domain $V" begin + @timedtestset "Basic properties and algebra" begin + for T in (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) === TK.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)) ≈ 2 * norm(t) + + @test norm(zerovector!(t)) == 0 + @test norm(one!(t)) ≈ sqrt(dim(V)) + @test one!(t) == id(V) + @test norm(one!(t) - id(V)) == 0 + + 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 + + @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 + @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))) + # no V * V' * V ← V or V^2 ← V tests due to Nsymbol erroring where fusion is forbidden + end + @timedtestset "Tensor contraction" begin + for W in (Vect[I](C0 => 2, C1 => 3), Vect[I](D0 => 2, D1 => 3)) + d = DiagonalTensorMap(rand(ComplexF64, reduceddim(W)), W) + t = TensorMap(d) + A = randn(ComplexF64, W ⊗ W' ⊗ W, W) + B = randn(ComplexF64, W ⊗ W' ⊗ W, W ⊗ W') # empty for modules so untested + + @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 + end + @timedtestset "Factorization" begin + for T in (Float32, ComplexF64) + t = DiagonalTensorMap(rand(T, reduceddim(V)), V) + @testset "eig" begin + D, W = @constinferred eig(t) + @test t * W ≈ W * D + t2 = t + t' + D2, V2 = @constinferred eigh(t2) + VdV2 = V2' * V2 + @test VdV2 ≈ one(VdV2) + @test t2 * V2 ≈ V2 * D2 + + @test rank(D) ≈ rank(t) + @test cond(D) ≈ cond(t) + @test all(((s, t),) -> isapprox(s, t), + zip(values(LinearAlgebra.eigvals(D)), + values(LinearAlgebra.eigvals(t)))) + end + @testset "leftorth with $alg" for alg in (TK.QR(), TK.QL()) + Q, R = @constinferred leftorth(t; alg=alg) + QdQ = Q' * Q + @test QdQ ≈ one(QdQ) + @test Q * R ≈ t + if alg isa Polar + @test isposdef(R) + end + end + @testset "rightorth with $alg" for alg in (TK.RQ(), TK.LQ()) + L, Q = @constinferred rightorth(t; alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ t + if alg isa Polar + @test isposdef(L) + end + end + @testset "tsvd with $alg" for alg in (TK.SVD(), TK.SDD()) + U, S, Vᴴ = @constinferred tsvd(t; alg=alg) + UdU = U' * U + @test UdU ≈ one(UdU) + VdV = Vᴴ * Vᴴ' + @test VdV ≈ one(VdV) + @test U * S * Vᴴ ≈ t + + @test rank(S) ≈ rank(t) + @test cond(S) ≈ cond(t) + @test all(((s, t),) -> isapprox(s, t), + zip(values(LinearAlgebra.svdvals(S)), + values(LinearAlgebra.svdvals(t)))) + end + end + 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 +end for V in (VIBC, VIBD) V1, V2, V3, V4, V5 = V From 6c20fc62a5e924574c2d114bd2f88f41828f2cc7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 7 Aug 2025 11:52:05 +0200 Subject: [PATCH 68/86] add custom `scalar` to deal with semisimple unit --- src/spaces/multifusionspace.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index dddf09fa6..0601033c7 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -9,6 +9,13 @@ function dim(V::Vect[IsingBimod]) init=zero(T)) end +function scalar(t::AbstractTensorMap{T,Vect[IsingBimod],0,0}) where {T} + Bs = collect(blocks(t)) + inds = findall(!iszero ∘ last, Bs) + isempty(inds) && return zero(scalartype(t)) + return only(last(Bs[only(inds)])) +end + function Base.oneunit(S::Vect[IsingBimod]) allequal(a.row for a in sectors(S)) && allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S are not all equal")) @@ -23,15 +30,18 @@ Base.zero(S::Type{Vect[IsingBimod]}) = Vect[IsingBimod]() function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} codom = codomain(W) dom = domain(W) + @info "in blocksectors with $W" if N₁ == 0 && N₂ == 0 + @info "no sectors, returning empty set" return (IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) elseif N₁ == 0 @assert N₂ != 0 "one of Type IsingBimod doesn't exist" return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(dom))) # is this what we want? doesn't allow M/Mop to end at empty space - elseif N₂ == 0 # also causes traces over module legs to vanish + elseif N₂ == 0 @assert N₁ != 0 "one of Type IsingBimod doesn't exist" return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(codom))) elseif N₂ <= N₁ # keep intersection + @info "returning sectors in domain" return filter!(c -> hasblock(codom, c), collect(blocksectors(dom))) else return filter!(c -> hasblock(dom, c), collect(blocksectors(codom))) From 3dd1257ab907bd8ad5d1fd6ed081a3222f70097c Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 7 Aug 2025 12:07:32 +0200 Subject: [PATCH 69/86] move around include file ordering --- src/TensorKit.jl | 4 ++++ src/spaces/vectorspaces.jl | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 0003bfddf..01e4d4ad9 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -228,6 +228,10 @@ include("planar/planaroperations.jl") # deprecations: to be removed in version 1.0 or sooner include("auxiliary/deprecate.jl") +# Additional methods for IsingBimod Sector +# ---------------------------------------- +include("spaces/multifusionspace.jl") + # Extensions # ---------- function __init__() diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index fa556387e..0eb647fc6 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -323,10 +323,6 @@ include("deligne.jl") #------------------------------ include("homspace.jl") -# Additional methods for IsingBimod Sector -# ---------------------------------------- -include("multifusionspace.jl") - # Partial order for vector spaces #--------------------------------- """ From 69e7423ece8fc38d2037a121b1d9ec865a10875e Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 7 Aug 2025 12:08:59 +0200 Subject: [PATCH 70/86] remove debug elements --- src/spaces/multifusionspace.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 0601033c7..163c1f828 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -30,9 +30,7 @@ Base.zero(S::Type{Vect[IsingBimod]}) = Vect[IsingBimod]() function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} codom = codomain(W) dom = domain(W) - @info "in blocksectors with $W" if N₁ == 0 && N₂ == 0 - @info "no sectors, returning empty set" return (IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) elseif N₁ == 0 @assert N₂ != 0 "one of Type IsingBimod doesn't exist" @@ -41,7 +39,6 @@ function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁ @assert N₁ != 0 "one of Type IsingBimod doesn't exist" return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(codom))) elseif N₂ <= N₁ # keep intersection - @info "returning sectors in domain" return filter!(c -> hasblock(codom, c), collect(blocksectors(dom))) else return filter!(c -> hasblock(dom, c), collect(blocksectors(codom))) From ae10a73194dff6cc1b8d29ac26560199d943c2fb Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 7 Aug 2025 12:20:42 +0200 Subject: [PATCH 71/86] remove fixme --- test/multifusion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index dc0e16dfc..15fd3e583 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -823,7 +823,7 @@ end @test conj(s) ≈ tr(t') try # needed for module cases: certain transposes with module legs will result in different colorings @planar s2 = t[a b; a b] # no twist needed bc permute avoided - @test s ≈ s2 #FIXME: currently this trace gives zero for VIBD, but not for VIBC + @test s ≈ s2 catch e @test isa(e, SectorMismatch) end From 7f0130692d474c7e4ec412d2fdd69654315fb377 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Thu, 7 Aug 2025 12:56:09 +0200 Subject: [PATCH 72/86] add `left/rightoneunit` test for other `Sector`s --- test/spaces.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/spaces.jl b/test/spaces.jl index 4591fb53d..8c7703733 100644 --- a/test/spaces.jl +++ b/test/spaces.jl @@ -209,6 +209,8 @@ println("------------------------------------") W = @constinferred GradedSpace(one(I) => 1) @test W == GradedSpace(one(I) => 1, randsector(I) => 0) @test @constinferred(oneunit(V)) == W == oneunit(typeof(V)) + @test @constinferred(leftoneunit(V)) == oneunit(V) == + @constinferred(rightoneunit(V)) @test @constinferred(zero(V)) == GradedSpace(one(I) => 0) # randsector never returns trivial sector, so this cannot error @test_throws ArgumentError GradedSpace(one(I) => 1, randsector(I) => 0, one(I) => 3) From 8d2e94f175456c333b16e5a27a5ebfbbbae7c7d2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 12 Aug 2025 09:37:18 +0200 Subject: [PATCH 73/86] typo --- test/fusiontrees.jl | 2 +- test/multifusion.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fusiontrees.jl b/test/fusiontrees.jl index 67fdc5ae1..a3da41c4f 100644 --- a/test/fusiontrees.jl +++ b/test/fusiontrees.jl @@ -379,7 +379,7 @@ ti = time() f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) - @testset "Double fusion tree $Istr: repartioning" begin + @testset "Double fusion tree $Istr: repartitioning" begin for n in 0:(2 * N) d = @constinferred TK.repartition(f1, f2, $n) @test dim(incoming) ≈ diff --git a/test/multifusion.jl b/test/multifusion.jl index 15fd3e583..ed4b38ffb 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -367,7 +367,7 @@ end f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) - @testset "Double fusion tree $Istr: repartioning" begin + @testset "Double fusion tree $Istr: repartitioning" begin for n in 0:(2 * N) d = @constinferred TK.repartition(f1, f2, $n) @test dim(incoming) ≈ From 6501e5facc634e2b5edd80081a70fc8e402454b6 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 13 Aug 2025 18:17:53 +0200 Subject: [PATCH 74/86] otimes between tensor maps change to account for `sectorscalartype` --- src/tensors/linalg.jl | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index f29bdf809..59af6f899 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -526,10 +526,17 @@ function ⊗(t1::AbstractTensorMap, t2::AbstractTensorMap) throw(SpaceMismatch("spacetype(t1) ≠ spacetype(t2)")) cod1, cod2 = codomain(t1), codomain(t2) dom1, dom2 = domain(t1), domain(t2) - cod = cod1 ⊗ cod2 - dom = dom1 ⊗ dom2 + p12 = ((codomainind(t1)..., (codomainind(t2) .+ numind(t1))...), + (domainind(t1)..., (domainind(t2) .+ numind(t1))...)) + T = promote_type(scalartype(t1), scalartype(t2)) - t = zerovector!(similar(t1, T, cod ← dom)) + TC = promote_type(sectorscalartype(sectortype(t1)), T) + t = TO.tensoralloc_contract(TC, + t1, ((codomainind(t1)..., domainind(t1)...), ()), false, + t2, ((), (codomainind(t2)..., domainind(t2)...)), false, + p12, Val(false)) + + zerovector!(t) for (f1l, f1r) in fusiontrees(t1) for (f2l, f2r) in fusiontrees(t2) c1 = f1l.coupled # = f1r.coupled From 07bcc514b138ff0e7504fd7d74bbc7b74d720733 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 11:17:53 +0200 Subject: [PATCH 75/86] export `left/rightone` --- src/TensorKit.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 01e4d4ad9..d08321da4 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -16,6 +16,7 @@ export Trivial, Z2Irrep, Z3Irrep, Z4Irrep, ZNIrrep, U1Irrep, SU2Irrep, CU1Irrep export ProductSector export FermionParity, FermionNumber, FermionSpin export FibonacciAnyon, IsingAnyon, IsingBimod +export leftone, rightone export VectorSpace, Field, ElementarySpace # abstract vector spaces export InnerProductStyle, NoInnerProduct, HasInnerProduct, EuclideanInnerProduct From 52fa8d48ba3c3b033684bff7fc84caa14e770f72 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 11:19:25 +0200 Subject: [PATCH 76/86] redefine `left/rightoneunit` for the usual `GradedSpace`s --- src/spaces/gradedspace.jl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 7c8b4cbea..24a82e603 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -139,10 +139,7 @@ Base.oneunit(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 1) Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial one-dimensional space consisting of the left unit of the objects in `Sector` `I`. """ -function leftoneunit(S::GradedSpace{I}) where {I<:Sector} - sector = leftone(first(sectors(S))) - return spacetype(S)(sector => 1) -end +leftoneunit(S::GradedSpace{I}) where {I<:Sector} = oneunit(typeof(S)) """ rightoneunit(S::GradedSpace{I}) where {I<:Sector} -> GradedSpace{I} @@ -150,10 +147,7 @@ end Return the corresponding vector space of type `GradedSpace{I}` that represents the trivial one-dimensional space consisting of the right unit of the objects in `Sector` `I`. """ -function rightoneunit(S::GradedSpace{I}) where {I<:Sector} - sector = rightone(first(sectors(S))) - return spacetype(S)(sector => 1) -end +rightoneunit(S::GradedSpace{I}) where {I<:Sector} = oneunit(typeof(S)) Base.zero(S::Type{<:GradedSpace{I}}) where {I<:Sector} = S(one(I) => 0) From 41013ece70b9cd0a32f601e820c12cc68c254ff5 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 11:20:59 +0200 Subject: [PATCH 77/86] add `left/rightoneunit` test on empty space --- test/spaces.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/spaces.jl b/test/spaces.jl index 8c7703733..de6d28ccd 100644 --- a/test/spaces.jl +++ b/test/spaces.jl @@ -211,6 +211,8 @@ println("------------------------------------") @test @constinferred(oneunit(V)) == W == oneunit(typeof(V)) @test @constinferred(leftoneunit(V)) == oneunit(V) == @constinferred(rightoneunit(V)) + Vempty = @constinferred zero(V) + @test oneunit(Vempty) == leftoneunit(Vempty) == rightoneunit(Vempty) == W @test @constinferred(zero(V)) == GradedSpace(one(I) => 0) # randsector never returns trivial sector, so this cannot error @test_throws ArgumentError GradedSpace(one(I) => 1, randsector(I) => 0, one(I) => 3) From c71b15cd141926de72c408fb29b109d129018dd8 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 12:34:36 +0200 Subject: [PATCH 78/86] add check to `left/rightoneunit` of `IsingBimod` spaces when empty --- src/spaces/multifusionspace.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 163c1f828..d2c175716 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -1,7 +1,7 @@ -# additional interface to deal with IsingBimod Sector +# additional interface to deal with IsingBimodule Sector #------------------------------------------------------------------------------ -# make this a separate module? +const IsingBimod = IsingBimodule #TODO: do the rename after compat fix function dim(V::Vect[IsingBimod]) T = Base.promote_op(*, Int, real(sectorscalartype(sectortype(V)))) @@ -16,7 +16,10 @@ function scalar(t::AbstractTensorMap{T,Vect[IsingBimod],0,0}) where {T} return only(last(Bs[only(inds)])) end +# no custom fuse: we choose to return empty graded space when fusion is forbidden + function Base.oneunit(S::Vect[IsingBimod]) + !isempty(sectors(S)) || throw(ArgumentError("Cannot determine type of empty space")) allequal(a.row for a in sectors(S)) && allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S are not all equal")) first(sectors(S)).row == first(sectors(S)).col || @@ -46,6 +49,7 @@ function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁ end function rightoneunit(S::Vect[IsingBimod]) + !isempty(sectors(S)) || throw(ArgumentError("Cannot determine type of empty space")) allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same rightone")) @@ -54,6 +58,7 @@ function rightoneunit(S::Vect[IsingBimod]) end function leftoneunit(S::Vect[IsingBimod]) + !isempty(sectors(S)) || throw(ArgumentError("Cannot determine type of empty space")) allequal(a.row for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same leftone")) From ddc16124b15f120f111070e1a219546ce7155ac2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 12:35:06 +0200 Subject: [PATCH 79/86] return vector in `IsingBimod` `blocksectors` --- src/spaces/multifusionspace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index d2c175716..a570e87c2 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -34,7 +34,7 @@ function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁ codom = codomain(W) dom = domain(W) if N₁ == 0 && N₂ == 0 - return (IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)) + return [IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)] elseif N₁ == 0 @assert N₂ != 0 "one of Type IsingBimod doesn't exist" return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(dom))) # is this what we want? doesn't allow M/Mop to end at empty space From 17f1e1be29f7551377df751b9361d2aaafd1b996 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 12:35:30 +0200 Subject: [PATCH 80/86] use `isone` again in `blocksectors` --- src/spaces/multifusionspace.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index a570e87c2..27d47970a 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -37,10 +37,10 @@ function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁ return [IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)] elseif N₁ == 0 @assert N₂ != 0 "one of Type IsingBimod doesn't exist" - return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(dom))) # is this what we want? doesn't allow M/Mop to end at empty space + return filter!(isone, collect(blocksectors(dom))) # is this what we want? doesn't allow M/Mop to end at empty space elseif N₂ == 0 @assert N₁ != 0 "one of Type IsingBimod doesn't exist" - return filter!(c -> c == leftone(c) == rightone(c), collect(blocksectors(codom))) + return filter!(isone, collect(blocksectors(codom))) elseif N₂ <= N₁ # keep intersection return filter!(c -> hasblock(codom, c), collect(blocksectors(dom))) else From e8f6a7688e229b76c6a9ee6c4ea49830805f96a7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 12:39:17 +0200 Subject: [PATCH 81/86] use `isone` again in `artin_braid` --- src/fusiontrees/manipulations.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 3bbcf71ec..4d67d1dfc 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -835,15 +835,13 @@ function artin_braid(f::FusionTree{I,N}, i; inv::Bool=false) where {I<:Sector,N} vertices = f.vertices oneT = one(sectorscalartype(I)) - diaga = a == leftone(a) == rightone(a) # might be excessive because multifusion doesn't support braiding currently - diagb = b == leftone(b) == rightone(b) - if diaga || diagb + if isone(a) || isone(b) # braiding with trivial sector: simple and always possible inner′ = inner vertices′ = vertices if i > 1 # we also need to alter innerlines and vertices inner′ = TupleTools.setindex(inner, - inner_extended[diaga ? (i + 1) : (i - 1)], + inner_extended[isone(a) ? (i + 1) : (i - 1)], i - 1) vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) From df0115eb2fcff1b882aefcadb95280a2fc823a8b Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 12:44:41 +0200 Subject: [PATCH 82/86] Revert "add TensorKitSectors to extras for tests" This reverts commit 772452fe5da790f2bdc539e60edc2f0e5e32b05e. --- Project.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index dd65d28a1..b063816f7 100644 --- a/Project.toml +++ b/Project.toml @@ -49,11 +49,10 @@ ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [targets] -test = ["Aqua", "Combinatorics", "LinearAlgebra", "TensorKitSectors", "TensorOperations", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "FiniteDifferences", "Zygote"] +test = ["Aqua", "Combinatorics", "LinearAlgebra", "TensorOperations", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "FiniteDifferences", "Zygote"] From ac56fae4e17db9dd33bcb2acbbc4c81197ae34e0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 12:45:45 +0200 Subject: [PATCH 83/86] remove import TKS to tests + use `isone` again --- test/runtests.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index f2ef0a36d..1273e0504 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,8 +4,6 @@ using Random using TensorKit using Combinatorics using TensorKit: ProductSector, fusiontensor -using TensorKitSectors -using TensorKitSectors: leftone, rightone using TensorOperations using Base.Iterators: take, product # using SUNRepresentations: SUNIrrep @@ -31,7 +29,7 @@ end function randsector(::Type{I}) where {I<:Sector} s = collect(smallset(I)) a = rand(s) - while a == leftone(a) == rightone(a) # don't use trivial label + while isone(a) # don't use trivial label a = rand(s) end return a From 061265619e05ff6e2454c015701d8e54afa86dc0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 14:18:32 +0200 Subject: [PATCH 84/86] fix argument errors in tests + tests for empty space --- test/multifusion.jl | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index ed4b38ffb..9b929e071 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -85,9 +85,16 @@ VIBMop2 = (Vect[I](D0 => 1, D1 => 1), @test @constinferred(rightoneunit(⊕(Wright, WM))) == oneunit(Wright) @test @constinferred(rightoneunit(⊕(Wleft, WMop))) == oneunit(Wleft) - @test_throws ArgumentError oneunit(I) - @test_throws ArgumentError oneunit(WM) - @test_throws ArgumentError oneunit(WMop) + @test_throws ArgumentError("one of Type IsingBimodule doesn't exist") oneunit(I) + @test_throws ArgumentError("sectors of $WM are non-diagonal") oneunit(WM) + @test_throws ArgumentError("sectors of $WMop are non-diagonal") oneunit(WMop) + + # empty space + Wempty = Vect[I]() + @test @constinferred(zero(V)) == Wempty + for f in (oneunit, leftoneunit, rightoneunit) + @test_throws ArgumentError("Cannot determine type of empty space") f(Wempty) + end @test isa(V, VectorSpace) @test isa(V, ElementarySpace) @@ -671,9 +678,9 @@ for V in (VIBC, VIBD) @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests -> this condition makes it hard to test non-diagonal sectors end -@timedtestset "Tensors with symmetry: $Istr" verbose = true for V in - (VIBC, VIBD, VIBM1, VIBM2, - VIBMop1, VIBMop2) +@timedtestset "Tensors with symmetry: $Istr $i" verbose = true for (i, V) in + enumerate((VIBC, VIBD, VIBM1, VIBM2, + VIBMop1, VIBMop2)) V1, V2, V3, V4, V5 = V @timedtestset "Basic tensor properties" begin W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 # fusion matters From 698d652bcc6bb40501ec567efdd5c453bd674035 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 14:19:56 +0200 Subject: [PATCH 85/86] renaming to `IsingBimodule` --- src/TensorKit.jl | 4 ++-- src/spaces/multifusionspace.jl | 34 ++++++++++++++++------------------ test/multifusion.jl | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index d08321da4..3f25efa5f 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -15,7 +15,7 @@ export BraidingStyle, SymmetricBraiding, Bosonic, Fermionic, Anyonic, NoBraiding export Trivial, Z2Irrep, Z3Irrep, Z4Irrep, ZNIrrep, U1Irrep, SU2Irrep, CU1Irrep export ProductSector export FermionParity, FermionNumber, FermionSpin -export FibonacciAnyon, IsingAnyon, IsingBimod +export FibonacciAnyon, IsingAnyon, IsingBimodule export leftone, rightone export VectorSpace, Field, ElementarySpace # abstract vector spaces @@ -229,7 +229,7 @@ include("planar/planaroperations.jl") # deprecations: to be removed in version 1.0 or sooner include("auxiliary/deprecate.jl") -# Additional methods for IsingBimod Sector +# Additional methods for IsingBimodule Sector # ---------------------------------------- include("spaces/multifusionspace.jl") diff --git a/src/spaces/multifusionspace.jl b/src/spaces/multifusionspace.jl index 27d47970a..12bbf45d1 100644 --- a/src/spaces/multifusionspace.jl +++ b/src/spaces/multifusionspace.jl @@ -1,15 +1,13 @@ # additional interface to deal with IsingBimodule Sector #------------------------------------------------------------------------------ -const IsingBimod = IsingBimodule #TODO: do the rename after compat fix - -function dim(V::Vect[IsingBimod]) +function dim(V::Vect[IsingBimodule]) T = Base.promote_op(*, Int, real(sectorscalartype(sectortype(V)))) return reduce(+, dim(V, c) * dim(c) for c in sectors(V); init=zero(T)) end -function scalar(t::AbstractTensorMap{T,Vect[IsingBimod],0,0}) where {T} +function scalar(t::AbstractTensorMap{T,Vect[IsingBimodule],0,0}) where {T} Bs = collect(blocks(t)) inds = findall(!iszero ∘ last, Bs) isempty(inds) && return zero(scalartype(t)) @@ -18,7 +16,7 @@ end # no custom fuse: we choose to return empty graded space when fusion is forbidden -function Base.oneunit(S::Vect[IsingBimod]) +function Base.oneunit(S::Vect[IsingBimodule]) !isempty(sectors(S)) || throw(ArgumentError("Cannot determine type of empty space")) allequal(a.row for a in sectors(S)) && allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S are not all equal")) @@ -28,18 +26,18 @@ function Base.oneunit(S::Vect[IsingBimod]) return spacetype(S)(sector => 1) end -Base.zero(S::Type{Vect[IsingBimod]}) = Vect[IsingBimod]() +Base.zero(S::Type{Vect[IsingBimodule]}) = Vect[IsingBimodule]() -function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁,N₂} +function blocksectors(W::TensorMapSpace{Vect[IsingBimodule],N₁,N₂}) where {N₁,N₂} codom = codomain(W) dom = domain(W) if N₁ == 0 && N₂ == 0 - return [IsingBimod(1, 1, 0), IsingBimod(2, 2, 0)] + return [IsingBimodule(1, 1, 0), IsingBimodule(2, 2, 0)] elseif N₁ == 0 - @assert N₂ != 0 "one of Type IsingBimod doesn't exist" + @assert N₂ != 0 "one of Type IsingBimodule doesn't exist" return filter!(isone, collect(blocksectors(dom))) # is this what we want? doesn't allow M/Mop to end at empty space elseif N₂ == 0 - @assert N₁ != 0 "one of Type IsingBimod doesn't exist" + @assert N₁ != 0 "one of Type IsingBimodule doesn't exist" return filter!(isone, collect(blocksectors(codom))) elseif N₂ <= N₁ # keep intersection return filter!(c -> hasblock(codom, c), collect(blocksectors(dom))) @@ -48,7 +46,7 @@ function blocksectors(W::TensorMapSpace{Vect[IsingBimod],N₁,N₂}) where {N₁ end end -function rightoneunit(S::Vect[IsingBimod]) +function rightoneunit(S::Vect[IsingBimodule]) !isempty(sectors(S)) || throw(ArgumentError("Cannot determine type of empty space")) allequal(a.col for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same rightone")) @@ -57,7 +55,7 @@ function rightoneunit(S::Vect[IsingBimod]) return spacetype(S)(sector => 1) end -function leftoneunit(S::Vect[IsingBimod]) +function leftoneunit(S::Vect[IsingBimodule]) !isempty(sectors(S)) || throw(ArgumentError("Cannot determine type of empty space")) allequal(a.row for a in sectors(S)) || throw(ArgumentError("sectors of $S do not have the same leftone")) @@ -66,7 +64,7 @@ function leftoneunit(S::Vect[IsingBimod]) return spacetype(S)(sector => 1) end -function insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; +function insertrightunit(P::ProductSpace{Vect[IsingBimodule],N}, ::Val{i}; conj::Bool=false, dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible right unit onto $P at index $(i+1)") @@ -82,7 +80,7 @@ function insertrightunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; end # TODO?: overwrite defaults at level of HomSpace and TensorMap? -function insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no defaults? +function insertleftunit(P::ProductSpace{Vect[IsingBimodule],N}, ::Val{i}; # want no defaults? conj::Bool=false, dual::Bool=false) where {i,N} i > N && error("cannot insert a sensible left unit onto $P at index $i") # do we want this to error in the diagonal case? @@ -96,9 +94,9 @@ function insertleftunit(P::ProductSpace{Vect[IsingBimod],N}, ::Val{i}; # want no return ProductSpace(TupleTools.insertafter(P.spaces, i - 1, (u,))) end -# is this even necessary? can let it error at fusiontrees.jl:93 from the one(IsingBimod) call +# is this even necessary? can let it error at fusiontrees.jl:93 from the one(IsingBimodule) call # but these errors are maybe more informative -function FusionTree(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) +function FusionTree(uncoupled::Tuple{IsingBimodule,Vararg{IsingBimodule}}) coupled = collect(⊗(uncoupled...)) if length(coupled) == 0 # illegal fusion somewhere throw(ArgumentError("Forbidden fusion with uncoupled sectors $uncoupled")) @@ -108,6 +106,6 @@ function FusionTree(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) end # this one might also be overkill, since `FusionTreeIterator`s don't check whether the fusion is allowed -function fusiontrees(uncoupled::Tuple{IsingBimod,Vararg{IsingBimod}}) - return throw(ArgumentError("coupled sector must be provided for IsingBimod fusion")) +function fusiontrees(uncoupled::Tuple{IsingBimodule,Vararg{IsingBimodule}}) + return throw(ArgumentError("coupled sector must be provided for IsingBimodule fusion")) end diff --git a/test/multifusion.jl b/test/multifusion.jl index 9b929e071..36a2fff77 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -1,4 +1,4 @@ -I = IsingBimod +I = IsingBimodule Istr = TK.type_repr(I) println("------------------------------------") From 9b5ba0b0b538b7f41c32d49b9aef65448e769b1d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 26 Aug 2025 14:22:06 +0200 Subject: [PATCH 86/86] format --- test/multifusion.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/multifusion.jl b/test/multifusion.jl index 36a2fff77..532317eff 100644 --- a/test/multifusion.jl +++ b/test/multifusion.jl @@ -679,8 +679,10 @@ for V in (VIBC, VIBD) end @timedtestset "Tensors with symmetry: $Istr $i" verbose = true for (i, V) in - enumerate((VIBC, VIBD, VIBM1, VIBM2, - VIBMop1, VIBMop2)) + enumerate((VIBC, VIBD, + VIBM1, VIBM2, + VIBMop1, + VIBMop2)) V1, V2, V3, V4, V5 = V @timedtestset "Basic tensor properties" begin W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 # fusion matters