From 525aa0c6333e349f4711b272f8bd9fd117b373e5 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 22 Oct 2026 17:04:11 -0400 Subject: [PATCH 01/12] 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 40a2961b6..cdef3eedb 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -376,6 +376,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") @@ -384,6 +387,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 180e19bdc..62ed9e3f3 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -248,6 +248,26 @@ using TensorKitSectors @test allunique(map(block -> block.uncoupled, blocks)) 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 afd7898370cefd6a0d56371301839edc7c00b64d Mon Sep 17 00:00:00 2001 From: lkdvos Date: Fri, 23 Oct 2026 09:53:15 -0400 Subject: [PATCH 02/12] 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 cdef3eedb..20b559530 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -377,7 +377,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") @@ -394,8 +394,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) @@ -425,6 +428,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)) @@ -433,8 +452,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₁) @@ -447,9 +466,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 @@ -457,17 +474,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 59ef469ef77aa395a2667ef05304e09741cc20ab Mon Sep 17 00:00:00 2001 From: lkdvos Date: Fri, 23 Oct 2026 10:31:19 -0400 Subject: [PATCH 03/12] 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 20b559530..706d7dff0 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -389,16 +389,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) @@ -417,21 +463,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}}, @@ -441,7 +493,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 @@ -452,7 +504,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₂) @@ -466,7 +518,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 22ed21f684f002749717385cbfb0df3742128ceb Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sat, 24 Oct 2026 06:11:46 -0400 Subject: [PATCH 04/12] 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 706d7dff0..ea8334462 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) @@ -376,159 +383,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 aa50e10c20d2d99fc9d75c146174ea9cdfd3d25c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sat, 24 Oct 2026 06:57:37 -0400 Subject: [PATCH 05/12] 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 62ed9e3f3..d709ad1be 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -255,16 +255,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 d3df65d536927b1c59459012f7366e8c177f3689 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 26 Oct 2026 04:11:18 -0400 Subject: [PATCH 06/12] 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 d709ad1be..38d0aaea0 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)) @@ -248,37 +248,6 @@ using TensorKitSectors @test allunique(map(block -> block.uncoupled, blocks)) 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 59d56c1e280f127fac46f8774496d47740cc5b92 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 26 Oct 2026 04:45:31 -0400 Subject: [PATCH 07/12] 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 b91e0e68b..8c49d2c55 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -43,6 +43,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec - Recoupling matrices are stored such that they can leverage BLAS, also for mixed complex tensor with real recoupling coefficients. - Constructing a `FusionTreeBlock` only takes the fusion product of the shorter side, and `fusionblocks` groups the cached fusion trees of the space instead of constructing a block for every combination of sectors (internal) +- 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 b2da18f1d0ae5f0e15266800cefd80c12312a7af Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 26 Oct 2026 19:51:33 -0400 Subject: [PATCH 08/12] 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₂}} From bdd61c0aaa8c175d13ab42378a6d5c760b6496cc Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 27 Oct 2026 09:21:03 -0400 Subject: [PATCH 09/12] Rename the product sector helpers, and derive splitting and joining from the part positions The two parts of a product are now determined once by `_partpositions`, from which `productparts`, `productpartstypes` and `_joinparts` follow, for sectors and fusion trees alike. Generic helper names are made private. Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/producttrees.jl | 206 +++++++++++++++----------------- 1 file changed, 97 insertions(+), 109 deletions(-) diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl index bfbf75992..969b8d0db 100644 --- a/src/fusiontrees/producttrees.jl +++ b/src/fusiontrees/producttrees.jl @@ -1,109 +1,96 @@ # Product sectors # --------------- -# 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. +# Fusion tree manipulations of a product sector are computed from those of two parts of the product, +# as the Kronecker product of their recoupling matrices. -# products of at least two sectors, also with named factors -const MultiSectorTuple = Tuple{Sector, Sector, Vararg{Sector}} +# products of at least two sectors, also with names +const _MultiSectorTuple = Tuple{Sector, Sector, Vararg{Sector}} const ProductSectorType = Union{ - ProductSector{<:MultiSectorTuple}, NamedSector{<:NamedTuple{<:Any, <:MultiSectorTuple}}, + 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) -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{ProductSector{T}}) where {T} - isunique = map(S -> FusionStyle(S) isa UniqueFusion, fieldtypes(T)) - 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) +# The two parts of a product are its non-`UniqueFusion` and its `UniqueFusion` sectors if it has both, +# and otherwise its first and its remaining sectors, on which we recurse. +Base.@assume_effects :foldable function _partpositions(::Type{ProductSector{T}}) where {T} + positions = ntuple(identity, fieldcount(T)) + isunique = map(i -> FusionStyle(fieldtype(T, i)) isa UniqueFusion, positions) + any(isunique) && !all(isunique) && + return Base.filter(i -> !isunique[i], positions), Base.filter(i -> isunique[i], positions) + return (first(positions),), Base.tail(positions) 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) +# parts with a single sector are not wrapped in a `ProductSector`, to share the caches of that sector +_sectorproduct(sectors::Tuple{Sector}) = only(sectors) +_sectorproduct(sectors::Tuple) = ProductSector(sectors) +_sectortuple(s::ProductSector) = s.sectors +_sectortuple(s::Sector) = (s,) +# select the entries at compile-time positions, which keeps heterogeneous tuples inferable +@generated _getentries(t::Tuple, ::Val{P}) where {P} = Expr(:tuple, (:(t[$i]) for i in P)...) + +function productparts(s::ProductSector) + positions₁, positions₂ = _partpositions(typeof(s)) + return _sectorproduct(_getentries(s.sectors, Val(positions₁))), + _sectorproduct(_getentries(s.sectors, Val(positions₂))) 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)...) +Base.@assume_effects :foldable function productpartstypes(::Type{ProductSector{T}}) where {T} + return map(_partpositions(ProductSector{T})) do positions + return length(positions) == 1 ? fieldtype(T, only(positions)) : + ProductSector{Tuple{map(i -> fieldtype(T, i), positions)...}} 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 +function _joinparts(::Type{ProductSector{T}}, s₁::Sector, s₂::Sector) where {T} + sectors = (_sectortuple(s₁)..., _sectortuple(s₂)...) + positions = TupleTools.invperm(TupleTools.vcat(_partpositions(ProductSector{T})...)) + return ProductSector{T}(_getentries(sectors, Val(positions))) +end # 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, 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) - -maptree(proj, f::FusionTree, vertices) = FusionTree( +_productsectortype(::Type{NamedSector{NamedTuple{K, T}}}) where {K, T} = ProductSector{T} +productparts(s::NamedSector) = productparts(ProductSector(s)) +productpartstypes(::Type{I}) where {I <: NamedSector} = productpartstypes(_productsectortype(I)) +_joinparts(::Type{I}, s₁::Sector, s₂::Sector) where {I <: NamedSector} = + I(_joinparts(_productsectortype(I), s₁, s₂)) + +# the vertex labels of a product are the linear indices of the labels of its parts, with the second +# part running fastest, such that a `UniqueFusion` second part leaves the labels of the first 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) + +_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(tailsectortype(sectortype(f))) isa UniqueFusion - vertices_t = map(Returns(1), f.vertices) +function productparts(f::FusionTree) + vertices₁ = vertices₂ = f.vertices + if FusionStyle(last(productpartstypes(sectortype(f)))) isa UniqueFusion + vertices₂ = 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) + ns = _vertexmultiplicities(_maptree(last ∘ productparts, f, map(Returns(1), f.vertices))) + vertices₁ = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) + vertices₂ = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) end - return maptree(headsector, f, vertices_h), maptree(tailsector, f, vertices_t) + return _maptree(first ∘ productparts, f, vertices₁), _maptree(last ∘ productparts, f, vertices₂) end -function jointree(::Type{I}, h::FusionTree, t::FusionTree) where {I} - vertices = if FusionStyle(I) isa GenericFusion && length(h) > 1 && !(FusionStyle(t) isa UniqueFusion) - map((μ, ν, n) -> ν + (μ - 1) * n, h.vertices, t.vertices, vertexmultiplicities(t)) +function _joinparts(::Type{I}, f₁::FusionTree, f₂::FusionTree) where {I} + vertices = if FusionStyle(I) isa GenericFusion && length(f₁) > 1 && !(FusionStyle(f₂) isa UniqueFusion) + map((μ, ν, n) -> ν + (μ - 1) * n, f₁.vertices, f₂.vertices, _vertexmultiplicities(f₂)) else - h.vertices + f₁.vertices 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 + join(a, b) = _joinparts(I, a, b) + return fusiontreetype(I, length(f₁))( + map(join, f₁.uncoupled, f₂.uncoupled), join(f₁.coupled, f₂.coupled), f₁.isdual, + map(join, f₁.innerlines, f₂.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 -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)] +# the canonical order of a product block is that of the Kronecker product of the blocks of its +# parts, with the first part running fastest +function productblock(::Type{I}, block₁::FusionTreeBlock, block₂::FusionTreeBlock) where {I} + trees = [ + (_joinparts(I, f₁, g₁), _joinparts(I, f₂, g₂)) + for (f₁, f₂) in fusiontrees(block₁), (g₁, g₂) in fusiontrees(block₂) + ] return FusionTreeBlock(vec(trees)) end @@ -115,56 +102,57 @@ function FusionTreeBlock{I}( # 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 - tail = FusionTreeBlock{tailsectortype(I)}(map.(tailsector, uncoupled), isdual) - isempty(tail) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) + # the second part holds the `UniqueFusion` sectors of a mixed product, which are cheaper to construct + I₁, I₂ = productpartstypes(I) + block₂ = FusionTreeBlock{I₂}(map.(last ∘ productparts, uncoupled), isdual) + isempty(block₂) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) - head = FusionTreeBlock{headsectortype(I)}(map.(headsector, uncoupled), isdual) - return productblock(I, head, tail) + block₁ = FusionTreeBlock{I₁}(map.(first ∘ productparts, uncoupled), isdual) + return productblock(I, block₁, block₂) end # the distinct tree pairs, and the position of every tree pair among them -function distincttrees(trees) +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₂))) +# the tree pairs of the two parts of a tree pair +_partstrees((f₁, f₂)) = map(tuple, productparts(f₁), productparts(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} +function _unique_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` +# apply `transform` to the block `src` through its parts, 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) - 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)))) + return _unique_transform(transform, T, src) + elseif FusionStyle(last(productpartstypes(I))) isa UniqueFusion && numind(src) > 0 + # the second part holds the `UniqueFusion` sectors of a mixed product, with a single tree pair + (g₁, g₂), coeff = transform(last(_partstrees(first(fusiontrees(src))))) + dst₁, U₁ = transform(FusionTreeBlock(map(first ∘ _partstrees, fusiontrees(src)))) dst = FusionTreeBlock( - map(((h₁, h₂),) -> (jointree(I, h₁, t₁), jointree(I, h₂, t₂)), fusiontrees(dst_h)) + map(((f₁, f₂),) -> (_joinparts(I, f₁, g₁), _joinparts(I, f₂, g₂)), fusiontrees(dst₁)) ) - U = Matrix{T}(undef, size(U_h)) - U .= coeff .* U_h + U = Matrix{T}(undef, size(U₁)) + U .= coeff .* U₁ return dst => U end - 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)) + src₁, cols₁ = _distincttrees(map(first ∘ _partstrees, fusiontrees(src))) + src₂, cols₂ = _distincttrees(map(last ∘ _partstrees, fusiontrees(src))) + dst₁, U₁ = _unique_transform(transform, T, FusionTreeBlock(src₁)) + dst₂, U₂ = _unique_transform(transform, T, FusionTreeBlock(src₂)) - dst = productblock(I, dst_h, dst_t) + dst = productblock(I, dst₁, dst₂) U = Matrix{T}(undef, length(dst), length(src)) - for (col, i, j) in zip(axes(U, 2), cols_h, cols_t) + for (col, i, j) in zip(axes(U, 2), cols₁, cols₂) 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] + for j₂ in axes(U₂, 1), j₁ in axes(U₁, 1) + U[row += 1, col] = U₁[j₁, i] * U₂[j₂, j] end end return dst => U From d7d259e404903655c2eaa296a44331ce44dd6575 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 27 Oct 2026 11:29:18 -0400 Subject: [PATCH 10/12] Use the deconstruct/reconstruct terminology for splitting and joining product sectors and trees Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/producttrees.jl | 49 +++++++++++++++++---------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl index 969b8d0db..267ad968f 100644 --- a/src/fusiontrees/producttrees.jl +++ b/src/fusiontrees/producttrees.jl @@ -27,18 +27,18 @@ _sectortuple(s::Sector) = (s,) # select the entries at compile-time positions, which keeps heterogeneous tuples inferable @generated _getentries(t::Tuple, ::Val{P}) where {P} = Expr(:tuple, (:(t[$i]) for i in P)...) -function productparts(s::ProductSector) +function deconstructsector(s::ProductSector) positions₁, positions₂ = _partpositions(typeof(s)) return _sectorproduct(_getentries(s.sectors, Val(positions₁))), _sectorproduct(_getentries(s.sectors, Val(positions₂))) end -Base.@assume_effects :foldable function productpartstypes(::Type{ProductSector{T}}) where {T} +Base.@assume_effects :foldable function deconstructsectortype(::Type{ProductSector{T}}) where {T} return map(_partpositions(ProductSector{T})) do positions return length(positions) == 1 ? fieldtype(T, only(positions)) : ProductSector{Tuple{map(i -> fieldtype(T, i), positions)...}} end end -function _joinparts(::Type{ProductSector{T}}, s₁::Sector, s₂::Sector) where {T} +function reconstructsector(::Type{ProductSector{T}}, s₁::Sector, s₂::Sector) where {T} sectors = (_sectortuple(s₁)..., _sectortuple(s₂)...) positions = TupleTools.invperm(TupleTools.vcat(_partpositions(ProductSector{T})...)) return ProductSector{T}(_getentries(sectors, Val(positions))) @@ -46,10 +46,10 @@ end # named products are split and joined as the corresponding `ProductSector` _productsectortype(::Type{NamedSector{NamedTuple{K, T}}}) where {K, T} = ProductSector{T} -productparts(s::NamedSector) = productparts(ProductSector(s)) -productpartstypes(::Type{I}) where {I <: NamedSector} = productpartstypes(_productsectortype(I)) -_joinparts(::Type{I}, s₁::Sector, s₂::Sector) where {I <: NamedSector} = - I(_joinparts(_productsectortype(I), s₁, s₂)) +deconstructsector(s::NamedSector) = deconstructsector(ProductSector(s)) +deconstructsectortype(::Type{I}) where {I <: NamedSector} = deconstructsectortype(_productsectortype(I)) +reconstructsector(::Type{I}, s₁::Sector, s₂::Sector) where {I <: NamedSector} = + I(reconstructsector(_productsectortype(I), s₁, s₂)) # the vertex labels of a product are the linear indices of the labels of its parts, with the second # part running fastest, such that a `UniqueFusion` second part leaves the labels of the first unchanged @@ -60,24 +60,25 @@ _vertexmultiplicities(f::FusionTree) = _maptree(proj, f::FusionTree, vertices) = FusionTree( map(proj, f.uncoupled), proj(f.coupled), f.isdual, map(proj, f.innerlines), vertices ) -function productparts(f::FusionTree) +function deconstructtree(f::FusionTree) vertices₁ = vertices₂ = f.vertices - if FusionStyle(last(productpartstypes(sectortype(f)))) isa UniqueFusion + if FusionStyle(last(deconstructsectortype(sectortype(f)))) isa UniqueFusion vertices₂ = map(Returns(1), f.vertices) elseif FusionStyle(f) isa GenericFusion && length(f) > 1 - ns = _vertexmultiplicities(_maptree(last ∘ productparts, f, map(Returns(1), f.vertices))) + ns = _vertexmultiplicities(_maptree(last ∘ deconstructsector, f, map(Returns(1), f.vertices))) vertices₁ = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) vertices₂ = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) end - return _maptree(first ∘ productparts, f, vertices₁), _maptree(last ∘ productparts, f, vertices₂) + return _maptree(first ∘ deconstructsector, f, vertices₁), + _maptree(last ∘ deconstructsector, f, vertices₂) end -function _joinparts(::Type{I}, f₁::FusionTree, f₂::FusionTree) where {I} +function reconstructtree(::Type{I}, f₁::FusionTree, f₂::FusionTree) where {I} vertices = if FusionStyle(I) isa GenericFusion && length(f₁) > 1 && !(FusionStyle(f₂) isa UniqueFusion) map((μ, ν, n) -> ν + (μ - 1) * n, f₁.vertices, f₂.vertices, _vertexmultiplicities(f₂)) else f₁.vertices end - join(a, b) = _joinparts(I, a, b) + join(a, b) = reconstructsector(I, a, b) return fusiontreetype(I, length(f₁))( map(join, f₁.uncoupled, f₂.uncoupled), join(f₁.coupled, f₂.coupled), f₁.isdual, map(join, f₁.innerlines, f₂.innerlines), vertices @@ -88,7 +89,7 @@ end # parts, with the first part running fastest function productblock(::Type{I}, block₁::FusionTreeBlock, block₂::FusionTreeBlock) where {I} trees = [ - (_joinparts(I, f₁, g₁), _joinparts(I, f₂, g₂)) + (reconstructtree(I, f₁, g₁), reconstructtree(I, f₂, g₂)) for (f₁, f₂) in fusiontrees(block₁), (g₁, g₂) in fusiontrees(block₂) ] return FusionTreeBlock(vec(trees)) @@ -103,11 +104,11 @@ function FusionTreeBlock{I}( FusionStyle(I) isa UniqueFusion && return fusiontreeblock(I, uncoupled, isdual; sizehint) # the second part holds the `UniqueFusion` sectors of a mixed product, which are cheaper to construct - I₁, I₂ = productpartstypes(I) - block₂ = FusionTreeBlock{I₂}(map.(last ∘ productparts, uncoupled), isdual) + I₁, I₂ = deconstructsectortype(I) + block₂ = FusionTreeBlock{I₂}(map.(last ∘ deconstructsector, uncoupled), isdual) isempty(block₂) && return FusionTreeBlock(Vector{fusiontreetype(I, N₁, N₂)}()) - block₁ = FusionTreeBlock{I₁}(map.(first ∘ productparts, uncoupled), isdual) + block₁ = FusionTreeBlock{I₁}(map.(first ∘ deconstructsector, uncoupled), isdual) return productblock(I, block₁, block₂) end @@ -117,7 +118,7 @@ function _distincttrees(trees) return distinct, indexin(trees, distinct) end # the tree pairs of the two parts of a tree pair -_partstrees((f₁, f₂)) = map(tuple, productparts(f₁), productparts(f₂)) +_deconstructtrees((f₁, f₂)) = map(tuple, deconstructtree(f₁), deconstructtree(f₂)) # apply `transform` to the block `src`, where every tree pair of a `UniqueFusion` block maps to a # single tree pair @@ -131,19 +132,19 @@ end function product_transform(transform::F, ::Type{T}, src::FusionTreeBlock{I}) where {F, T, I} if FusionStyle(I) isa UniqueFusion return _unique_transform(transform, T, src) - elseif FusionStyle(last(productpartstypes(I))) isa UniqueFusion && numind(src) > 0 + elseif FusionStyle(last(deconstructsectortype(I))) isa UniqueFusion && numind(src) > 0 # the second part holds the `UniqueFusion` sectors of a mixed product, with a single tree pair - (g₁, g₂), coeff = transform(last(_partstrees(first(fusiontrees(src))))) - dst₁, U₁ = transform(FusionTreeBlock(map(first ∘ _partstrees, fusiontrees(src)))) + (g₁, g₂), coeff = transform(last(_deconstructtrees(first(fusiontrees(src))))) + dst₁, U₁ = transform(FusionTreeBlock(map(first ∘ _deconstructtrees, fusiontrees(src)))) dst = FusionTreeBlock( - map(((f₁, f₂),) -> (_joinparts(I, f₁, g₁), _joinparts(I, f₂, g₂)), fusiontrees(dst₁)) + map(((f₁, f₂),) -> (reconstructtree(I, f₁, g₁), reconstructtree(I, f₂, g₂)), fusiontrees(dst₁)) ) U = Matrix{T}(undef, size(U₁)) U .= coeff .* U₁ return dst => U end - src₁, cols₁ = _distincttrees(map(first ∘ _partstrees, fusiontrees(src))) - src₂, cols₂ = _distincttrees(map(last ∘ _partstrees, fusiontrees(src))) + src₁, cols₁ = _distincttrees(map(first ∘ _deconstructtrees, fusiontrees(src))) + src₂, cols₂ = _distincttrees(map(last ∘ _deconstructtrees, fusiontrees(src))) dst₁, U₁ = _unique_transform(transform, T, FusionTreeBlock(src₁)) dst₂, U₂ = _unique_transform(transform, T, FusionTreeBlock(src₂)) From 117dee012997fc23432d31f0a4a6e01f7f0db589 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 27 Oct 2026 12:05:23 -0400 Subject: [PATCH 11/12] Reword the changelog entry for product sector manipulations Co-Authored-By: Claude Opus 5.5 --- docs/src/Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index 8c49d2c55..a6ab14c3b 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -43,7 +43,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec - Recoupling matrices are stored such that they can leverage BLAS, also for mixed complex tensor with real recoupling coefficients. - Constructing a `FusionTreeBlock` only takes the fusion product of the shorter side, and `fusionblocks` groups the cached fusion trees of the space instead of constructing a block for every combination of sectors (internal) -- 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)) +- Fusion tree block manipulations of (named) product sectors are computed as Kronecker products of the manipulations of their parts, splitting off the `UniqueFusion` sectors, such that they share the caches of the individual sectors ([#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 From a8a39b337fc93373927a547212e033e323fe7687 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 29 Oct 2026 13:34:35 -0400 Subject: [PATCH 12/12] Use TupleTools.getindices and divrem, and replace comprehensions with explicit loops and broadcasts Co-Authored-By: Claude Opus 5.5 --- src/fusiontrees/producttrees.jl | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/fusiontrees/producttrees.jl b/src/fusiontrees/producttrees.jl index 267ad968f..0fba6aec4 100644 --- a/src/fusiontrees/producttrees.jl +++ b/src/fusiontrees/producttrees.jl @@ -24,13 +24,11 @@ _sectorproduct(sectors::Tuple{Sector}) = only(sectors) _sectorproduct(sectors::Tuple) = ProductSector(sectors) _sectortuple(s::ProductSector) = s.sectors _sectortuple(s::Sector) = (s,) -# select the entries at compile-time positions, which keeps heterogeneous tuples inferable -@generated _getentries(t::Tuple, ::Val{P}) where {P} = Expr(:tuple, (:(t[$i]) for i in P)...) function deconstructsector(s::ProductSector) positions₁, positions₂ = _partpositions(typeof(s)) - return _sectorproduct(_getentries(s.sectors, Val(positions₁))), - _sectorproduct(_getentries(s.sectors, Val(positions₂))) + return _sectorproduct(TupleTools.getindices(s.sectors, positions₁)), + _sectorproduct(TupleTools.getindices(s.sectors, positions₂)) end Base.@assume_effects :foldable function deconstructsectortype(::Type{ProductSector{T}}) where {T} return map(_partpositions(ProductSector{T})) do positions @@ -41,7 +39,7 @@ end function reconstructsector(::Type{ProductSector{T}}, s₁::Sector, s₂::Sector) where {T} sectors = (_sectortuple(s₁)..., _sectortuple(s₂)...) positions = TupleTools.invperm(TupleTools.vcat(_partpositions(ProductSector{T})...)) - return ProductSector{T}(_getentries(sectors, Val(positions))) + return ProductSector{T}(TupleTools.getindices(sectors, positions)) end # named products are split and joined as the corresponding `ProductSector` @@ -66,8 +64,9 @@ function deconstructtree(f::FusionTree) vertices₂ = map(Returns(1), f.vertices) elseif FusionStyle(f) isa GenericFusion && length(f) > 1 ns = _vertexmultiplicities(_maptree(last ∘ deconstructsector, f, map(Returns(1), f.vertices))) - vertices₁ = map((μ, n) -> div(μ - 1, n) + 1, f.vertices, ns) - vertices₂ = map((μ, n) -> mod(μ - 1, n) + 1, f.vertices, ns) + vertices₁₂ = map((μ, n) -> (divrem(μ - 1, n) .+ 1), f.vertices, ns) + vertices₁ = first.(vertices₁₂) + vertices₂ = last.(vertices₁₂) end return _maptree(first ∘ deconstructsector, f, vertices₁), _maptree(last ∘ deconstructsector, f, vertices₂) @@ -88,11 +87,14 @@ end # the canonical order of a product block is that of the Kronecker product of the blocks of its # parts, with the first part running fastest function productblock(::Type{I}, block₁::FusionTreeBlock, block₂::FusionTreeBlock) where {I} - trees = [ - (reconstructtree(I, f₁, g₁), reconstructtree(I, f₂, g₂)) - for (f₁, f₂) in fusiontrees(block₁), (g₁, g₂) in fusiontrees(block₂) - ] - return FusionTreeBlock(vec(trees)) + trees = fusiontreetype(I, numout(block₁), numin(block₁))[] + sizehint!(trees, length(block₁) * length(block₂)) + for (g₁, g₂) in fusiontrees(block₂) + for (f₁, f₂) in fusiontrees(block₁) + push!(trees, (reconstructtree(I, f₁, g₁), reconstructtree(I, f₂, g₂))) + end + end + return FusionTreeBlock(trees) end function FusionTreeBlock{I}( @@ -125,7 +127,7 @@ _deconstructtrees((f₁, f₂)) = map(tuple, deconstructtree(f₁), deconstructt function _unique_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]) + return FusionTreeBlock(map(first, dst)) => LinearAlgebra.diagm(convert.(T, last.(dst))) end # apply `transform` to the block `src` through its parts, with the recoupling matrix with scalar type `T`