diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index 2a853124a..13f8916f2 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -42,6 +42,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec ### Performance - Recoupling matrices are stored such that they can leverage BLAS, also for mixed complex tensor with real recoupling coefficients. +- Braiding fusion tree blocks no longer constructs and indexes the canonical intermediate blocks of every step: these keep their trees in the order in which they are produced, their buffers are recycled, and blocks with a single tree are braided as a product of scalars (internal) ## [0.17.2](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.1...v0.17.2) - 2026-09-20 diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 368e326a7..6025eb695 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -16,8 +16,13 @@ tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword ha if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} - 1 <= i < N || throw(ArgumentError(lazy"Cannot swap outputs i=$i and i+1 out of only $N outputs")) @assert FusionStyle(I) === UniqueFusion() + return _artin_braid_single(f, i, inv) +end +# also for a tree that is the only one with its uncoupled sectors, whose braid is again a single +# tree (`only` throws otherwise) +function _artin_braid_single(f::FusionTree{I, N}, i, inv::Bool) where {I, N} + 1 <= i < N || throw(ArgumentError(lazy"Cannot swap outputs i=$i and i+1 out of only $N outputs")) uncoupled = f.uncoupled a, b = uncoupled[i], uncoupled[i + 1] @@ -53,7 +58,7 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} if i == 1 c = N > 2 ? inner[1] : coupled′ - R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) + R = oftype(oneT, (inv ? conj(only(Rsymbol(b, a, c))) : only(Rsymbol(a, b, c)))) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) return f′ => R end @@ -64,13 +69,13 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} a = inner_extended[i - 1] c = inner_extended[i] e = inner_extended[i + 1] - c′ = only(a ⊗ d) + c′ = _artin_channel(a, b, d, e) coeff = oftype( oneT, if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + conj(only(Rsymbol(d, c, e)) * only(Fsymbol(d, a, b, e, c′, c))) * only(Rsymbol(d, a, c′)) else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + only(Rsymbol(c, d, e)) * conj(only(Fsymbol(d, a, b, e, c′, c)) * only(Rsymbol(a, d, c′))) end ) inner′ = TupleTools.setindex(inner, c′, i - 1) @@ -78,9 +83,30 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} return f′ => coeff end +# the inner lines `c′` of `a ⊗ d → c′ → c′ ⊗ b → e`, i.e. `intersect(a ⊗ d, e ⊗ conj(b))`, +# without constructing the second fusion product for sectors with a simple unit +function _artin_channels(a::I, b::I, d::I, e::I) where {I <: Sector} + UnitStyle(I) isa SimpleUnit || return intersect(a ⊗ d, e ⊗ conj(b)) + return Iterators.filter(c′ -> !iszero(Nsymbol(c′, b, e)), a ⊗ d) +end +# the only such line, for a fusion tree that braids into a single one +_artin_channel(a, b, d, e) = _artin_channel(FusionStyle(a), a, b, d, e) +_artin_channel(::UniqueFusion, a, b, d, e) = only(a ⊗ d) +_artin_channel(::FusionStyle, a, b, d, e) = first(_artin_channels(a, b, d, e)) + function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where {I, N} + n = length(src) # braiding a complete block keeps its size + return artin_braid!(Matrix{sectorscalartype(I)}(undef, n, n), src, i; inv) # zeroed by `artin_braid!` +end +# `U` is overwritten with the braid matrix of `src`, a complete block; the trees of the result are +# in canonical order or in the order in which they are produced, which recycles `trees` if given +function artin_braid!( + U::AbstractMatrix, src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false, + canonical::Val = Val(true), trees = nothing + ) where {I, N} 1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) - isempty(src) && return src => zeros(sectorscalartype(I), 0, 0) + size(U) == (length(src), length(src)) || throw(DimensionMismatch(lazy"braid matrix of size $(size(U)) for a block of $(length(src)) trees")) + isempty(src) && return src => U uncoupled = src.uncoupled[1] a, b = uncoupled[i], uncoupled[i + 1] @@ -91,12 +117,10 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where isdual = src.isdual[1] isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) - dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ()); sizehint = length(src)) + rows = TreeRows(I, (uncoupled′, ()), (isdual′, ()), length(src), canonical, trees) oneT = one(sectorscalartype(I)) - - indexmap = treeindex_map(dst) - U = zeros(sectorscalartype(I), length(dst), length(src)) + fill!(U, zero(oneT)) if isunit(a) || isunit(b) # braiding with trivial sector: simple and always possible for (col, (f, f₂)) in enumerate(fusiontrees(src)) @@ -113,10 +137,10 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) end f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - row = indexmap[treeindex_data((f′, f₂))] + row = row!(rows, (f′, f₂)) @inbounds U[row, col] = oneT end - return dst => U + return FusionTreeBlock(rows.trees) => U end BraidingStyle(I) isa NoBraiding && @@ -132,7 +156,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where if FusionStyle(I) isa MultiplicityFreeFusion R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) - row = indexmap[treeindex_data((f′, f₂))] + row = row!(rows, (f′, f₂)) @inbounds U[row, col] = R else # GenericFusion μ = vertices[1] @@ -142,7 +166,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where iszero(R) && continue vertices′ = TupleTools.setindex(vertices, ν, 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) - row = indexmap[treeindex_data((f′, f₂))] + row = row!(rows, (f′, f₂)) @inbounds U[row, col] = R end end @@ -155,29 +179,30 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where c = inner_extended[i] e = inner_extended[i + 1] if FusionStyle(I) isa MultiplicityFreeFusion - for c′ in intersect(a ⊗ d, e ⊗ conj(b)) + R = inv ? Rsymbol(d, c, e) : Rsymbol(c, d, e) + for c′ in _artin_channels(a, b, d, e) coeff = if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + conj(R * 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′)) + R * 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′) - row = indexmap[treeindex_data((f′, f₂))] + row = row!(rows, (f′, f₂)) @inbounds U[row, col] = coeff end else # GenericFusion - for c′ in intersect(a ⊗ d, e ⊗ conj(b)) - Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) + Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) + μ = vertices[i - 1] + ν = vertices[i] + for c′ in _artin_channels(a, b, d, e) Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) Fmat = Fsymbol(d, a, b, e, c′, c) - μ = vertices[i - 1] - ν = vertices[i] - for σ in 1:Nsymbol(a, d, c′) - for λ in 1:Nsymbol(c′, b, e) + for σ in axes(Rmat2, 1) + for λ in axes(Fmat, 2) coeff = zero(oneT) - for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) + for ρ in axes(Rmat1, 2), κ in axes(Rmat2, 2) coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * conj(Rmat2[σ, κ]) end @@ -186,7 +211,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where vertices′ = TupleTools.setindex(vertices′, λ, i) inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - row = indexmap[treeindex_data((f′, f₂))] + row = row!(rows, (f′, f₂)) @inbounds U[row, col] = coeff end end @@ -194,7 +219,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where end end - return dst => U + return FusionTreeBlock(rows.trees) => U end # braid fusion tree @@ -312,29 +337,54 @@ end return (f₁′, f₂′) => coeff1 * coeff2 * coeff3 else src, (p1, p2), (l1, l2) = key + src = src::FusionTreeBlock{I} p = linearizepermutation(p1, p2, numout(src), numin(src)) levels = (l1..., reverse(l2)...) - dst, U = repartition(src, numind(src)) + if length(src) == 1 # then so is every intermediate block: the braid is a product of scalars + (f, f₀), coeff = _bend_single(_bendleft_single, only(fusiontrees(src)), Val(numin(src))) + for s in permutation2swaps(p) + _check_levels(BraidingStyle(I), levels, s) + inv = levels[s] > levels[s + 1] + f, c = _artin_braid_single(f, s, inv) + coeff *= c + l = levels[s] + levels = TupleTools.setindex(levels, levels[s + 1], s) + levels = TupleTools.setindex(levels, l, s + 1) + end + (f₁′, f₂′), c = _bend_single(_bendright_single, (f, f₀), Val(N₂)) + return FusionTreeBlock([(f₁′, f₂′)]) => fill(coeff * c, 1, 1) + end + # the intermediate blocks keep their trees in the order in which they are produced, + # and only the result is brought into canonical order + dst, U = repartition_noncanonical(src, Val(numind(src))) + + # the swaps keep the size of the block, so that the tree vector and the matrices of the + # previous step are recycled (except for the trees of `src`, which are part of the key) + trees = nothing + U_tmp = Matrix{sectorscalartype(I)}(undef, length(dst), length(dst)) + U_spare = nothing braid_style = BraidingStyle(I) for s in permutation2swaps(p) _check_levels(braid_style, levels, s) inv = levels[s] > levels[s + 1] - dst, U_tmp = artin_braid(dst, s; inv) - U = U_tmp * U + dst′, _ = artin_braid!(U_tmp, dst, s; inv, canonical = Val(false), trees) + trees = dst.trees === src.trees ? nothing : dst.trees + dst = dst′ + U_spare, U = U, U_spare isa typeof(U_tmp) ? mul!(U_spare, U_tmp, U) : U_tmp * U l = levels[s] levels = TupleTools.setindex(levels, levels[s + 1], s) levels = TupleTools.setindex(levels, l, s + 1) end if N₂ == 0 - return dst => U + return canonicalize(dst, U) else - dst, U_tmp = repartition(dst::FusionTreeBlock{I}, N₁) + dst, U_tmp = repartition_noncanonical(dst::FusionTreeBlock{I}, Val(N₁)) U = U_tmp * U - return dst => U + return canonicalize(dst, U) end end end diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index e399f87ab..94c24933e 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -54,19 +54,22 @@ function _bendright_treepair((f₁, f₂)::FusionTreePair) end function bendright((f₁, f₂)::FusionTreePair) - I = sectortype((f₁, f₂)) + @assert FusionStyle((f₁, f₂)) === UniqueFusion() + return _bendright_single((f₁, f₂)) +end +# also for a final fusion vertex without multiplicity (`only` throws otherwise) +function _bendright_single((f₁, f₂)::FusionTreePair) N₁ = numout((f₁, f₂)) - @assert FusionStyle(I) === UniqueFusion() (a, b, c), (f₁′, f₂′) = _bendright_treepair((f₁, f₂)) # compute the coefficient coeff₀ = sqrtdim(c) * invsqrtdim(a) f₁.isdual[N₁] && (coeff₀ *= conj(frobenius_schur_phase(dual(b)))) - coeff = coeff₀ * Bsymbol(a, b, c) + coeff = coeff₀ * only(Bsymbol(a, b, c)) return (f₁′, f₂′) => coeff end -function bendright(src::FusionTreeBlock) +function bendright(src::FusionTreeBlock, canonical::Val = Val(true)) I = sectortype(src) N₁ = numout(src) N₂ = numin(src) @@ -80,9 +83,10 @@ function bendright(src::FusionTreeBlock) (src.isdual[2]..., !(src.isdual[1][N₁])), ) - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) - indexmap = treeindex_map(dst) - U = zeros(fusionscalartype(I), length(dst), length(src)) + rows = TreeRows(I, uncoupled_dst, isdual_dst, length(src), canonical) + # the canonical destination block has more trees than `src` if `src` holds only some of the + # trees with its uncoupled sectors (as in `planar_trace`) + U = zeros(fusionscalartype(I), max(length(rows.trees), length(src)), length(src)) for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) (a, b, c), (f₁′, f₂′) = _bendright_treepair((f₁, f₂)) @@ -92,7 +96,7 @@ function bendright(src::FusionTreeBlock) end if FusionStyle(I) isa MultiplicityFreeFusion coeff = coeff₀ * Bsymbol(a, b, c) - row = indexmap[treeindex_data((f₁′, f₂′))] + row = row!(rows, (f₁′, f₂′)) @inbounds U[row, col] = coeff else Bmat = Bsymbol(a, b, c) @@ -106,12 +110,12 @@ function bendright(src::FusionTreeBlock) iszero(coeff) && continue vertices₂ = N₂ > 0 ? (f₂.vertices..., ν) : () f₂′ = FusionTree(uncoupled₂, coupled₂, isdual₂, inner₂, vertices₂) - row = indexmap[treeindex_data((f₁′, f₂′))] + row = row!(rows, (f₁′, f₂′)) @inbounds U[row, col] = coeff end end end - return dst => U + return FusionTreeBlock(rows.trees) => U end @doc """ @@ -142,10 +146,22 @@ function bendleft((f₁, f₂)::FusionTreePair) (f₂′, f₁′), coeff = bendright((f₂, f₁)) return (f₁′, f₂′) => conj(coeff) end +function _bendleft_single((f₁, f₂)::FusionTreePair) + (f₂′, f₁′), coeff = _bendright_single((f₂, f₁)) + return (f₁′, f₂′) => conj(coeff) +end + +# `n` bends of a tree pair that stays a single tree pair, and the product of their coefficients +_bend_single(bend, f, ::Val{0}) = f => one(sectorscalartype(sectortype(f))) +function _bend_single(bend, f, ::Val{n}) where {n} + f′, c′ = bend(f) + f″, c″ = _bend_single(bend, f′, Val(n - 1)) + return f″ => c′ * c″ +end # !! note that this is more or less a copy of bendright through # (f1, f2) => conj(coeff) for ((f2, f1), coeff) in bendleft(src) -function bendleft(src::FusionTreeBlock) +function bendleft(src::FusionTreeBlock, canonical::Val = Val(true)) I = sectortype(src) N₁ = numout(src) N₂ = numin(src) @@ -159,9 +175,10 @@ function bendleft(src::FusionTreeBlock) TupleTools.front(src.isdual[2]), ) - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) - indexmap = treeindex_map(dst) - U = zeros(fusionscalartype(I), length(dst), length(src)) + rows = TreeRows(I, uncoupled_dst, isdual_dst, length(src), canonical) + # the canonical destination block has more trees than `src` if `src` holds only some of the + # trees with its uncoupled sectors (as in `planar_trace`) + U = zeros(fusionscalartype(I), max(length(rows.trees), length(src)), length(src)) for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) (a, b, c), (f₂′, f₁′) = _bendright_treepair((f₂, f₁)) @@ -171,7 +188,7 @@ function bendleft(src::FusionTreeBlock) end if FusionStyle(I) isa MultiplicityFreeFusion coeff = coeff₀ * Bsymbol(a, b, c) - row = indexmap[treeindex_data((f₁′, f₂′))] + row = row!(rows, (f₁′, f₂′)) @inbounds U[row, col] = conj(coeff) else Bmat = Bsymbol(a, b, c) @@ -185,12 +202,12 @@ function bendleft(src::FusionTreeBlock) iszero(coeff) && continue vertices₁ = N₁ > 0 ? (f₁.vertices..., ν) : () f₁′ = FusionTree(uncoupled₁, coupled₁, isdual₁, inner₁, vertices₁) - row = indexmap[treeindex_data((f₁′, f₂′))] + row = row!(rows, (f₁′, f₂′)) @inbounds U[row, col] = conj(coeff) end end end - return dst => U + return FusionTreeBlock(rows.trees) => U end @doc """ @@ -478,7 +495,7 @@ return dst, U # it, so that it is visible to the generator's world age. Otherwise a precompile workload that # first triggers `repartition` fails with `UndefVarError: _repartition_body` (at runtime the # first call happens at a later world age, which masks the ordering issue). -function _repartition_body(N) +function _repartition_body(N, canonical = true) if N == 0 ex = quote T = fusionscalartype(sectortype(src)) @@ -491,13 +508,14 @@ function _repartition_body(N) end else f = N < 0 ? bendleft : bendright + args = canonical ? () : (Val(false),) ex_rep = Expr(:block) for _ in 1:(abs(N) - 1) - push!(ex_rep.args, :((dst, Utmp) = $f(dst))) + push!(ex_rep.args, :((dst, Utmp) = $f(dst, $(args...)))) push!(ex_rep.args, :(U = Utmp * U)) end ex = quote - dst, U = $f(src) + dst, U = $f(src, $(args...)) $ex_rep return dst => U end @@ -507,6 +525,10 @@ end @generated function repartition(src::Union{FusionTreePair, FusionTreeBlock}, ::Val{N}) where {N} return _repartition_body(numout(src) - N) end +# the same for a block, with the trees of the result in the order in which they are produced +@generated function repartition_noncanonical(src::FusionTreeBlock, ::Val{N}) where {N} + return _repartition_body(numout(src) - N, false) +end """ transpose((f₁, f₂)::FusionTreePair{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index f20445f36..7dfc5687b 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -117,6 +117,8 @@ const FusionTreePair{I, N₁, N₂} = Tuple{FusionTree{I, N₁}, FusionTree{I, N Collection of fusion-splitting tree pairs that share the same uncoupled sectors. Mostly internal structure to speed up non-`UniqueFusion` fusiontree manipulation computations. +Braiding a block requires it to be complete, i.e. to contain all the fusion-splitting tree pairs +with its uncoupled sectors and duality flags, as `FusionTreeBlock{I}(uncoupled, isdual)` does. """ struct FusionTreeBlock{I, N₁, N₂, F <: FusionTreePair{I, N₁, N₂}} trees::Vector{F} @@ -256,6 +258,57 @@ function treeindex_map(fs::FusionTreeBlock) return fusiontreedict(I)(treeindex_data(f) => ind for (ind, f) in enumerate(fusiontrees(fs))) end +# The trees of the destination block of a block manipulation, and the row of each of them in the +# transformation matrix. With `Val(true)`, these are the trees of the destination block in their +# canonical order; with `Val(false)`, the trees are added in the order in which they are produced, +# which avoids constructing the destination block. The latter is used for the intermediate blocks +# of a composite manipulation, of which only the result is brought into canonical order; the tree +# vector of an intermediate block that is no longer needed can then be passed on as `buffer`. +# With `Val(false)`, the source block has to be complete: the destination block then has as many +# trees (`n`), and an incomplete source block raises an error if its destination block has more. +struct TreeRows{F, D} + trees::Vector{F} + index::Union{D, Nothing} # `nothing`: few trees, the rows are found by a linear search + n::Int +end +function TreeRows( + ::Type{I}, uncoupled, isdual, n::Int, ::Val{canonical}, buffer = nothing + ) where {I, canonical} + F = fusiontreetype(I, length(uncoupled[1]), length(uncoupled[2])) + trees = if canonical + fusiontrees(FusionTreeBlock{I}(uncoupled, isdual; sizehint = n)) + else + buffer isa Vector{F} ? empty!(buffer) : sizehint!(Vector{F}(), n) + end + K = Base.promote_op(treeindex_data, eltype(trees)) + index = n > 32 ? Dict{K, Int}(treeindex_data(f) => i for (i, f) in enumerate(trees)) : nothing + return TreeRows{eltype(trees), Dict{K, Int}}(trees, index, n) +end +function row!(rows::TreeRows, f) + key = treeindex_data(f) + i = if isnothing(rows.index) + findfirst(f′ -> treeindex_data(f′) == key, rows.trees) + else + get(rows.index, key, nothing) + end + isnothing(i) || return i + length(rows.trees) < rows.n || + throw(ArgumentError("braiding requires a complete `FusionTreeBlock`")) + push!(rows.trees, f) + isnothing(rows.index) || (rows.index[key] = length(rows.trees)) + return length(rows.trees) +end + +# bring a block whose trees are not in canonical order, together with the transformation matrix +# whose rows correspond to these trees, into canonical order +function canonicalize(src::FusionTreeBlock{I}, U::AbstractMatrix) where {I} + length(src) == 1 && return src => U + rows = TreeRows(I, src.uncoupled, src.isdual, length(src), Val(true)) + U′ = zeros(eltype(U), length(rows.trees), size(U, 2)) + U′[map(f -> row!(rows, f), fusiontrees(src)), :] = U + return FusionTreeBlock(rows.trees) => U′ +end + # Facilitate getting correct fusion tree types Base.@assume_effects :foldable function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index 4fbe43e90..7776283ea 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -235,6 +235,31 @@ using TensorKitSectors end end + FusionStyle(I) isa UniqueFusion || !(BraidingStyle(I) isa HasBraiding) || + @testset "Double fusion tree: braiding a block with a single tree" begin + a, b = random_fusion(I, Val(2)) + cs = filter(c -> Nsymbol(a, b, c) == 1, collect(a ⊗ b)) + if !isempty(cs) + src1 = FusionTreeBlock{I}(((a, b), (rand(cs),)), ((rand(Bool), rand(Bool)), (rand(Bool),))) + @test length(src1) == 1 + for n in 0:3 + p = (randperm(3)...,) + p1, p2 = p[1:n], p[(n + 1):3] + ip = invperm(p) + ilevels = TupleTools.getindices((1, 2, 3), p) + dst, U = @constinferred TensorKit.braid(src1, (p1, p2), ((1, 2), (3,))) + @test length(dst) == 1 && size(U) == (1, 1) + dst′, U′ = @constinferred TensorKit.braid(dst, (ip[1:2], ip[3:3]), (ilevels[1:n], ilevels[(n + 1):3])) + @test dst′ == src1 + @test _isone(U * U′) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A1 = fusiontensor(only(fusiontrees(src1))) + @test permutedims(A1, (p1..., p2...)) ≈ only(U) * fusiontensor(only(fusiontrees(dst))) + end + end + end + end + @testset "Double fusion tree: planar trace" begin if FusionStyle(I) isa UniqueFusion f1, f1 = src