From 75d75ff0018cd95a677cfd86daf74dcf2f65cef5 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 22 Oct 2026 17:04:11 -0400 Subject: [PATCH 1/8] Factorize fusion tree block manipulations of product sectors Braid and transpose a block of a ProductSector through its head and tail factors: the recoupling matrix is the Kronecker product of those of the factors, so blocks that only differ in their abelian charges share the cached manipulations of the non-abelian factors. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/braiding_manipulations.jl | 10 +++ src/fusiontrees/duality_manipulations.jl | 10 +++ src/fusiontrees/fusiontrees.jl | 86 +++++++++++++++++++++++ test/symmetries/doubletree.jl | 20 ++++++ 4 files changed, 126 insertions(+) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 368e326a7..fbad3a5d1 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -286,6 +286,12 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve @assert TupleTools.isperm((p[1]..., p[2]...)) return fsbraid((src, p, levels)) end +function braid(src::ProductSectorBlock, p::Index2Tuple, levels::Index2Tuple) + isempty(src) && return fsbraid((src, p, levels)) + return product_transform(sectorscalartype(sectortype(src)), src) do x + return braid(x, p, levels) + end +end const FSPBraidKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁, N₂}, Index2Tuple} const FSBBraidKey{I, N₁, N₂} = Tuple{FusionTreeBlock{I}, Index2Tuple{N₁, N₂}, Index2Tuple} @@ -312,6 +318,10 @@ end return (f₁′, f₂′) => coeff1 * coeff2 * coeff3 else src, (p1, p2), (l1, l2) = key + if FusionStyle(I) isa UniqueFusion && length(src) == 1 # a single tree pair + f, coeff = fsbraid((only(fusiontrees(src)), (p1, p2), (l1, l2))) + return FusionTreeBlock([f]) => fill(coeff, 1, 1) + end p = linearizepermutation(p1, p2, numout(src), numin(src)) levels = (l1..., reverse(l2)...) diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index e399f87ab..0e9e83fdf 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -526,6 +526,12 @@ function Base.transpose(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tu iscyclicpermutation(p′) || throw(ArgumentError("invalid cyclic or planar permutation p = $p")) return fstranspose((src, p)) end +function Base.transpose(src::ProductSectorBlock, p::Index2Tuple) + isempty(src) && return fstranspose((src, p)) + return product_transform(fusionscalartype(sectortype(src)), src) do x + return transpose(x, p) + end +end const FSPTransposeKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁, N₂}} const FSBTransposeKey{I, N₁, N₂} = Tuple{FusionTreeBlock{I}, Index2Tuple{N₁, N₂}} @@ -543,6 +549,10 @@ end @cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: Union{FSPTransposeKey{I, N₁, N₂}, FSBTransposeKey{I, N₁, N₂}}} src, (p1, p2) = key + if K <: FSBTransposeKey && FusionStyle(I) isa UniqueFusion && length(src) == 1 # a single tree pair + f, coeff = fstranspose((only(fusiontrees(src)), (p1, p2))) + return FusionTreeBlock([f]) => fill(coeff, 1, 1) + end N = N₁ + N₂ p = linearizepermutation(p1, p2, numout(src), numin(src)) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index f20445f36..5f64cafb1 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -359,6 +359,9 @@ function Base.show(io::IO, t::FusionTree{I}) where {I <: Sector} end end +# blocks of products of at least two sectors, see `product_transform` +const ProductSectorBlock = FusionTreeBlock{<:ProductSector{<:Tuple{Any, Any, Vararg}}} + # Fusion tree iterators include("iterator.jl") @@ -367,6 +370,89 @@ include("basic_manipulations.jl") include("duality_manipulations.jl") include("braiding_manipulations.jl") +# Product sectors +# --------------- +# The fusion trees of a `ProductSector` are pairs of fusion trees of its first factor (head) and of +# its remaining factors (tail), such that the manipulations of a block are the Kronecker products +# of the manipulations of the head and tail blocks. + +headsector(s::ProductSector) = first(s.sectors) +tailsector(s::ProductSector{<:Tuple{Any, Any}}) = last(s.sectors) +tailsector(s::ProductSector) = ProductSector(Base.tail(s.sectors)) + +# the vertex labels of a product are the linear indices of the head and tail labels, tail first +vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) +vertexmultiplicities(f::FusionTree) = + ntuple(i -> Nsymbol(vertexlines(f)[i], f.uncoupled[i + 1], vertexlines(f)[i + 1]), length(f) - 1) + +maptree(proj, f::FusionTree, vertices) = FusionTree( + map(proj, f.uncoupled), proj(f.coupled), f.isdual, map(proj, f.innerlines), vertices +) +function headtail(f::FusionTree) + vertices_h = vertices_t = f.vertices + if FusionStyle(f) isa GenericFusion && length(f) > 1 + ns = vertexmultiplicities(maptree(tailsector, f, map(Returns(1), f.vertices))) + vertices_h = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) + vertices_t = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) + end + return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) +end +function ⊠(h::FusionTree, t::FusionTree) + vertices = h.vertices + if FusionStyle(h.coupled ⊠ t.coupled) isa GenericFusion && length(h) > 1 + vertices = map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) + end + return FusionTree( + map(⊠, h.uncoupled, t.uncoupled), h.coupled ⊠ t.coupled, h.isdual, + map(⊠, h.innerlines, t.innerlines), vertices + ) +end + +# the distinct tree pairs of one factor, and the position of every tree pair among them +function distincttrees(trees, othertrees) + FusionStyle(first(trees[1])) isa UniqueFusion && return trees[1:1], fill(1, length(trees)) + FusionStyle(first(othertrees[1])) isa UniqueFusion && return trees, eachindex(trees) + distinct = unique(trees) + return distinct, indexin(trees, distinct) +end + +# apply `transform` to the block `src` through its head and tail factors, with the resulting block +# in canonical order and the recoupling matrix with scalar type `T` +function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} + trees = map(fusiontrees(src)) do (f₁, f₂) + h₁, t₁ = headtail(f₁) + h₂, t₂ = headtail(f₂) + return (h₁, h₂), (t₁, t₂) + end + trees_h, trees_t = map(first, trees), map(last, trees) + src_h, cols_h = distincttrees(trees_h, trees_t) + src_t, cols_t = distincttrees(trees_t, trees_h) + dst_h, U_h = transform(FusionTreeBlock(src_h)) + dst_t, U_t = transform(FusionTreeBlock(src_t)) + + dst = FusionTreeBlock( + vec([(h₁ ⊠ t₁, h₂ ⊠ t₂) for (h₁, h₂) in fusiontrees(dst_h), (t₁, t₂) in fusiontrees(dst_t)]) + ) + U = Matrix{T}(undef, length(dst), length(src)) + for (col, i, j) in zip(axes(U, 2), cols_h, cols_t) + row = 0 + for jt in axes(U_t, 1), jh in axes(U_h, 1) + U[row += 1, col] = U_h[jh, i] * U_t[jt, j] + end + end + + canonical = FusionTreeBlock{I}(dst.uncoupled, dst.isdual; sizehint = length(dst)) + fusiontrees(canonical) == fusiontrees(dst) && return dst => U + # trees missing from `dst` (for incomplete `src`, as in `planar_trace`) get zero rows + indexmap = treeindex_map(dst) + U′ = zeros(T, length(canonical), length(src)) + for (row, f) in enumerate(fusiontrees(canonical)) + i = get(indexmap, treeindex_data(f), 0) + i == 0 || (U′[row, :] = view(U, i, :)) + end + return canonical => U′ +end + # auxiliary routines # _abelianinner: generate the inner indices for given outer indices in the abelian case _abelianinner(outer::Tuple{}) = () diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index 4fbe43e90..cbad27ed9 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -235,6 +235,26 @@ using TensorKitSectors end end + I <: ProductSector && FusionStyle(I) isa MultipleFusion && @testset "Double fusion tree: factorized product sector" begin + # the factorized manipulations agree with the manipulations of the full product sector + i0 = rand(1:(2N)) + p = mod1.(i0 .+ (1:(2N)), 2N) + p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) + n = rand(0:(2N)) + pt = (p′[1:n], p′[(2N):-1:(n + 1)]) + results = [(transpose(src, pt), TK.fstranspose((src, pt)))] + if BraidingStyle(I) isa HasBraiding + p = (randperm(2N)...,) + pb = (p[1:n], p[(n + 1):(2N)]) + levels = (ntuple(identity, N), ntuple(i -> i + N, N)) + push!(results, (TK.braid(src, pb, levels), TK.fsbraid((src, pb, levels)))) + end + for ((dst, U), (dst′, U′)) in results + @test fusiontrees(dst) == fusiontrees(dst′) + @test U ≈ U′ + end + end + @testset "Double fusion tree: planar trace" begin if FusionStyle(I) isa UniqueFusion f1, f1 = src From 5e28036891e45f05fe5b67ac1a9a061b31a27e8c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Fri, 23 Oct 2026 09:53:15 -0400 Subject: [PATCH 2/8] Use the Kronecker order as the canonical order of product sector blocks Construct a FusionTreeBlock of a ProductSector as the Kronecker product of its head and tail blocks, such that the factorized manipulations produce canonical blocks without reordering. Bound the tuple elements of the product signatures by Sector, which fixes dispatch to the two-factor tailsector method. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/fusiontrees.jl | 43 ++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 5f64cafb1..442c0be12 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -360,7 +360,7 @@ function Base.show(io::IO, t::FusionTree{I}) where {I <: Sector} end # blocks of products of at least two sectors, see `product_transform` -const ProductSectorBlock = FusionTreeBlock{<:ProductSector{<:Tuple{Any, Any, Vararg}}} +const ProductSectorBlock = FusionTreeBlock{<:ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}} # Fusion tree iterators include("iterator.jl") @@ -377,8 +377,11 @@ include("braiding_manipulations.jl") # of the manipulations of the head and tail blocks. headsector(s::ProductSector) = first(s.sectors) -tailsector(s::ProductSector{<:Tuple{Any, Any}}) = last(s.sectors) +tailsector(s::ProductSector{<:Tuple{Sector, Sector}}) = last(s.sectors) tailsector(s::ProductSector) = ProductSector(Base.tail(s.sectors)) +headsectortype(::Type{ProductSector{T}}) where {T} = fieldtype(T, 1) +tailsectortype(::Type{ProductSector{Tuple{H, T}}}) where {H <: Sector, T <: Sector} = T +tailsectortype(::Type{ProductSector{T}}) where {T} = ProductSector{Base.tuple_type_tail(T)} # the vertex labels of a product are the linear indices of the head and tail labels, tail first vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) @@ -408,6 +411,22 @@ function ⊠(h::FusionTree, t::FusionTree) ) end +# the canonical order of a product block is that of the Kronecker product of the head and tail +# blocks, with the head running fastest +productblock(head::FusionTreeBlock, tail::FusionTreeBlock) = FusionTreeBlock( + vec([(h₁ ⊠ t₁, h₂ ⊠ t₂) for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail)]) +) +function FusionTreeBlock{I}( + uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, + isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; + sizehint::Int = 0 + ) where {I <: ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}, N₁, N₂} + head = FusionTreeBlock{headsectortype(I)}(map.(headsector, uncoupled), isdual) + isempty(head) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) + tail = FusionTreeBlock{tailsectortype(I)}(map.(tailsector, uncoupled), isdual) + return productblock(head, tail) +end + # the distinct tree pairs of one factor, and the position of every tree pair among them function distincttrees(trees, othertrees) FusionStyle(first(trees[1])) isa UniqueFusion && return trees[1:1], fill(1, length(trees)) @@ -416,8 +435,8 @@ function distincttrees(trees, othertrees) return distinct, indexin(trees, distinct) end -# apply `transform` to the block `src` through its head and tail factors, with the resulting block -# in canonical order and the recoupling matrix with scalar type `T` +# apply `transform` to the block `src` through its head and tail factors, with the recoupling +# matrix with scalar type `T` function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} trees = map(fusiontrees(src)) do (f₁, f₂) h₁, t₁ = headtail(f₁) @@ -430,9 +449,7 @@ function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) whe dst_h, U_h = transform(FusionTreeBlock(src_h)) dst_t, U_t = transform(FusionTreeBlock(src_t)) - dst = FusionTreeBlock( - vec([(h₁ ⊠ t₁, h₂ ⊠ t₂) for (h₁, h₂) in fusiontrees(dst_h), (t₁, t₂) in fusiontrees(dst_t)]) - ) + dst = productblock(dst_h, dst_t) U = Matrix{T}(undef, length(dst), length(src)) for (col, i, j) in zip(axes(U, 2), cols_h, cols_t) row = 0 @@ -440,17 +457,7 @@ function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) whe U[row += 1, col] = U_h[jh, i] * U_t[jt, j] end end - - canonical = FusionTreeBlock{I}(dst.uncoupled, dst.isdual; sizehint = length(dst)) - fusiontrees(canonical) == fusiontrees(dst) && return dst => U - # trees missing from `dst` (for incomplete `src`, as in `planar_trace`) get zero rows - indexmap = treeindex_map(dst) - U′ = zeros(T, length(canonical), length(src)) - for (row, f) in enumerate(fusiontrees(canonical)) - i = get(indexmap, treeindex_data(f), 0) - i == 0 || (U′[row, :] = view(U, i, :)) - end - return canonical => U′ + return dst => U end # auxiliary routines From 361643b95ce19872bbac9fbcf0d6159a10b3c18d Mon Sep 17 00:00:00 2001 From: lkdvos Date: Fri, 23 Oct 2026 10:31:19 -0400 Subject: [PATCH 3/8] Split mixed product sectors into their UniqueFusion and other factors A product with both UniqueFusion and other factors is split into its other factors and its UniqueFusion factors, instead of its first and remaining factors. The UniqueFusion part has a single tree pair, such that block construction and manipulation take a single step instead of recursing through every factor. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/fusiontrees.jl | 90 +++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 442c0be12..eddade0d2 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -372,16 +372,62 @@ include("braiding_manipulations.jl") # Product sectors # --------------- -# The fusion trees of a `ProductSector` are pairs of fusion trees of its first factor (head) and of -# its remaining factors (tail), such that the manipulations of a block are the Kronecker products -# of the manipulations of the head and tail blocks. +# The fusion trees of a `ProductSector` are pairs of fusion trees of two parts of the product (head +# and tail), such that the manipulations of a block are the Kronecker products of the manipulations +# of the head and tail blocks. A product with both `UniqueFusion` and other factors is split into +# its other factors and its `UniqueFusion` factors, which have a single tree pair; any other product +# is split into its first and its remaining factors. + +firstfactor(s::ProductSector) = first(s.sectors) +otherfactors(s::ProductSector{<:Tuple{Sector, Sector}}) = last(s.sectors) +otherfactors(s::ProductSector) = ProductSector(Base.tail(s.sectors)) +firstfactortype(::Type{ProductSector{T}}) where {T} = fieldtype(T, 1) +otherfactorstype(::Type{ProductSector{Tuple{H, T}}}) where {H <: Sector, T <: Sector} = T +otherfactorstype(::Type{ProductSector{T}}) where {T} = ProductSector{Base.tuple_type_tail(T)} + +Base.@assume_effects :foldable function ismixedproduct(::Type{I}) where {I <: ProductSector} + isunique = map(S -> FusionStyle(S) isa UniqueFusion, TensorKitSectors._sectors(I)) + return any(isunique) && !all(isunique) +end + +# the non-`UniqueFusion` and the `UniqueFusion` factors, with `Trivial` for no factors +partitionfactors(s::Sector) = FusionStyle(s) isa UniqueFusion ? (Trivial(), s) : (s, Trivial()) +function partitionfactors(s::ProductSector) + g, a = partitionfactors(otherfactors(s)) + f = firstfactor(s) + return FusionStyle(f) isa UniqueFusion ? (g, f ⊠ a) : (f ⊠ g, a) +end +partitionfactortypes(::Type{I}) where {I <: Sector} = + FusionStyle(I) isa UniqueFusion ? (Trivial, I) : (I, Trivial) +function partitionfactortypes(::Type{I}) where {I <: ProductSector} + G, A = partitionfactortypes(otherfactorstype(I)) + F = firstfactortype(I) + return FusionStyle(F) isa UniqueFusion ? (G, F ⊠ A) : (F ⊠ G, A) +end +# the inverse of `partitionfactors` for the factors of `I` +popfactor(s::ProductSector) = firstfactor(s), otherfactors(s) +popfactor(s::Sector) = s, Trivial() +unpartition(::Type{I}, g, a) where {I <: Sector} = FusionStyle(I) isa UniqueFusion ? a : g +function unpartition(::Type{I}, g, a) where {I <: ProductSector} + if FusionStyle(firstfactortype(I)) isa UniqueFusion + f, a′ = popfactor(a) + return f ⊠ unpartition(otherfactorstype(I), g, a′) + else + f, g′ = popfactor(g) + return f ⊠ unpartition(otherfactorstype(I), g′, a) + end +end -headsector(s::ProductSector) = first(s.sectors) -tailsector(s::ProductSector{<:Tuple{Sector, Sector}}) = last(s.sectors) -tailsector(s::ProductSector) = ProductSector(Base.tail(s.sectors)) -headsectortype(::Type{ProductSector{T}}) where {T} = fieldtype(T, 1) -tailsectortype(::Type{ProductSector{Tuple{H, T}}}) where {H <: Sector, T <: Sector} = T -tailsectortype(::Type{ProductSector{T}}) where {T} = ProductSector{Base.tuple_type_tail(T)} +headsector(s::ProductSector) = + ismixedproduct(typeof(s)) ? first(partitionfactors(s)) : firstfactor(s) +tailsector(s::ProductSector) = + ismixedproduct(typeof(s)) ? last(partitionfactors(s)) : otherfactors(s) +headsectortype(::Type{I}) where {I <: ProductSector} = + ismixedproduct(I) ? first(partitionfactortypes(I)) : firstfactortype(I) +tailsectortype(::Type{I}) where {I <: ProductSector} = + ismixedproduct(I) ? last(partitionfactortypes(I)) : otherfactorstype(I) +joinsectors(::Type{I}, h, t) where {I <: ProductSector} = + ismixedproduct(I) ? unpartition(I, h, t) : h ⊠ t # the vertex labels of a product are the linear indices of the head and tail labels, tail first vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) @@ -400,21 +446,27 @@ function headtail(f::FusionTree) end return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) end -function ⊠(h::FusionTree, t::FusionTree) +function jointree(::Type{I}, h::FusionTree, t::FusionTree) where {I} vertices = h.vertices - if FusionStyle(h.coupled ⊠ t.coupled) isa GenericFusion && length(h) > 1 + if FusionStyle(I) isa GenericFusion && length(h) > 1 vertices = map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) end - return FusionTree( - map(⊠, h.uncoupled, t.uncoupled), h.coupled ⊠ t.coupled, h.isdual, - map(⊠, h.innerlines, t.innerlines), vertices + lift(a, b) = joinsectors(I, a, b) + return fusiontreetype(I, length(h))( + map(lift, h.uncoupled, t.uncoupled), lift(h.coupled, t.coupled), h.isdual, + map(lift, h.innerlines, t.innerlines), vertices ) end # the canonical order of a product block is that of the Kronecker product of the head and tail # blocks, with the head running fastest -productblock(head::FusionTreeBlock, tail::FusionTreeBlock) = FusionTreeBlock( - vec([(h₁ ⊠ t₁, h₂ ⊠ t₂) for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail)]) +productblock(::Type{I}, head::FusionTreeBlock, tail::FusionTreeBlock) where {I} = FusionTreeBlock( + vec( + [ + (jointree(I, h₁, t₁), jointree(I, h₂, t₂)) + for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail) + ] + ) ) function FusionTreeBlock{I}( uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, @@ -424,7 +476,7 @@ function FusionTreeBlock{I}( head = FusionTreeBlock{headsectortype(I)}(map.(headsector, uncoupled), isdual) isempty(head) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) tail = FusionTreeBlock{tailsectortype(I)}(map.(tailsector, uncoupled), isdual) - return productblock(head, tail) + return productblock(I, head, tail) end # the distinct tree pairs of one factor, and the position of every tree pair among them @@ -435,7 +487,7 @@ function distincttrees(trees, othertrees) return distinct, indexin(trees, distinct) end -# apply `transform` to the block `src` through its head and tail factors, with the recoupling +# apply `transform` to the block `src` through its head and tail, with the recoupling # matrix with scalar type `T` function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} trees = map(fusiontrees(src)) do (f₁, f₂) @@ -449,7 +501,7 @@ function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) whe dst_h, U_h = transform(FusionTreeBlock(src_h)) dst_t, U_t = transform(FusionTreeBlock(src_t)) - dst = productblock(dst_h, dst_t) + dst = productblock(I, dst_h, dst_t) U = Matrix{T}(undef, length(dst), length(src)) for (col, i, j) in zip(axes(U, 2), cols_h, cols_t) row = 0 From 125aa6fade26e8e43ed430598b4da5d5f91ba435 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sat, 24 Oct 2026 06:11:46 -0400 Subject: [PATCH 4/8] Move the product sector fusion tree code to its own file, and handle the UniqueFusion factors in one step Construct the UniqueFusion part of a mixed product block first, such that fusionblocks skips the non-abelian construction for blocks with mismatching abelian charges. Construct and manipulate blocks of UniqueFusion products, which hold a single tree pair, directly instead of recursing over their factors. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/braiding_manipulations.jl | 2 +- src/fusiontrees/duality_manipulations.jl | 2 +- src/fusiontrees/fusiontrees.jl | 155 ++-------------------- src/fusiontrees/producttrees.jl | 148 +++++++++++++++++++++ 4 files changed, 160 insertions(+), 147 deletions(-) create mode 100644 src/fusiontrees/producttrees.jl diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index fbad3a5d1..83aad9e0d 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -287,7 +287,7 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve return fsbraid((src, p, levels)) end function braid(src::ProductSectorBlock, p::Index2Tuple, levels::Index2Tuple) - isempty(src) && return fsbraid((src, p, levels)) + (isempty(src) || FusionStyle(src) isa UniqueFusion) && return fsbraid((src, p, levels)) return product_transform(sectorscalartype(sectortype(src)), src) do x return braid(x, p, levels) end diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index 0e9e83fdf..664ace7dc 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -527,7 +527,7 @@ function Base.transpose(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tu return fstranspose((src, p)) end function Base.transpose(src::ProductSectorBlock, p::Index2Tuple) - isempty(src) && return fstranspose((src, p)) + (isempty(src) || FusionStyle(src) isa UniqueFusion) && return fstranspose((src, p)) return product_transform(fusionscalartype(sectortype(src)), src) do x return transpose(x, p) end diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index eddade0d2..717f8b43c 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -127,6 +127,13 @@ function FusionTreeBlock{I}( isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; sizehint::Int = 0 ) where {I <: Sector, N₁, N₂} + return fusiontreeblock(I, uncoupled, isdual; sizehint) +end +# construct the block by enumerating its coupled sectors and fusion trees +function fusiontreeblock( + ::Type{I}, uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, + isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; sizehint::Int = 0 + ) where {I <: Sector, N₁, N₂} F = fusiontreetype(I, N₁, N₂) trees = Vector{F}(undef, 0) sizehint > 0 && sizehint!(trees, sizehint) @@ -359,159 +366,17 @@ function Base.show(io::IO, t::FusionTree{I}) where {I <: Sector} end end -# blocks of products of at least two sectors, see `product_transform` -const ProductSectorBlock = FusionTreeBlock{<:ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}} - # Fusion tree iterators include("iterator.jl") +# Fusion trees of product sectors +include("producttrees.jl") + # Manipulate fusion trees include("basic_manipulations.jl") include("duality_manipulations.jl") include("braiding_manipulations.jl") -# Product sectors -# --------------- -# The fusion trees of a `ProductSector` are pairs of fusion trees of two parts of the product (head -# and tail), such that the manipulations of a block are the Kronecker products of the manipulations -# of the head and tail blocks. A product with both `UniqueFusion` and other factors is split into -# its other factors and its `UniqueFusion` factors, which have a single tree pair; any other product -# is split into its first and its remaining factors. - -firstfactor(s::ProductSector) = first(s.sectors) -otherfactors(s::ProductSector{<:Tuple{Sector, Sector}}) = last(s.sectors) -otherfactors(s::ProductSector) = ProductSector(Base.tail(s.sectors)) -firstfactortype(::Type{ProductSector{T}}) where {T} = fieldtype(T, 1) -otherfactorstype(::Type{ProductSector{Tuple{H, T}}}) where {H <: Sector, T <: Sector} = T -otherfactorstype(::Type{ProductSector{T}}) where {T} = ProductSector{Base.tuple_type_tail(T)} - -Base.@assume_effects :foldable function ismixedproduct(::Type{I}) where {I <: ProductSector} - isunique = map(S -> FusionStyle(S) isa UniqueFusion, TensorKitSectors._sectors(I)) - return any(isunique) && !all(isunique) -end - -# the non-`UniqueFusion` and the `UniqueFusion` factors, with `Trivial` for no factors -partitionfactors(s::Sector) = FusionStyle(s) isa UniqueFusion ? (Trivial(), s) : (s, Trivial()) -function partitionfactors(s::ProductSector) - g, a = partitionfactors(otherfactors(s)) - f = firstfactor(s) - return FusionStyle(f) isa UniqueFusion ? (g, f ⊠ a) : (f ⊠ g, a) -end -partitionfactortypes(::Type{I}) where {I <: Sector} = - FusionStyle(I) isa UniqueFusion ? (Trivial, I) : (I, Trivial) -function partitionfactortypes(::Type{I}) where {I <: ProductSector} - G, A = partitionfactortypes(otherfactorstype(I)) - F = firstfactortype(I) - return FusionStyle(F) isa UniqueFusion ? (G, F ⊠ A) : (F ⊠ G, A) -end -# the inverse of `partitionfactors` for the factors of `I` -popfactor(s::ProductSector) = firstfactor(s), otherfactors(s) -popfactor(s::Sector) = s, Trivial() -unpartition(::Type{I}, g, a) where {I <: Sector} = FusionStyle(I) isa UniqueFusion ? a : g -function unpartition(::Type{I}, g, a) where {I <: ProductSector} - if FusionStyle(firstfactortype(I)) isa UniqueFusion - f, a′ = popfactor(a) - return f ⊠ unpartition(otherfactorstype(I), g, a′) - else - f, g′ = popfactor(g) - return f ⊠ unpartition(otherfactorstype(I), g′, a) - end -end - -headsector(s::ProductSector) = - ismixedproduct(typeof(s)) ? first(partitionfactors(s)) : firstfactor(s) -tailsector(s::ProductSector) = - ismixedproduct(typeof(s)) ? last(partitionfactors(s)) : otherfactors(s) -headsectortype(::Type{I}) where {I <: ProductSector} = - ismixedproduct(I) ? first(partitionfactortypes(I)) : firstfactortype(I) -tailsectortype(::Type{I}) where {I <: ProductSector} = - ismixedproduct(I) ? last(partitionfactortypes(I)) : otherfactorstype(I) -joinsectors(::Type{I}, h, t) where {I <: ProductSector} = - ismixedproduct(I) ? unpartition(I, h, t) : h ⊠ t - -# the vertex labels of a product are the linear indices of the head and tail labels, tail first -vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) -vertexmultiplicities(f::FusionTree) = - ntuple(i -> Nsymbol(vertexlines(f)[i], f.uncoupled[i + 1], vertexlines(f)[i + 1]), length(f) - 1) - -maptree(proj, f::FusionTree, vertices) = FusionTree( - map(proj, f.uncoupled), proj(f.coupled), f.isdual, map(proj, f.innerlines), vertices -) -function headtail(f::FusionTree) - vertices_h = vertices_t = f.vertices - if FusionStyle(f) isa GenericFusion && length(f) > 1 - ns = vertexmultiplicities(maptree(tailsector, f, map(Returns(1), f.vertices))) - vertices_h = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) - vertices_t = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) - end - return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) -end -function jointree(::Type{I}, h::FusionTree, t::FusionTree) where {I} - vertices = h.vertices - if FusionStyle(I) isa GenericFusion && length(h) > 1 - vertices = map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) - end - lift(a, b) = joinsectors(I, a, b) - return fusiontreetype(I, length(h))( - map(lift, h.uncoupled, t.uncoupled), lift(h.coupled, t.coupled), h.isdual, - map(lift, h.innerlines, t.innerlines), vertices - ) -end - -# the canonical order of a product block is that of the Kronecker product of the head and tail -# blocks, with the head running fastest -productblock(::Type{I}, head::FusionTreeBlock, tail::FusionTreeBlock) where {I} = FusionTreeBlock( - vec( - [ - (jointree(I, h₁, t₁), jointree(I, h₂, t₂)) - for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail) - ] - ) -) -function FusionTreeBlock{I}( - uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, - isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; - sizehint::Int = 0 - ) where {I <: ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}, N₁, N₂} - head = FusionTreeBlock{headsectortype(I)}(map.(headsector, uncoupled), isdual) - isempty(head) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) - tail = FusionTreeBlock{tailsectortype(I)}(map.(tailsector, uncoupled), isdual) - return productblock(I, head, tail) -end - -# the distinct tree pairs of one factor, and the position of every tree pair among them -function distincttrees(trees, othertrees) - FusionStyle(first(trees[1])) isa UniqueFusion && return trees[1:1], fill(1, length(trees)) - FusionStyle(first(othertrees[1])) isa UniqueFusion && return trees, eachindex(trees) - distinct = unique(trees) - return distinct, indexin(trees, distinct) -end - -# apply `transform` to the block `src` through its head and tail, with the recoupling -# matrix with scalar type `T` -function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} - trees = map(fusiontrees(src)) do (f₁, f₂) - h₁, t₁ = headtail(f₁) - h₂, t₂ = headtail(f₂) - return (h₁, h₂), (t₁, t₂) - end - trees_h, trees_t = map(first, trees), map(last, trees) - src_h, cols_h = distincttrees(trees_h, trees_t) - src_t, cols_t = distincttrees(trees_t, trees_h) - dst_h, U_h = transform(FusionTreeBlock(src_h)) - dst_t, U_t = transform(FusionTreeBlock(src_t)) - - dst = productblock(I, dst_h, dst_t) - U = Matrix{T}(undef, length(dst), length(src)) - for (col, i, j) in zip(axes(U, 2), cols_h, cols_t) - row = 0 - for jt in axes(U_t, 1), jh in axes(U_h, 1) - U[row += 1, col] = U_h[jh, i] * U_t[jt, j] - end - end - return dst => U -end - # auxiliary routines # _abelianinner: generate the inner indices for given outer indices in the abelian case _abelianinner(outer::Tuple{}) = () diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl new file mode 100644 index 000000000..566d2caa5 --- /dev/null +++ b/src/fusiontrees/producttrees.jl @@ -0,0 +1,148 @@ +# Product sectors +# --------------- +# The fusion trees of a `ProductSector` are pairs of fusion trees of two parts of the product (head +# and tail), such that the manipulations of a block are the Kronecker products of the manipulations +# of the head and tail blocks. A product with both `UniqueFusion` and other factors is split into +# its other factors and its `UniqueFusion` factors, which have a single tree pair; any other product +# is split into its first and its remaining factors. + +# blocks of products of at least two sectors +const ProductSectorBlock = FusionTreeBlock{<:ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}} + +firstfactor(s::ProductSector) = first(s.sectors) +otherfactors(s::ProductSector{<:Tuple{Sector, Sector}}) = last(s.sectors) +otherfactors(s::ProductSector) = ProductSector(Base.tail(s.sectors)) +firstfactortype(::Type{ProductSector{T}}) where {T} = fieldtype(T, 1) +otherfactorstype(::Type{ProductSector{Tuple{H, T}}}) where {H <: Sector, T <: Sector} = T +otherfactorstype(::Type{ProductSector{T}}) where {T} = ProductSector{Base.tuple_type_tail(T)} + +Base.@assume_effects :foldable function ismixedproduct(::Type{I}) where {I <: ProductSector} + isunique = map(S -> FusionStyle(S) isa UniqueFusion, TensorKitSectors._sectors(I)) + return any(isunique) && !all(isunique) +end + +# the non-`UniqueFusion` and the `UniqueFusion` factors, with `Trivial` for no factors +partitionfactors(s::Sector) = FusionStyle(s) isa UniqueFusion ? (Trivial(), s) : (s, Trivial()) +function partitionfactors(s::ProductSector) + g, a = partitionfactors(otherfactors(s)) + f = firstfactor(s) + return FusionStyle(f) isa UniqueFusion ? (g, f ⊠ a) : (f ⊠ g, a) +end +partitionfactortypes(::Type{I}) where {I <: Sector} = + FusionStyle(I) isa UniqueFusion ? (Trivial, I) : (I, Trivial) +function partitionfactortypes(::Type{I}) where {I <: ProductSector} + G, A = partitionfactortypes(otherfactorstype(I)) + F = firstfactortype(I) + return FusionStyle(F) isa UniqueFusion ? (G, F ⊠ A) : (F ⊠ G, A) +end +# the inverse of `partitionfactors` for the factors of `I` +factortuple(s::ProductSector) = s.sectors +factortuple(::Trivial) = () +factortuple(s::Sector) = (s,) +interleave(::Type{Tuple{}}, gs, as) = () +function interleave(::Type{T}, gs, as) where {T <: Tuple} + return if FusionStyle(fieldtype(T, 1)) isa UniqueFusion + (first(as), interleave(Base.tuple_type_tail(T), gs, Base.tail(as))...) + else + (first(gs), interleave(Base.tuple_type_tail(T), Base.tail(gs), as)...) + end +end +unpartition(::Type{ProductSector{T}}, g, a) where {T} = + ProductSector{T}(interleave(T, factortuple(g), factortuple(a))) + +headsector(s::ProductSector) = + ismixedproduct(typeof(s)) ? first(partitionfactors(s)) : firstfactor(s) +tailsector(s::ProductSector) = + ismixedproduct(typeof(s)) ? last(partitionfactors(s)) : otherfactors(s) +headsectortype(::Type{I}) where {I <: ProductSector} = + ismixedproduct(I) ? first(partitionfactortypes(I)) : firstfactortype(I) +tailsectortype(::Type{I}) where {I <: ProductSector} = + ismixedproduct(I) ? last(partitionfactortypes(I)) : otherfactorstype(I) +joinsectors(::Type{I}, h, t) where {I <: ProductSector} = + ismixedproduct(I) ? unpartition(I, h, t) : h ⊠ t + +# the vertex labels of a product are the linear indices of the head and tail labels, tail first +vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) +vertexmultiplicities(f::FusionTree) = + ntuple(i -> Nsymbol(vertexlines(f)[i], f.uncoupled[i + 1], vertexlines(f)[i + 1]), length(f) - 1) + +maptree(proj, f::FusionTree, vertices) = FusionTree( + map(proj, f.uncoupled), proj(f.coupled), f.isdual, map(proj, f.innerlines), vertices +) +function headtail(f::FusionTree) + vertices_h = vertices_t = f.vertices + if FusionStyle(f) isa GenericFusion && length(f) > 1 + ns = vertexmultiplicities(maptree(tailsector, f, map(Returns(1), f.vertices))) + vertices_h = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) + vertices_t = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) + end + return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) +end +function jointree(::Type{I}, h::FusionTree, t::FusionTree) where {I} + vertices = h.vertices + if FusionStyle(I) isa GenericFusion && length(h) > 1 + vertices = map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) + end + lift(a, b) = joinsectors(I, a, b) + return fusiontreetype(I, length(h))( + map(lift, h.uncoupled, t.uncoupled), lift(h.coupled, t.coupled), h.isdual, + map(lift, h.innerlines, t.innerlines), vertices + ) +end + +# the canonical order of a product block is that of the Kronecker product of the head and tail +# blocks, with the head running fastest +productblock(::Type{I}, head::FusionTreeBlock, tail::FusionTreeBlock) where {I} = FusionTreeBlock( + vec( + [ + (jointree(I, h₁, t₁), jointree(I, h₂, t₂)) + for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail) + ] + ) +) +function FusionTreeBlock{I}( + uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, + isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; + sizehint::Int = 0 + ) where {I <: ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}, N₁, N₂} + # a block of `UniqueFusion` sectors has a single tree pair, in any order + FusionStyle(I) isa UniqueFusion && return fusiontreeblock(I, uncoupled, isdual; sizehint) + # the tail holds the `UniqueFusion` factors of a mixed product, which are cheaper to construct + tail = FusionTreeBlock{tailsectortype(I)}(map.(tailsector, uncoupled), isdual) + isempty(tail) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) + head = FusionTreeBlock{headsectortype(I)}(map.(headsector, uncoupled), isdual) + return productblock(I, head, tail) +end + +# the distinct tree pairs of one factor, and the position of every tree pair among them +function distincttrees(trees, othertrees) + FusionStyle(first(trees[1])) isa UniqueFusion && return trees[1:1], fill(1, length(trees)) + FusionStyle(first(othertrees[1])) isa UniqueFusion && return trees, eachindex(trees) + distinct = unique(trees) + return distinct, indexin(trees, distinct) +end + +# apply `transform` to the block `src` through its head and tail, with the recoupling +# matrix with scalar type `T` +function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} + trees = map(fusiontrees(src)) do (f₁, f₂) + h₁, t₁ = headtail(f₁) + h₂, t₂ = headtail(f₂) + return (h₁, h₂), (t₁, t₂) + end + trees_h, trees_t = map(first, trees), map(last, trees) + src_h, cols_h = distincttrees(trees_h, trees_t) + src_t, cols_t = distincttrees(trees_t, trees_h) + dst_h, U_h = transform(FusionTreeBlock(src_h)) + dst_t, U_t = transform(FusionTreeBlock(src_t)) + + dst = productblock(I, dst_h, dst_t) + U = Matrix{T}(undef, length(dst), length(src)) + for (col, i, j) in zip(axes(U, 2), cols_h, cols_t) + row = 0 + for jt in axes(U_t, 1), jh in axes(U_h, 1) + U[row += 1, col] = U_h[jh, i] * U_t[jt, j] + end + end + return dst => U +end From fdf12178328d710ee60b2502088ceb7c3675b6ae Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sat, 24 Oct 2026 06:57:37 -0400 Subject: [PATCH 5/8] Handle UniqueFusion parts in product_transform, and support named product sectors Transform the single tree pair of the UniqueFusion part of a mixed product, and every tree pair of a UniqueFusion product, directly in product_transform, instead of special-casing UniqueFusion blocks in fsbraid and fstranspose. Split and join NamedSector products through the corresponding ProductSector. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/braiding_manipulations.jl | 6 +- src/fusiontrees/duality_manipulations.jl | 6 +- src/fusiontrees/producttrees.jl | 68 ++++++++++++++++------- test/symmetries/doubletree.jl | 17 +++++- 4 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 83aad9e0d..1cc514387 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -287,7 +287,7 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve return fsbraid((src, p, levels)) end function braid(src::ProductSectorBlock, p::Index2Tuple, levels::Index2Tuple) - (isempty(src) || FusionStyle(src) isa UniqueFusion) && return fsbraid((src, p, levels)) + isempty(src) && return fsbraid((src, p, levels)) return product_transform(sectorscalartype(sectortype(src)), src) do x return braid(x, p, levels) end @@ -318,10 +318,6 @@ end return (f₁′, f₂′) => coeff1 * coeff2 * coeff3 else src, (p1, p2), (l1, l2) = key - if FusionStyle(I) isa UniqueFusion && length(src) == 1 # a single tree pair - f, coeff = fsbraid((only(fusiontrees(src)), (p1, p2), (l1, l2))) - return FusionTreeBlock([f]) => fill(coeff, 1, 1) - end p = linearizepermutation(p1, p2, numout(src), numin(src)) levels = (l1..., reverse(l2)...) diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index 664ace7dc..88826135e 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -527,7 +527,7 @@ function Base.transpose(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tu return fstranspose((src, p)) end function Base.transpose(src::ProductSectorBlock, p::Index2Tuple) - (isempty(src) || FusionStyle(src) isa UniqueFusion) && return fstranspose((src, p)) + isempty(src) && return fstranspose((src, p)) return product_transform(fusionscalartype(sectortype(src)), src) do x return transpose(x, p) end @@ -549,10 +549,6 @@ end @cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: Union{FSPTransposeKey{I, N₁, N₂}, FSBTransposeKey{I, N₁, N₂}}} src, (p1, p2) = key - if K <: FSBTransposeKey && FusionStyle(I) isa UniqueFusion && length(src) == 1 # a single tree pair - f, coeff = fstranspose((only(fusiontrees(src)), (p1, p2))) - return FusionTreeBlock([f]) => fill(coeff, 1, 1) - end N = N₁ + N₂ p = linearizepermutation(p1, p2, numout(src), numin(src)) diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl index 566d2caa5..3b0d6c0ea 100644 --- a/src/fusiontrees/producttrees.jl +++ b/src/fusiontrees/producttrees.jl @@ -1,13 +1,16 @@ # Product sectors # --------------- -# The fusion trees of a `ProductSector` are pairs of fusion trees of two parts of the product (head -# and tail), such that the manipulations of a block are the Kronecker products of the manipulations -# of the head and tail blocks. A product with both `UniqueFusion` and other factors is split into -# its other factors and its `UniqueFusion` factors, which have a single tree pair; any other product -# is split into its first and its remaining factors. +# FusionTree manipulations for `ProductSector` are done factor-by-factor: transformation matrices are +# computed by kronecker product of the individual ones. +# For efficiency we separate all `UniqueFusion` factors and handle these together, while handling the +# rest one-by-one. -# blocks of products of at least two sectors -const ProductSectorBlock = FusionTreeBlock{<:ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}} +# products of at least two sectors, also with named factors +const MultiSectorTuple = Tuple{Sector, Sector, Vararg{Sector}} +const ProductSectorType = Union{ + ProductSector{<:MultiSectorTuple}, NamedSector{<:NamedTuple{<:Any, <:MultiSectorTuple}}, +} +const ProductSectorBlock = FusionTreeBlock{<:ProductSectorType} firstfactor(s::ProductSector) = first(s.sectors) otherfactors(s::ProductSector{<:Tuple{Sector, Sector}}) = last(s.sectors) @@ -61,6 +64,14 @@ tailsectortype(::Type{I}) where {I <: ProductSector} = joinsectors(::Type{I}, h, t) where {I <: ProductSector} = ismixedproduct(I) ? unpartition(I, h, t) : h ⊠ t +# named products are split and joined as the corresponding `ProductSector` +productsectortype(::Type{NamedSector{NamedTuple{K, T}}}) where {K, T} = ProductSector{T} +headsector(s::NamedSector) = headsector(ProductSector(s)) +tailsector(s::NamedSector) = tailsector(ProductSector(s)) +headsectortype(::Type{I}) where {I <: NamedSector} = headsectortype(productsectortype(I)) +tailsectortype(::Type{I}) where {I <: NamedSector} = tailsectortype(productsectortype(I)) +joinsectors(::Type{I}, h, t) where {I <: NamedSector} = I(joinsectors(productsectortype(I), h, t)) + # the vertex labels of a product are the linear indices of the head and tail labels, tail first vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) vertexmultiplicities(f::FusionTree) = @@ -104,7 +115,7 @@ function FusionTreeBlock{I}( uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; sizehint::Int = 0 - ) where {I <: ProductSector{<:Tuple{Sector, Sector, Vararg{Sector}}}, N₁, N₂} + ) where {I <: ProductSectorType, N₁, N₂} # a block of `UniqueFusion` sectors has a single tree pair, in any order FusionStyle(I) isa UniqueFusion && return fusiontreeblock(I, uncoupled, isdual; sizehint) # the tail holds the `UniqueFusion` factors of a mixed product, which are cheaper to construct @@ -114,27 +125,42 @@ function FusionTreeBlock{I}( return productblock(I, head, tail) end -# the distinct tree pairs of one factor, and the position of every tree pair among them -function distincttrees(trees, othertrees) - FusionStyle(first(trees[1])) isa UniqueFusion && return trees[1:1], fill(1, length(trees)) - FusionStyle(first(othertrees[1])) isa UniqueFusion && return trees, eachindex(trees) +# the distinct tree pairs, and the position of every tree pair among them +function distincttrees(trees) distinct = unique(trees) return distinct, indexin(trees, distinct) end +headtrees((f₁, f₂)) = (first(headtail(f₁)), first(headtail(f₂))) +tailtrees((f₁, f₂)) = (last(headtail(f₁)), last(headtail(f₂))) + +# apply `transform` to the block `src`, where every tree pair of a `UniqueFusion` block maps to a +# single tree pair +function factor_transform(transform::F, ::Type{T}, src::FusionTreeBlock) where {F, T} + FusionStyle(src) isa UniqueFusion || return transform(src) + dst = map(transform, fusiontrees(src)) + return FusionTreeBlock(map(first, dst)) => LinearAlgebra.diagm(T[last(d) for d in dst]) +end # apply `transform` to the block `src` through its head and tail, with the recoupling # matrix with scalar type `T` function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} - trees = map(fusiontrees(src)) do (f₁, f₂) - h₁, t₁ = headtail(f₁) - h₂, t₂ = headtail(f₂) - return (h₁, h₂), (t₁, t₂) + if FusionStyle(I) isa UniqueFusion + return factor_transform(transform, T, src) + elseif FusionStyle(tailsectortype(I)) isa UniqueFusion && numind(src) > 0 + # the tail holds the `UniqueFusion` factors of a mixed product, with a single tree pair + (t₁, t₂), coeff = transform(tailtrees(first(fusiontrees(src)))) + dst_h, U_h = transform(FusionTreeBlock(map(headtrees, fusiontrees(src)))) + dst = FusionTreeBlock( + map(((h₁, h₂),) -> (jointree(I, h₁, t₁), jointree(I, h₂, t₂)), fusiontrees(dst_h)) + ) + U = Matrix{T}(undef, size(U_h)) + U .= coeff .* U_h + return dst => U end - trees_h, trees_t = map(first, trees), map(last, trees) - src_h, cols_h = distincttrees(trees_h, trees_t) - src_t, cols_t = distincttrees(trees_t, trees_h) - dst_h, U_h = transform(FusionTreeBlock(src_h)) - dst_t, U_t = transform(FusionTreeBlock(src_t)) + src_h, cols_h = distincttrees(map(headtrees, fusiontrees(src))) + src_t, cols_t = distincttrees(map(tailtrees, fusiontrees(src))) + dst_h, U_h = factor_transform(transform, T, FusionTreeBlock(src_h)) + dst_t, U_t = factor_transform(transform, T, FusionTreeBlock(src_t)) dst = productblock(I, dst_h, dst_t) U = Matrix{T}(undef, length(dst), length(src)) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index cbad27ed9..07df0a7cf 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -242,16 +242,27 @@ using TensorKitSectors p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) n = rand(0:(2N)) pt = (p′[1:n], p′[(2N):-1:(n + 1)]) - results = [(transpose(src, pt), TK.fstranspose((src, pt)))] + manipulations = Any[(x -> transpose(x, pt), x -> TK.fstranspose((x, pt)))] if BraidingStyle(I) isa HasBraiding p = (randperm(2N)...,) pb = (p[1:n], p[(n + 1):(2N)]) levels = (ntuple(identity, N), ntuple(i -> i + N, N)) - push!(results, (TK.braid(src, pb, levels), TK.fsbraid((src, pb, levels)))) + push!(manipulations, (x -> TK.braid(x, pb, levels), x -> TK.fsbraid((x, pb, levels)))) end - for ((dst, U), (dst′, U′)) in results + # the same block with named factors + T = I.parameters[1] + Iₙ = NamedSector{NamedTuple{ntuple(i -> Symbol(:s, i), fieldcount(T)), T}} + nametree(f) = FusionTree(map(Iₙ, f.uncoupled), Iₙ(f.coupled), f.isdual, map(Iₙ, f.innerlines), f.vertices) + nametrees(block) = map(((f₁, f₂),) -> (nametree(f₁), nametree(f₂)), fusiontrees(block)) + srcₙ = FusionTreeBlock(nametrees(src)) + for (manipulation, unfactorized) in manipulations + dst, U = manipulation(src) + dst′, U′ = unfactorized(src) @test fusiontrees(dst) == fusiontrees(dst′) @test U ≈ U′ + dstₙ, Uₙ = manipulation(srcₙ) + @test fusiontrees(dstₙ) == nametrees(dst) + @test Uₙ ≈ U end end From a8989da1c84d0a9ea85acf7b764cd9bb68a5a87c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 26 Oct 2026 04:11:18 -0400 Subject: [PATCH 6/8] Test named product sectors through the generic tests instead of a factorized testset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `FermionParity ⊠ U1Irrep ⊠ SU2Irrep` in the sector list by a named Hubbard-like product sector, and drop the testset comparing with the unfactorized manipulations. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/producttrees.jl | 38 ++++++++++++++------------------- test/setup.jl | 3 ++- test/symmetries/doubletree.jl | 33 +--------------------------- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl index 3b0d6c0ea..c56d35aca 100644 --- a/src/fusiontrees/producttrees.jl +++ b/src/fusiontrees/producttrees.jl @@ -53,16 +53,11 @@ end unpartition(::Type{ProductSector{T}}, g, a) where {T} = ProductSector{T}(interleave(T, factortuple(g), factortuple(a))) -headsector(s::ProductSector) = - ismixedproduct(typeof(s)) ? first(partitionfactors(s)) : firstfactor(s) -tailsector(s::ProductSector) = - ismixedproduct(typeof(s)) ? last(partitionfactors(s)) : otherfactors(s) -headsectortype(::Type{I}) where {I <: ProductSector} = - ismixedproduct(I) ? first(partitionfactortypes(I)) : firstfactortype(I) -tailsectortype(::Type{I}) where {I <: ProductSector} = - ismixedproduct(I) ? last(partitionfactortypes(I)) : otherfactorstype(I) -joinsectors(::Type{I}, h, t) where {I <: ProductSector} = - ismixedproduct(I) ? unpartition(I, h, t) : h ⊠ t +headsector(s::ProductSector) = ismixedproduct(typeof(s)) ? first(partitionfactors(s)) : firstfactor(s) +tailsector(s::ProductSector) = ismixedproduct(typeof(s)) ? last(partitionfactors(s)) : otherfactors(s) +headsectortype(::Type{I}) where {I <: ProductSector} = ismixedproduct(I) ? first(partitionfactortypes(I)) : firstfactortype(I) +tailsectortype(::Type{I}) where {I <: ProductSector} = ismixedproduct(I) ? last(partitionfactortypes(I)) : otherfactorstype(I) +joinsectors(::Type{I}, h, t) where {I <: ProductSector} = ismixedproduct(I) ? unpartition(I, h, t) : h ⊠ t # named products are split and joined as the corresponding `ProductSector` productsectortype(::Type{NamedSector{NamedTuple{K, T}}}) where {K, T} = ProductSector{T} @@ -90,9 +85,10 @@ function headtail(f::FusionTree) return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) end function jointree(::Type{I}, h::FusionTree, t::FusionTree) where {I} - vertices = h.vertices - if FusionStyle(I) isa GenericFusion && length(h) > 1 + vertices = if FusionStyle(I) isa GenericFusion && length(h) > 1 vertices = map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) + else + h.vertices end lift(a, b) = joinsectors(I, a, b) return fusiontreetype(I, length(h))( @@ -103,14 +99,11 @@ end # the canonical order of a product block is that of the Kronecker product of the head and tail # blocks, with the head running fastest -productblock(::Type{I}, head::FusionTreeBlock, tail::FusionTreeBlock) where {I} = FusionTreeBlock( - vec( - [ - (jointree(I, h₁, t₁), jointree(I, h₂, t₂)) - for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail) - ] - ) -) +function productblock(::Type{I}, head::FusionTreeBlock, tail::FusionTreeBlock) where {I} + trees = [(jointree(I, h₁, t₁), jointree(I, h₂, t₂)) for (h₁, h₂) in fusiontrees(head), (t₁, t₂) in fusiontrees(tail)] + return FusionTreeBlock(vec(trees)) +end + function FusionTreeBlock{I}( uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; @@ -118,9 +111,11 @@ function FusionTreeBlock{I}( ) where {I <: ProductSectorType, N₁, N₂} # a block of `UniqueFusion` sectors has a single tree pair, in any order FusionStyle(I) isa UniqueFusion && return fusiontreeblock(I, uncoupled, isdual; sizehint) + # the tail holds the `UniqueFusion` factors of a mixed product, which are cheaper to construct tail = FusionTreeBlock{tailsectortype(I)}(map.(tailsector, uncoupled), isdual) isempty(tail) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) + head = FusionTreeBlock{headsectortype(I)}(map.(headsector, uncoupled), isdual) return productblock(I, head, tail) end @@ -141,8 +136,7 @@ function factor_transform(transform::F, ::Type{T}, src::FusionTreeBlock) where { return FusionTreeBlock(map(first, dst)) => LinearAlgebra.diagm(T[last(d) for d in dst]) end -# apply `transform` to the block `src` through its head and tail, with the recoupling -# matrix with scalar type `T` +# apply `transform` to the block `src` through its head and tail, with the recoupling matrix with scalar type `T` function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} if FusionStyle(I) isa UniqueFusion return factor_transform(transform, T, src) diff --git a/test/setup.jl b/test/setup.jl index 2030228a5..ef8f1896f 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -158,7 +158,8 @@ uniquefusionsectorlist = ( ) simplefusionsectorlist = ( CU1Irrep, SU2Irrep, FibonacciAnyon, IsingAnyon, - FermionParity ⊠ U1Irrep ⊠ SU2Irrep, FermionParity ⊠ SU2Irrep ⊠ SU2Irrep, + @NamedSector{f::FermionParity, s::SU2Irrep, c::U1Irrep}, # named Hubbard-like + FermionParity ⊠ SU2Irrep ⊠ SU2Irrep, ZNElement{6, 3} ⊠ SU2Irrep, # complex F symbols, anyonic braiding Z3Element{1} ⊠ FibonacciAnyon ⊠ FibonacciAnyon, # complex F symbols, no braiding ) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index 07df0a7cf..56b930b43 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -12,7 +12,7 @@ using TensorKitSectors @timedtestset "Double fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) Istr = TensorKit.type_repr(I) - N = I <: ProductSector ? 3 : 4 + N = I <: Union{ProductSector, NamedSector} ? 3 : 4 if UnitStyle(I) isa SimpleUnit out = random_fusion(I, Val(N)) @@ -235,37 +235,6 @@ using TensorKitSectors end end - I <: ProductSector && FusionStyle(I) isa MultipleFusion && @testset "Double fusion tree: factorized product sector" begin - # the factorized manipulations agree with the manipulations of the full product sector - i0 = rand(1:(2N)) - p = mod1.(i0 .+ (1:(2N)), 2N) - p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) - n = rand(0:(2N)) - pt = (p′[1:n], p′[(2N):-1:(n + 1)]) - manipulations = Any[(x -> transpose(x, pt), x -> TK.fstranspose((x, pt)))] - if BraidingStyle(I) isa HasBraiding - p = (randperm(2N)...,) - pb = (p[1:n], p[(n + 1):(2N)]) - levels = (ntuple(identity, N), ntuple(i -> i + N, N)) - push!(manipulations, (x -> TK.braid(x, pb, levels), x -> TK.fsbraid((x, pb, levels)))) - end - # the same block with named factors - T = I.parameters[1] - Iₙ = NamedSector{NamedTuple{ntuple(i -> Symbol(:s, i), fieldcount(T)), T}} - nametree(f) = FusionTree(map(Iₙ, f.uncoupled), Iₙ(f.coupled), f.isdual, map(Iₙ, f.innerlines), f.vertices) - nametrees(block) = map(((f₁, f₂),) -> (nametree(f₁), nametree(f₂)), fusiontrees(block)) - srcₙ = FusionTreeBlock(nametrees(src)) - for (manipulation, unfactorized) in manipulations - dst, U = manipulation(src) - dst′, U′ = unfactorized(src) - @test fusiontrees(dst) == fusiontrees(dst′) - @test U ≈ U′ - dstₙ, Uₙ = manipulation(srcₙ) - @test fusiontrees(dstₙ) == nametrees(dst) - @test Uₙ ≈ U - end - end - @testset "Double fusion tree: planar trace" begin if FusionStyle(I) isa UniqueFusion f1, f1 = src From 65a061cb1855d8d5287128b02085292a83be2522 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 26 Oct 2026 04:45:31 -0400 Subject: [PATCH 7/8] Skip splitting the vertex labels for UniqueFusion tails, and add a changelog entry Also drop a redundant assignment in `jointree`, avoid the internal `TensorKitSectors._sectors`, and fix two comments. Co-Authored-By: Claude Opus 5.5 --- docs/src/Changelog.md | 1 + src/fusiontrees/producttrees.jl | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index 2a853124a..27cb8b389 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. +- Fusion tree block manipulations of (named) product sectors are computed factor by factor, as Kronecker products of the manipulations of the `UniqueFusion` and the other factors, such that they share the caches of the individual factors ([#592](https://github.com/QuantumKitHub/TensorKit.jl/pull/592)) ## [0.17.2](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.1...v0.17.2) - 2026-09-20 diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl index c56d35aca..bfbf75992 100644 --- a/src/fusiontrees/producttrees.jl +++ b/src/fusiontrees/producttrees.jl @@ -19,8 +19,8 @@ firstfactortype(::Type{ProductSector{T}}) where {T} = fieldtype(T, 1) otherfactorstype(::Type{ProductSector{Tuple{H, T}}}) where {H <: Sector, T <: Sector} = T otherfactorstype(::Type{ProductSector{T}}) where {T} = ProductSector{Base.tuple_type_tail(T)} -Base.@assume_effects :foldable function ismixedproduct(::Type{I}) where {I <: ProductSector} - isunique = map(S -> FusionStyle(S) isa UniqueFusion, TensorKitSectors._sectors(I)) +Base.@assume_effects :foldable function ismixedproduct(::Type{ProductSector{T}}) where {T} + isunique = map(S -> FusionStyle(S) isa UniqueFusion, fieldtypes(T)) return any(isunique) && !all(isunique) end @@ -67,7 +67,8 @@ headsectortype(::Type{I}) where {I <: NamedSector} = headsectortype(productsecto tailsectortype(::Type{I}) where {I <: NamedSector} = tailsectortype(productsectortype(I)) joinsectors(::Type{I}, h, t) where {I <: NamedSector} = I(joinsectors(productsectortype(I), h, t)) -# the vertex labels of a product are the linear indices of the head and tail labels, tail first +# the vertex labels of a product are the linear indices of the head and tail labels, with the tail +# running fastest, such that a `UniqueFusion` tail leaves the labels of the head unchanged vertexlines(f::FusionTree) = (f.uncoupled[1], f.innerlines..., f.coupled) vertexmultiplicities(f::FusionTree) = ntuple(i -> Nsymbol(vertexlines(f)[i], f.uncoupled[i + 1], vertexlines(f)[i + 1]), length(f) - 1) @@ -77,7 +78,9 @@ maptree(proj, f::FusionTree, vertices) = FusionTree( ) function headtail(f::FusionTree) vertices_h = vertices_t = f.vertices - if FusionStyle(f) isa GenericFusion && length(f) > 1 + if FusionStyle(tailsectortype(sectortype(f))) isa UniqueFusion + vertices_t = map(Returns(1), f.vertices) + elseif FusionStyle(f) isa GenericFusion && length(f) > 1 ns = vertexmultiplicities(maptree(tailsector, f, map(Returns(1), f.vertices))) vertices_h = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) vertices_t = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) @@ -85,8 +88,8 @@ function headtail(f::FusionTree) return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) end function jointree(::Type{I}, h::FusionTree, t::FusionTree) where {I} - vertices = if FusionStyle(I) isa GenericFusion && length(h) > 1 - vertices = map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) + vertices = if FusionStyle(I) isa GenericFusion && length(h) > 1 && !(FusionStyle(t) isa UniqueFusion) + map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) else h.vertices end @@ -109,7 +112,7 @@ function FusionTreeBlock{I}( isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; sizehint::Int = 0 ) where {I <: ProductSectorType, N₁, N₂} - # a block of `UniqueFusion` sectors has a single tree pair, in any order + # blocks of `UniqueFusion` sectors are enumerated directly FusionStyle(I) isa UniqueFusion && return fusiontreeblock(I, uncoupled, isdual; sizehint) # the tail holds the `UniqueFusion` factors of a mixed product, which are cheaper to construct From 4f37082d848a0971a71f4a1fc9426d4ff86d7ab9 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 26 Oct 2026 19:51:33 -0400 Subject: [PATCH 8/8] Fix inference of nested product manipulations on Julia 1.10, and mark product_transform inactive for Enzyme Co-Authored-By: Claude Opus 5.5 --- ext/TensorKitEnzymeExt/utility.jl | 1 + src/fusiontrees/braiding_manipulations.jl | 3 ++- src/fusiontrees/duality_manipulations.jl | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ext/TensorKitEnzymeExt/utility.jl b/ext/TensorKitEnzymeExt/utility.jl index 0994bd850..bb9a21db0 100644 --- a/ext/TensorKitEnzymeExt/utility.jl +++ b/ext/TensorKitEnzymeExt/utility.jl @@ -147,6 +147,7 @@ end @inline EnzymeRules.inactive(::typeof(TensorKit.artin_braid), ::Any, ::Any) = nothing @inline EnzymeRules.inactive(::typeof(TensorKit.fstranspose), ::Any) = nothing @inline EnzymeRules.inactive(::typeof(TensorKit.fstranspose), ::Any, ::Any) = nothing +@inline EnzymeRules.inactive(::typeof(TensorKit.product_transform), ::Any...) = nothing @inline EnzymeRules.inactive(::typeof(TensorKit.treetransposer), ::Any...) = nothing @inline EnzymeRules.inactive(::typeof(TensorKit.treebraider), ::Any...) = nothing @inline EnzymeRules.inactive(::typeof(TensorKit.insertleftunit), ::HomSpace, ::Any) = nothing diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 1cc514387..0ad6353ed 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -288,9 +288,10 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve end function braid(src::ProductSectorBlock, p::Index2Tuple, levels::Index2Tuple) isempty(src) && return fsbraid((src, p, levels)) + # the return type is asserted since inference gives up on the recursion on older Julia versions return product_transform(sectorscalartype(sectortype(src)), src) do x return braid(x, p, levels) - end + end::_fsdicttype(typeof((src, p, levels))) end const FSPBraidKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁, N₂}, Index2Tuple} diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index 88826135e..cae44aca3 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -528,9 +528,10 @@ function Base.transpose(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tu end function Base.transpose(src::ProductSectorBlock, p::Index2Tuple) isempty(src) && return fstranspose((src, p)) + # the return type is asserted since inference gives up on the recursion on older Julia versions return product_transform(fusionscalartype(sectortype(src)), src) do x return transpose(x, p) - end + end::_fsdicttype(typeof((src, p))) end const FSPTransposeKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁, N₂}}