From 65ea338ac87316dbbf52ca1633a6db5e8afd47ac Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 16:36:53 -0400 Subject: [PATCH 01/39] FusionTreePair --- src/fusiontrees/fusiontrees.jl | 6 +- src/fusiontrees/manipulations.jl | 171 +++++++++++++----------------- src/planar/planaroperations.jl | 6 +- src/tensors/braidingtensor.jl | 4 +- src/tensors/diagonal.jl | 2 +- src/tensors/indexmanipulations.jl | 20 ++-- src/tensors/tensoroperations.jl | 2 +- src/tensors/treetransformers.jl | 10 +- test/symmetries/fusiontrees.jl | 33 +++--- 9 files changed, 118 insertions(+), 136 deletions(-) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 1665cdb27..f91033903 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -105,6 +105,8 @@ function FusionTree( end FusionTree(uncoupled::Tuple{I, Vararg{I}}) where {I <: Sector} = FusionTree(uncoupled, unit(I)) +const FusionTreePair{I, N₁, N₂} = Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}} + # Properties sectortype(::Type{<:FusionTree{I}}) where {I <: Sector} = I FusionStyle(::Type{<:FusionTree{I}}) where {I <: Sector} = FusionStyle(I) @@ -215,9 +217,7 @@ function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, N}) where {I, N end # TODO: is this piracy? -function Base.convert( - A::Type{<:AbstractArray}, (f₁, f₂)::Tuple{FusionTree{I}, FusionTree{I}} - ) where {I} +function Base.convert(A::Type{<:AbstractArray}, (f₁, f₂)::FusionTreePair{I}) where {I} F₁ = convert(A, f₁) F₂ = convert(A, f₂) sz1 = size(F₁) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 1564b1b67..d95502fdf 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -242,7 +242,7 @@ end # -> A-move (foldleft, foldright) is complicated, needs to be reexpressed in standard form # flip a duality flag of a fusion tree -function flip(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, i::Int; inv::Bool = false) where {I <: Sector, N₁, N₂} +function flip((f₁, f₂)::FusionTreePair{I, N₁, N₂}, i::Int; inv::Bool = false) where {I, N₁, N₂} @assert 0 < i ≤ N₁ + N₂ if i ≤ N₁ a = f₁.uncoupled[i] @@ -271,18 +271,18 @@ function flip(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, i::Int; inv: return SingletonDict((f₁, f₂′) => factor) end end -function flip(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, ind; inv::Bool = false) where {I <: Sector, N₁, N₂} +function flip((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ind; inv::Bool = false) where {I, N₁, N₂} f₁′, f₂′ = f₁, f₂ factor = one(sectorscalartype(I)) for i in ind - (f₁′, f₂′), s = only(flip(f₁′, f₂′, i; inv)) + (f₁′, f₂′), s = only(flip((f₁′, f₂′), i; inv)) factor *= s end return SingletonDict((f₁′, f₂′) => factor) end # change to N₁ - 1, N₂ + 1 -function bendright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where {I <: Sector, N₁, N₂} +function bendright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} # map final splitting vertex (a, b)<-c to fusion vertex a<-(c, dual(b)) @assert N₁ > 0 c = f₁.coupled @@ -328,15 +328,16 @@ function bendright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where { end end # change to N₁ + 1, N₂ - 1 -function bendleft(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I} +function bendleft((f₁, f₂)::FusionTreePair{I}) where {I} # map final fusion vertex c<-(a, b) to splitting vertex (c, dual(b))<-a return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in bendright(f₂, f₁) + (f₁′, f₂′) => conj(coeff) + for ((f₂′, f₁′), coeff) in bendright((f₂, f₁)) ) end # change to N₁ - 1, N₂ + 1 -function foldright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where {I <: Sector, N₁, N₂} +function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) @assert N₁ > 0 a = f₁.uncoupled[1] @@ -392,10 +393,11 @@ function foldright(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}) where { end end # change to N₁ + 1, N₂ - 1 -function foldleft(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I} +function foldleft((f₁, f₂)::FusionTreePair{I}) where {I} # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in foldright(f₂, f₁) + (f₁′, f₂′) => conj(coeff) + for ((f₂′, f₁′), coeff) in foldright((f₂, f₁)) ) end @@ -419,11 +421,11 @@ function iscyclicpermutation(v1, v2) end # clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft -function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Sector} +function cycleclockwise((f₁, f₂)::FusionTreePair{I}) where {I} local newtrees if length(f₁) > 0 - for ((f1a, f2a), coeffa) in foldright(f₁, f₂) - for ((f1b, f2b), coeffb) in bendleft(f1a, f2a) + for ((f1a, f2a), coeffa) in foldright((f₁, f₂)) + for ((f1b, f2b), coeffb) in bendleft((f1a, f2a)) coeff = coeffa * coeffb if (@isdefined newtrees) newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff @@ -433,8 +435,8 @@ function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Se end end else - for ((f1a, f2a), coeffa) in bendleft(f₁, f₂) - for ((f1b, f2b), coeffb) in foldright(f1a, f2a) + for ((f1a, f2a), coeffa) in bendleft((f₁, f₂)) + for ((f1b, f2b), coeffb) in foldright((f1a, f2a)) coeff = coeffa * coeffb if (@isdefined newtrees) newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff @@ -448,11 +450,11 @@ function cycleclockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Se end # anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright -function cycleanticlockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I <: Sector} +function cycleanticlockwise((f₁, f₂)::FusionTreePair{I}) where {I} local newtrees if length(f₂) > 0 - for ((f1a, f2a), coeffa) in foldleft(f₁, f₂) - for ((f1b, f2b), coeffb) in bendright(f1a, f2a) + for ((f1a, f2a), coeffa) in foldleft((f₁, f₂)) + for ((f1b, f2b), coeffb) in bendright((f1a, f2a)) coeff = coeffa * coeffb if (@isdefined newtrees) newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff @@ -462,8 +464,8 @@ function cycleanticlockwise(f₁::FusionTree{I}, f₂::FusionTree{I}) where {I < end end else - for ((f1a, f2a), coeffa) in bendright(f₁, f₂) - for ((f1b, f2b), coeffb) in foldleft(f1a, f2a) + for ((f1a, f2a), coeffa) in bendright((f₁, f₂)) + for ((f1b, f2b), coeffb) in foldleft((f1a, f2a)) coeff = coeffa * coeffb if (@isdefined newtrees) newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff @@ -478,8 +480,8 @@ end # repartition double fusion tree """ - repartition(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, N::Int) where {I, N₁, N₂} - -> <:AbstractDict{Tuple{FusionTree{I, N}, FusionTree{I, N₁+N₂-N}}, <:Number} + repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, N::Int) where {I, N₁, N₂} + -> <:AbstractDict{<:FusionTreePair{I, N, N₁+N₂-N}}, <:Number} Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of @@ -488,36 +490,31 @@ outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical cou repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have `N` outgoing sectors. """ -@inline function repartition( - f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, N::Int - ) where {I <: Sector, N₁, N₂} +@inline function repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, N::Int) where {I, N₁, N₂} f₁.coupled == f₂.coupled || throw(SectorMismatch()) @assert 0 <= N <= N₁ + N₂ - return _recursive_repartition(f₁, f₂, Val(N)) + return _recursive_repartition((f₁, f₂), Val(N)) end -function _recursive_repartition( - f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, ::Val{N} - ) where {I <: Sector, N₁, N₂, N} +function _recursive_repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ::Val{N}) where {I, N₁, N₂, N} # recursive definition is only way to get correct number of loops for # GenericFusion, but is too complex for type inference to handle, so we # precompute the parameters of the return type F₁ = fusiontreetype(I, N) F₂ = fusiontreetype(I, N₁ + N₂ - N) + FF = Tuple{F₁, F₂} T = sectorscalartype(I) coeff = one(T) if N == N₁ return fusiontreedict(I){Tuple{F₁, F₂}, T}((f₁, f₂) => coeff) else local newtrees::fusiontreedict(I){Tuple{F₁, F₂}, T} - for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright(f₁, f₂) : bendleft(f₁, f₂)) - for ((f₁′′, f₂′′), coeff2) in _recursive_repartition(f₁′, f₂′, Val(N)) + for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright((f₁, f₂)) : bendleft((f₁, f₂))) + for ((f₁′′, f₂′′), coeff2) in _recursive_repartition((f₁′, f₂′), Val(N)) if (@isdefined newtrees) push!(newtrees, (f₁′′, f₂′′) => coeff1 * coeff2) else - newtrees = fusiontreedict(I){Tuple{F₁, F₂}, T}( - (f₁′′, f₂′′) => coeff1 * coeff2 - ) + newtrees = fusiontreedict(I){FF, T}((f₁′′, f₂′′) => coeff1 * coeff2) end end end @@ -526,9 +523,8 @@ function _recursive_repartition( end """ - transpose(f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::NTuple{N₁, Int}, p2::NTuple{N₂, Int}) where {I, N₁, N₂} - -> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number} + transpose((f₁, f₂)::FusionTreePair{I}, p::(Index2Tuple{N₁, N₂}) where {I, N₁, N₂} + -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of @@ -537,20 +533,15 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function Base.transpose( - f₁::FusionTree{I}, f₂::FusionTree{I}, p1::IndexTuple{N₁}, p2::IndexTuple{N₂} - ) where {I <: Sector, N₁, N₂} +function Base.transpose((f₁, f₂)::FusionTreePair{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} N = N₁ + N₂ @assert length(f₁) + length(f₂) == N - p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - @assert iscyclicpermutation(p) - return fstranspose((f₁, f₂, p1, p2)) + p′ = linearizepermutation(p..., length(f₁), length(f₂)) + @assert iscyclicpermutation(p′) + return fstranspose(((f₁, f₂), p)) end -const FSTransposeKey{I <: Sector, N₁, N₂} = Tuple{ - <:FusionTree{I}, <:FusionTree{I}, - IndexTuple{N₁}, IndexTuple{N₂}, -} +const FSTransposeKey{I, N₁, N₂} = Tuple{<:FusionTreePair{I}, Index2Tuple{N₁, N₂}} function _fsdicttype(I, N₁, N₂) F₁ = fusiontreetype(I, N₁) @@ -559,11 +550,11 @@ function _fsdicttype(I, N₁, N₂) return fusiontreedict(I){Tuple{F₁, F₂}, T} end -@cached function fstranspose(key::FSTransposeKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I <: Sector, N₁, N₂} - f₁, f₂, p1, p2 = key +@cached function fstranspose(key::FSTransposeKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I, N₁, N₂} + (f₁, f₂), (p1, p2) = key N = N₁ + N₂ p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - newtrees = repartition(f₁, f₂, N₁) + newtrees = repartition((f₁, f₂), N₁) length(p) == 0 && return newtrees i1 = findfirst(==(1), p) @assert i1 !== nothing @@ -572,7 +563,7 @@ end while 1 < i1 <= Nhalf local newtrees′ for ((f1a, f2a), coeffa) in newtrees - for ((f1b, f2b), coeffb) in cycleanticlockwise(f1a, f2a) + for ((f1b, f2b), coeffb) in cycleanticlockwise((f1a, f2a)) coeff = coeffa * coeffb if (@isdefined newtrees′) newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff @@ -587,7 +578,7 @@ end while Nhalf < i1 local newtrees′ for ((f1a, f2a), coeffa) in newtrees - for ((f1b, f2b), coeffb) in cycleclockwise(f1a, f2a) + for ((f1b, f2b), coeffb) in cycleclockwise((f1a, f2a)) coeff = coeffa * coeffb if (@isdefined newtrees′) newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff @@ -602,7 +593,7 @@ end return newtrees end -function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I <: Sector} +function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I} if FusionStyle(I) isa UniqueFusion return NoCache() else @@ -616,14 +607,12 @@ end # -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) function planar_trace( - f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}, - q1::IndexTuple{N₃}, q2::IndexTuple{N₃} - ) where {I <: Sector, N₁, N₂, N₃} + (f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}, (q1, q2)::Index2Tuple{N₃, N₃} + ) where {I, N₁, N₂, N₃} N = N₁ + N₂ + 2N₃ @assert length(f₁) + length(f₂) == N if N₃ == 0 - return transpose(f₁, f₂, p1, p2) + return transpose((f₁, f₂), (p1, p2)) end linearindex = ( @@ -644,9 +633,9 @@ function planar_trace( F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) newtrees = FusionTreeDict{Tuple{F₁, F₂}, T}() - for ((f₁′, f₂′), coeff′) in repartition(f₁, f₂, N) - for (f₁′′, coeff′′) in planar_trace(f₁′, q1′, q2′) - for (f12′′′, coeff′′′) in transpose(f₁′′, f₂′, p1′, p2′) + for ((f₁′, f₂′), coeff′) in repartition((f₁, f₂), N) + for (f₁′′, coeff′′) in planar_trace(f₁′, (q1′, q2′)) + for (f12′′′, coeff′′′) in transpose((f₁′′, f₂′), (p1′, p2′)) coeff = coeff′ * coeff′′ * coeff′′′ if !iszero(coeff) newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff @@ -658,16 +647,14 @@ function planar_trace( end """ - planar_trace(f::FusionTree{I,N}, q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N,N₃} + planar_trace(f::FusionTree{I,N}, (q1, q2)::Index2Tuple{N₃,N₃}) where {I,N,N₃} -> <:AbstractDict{FusionTree{I,N-2*N₃}, <:Number} Perform a planar trace of the uncoupled indices of the fusion tree `f` at `q1` with those at `q2`, where `q1[i]` is connected to `q2[i]` for all `i`. The result is returned as a dictionary of output trees and corresponding coefficients. """ -function planar_trace( - f::FusionTree{I, N}, q1::IndexTuple{N₃}, q2::IndexTuple{N₃} - ) where {I <: Sector, N, N₃} +function planar_trace(f::FusionTree{I, N}, (q1, q2)::Index2Tuple{N₃, N₃}) where {I, N, N₃} T = sectorscalartype(I) F = fusiontreetype(I, N - 2 * N₃) newtrees = FusionTreeDict{F, T}() @@ -701,7 +688,7 @@ function planar_trace( map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q2, k)) end for (f′, coeff′) in elementary_trace(f, i) - for (f′′, coeff′′) in planar_trace(f′, q1′, q2′) + for (f′′, coeff′′) in planar_trace(f′, (q1′, q2′)) coeff = coeff′ * coeff′′ if !iszero(coeff) newtrees[f′′] = get(newtrees, f′′, zero(coeff)) + coeff @@ -713,13 +700,13 @@ end # trace two neighbouring indices of a single fusion tree """ - elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} -> <:AbstractDict{FusionTree{I,N-2}, <:Number} + elementary_trace(f::FusionTree{I,N}, i) where {I,N} -> <:AbstractDict{FusionTree{I,N-2}, <:Number} Perform an elementary trace of neighbouring uncoupled indices `i` and `i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and corresponding coefficients. """ -function elementary_trace(f::FusionTree{I, N}, i) where {I <: Sector, N} +function elementary_trace(f::FusionTree{I, N}, i) where {I, N} (N > 1 && 1 <= i <= N) || throw(ArgumentError("Cannot trace outputs i=$i and i+1 out of only $N outputs")) i < N || isunit(f.coupled) || @@ -823,7 +810,7 @@ applying `artin_braid(f′, i; inv = true)` to all the outputs `f′` of tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword has no effect if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ -function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I <: Sector, N} +function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} 1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) uncoupled = f.uncoupled @@ -968,9 +955,7 @@ that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` an ``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid( - f::FusionTree{I, N}, levels::NTuple{N, Int}, p::NTuple{N, Int} - ) where {I <: Sector, N} +function braid(f::FusionTree{I, N}, levels::NTuple{N, Int}, p::NTuple{N, Int}) where {I, N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding coeff = one(sectorscalartype(I)) @@ -1017,17 +1002,16 @@ Perform a permutation of the uncoupled indices of the fusion tree `f` and return as a `<:AbstractDict` of output trees and corresponding coefficients; this requires that `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ -function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I <: Sector, N} +function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I, N} @assert BraidingStyle(I) isa SymmetricBraiding return braid(f, ntuple(identity, Val(N)), p) end # braid double fusion tree """ - braid(f₁::FusionTree{I}, f₂::FusionTree{I}, - levels1::IndexTuple, levels2::IndexTuple, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector, N₁, N₂} - -> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number} + braid((f₁, f₂)::FusionTreePair{I}, (levels1, levels2)::Index2Tuple, + (p1, p2)::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} + -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} Input is a fusion-splitting tree pair that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the splitting @@ -1042,29 +1026,24 @@ levels[j]`. This does not allow to encode the most general braid, but a general be obtained by combining such operations. """ function braid( - f₁::FusionTree{I}, f₂::FusionTree{I}, - levels1::IndexTuple, levels2::IndexTuple, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂} - ) where {I <: Sector, N₁, N₂} + (f₁, f₂)::FusionTreePair{I}, (levels1, levels2)::Index2Tuple, + (p1, p2)::Index2Tuple{N₁, N₂} + ) where {I, N₁, N₂} @assert length(f₁) + length(f₂) == N₁ + N₂ @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) - return fsbraid((f₁, f₂, levels1, levels2, p1, p2)) + return fsbraid(((f₁, f₂), (levels1, levels2), (p1, p2))) end -const FSBraidKey{I <: Sector, N₁, N₂} = Tuple{ - <:FusionTree{I}, <:FusionTree{I}, - IndexTuple, IndexTuple, - IndexTuple{N₁}, IndexTuple{N₂}, -} +const FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreePair{I}, Index2Tuple, Index2Tuple{N₁, N₂}} -@cached function fsbraid(key::FSBraidKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I <: Sector, N₁, N₂} - (f₁, f₂, l1, l2, p1, p2) = key +@cached function fsbraid(key::FSBraidKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I, N₁, N₂} + ((f₁, f₂), (l1, l2), (p1, p2)) = key p = linearizepermutation(p1, p2, length(f₁), length(f₂)) levels = (l1..., reverse(l2)...) local newtrees - for ((f, f0), coeff1) in repartition(f₁, f₂, N₁ + N₂) + for ((f, f0), coeff1) in repartition((f₁, f₂), N₁ + N₂) for (f′, coeff2) in braid(f, levels, p) - for ((f₁′, f₂′), coeff3) in repartition(f′, f0, N₁) + for ((f₁′, f₂′), coeff3) in repartition((f′, f0), N₁) if @isdefined newtrees newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + coeff1 * coeff2 * coeff3 @@ -1086,9 +1065,8 @@ function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I <: Sector} end """ - permute(f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::NTuple{N₁, Int}, p2::NTuple{N₂, Int}) where {I, N₁, N₂} - -> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number} + permute((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} + -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of @@ -1097,12 +1075,9 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function permute( - f₁::FusionTree{I}, f₂::FusionTree{I}, - p1::IndexTuple{N₁}, p2::IndexTuple{N₂} - ) where {I <: Sector, N₁, N₂} +function permute((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} @assert BraidingStyle(I) isa SymmetricBraiding levels1 = ntuple(identity, length(f₁)) levels2 = length(f₁) .+ ntuple(identity, length(f₂)) - return braid(f₁, f₂, levels1, levels2, p1, p2) + return braid((f₁, f₂), (levels1, levels2), (p1, p2)) end diff --git a/src/planar/planaroperations.jl b/src/planar/planaroperations.jl index a06f4431d..b1cb7d438 100644 --- a/src/planar/planaroperations.jl +++ b/src/planar/planaroperations.jl @@ -99,9 +99,11 @@ function planartrace!( end β′ = One() for (f₁, f₂) in fusiontrees(A) - for ((f₁′, f₂′), coeff) in planar_trace(f₁, f₂, p₁, p₂, q₁, q₂) + for ((f₁′, f₂′), coeff) in planar_trace((f₁, f₂), (p₁, p₂), (q₁, q₂)) TO.tensortrace!( - C[f₁′, f₂′], A[f₁, f₂], (p₁, p₂), (q₁, q₂), false, α * coeff, β′, + C[f₁′, f₂′], + A[f₁, f₂], (p₁, p₂), (q₁, q₂), false, + α * coeff, β′, backend, allocator ) end diff --git a/src/tensors/braidingtensor.jl b/src/tensors/braidingtensor.jl index 9c92c67a1..f9e78a965 100644 --- a/src/tensors/braidingtensor.jl +++ b/src/tensors/braidingtensor.jl @@ -239,7 +239,7 @@ function planarcontract!( inv_braid = τ_levels[cindA[1]] > τ_levels[cindA[2]] for (f₁, f₂) in fusiontrees(B) local newtrees - for ((f₁′, f₂′), coeff′) in transpose(f₁, f₂, cindB, oindB) + for ((f₁′, f₂′), coeff′) in transpose((f₁, f₂), (cindB, oindB)) for (f₁′′, coeff′′) in artin_braid(f₁′, 1; inv = inv_braid) f12 = (f₁′′, f₂′) coeff = coeff′ * coeff′′ @@ -294,7 +294,7 @@ function planarcontract!( for (f₁, f₂) in fusiontrees(A) local newtrees - for ((f₁′, f₂′), coeff′) in transpose(f₁, f₂, oindA, cindA) + for ((f₁′, f₂′), coeff′) in transpose((f₁, f₂), (oindA, cindA)) for (f₂′′, coeff′′) in artin_braid(f₂′, 1; inv = inv_braid) f12 = (f₁′, f₂′′) coeff = coeff′ * conj(coeff′′) diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 904e40cba..0062c44e5 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -203,7 +203,7 @@ function permute( d′ = typeof(d)(undef, dual(d.domain)) for (c, b) in blocks(d) f = only(fusiontrees(codomain(d), c)) - ((f′, _), coeff) = only(permute(f, f, p₁, p₂)) + ((f′, _), coeff) = only(permute((f, f), (p₁, p₂))) c′ = f′.coupled scale!(block(d′, c′), b, coeff) end diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 408de09f0..ef3d89be3 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -17,7 +17,7 @@ function flip(t::AbstractTensorMap, I; inv::Bool = false) P = flip(space(t), I) t′ = similar(t, P) for (f₁, f₂) in fusiontrees(t) - (f₁′, f₂′), factor = only(flip(f₁, f₂, I; inv)) + (f₁′, f₂′), factor = only(flip((f₁, f₂), I; inv)) scale!(t′[f₁′, f₂′], t[f₁, f₂], factor) end return t′ @@ -575,8 +575,10 @@ function _add_abelian_kernel_threaded!(tdst, tsrc, p, transformer, α, β, backe end function _add_abelian_block!(tdst, tsrc, p, transformer, f₁, f₂, α, β, backend...) - (f₁′, f₂′), coeff = first(transformer(f₁, f₂)) - @inbounds TO.tensoradd!(tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, β, backend...) + (f₁′, f₂′), coeff = first(transformer((f₁, f₂))) + @inbounds TO.tensoradd!( + tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, β, backend... + ) return nothing end @@ -634,8 +636,10 @@ function _add_general_kernel_nonthreaded!(tdst, tsrc, p, transformer, α, β, ba tdst = scale!(tdst, β) end for (f₁, f₂) in fusiontrees(tsrc) - for ((f₁′, f₂′), coeff) in transformer(f₁, f₂) - @inbounds TO.tensoradd!(tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, One(), backend...) + for ((f₁′, f₂′), coeff) in transformer((f₁, f₂)) + @inbounds TO.tensoradd!( + tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, One(), backend... + ) end end return nothing @@ -699,8 +703,10 @@ end function _add_nonabelian_sector!(tdst, tsrc, p, fusiontreetransform, s₁, s₂, α, backend...) for (f₁, f₂) in fusiontrees(tsrc) (f₁.uncoupled == s₁ && f₂.uncoupled == s₂) || continue - for ((f₁′, f₂′), coeff) in fusiontreetransform(f₁, f₂) - @inbounds TO.tensoradd!(tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, One(), backend...) + for ((f₁′, f₂′), coeff) in fusiontreetransform((f₁, f₂)) + @inbounds TO.tensoradd!( + tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, One(), backend... + ) end end return nothing diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 7bd0ab96b..9cf3d1b08 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -215,7 +215,7 @@ function trace_permute!( r₁ = (p₁..., q₁...) r₂ = (p₂..., q₂...) for (f₁, f₂) in fusiontrees(tsrc) - for ((f₁′, f₂′), coeff) in permute(f₁, f₂, r₁, r₂) + for ((f₁′, f₂′), coeff) in permute((f₁, f₂), (r₁, r₂)) f₁′′, g₁ = split(f₁′, N₁) f₂′′, g₂ = split(f₂′, N₂) g₁ == g₂ || continue diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 36cd3926d..d8f3f9cfd 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -26,7 +26,7 @@ function AbelianTreeTransformer(transform, p, Vdst, Vsrc) for i in 1:L f₁, f₂ = structure_src.fusiontreelist[i] - (f₃, f₄), coeff = only(transform(f₁, f₂)) + (f₃, f₄), coeff = only(transform((f₁, f₂))) j = structure_dst.fusiontreeindices[(f₃, f₄)] stridestructure_dst = structure_dst.fusiontreestructure[j] stridestructure_src = structure_src.fusiontreestructure[i] @@ -171,7 +171,7 @@ end # braid is special because it has levels function treebraider(::AbstractTensorMap, ::AbstractTensorMap, p::Index2Tuple, levels) - return fusiontreetransform(f1, f2) = braid(f1, f2, levels..., p...) + return fusiontreetransform((f1, f2)) = braid((f1, f2), levels, p) end function treebraider(tdst::TensorMap, tsrc::TensorMap, p::Index2Tuple, levels) return treebraider(space(tdst), space(tsrc), p, levels) @@ -179,7 +179,7 @@ end @cached function treebraider( Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, levels )::treetransformertype(Vdst, Vsrc) - fusiontreebraider(f1, f2) = braid(f1, f2, levels..., p...) + fusiontreebraider((f1, f2)) = braid((f1, f2), levels, p) return TreeTransformer(fusiontreebraider, p, Vdst, Vsrc) end @@ -187,7 +187,7 @@ for (transform, treetransformer) in ((:permute, :treepermuter), (:transpose, :treetransposer)) @eval begin function $treetransformer(::AbstractTensorMap, ::AbstractTensorMap, p::Index2Tuple) - return fusiontreetransform(f1, f2) = $transform(f1, f2, p...) + return fusiontreetransform(f1, f2) = $transform((f1, f2), p) end function $treetransformer(tdst::TensorMap, tsrc::TensorMap, p::Index2Tuple) return $treetransformer(space(tdst), space(tsrc), p) @@ -195,7 +195,7 @@ for (transform, treetransformer) in @cached function $treetransformer( Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple )::treetransformertype(Vdst, Vsrc) - fusiontreetransform(f1, f2) = $transform(f1, f2, p...) + fusiontreetransform((f1, f2)) = $transform((f1, f2), p) return TreeTransformer(fusiontreetransform, p, Vdst, Vsrc) end end diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl index c001f4e26..ff2c69683 100644 --- a/test/symmetries/fusiontrees.jl +++ b/test/symmetries/fusiontrees.jl @@ -154,7 +154,7 @@ using .TestSetup @test bf ≈ bf′ atol = 1.0e-12 end - d2 = @constinferred TK.planar_trace(f, (1, 3), (2, 4)) + d2 = @constinferred TK.planar_trace(f, ((1, 3), (2, 4))) oind2 = (5, 6, 7) bf2 = tensortrace(af, (:a, :a, :b, :b, :c, :d, :e)) bf2′ = zero(bf2) @@ -163,7 +163,7 @@ using .TestSetup end @test bf2 ≈ bf2′ atol = 1.0e-12 - d2 = @constinferred TK.planar_trace(f, (5, 6), (2, 1)) + d2 = @constinferred TK.planar_trace(f, ((5, 6), (2, 1))) oind2 = (3, 4, 7) bf2 = tensortrace(af, (:a, :b, :c, :d, :b, :a, :e)) bf2′ = zero(bf2) @@ -172,7 +172,7 @@ using .TestSetup end @test bf2 ≈ bf2′ atol = 1.0e-12 - d2 = @constinferred TK.planar_trace(f, (1, 4), (6, 3)) + d2 = @constinferred TK.planar_trace(f, ((1, 4), (6, 3))) bf2 = tensortrace(af, (:a, :b, :c, :c, :d, :a, :e)) bf2′ = zero(bf2) for (f2′, coeff) in d2 @@ -182,7 +182,7 @@ using .TestSetup q1 = (1, 3, 5) q2 = (2, 4, 6) - d3 = @constinferred TK.planar_trace(f, q1, q2) + d3 = @constinferred TK.planar_trace(f, (q1, q2)) bf3 = tensortrace(af, (:a, :a, :b, :b, :c, :c, :d)) bf3′ = zero(bf3) for (f3′, coeff) in d3 @@ -192,7 +192,7 @@ using .TestSetup q1 = (1, 3, 5) q2 = (6, 2, 4) - d3 = @constinferred TK.planar_trace(f, q1, q2) + d3 = @constinferred TK.planar_trace(f, (q1, q2)) bf3 = tensortrace(af, (:a, :b, :b, :c, :c, :a, :d)) bf3′ = zero(bf3) for (f3′, coeff) in d3 @@ -202,7 +202,7 @@ using .TestSetup q1 = (1, 2, 3) q2 = (6, 5, 4) - d3 = @constinferred TK.planar_trace(f, q1, q2) + d3 = @constinferred TK.planar_trace(f, (q1, q2)) bf3 = tensortrace(af, (:a, :b, :c, :c, :b, :a, :d)) bf3′ = zero(bf3) for (f3′, coeff) in d3 @@ -212,7 +212,7 @@ using .TestSetup q1 = (1, 2, 4) q2 = (6, 3, 5) - d3 = @constinferred TK.planar_trace(f, q1, q2) + d3 = @constinferred TK.planar_trace(f, (q1, q2)) bf3 = tensortrace(af, (:a, :b, :b, :c, :c, :a, :d)) bf3′ = zero(bf3) for (f3′, coeff) in d3 @@ -398,12 +398,12 @@ using .TestSetup @testset "Double fusion tree $Istr: repartitioning" begin for n in 0:(2 * N) - d = @constinferred TK.repartition(f1, f2, $n) + d = @constinferred TK.repartition((f1, f2), $n) @test dim(incoming) ≈ sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) d2 = Dict{typeof((f1, f2)), valtype(d)}() for ((f1′, f2′), coeff) in d - for ((f1′′, f2′′), coeff2) in TK.repartition(f1′, f2′, N) + for ((f1′′, f2′′), coeff2) in TK.repartition((f1′, f2′), N) d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff end end @@ -453,12 +453,12 @@ using .TestSetup ip = invperm(p) ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] - d = @constinferred TK.permute(f1, f2, p1, p2) + d = @constinferred TensorKit.permute((f1, f2), (p1, p2)) @test dim(incoming) ≈ sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) d2 = Dict{typeof((f1, f2)), valtype(d)}() for ((f1′, f2′), coeff) in d - d′ = TK.permute(f1′, f2′, ip1, ip2) + d′ = TensorKit.permute((f1′, f2′), (ip1, ip2)) for ((f1′′, f2′′), coeff2) in d′ d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff @@ -515,12 +515,12 @@ using .TestSetup ip′ = tuple(getindex.(Ref(vcat(1:n, (2N):-1:(n + 1))), ip)...) ip1, ip2 = ip′[1:N], ip′[(2N):-1:(N + 1)] - d = @constinferred transpose(f1, f2, p1, p2) + d = @constinferred transpose((f1, f2), (p1, p2)) @test dim(incoming) ≈ sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) d2 = Dict{typeof((f1, f2)), valtype(d)}() for ((f1′, f2′), coeff) in d - d′ = transpose(f1′, f2′, ip1, ip2) + d′ = transpose((f1′, f2′), (ip1, ip2)) for ((f1′′, f2′′), coeff2) in d′ d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff end @@ -534,7 +534,7 @@ using .TestSetup end if BraidingStyle(I) isa Bosonic - d3 = permute(f1, f2, p1, p2) + d3 = permute((f1, f2), (p1, p2)) for (f1′, f2′) in union(keys(d), keys(d3)) coeff1 = get(d, (f1′, f2′), zero(valtype(d))) coeff3 = get(d3, (f1′, f2′), zero(valtype(d3))) @@ -575,15 +575,14 @@ using .TestSetup end end @testset "Double fusion tree $Istr: planar trace" begin - d1 = transpose(f1, f1, (N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,)) + d1 = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) f1front, = TK.split(f1, N - 1) T = TensorKitSectors._Fscalartype(I) d2 = Dict{typeof((f1front, f1front)), T}() for ((f1′, f2′), coeff′) in d1 for ((f1′′, f2′′), coeff′′) in TK.planar_trace( - f1′, f2′, (2:N...,), (1, ((2N):-1:(N + 3))...), (N + 1,), - (N + 2,) + (f1′, f2′), ((2:N...,), (1, ((2N):-1:(N + 3))...)), ((N + 1,), (N + 2,)) ) coeff = coeff′ * coeff′′ d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff From ba97ca1ec2e4727958a820f743a42214f8bc4bad Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 20:49:21 -0400 Subject: [PATCH 02/39] implement "vectorized" fusiontree manipulations --- src/fusiontrees/fusiontrees.jl | 7 +- src/fusiontrees/uncouplediterator.jl | 353 +++++++++++++++++++++++++++ 2 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 src/fusiontrees/uncouplediterator.jl diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index f91033903..defddf1ca 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -247,11 +247,12 @@ function Base.show(io::IO, t::FusionTree{I}) where {I <: Sector} end end -# Manipulate fusion trees -include("manipulations.jl") - # Fusion tree iterators include("iterator.jl") +include("uncouplediterator.jl") + +# Manipulate fusion trees +include("manipulations.jl") # auxiliary routines # _abelianinner: generate the inner indices for given outer indices in the abelian case diff --git a/src/fusiontrees/uncouplediterator.jl b/src/fusiontrees/uncouplediterator.jl new file mode 100644 index 000000000..e0e923167 --- /dev/null +++ b/src/fusiontrees/uncouplediterator.jl @@ -0,0 +1,353 @@ +struct OuterTreeIterator{I<:Sector,N₁,N₂} + uncoupled::Tuple{NTuple{N₁,I},NTuple{N₂,I}} + isdual::Tuple{NTuple{N₁,Bool},NTuple{N₂,Bool}} +end + +sectortype(::Type{<:OuterTreeIterator{I}}) where {I} = I +numout(fs::OuterTreeIterator) = numout(typeof(fs)) +numout(::Type{<:OuterTreeIterator{I,N₁}}) where {I,N₁} = N₁ +numin(fs::OuterTreeIterator) = numin(typeof(fs)) +numin(::Type{<:OuterTreeIterator{I,N₁,N₂}}) where {I,N₁,N₂} = N₂ +numind(fs::OuterTreeIterator) = numind(typeof(fs)) +numind(::Type{T}) where {T<:OuterTreeIterator} = numin(T) + numout(T) + +# TODO: should we make this an actual iterator? +function fusiontrees(iter::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + + trees = Vector{Tuple{F₁,F₂}}(undef, 0) + for c in blocksectors(iter), f₁ in fusiontrees(iter.uncoupled[1], c, iter.isdual[1]), + f₂ in fusiontrees(iter.uncoupled[2], c, iter.isdual[2]) + + push!(trees, (f₁, f₂)) + end + return trees +end + +# TODO: better implementation +Base.length(iter::OuterTreeIterator) = length(fusiontrees(iter)) + +function blocksectors(iter::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + I == Trivial && return (Trivial(),) + + bs_codomain = Vector{I}() + if N₁ == 0 + push!(bs_codomain, one(I)) + elseif N₁ == 1 + push!(bs_codomain, only(iter.uncoupled[1])) + else + for c in ⊗(iter.uncoupled[1]...) + if !(c in bs_codomain) + push!(bs_codomain, c) + end + end + end + + bs_domain = Vector{I}() + if N₂ == 0 + push!(bs_domain, one(I)) + elseif N₂ == 1 + push!(bs_domain, only(iter.uncoupled[2])) + else + for c in ⊗(iter.uncoupled[2]...) + if !(c in bs_domain) + push!(bs_domain, c) + end + end + end + + return sort!(collect(intersect(bs_codomain, bs_domain))) +end + +# Manipulations +# ------------- + +function bendright(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + uncoupled_dst = (TupleTools.front(fs_src.uncoupled[1]), + (fs_src.uncoupled[2]..., dual(fs_src.uncoupled[1][end]))) + isdual_dst = (TupleTools.front(fs_src.isdual[1]), + (fs_src.isdual[2]..., !(fs_src.isdual[1][end]))) + fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) + + trees_src = fusiontrees(fs_src) + trees_dst = fusiontrees(fs_dst) + indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) + U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) + + for (col, f) in enumerate(trees_src) + for (f′, c) in bendright(f) + row = indexmap[f′] + U[row, col] = c + end + end + + return fs_dst, U +end + +# TODO: verify if this can be computed through an adjoint +function bendleft(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + uncoupled_dst = ((fs_src.uncoupled[1]..., dual(fs_src.uncoupled[2][end])), + TupleTools.front(fs_src.uncoupled[2])) + isdual_dst = ((fs_src.isdual[1]..., !(fs_src.isdual[2][end])), + TupleTools.front(fs_src.isdual[2])) + fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) + + trees_src = fusiontrees(fs_src) + trees_dst = fusiontrees(fs_dst) + indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) + U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) + + for (col, f) in enumerate(trees_src) + for (f′, c) in bendleft(f) + row = indexmap[f′] + U[row, col] = c + end + end + + return fs_dst, U +end + +function foldright(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + uncoupled_dst = (Base.tail(fs_src.uncoupled[1]), + (dual(first(fs_src.uncoupled[1])), fs_src.uncoupled[2]...)) + isdual_dst = (Base.tail(fs_src.isdual[1]), + (!first(fs_src.isdual[1]), fs_src.isdual[2]...)) + fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) + + trees_src = fusiontrees(fs_src) + trees_dst = fusiontrees(fs_dst) + indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) + U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) + + for (col, f) in enumerate(trees_src) + for (f′, c) in foldright(f) + row = indexmap[f′] + U[row, col] = c + end + end + + return fs_dst, U +end + +# TODO: verify if this can be computed through an adjoint +function foldleft(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + uncoupled_dst = ((dual(first(fs_src.uncoupled[2])), fs_src.uncoupled[1]...), + Base.tail(fs_src.uncoupled[2])) + isdual_dst = ((!first(fs_src.isdual[2]), fs_src.isdual[1]...), + Base.tail(fs_src.isdual[2])) + fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) + + trees_src = fusiontrees(fs_src) + trees_dst = fusiontrees(fs_dst) + indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) + U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) + + for (col, f) in enumerate(trees_src) + for (f′, c) in foldleft(f) + row = indexmap[f′] + U[row, col] = c + end + end + + return fs_dst, U +end + +function cycleclockwise(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + if N₁ > 0 + fs_tmp, U₁ = foldright(fs_src) + fs_dst, U₂ = bendleft(fs_tmp) + else + fs_tmp, U₁ = bendleft(fs_src) + fs_dst, U₂ = foldright(fs_tmp) + end + return fs_dst, U₂ * U₁ +end + +function cycleanticlockwise(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} + if N₂ > 0 + fs_tmp, U₁ = foldleft(fs_src) + fs_dst, U₂ = bendright(fs_tmp) + else + fs_tmp, U₁ = bendright(fs_src) + fs_dst, U₂ = foldleft(fs_tmp) + end + return fs_dst, U₂ * U₁ +end + +@inline function repartition(fs_src::OuterTreeIterator{I,N₁,N₂}, N::Int) where {I,N₁,N₂} + @assert 0 <= N <= N₁ + N₂ + return _recursive_repartition(fs_src, Val(N)) +end + +function _repartition_type(I, N, N₁, N₂) + return Tuple{OuterTreeIterator{I,N,N₁ + N₂ - N},Matrix{sectorscalartype(I)}} +end +function _recursive_repartition(fs_src::OuterTreeIterator{I,N₁,N₂}, + ::Val{N})::_repartition_type(I, N, N₁, N₂) where {I,N₁,N₂,N} + if N == N₁ + fs_dst = fs_src + U = zeros(sectorscalartype(I), length(fs_dst), length(fs_src)) + copyto!(U, LinearAlgebra.I) + return fs_dst, U + end + + N == N₁ - 1 && return bendright(fs_src) + N == N₁ + 1 && return bendleft(fs_src) + + fs_tmp, U₁ = N < N₁ ? bendright(fs_src) : bendleft(fs_src) + fs_dst, U₂ = _recursive_repartition(fs_tmp, Val(N)) + return fs_dst, U₂ * U₁ +end + +function Base.transpose(fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁,N₂}) where {I,N₁,N₂} + N = N₁ + N₂ + @assert numind(fs_src) == N + p′ = linearizepermutation(p..., numout(fs_src), numin(fs_src)) + @assert iscyclicpermutation(p′) + return _fstranspose((fs_src, p)) +end + +const _FSTransposeKey{I,N₁,N₂} = Tuple{<:OuterTreeIterator{I},Index2Tuple{N₁,N₂}} + +@cached function _fstranspose(key::_FSTransposeKey{I,N₁,N₂})::Tuple{OuterTreeIterator{I,N₁, + N₂}, + Matrix{sectorscalartype(I)}} where {I, + N₁, + N₂} + fs_src, (p1, p2) = key + + N = N₁ + N₂ + p = linearizepermutation(p1, p2, numout(fs_src), numin(fs_src)) + + fs_dst, U = repartition(fs_src, N₁) + length(p) == 0 && return fs_dst, U + i1 = findfirst(==(1), p)::Int + i1 == 1 && return fs_dst, U + + Nhalf = N >> 1 + while 1 < i1 ≤ Nhalf + fs_dst, U_tmp = cycleanticlockwise(fs_dst) + U = U_tmp * U + i1 -= 1 + end + while Nhalf < i1 + fs_dst, U_tmp = cycleclockwise(fs_dst) + U = U_tmp * U + i1 = mod1(i1 + 1, N) + end + + return fs_dst, U +end + +function CacheStyle(::typeof(_fstranspose), k::_FSTransposeKey{I}) where {I} + if FusionStyle(I) == UniqueFusion() + return NoCache() + else + return GlobalLRUCache() + end +end + +function artin_braid(fs_src::OuterTreeIterator{I,N,0}, i; inv::Bool=false) where {I,N} + 1 <= i < N || + throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) + + uncoupled = fs_src.uncoupled[1] + uncoupled′ = TupleTools.setindex(uncoupled, uncoupled[i + 1], i) + uncoupled′ = TupleTools.setindex(uncoupled′, uncoupled[i], i + 1) + + isdual = fs_src.isdual[1] + isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) + isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) + + fs_dst = OuterTreeIterator((uncoupled′, ()), (isdual′, ())) + + trees_src = fusiontrees(fs_src) + trees_dst = fusiontrees(fs_dst) + indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) + U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) + + for (col, (f₁, f₂)) in enumerate(trees_src) + for (f₁′, c) in artin_braid(f₁, i; inv) + row = indexmap[(f₁′, f₂)] + U[row, col] = c + end + end + + return fs_dst, U +end + +function braid(fs_src::OuterTreeIterator{I,N,0}, levels::NTuple{N,Int}, + p::NTuple{N,Int}) where {I,N} + TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) + + if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding + uncoupled′ = TupleTools._permute(fs_src.uncoupled[1], p) + isdual′ = TupleTools._permute(fs_src.isdual[1], p) + fs_dst = OuterTreeIterator(uncoupled′, isdual′) + + trees_src = fusiontrees(fs_src) + trees_dst = fusiontrees(fs_dst) + indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) + U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) + + for (col, (f₁, f₂)) in enumerate(trees_src) + for (f₁′, c) in braid(f₁, levels, p) + row = indexmap[(f₁′, f₂)] + U[row, col] = c + end + end + + return fs_dst, U + end + + fs_dst, U = repartition(fs_src, N) # TODO: can we avoid this? + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + fs_dst, U_tmp = artin_braid(fs_dst, s; inv) + U = U_tmp * U + end + return fs_dst, U +end + +function braid(fs_src::OuterTreeIterator{I}, levels::Index2Tuple, + p::Index2Tuple{N₁,N₂}) where {I,N₁,N₂} + @assert numind(fs_src) == N₁ + N₂ + @assert numout(fs_src) == length(levels[1]) && numin(fs_src) == length(levels[2]) + @assert TupleTools.isperm((p[1]..., p[2]...)) + return _fsbraid((fs_src, levels, p)) +end + +const _FSBraidKey{I,N₁,N₂} = Tuple{<:OuterTreeIterator{I},Index2Tuple,Index2Tuple{N₁,N₂}} + +@cached function _fsbraid(key::_FSBraidKey{I,N₁,N₂})::Tuple{OuterTreeIterator{I,N₁,N₂}, + Matrix{sectorscalartype(I)}} where {I, + N₁, + N₂} + fs_src, (l1, l2), (p1, p2) = key + + p = linearizepermutation(p1, p2, numout(fs_src), numin(fs_src)) + levels = (l1..., reverse(l2)...) + + fs_dst, U = repartition(fs_src, numind(fs_src)) + fs_dst, U_tmp = braid(fs_dst, levels, p) + U = U_tmp * U + fs_dst, U_tmp = repartition(fs_dst, N₁) + U = U_tmp * U + return fs_dst, U +end + +function CacheStyle(::typeof(_fsbraid), k::_FSBraidKey{I}) where {I} + if FusionStyle(I) isa UniqueFusion + return NoCache() + else + return GlobalLRUCache() + end +end + +function permute(fs_src::OuterTreeIterator{I}, p::Index2Tuple) where {I} + @assert BraidingStyle(I) isa SymmetricBraiding + levels1 = ntuple(identity, numout(fs_src)) + levels2 = numout(fs_src) .+ ntuple(identity, numin(fs_src)) + return braid(fs_src, (levels1, levels2), p) +end From 74b323abfd20d4dd518c134598c90797f3cdf68f Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 21:22:19 -0400 Subject: [PATCH 03/39] Refactor treetransformer to make use of vectorized implementation --- src/fusiontrees/uncouplediterator.jl | 122 +++++++++++++++------------ src/tensors/treetransformers.jl | 49 ++++------- 2 files changed, 87 insertions(+), 84 deletions(-) diff --git a/src/fusiontrees/uncouplediterator.jl b/src/fusiontrees/uncouplediterator.jl index e0e923167..e00d8eed5 100644 --- a/src/fusiontrees/uncouplediterator.jl +++ b/src/fusiontrees/uncouplediterator.jl @@ -1,24 +1,24 @@ -struct OuterTreeIterator{I<:Sector,N₁,N₂} - uncoupled::Tuple{NTuple{N₁,I},NTuple{N₂,I}} - isdual::Tuple{NTuple{N₁,Bool},NTuple{N₂,Bool}} +struct OuterTreeIterator{I <: Sector, N₁, N₂} + uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}} + isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}} end sectortype(::Type{<:OuterTreeIterator{I}}) where {I} = I numout(fs::OuterTreeIterator) = numout(typeof(fs)) -numout(::Type{<:OuterTreeIterator{I,N₁}}) where {I,N₁} = N₁ +numout(::Type{<:OuterTreeIterator{I, N₁}}) where {I, N₁} = N₁ numin(fs::OuterTreeIterator) = numin(typeof(fs)) -numin(::Type{<:OuterTreeIterator{I,N₁,N₂}}) where {I,N₁,N₂} = N₂ +numin(::Type{<:OuterTreeIterator{I, N₁, N₂}}) where {I, N₁, N₂} = N₂ numind(fs::OuterTreeIterator) = numind(typeof(fs)) -numind(::Type{T}) where {T<:OuterTreeIterator} = numin(T) + numout(T) +numind(::Type{T}) where {T <: OuterTreeIterator} = numin(T) + numout(T) # TODO: should we make this an actual iterator? -function fusiontrees(iter::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} +function fusiontrees(iter::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) - trees = Vector{Tuple{F₁,F₂}}(undef, 0) + trees = Vector{Tuple{F₁, F₂}}(undef, 0) for c in blocksectors(iter), f₁ in fusiontrees(iter.uncoupled[1], c, iter.isdual[1]), - f₂ in fusiontrees(iter.uncoupled[2], c, iter.isdual[2]) + f₂ in fusiontrees(iter.uncoupled[2], c, iter.isdual[2]) push!(trees, (f₁, f₂)) end @@ -28,7 +28,7 @@ end # TODO: better implementation Base.length(iter::OuterTreeIterator) = length(fusiontrees(iter)) -function blocksectors(iter::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} +function blocksectors(iter::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} I == Trivial && return (Trivial(),) bs_codomain = Vector{I}() @@ -63,11 +63,15 @@ end # Manipulations # ------------- -function bendright(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} - uncoupled_dst = (TupleTools.front(fs_src.uncoupled[1]), - (fs_src.uncoupled[2]..., dual(fs_src.uncoupled[1][end]))) - isdual_dst = (TupleTools.front(fs_src.isdual[1]), - (fs_src.isdual[2]..., !(fs_src.isdual[1][end]))) +function bendright(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} + uncoupled_dst = ( + TupleTools.front(fs_src.uncoupled[1]), + (fs_src.uncoupled[2]..., dual(fs_src.uncoupled[1][end])), + ) + isdual_dst = ( + TupleTools.front(fs_src.isdual[1]), + (fs_src.isdual[2]..., !(fs_src.isdual[1][end])), + ) fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) trees_src = fusiontrees(fs_src) @@ -86,11 +90,15 @@ function bendright(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} end # TODO: verify if this can be computed through an adjoint -function bendleft(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} - uncoupled_dst = ((fs_src.uncoupled[1]..., dual(fs_src.uncoupled[2][end])), - TupleTools.front(fs_src.uncoupled[2])) - isdual_dst = ((fs_src.isdual[1]..., !(fs_src.isdual[2][end])), - TupleTools.front(fs_src.isdual[2])) +function bendleft(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} + uncoupled_dst = ( + (fs_src.uncoupled[1]..., dual(fs_src.uncoupled[2][end])), + TupleTools.front(fs_src.uncoupled[2]), + ) + isdual_dst = ( + (fs_src.isdual[1]..., !(fs_src.isdual[2][end])), + TupleTools.front(fs_src.isdual[2]), + ) fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) trees_src = fusiontrees(fs_src) @@ -108,11 +116,15 @@ function bendleft(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} return fs_dst, U end -function foldright(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} - uncoupled_dst = (Base.tail(fs_src.uncoupled[1]), - (dual(first(fs_src.uncoupled[1])), fs_src.uncoupled[2]...)) - isdual_dst = (Base.tail(fs_src.isdual[1]), - (!first(fs_src.isdual[1]), fs_src.isdual[2]...)) +function foldright(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} + uncoupled_dst = ( + Base.tail(fs_src.uncoupled[1]), + (dual(first(fs_src.uncoupled[1])), fs_src.uncoupled[2]...), + ) + isdual_dst = ( + Base.tail(fs_src.isdual[1]), + (!first(fs_src.isdual[1]), fs_src.isdual[2]...), + ) fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) trees_src = fusiontrees(fs_src) @@ -131,11 +143,15 @@ function foldright(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} end # TODO: verify if this can be computed through an adjoint -function foldleft(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} - uncoupled_dst = ((dual(first(fs_src.uncoupled[2])), fs_src.uncoupled[1]...), - Base.tail(fs_src.uncoupled[2])) - isdual_dst = ((!first(fs_src.isdual[2]), fs_src.isdual[1]...), - Base.tail(fs_src.isdual[2])) +function foldleft(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} + uncoupled_dst = ( + (dual(first(fs_src.uncoupled[2])), fs_src.uncoupled[1]...), + Base.tail(fs_src.uncoupled[2]), + ) + isdual_dst = ( + (!first(fs_src.isdual[2]), fs_src.isdual[1]...), + Base.tail(fs_src.isdual[2]), + ) fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) trees_src = fusiontrees(fs_src) @@ -153,7 +169,7 @@ function foldleft(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} return fs_dst, U end -function cycleclockwise(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} +function cycleclockwise(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} if N₁ > 0 fs_tmp, U₁ = foldright(fs_src) fs_dst, U₂ = bendleft(fs_tmp) @@ -164,7 +180,7 @@ function cycleclockwise(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N return fs_dst, U₂ * U₁ end -function cycleanticlockwise(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N₁,N₂} +function cycleanticlockwise(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} if N₂ > 0 fs_tmp, U₁ = foldleft(fs_src) fs_dst, U₂ = bendright(fs_tmp) @@ -175,16 +191,17 @@ function cycleanticlockwise(fs_src::OuterTreeIterator{I,N₁,N₂}) where {I,N return fs_dst, U₂ * U₁ end -@inline function repartition(fs_src::OuterTreeIterator{I,N₁,N₂}, N::Int) where {I,N₁,N₂} +@inline function repartition(fs_src::OuterTreeIterator{I, N₁, N₂}, N::Int) where {I, N₁, N₂} @assert 0 <= N <= N₁ + N₂ return _recursive_repartition(fs_src, Val(N)) end function _repartition_type(I, N, N₁, N₂) - return Tuple{OuterTreeIterator{I,N,N₁ + N₂ - N},Matrix{sectorscalartype(I)}} + return Tuple{OuterTreeIterator{I, N, N₁ + N₂ - N}, Matrix{sectorscalartype(I)}} end -function _recursive_repartition(fs_src::OuterTreeIterator{I,N₁,N₂}, - ::Val{N})::_repartition_type(I, N, N₁, N₂) where {I,N₁,N₂,N} +function _recursive_repartition( + fs_src::OuterTreeIterator{I, N₁, N₂}, ::Val{N} + )::_repartition_type(I, N, N₁, N₂) where {I, N₁, N₂, N} if N == N₁ fs_dst = fs_src U = zeros(sectorscalartype(I), length(fs_dst), length(fs_src)) @@ -200,7 +217,7 @@ function _recursive_repartition(fs_src::OuterTreeIterator{I,N₁,N₂}, return fs_dst, U₂ * U₁ end -function Base.transpose(fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁,N₂}) where {I,N₁,N₂} +function Base.transpose(fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} N = N₁ + N₂ @assert numind(fs_src) == N p′ = linearizepermutation(p..., numout(fs_src), numin(fs_src)) @@ -208,13 +225,11 @@ function Base.transpose(fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁,N₂}) return _fstranspose((fs_src, p)) end -const _FSTransposeKey{I,N₁,N₂} = Tuple{<:OuterTreeIterator{I},Index2Tuple{N₁,N₂}} +const _FSTransposeKey{I, N₁, N₂} = Tuple{<:OuterTreeIterator{I}, Index2Tuple{N₁, N₂}} -@cached function _fstranspose(key::_FSTransposeKey{I,N₁,N₂})::Tuple{OuterTreeIterator{I,N₁, - N₂}, - Matrix{sectorscalartype(I)}} where {I, - N₁, - N₂} +@cached function _fstranspose( + key::_FSTransposeKey{I, N₁, N₂} + )::Tuple{OuterTreeIterator{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} fs_src, (p1, p2) = key N = N₁ + N₂ @@ -248,7 +263,7 @@ function CacheStyle(::typeof(_fstranspose), k::_FSTransposeKey{I}) where {I} end end -function artin_braid(fs_src::OuterTreeIterator{I,N,0}, i; inv::Bool=false) where {I,N} +function artin_braid(fs_src::OuterTreeIterator{I, N, 0}, i; inv::Bool = false) where {I, N} 1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) @@ -277,8 +292,9 @@ function artin_braid(fs_src::OuterTreeIterator{I,N,0}, i; inv::Bool=false) where return fs_dst, U end -function braid(fs_src::OuterTreeIterator{I,N,0}, levels::NTuple{N,Int}, - p::NTuple{N,Int}) where {I,N} +function braid( + fs_src::OuterTreeIterator{I, N, 0}, levels::NTuple{N, Int}, p::NTuple{N, Int} + ) where {I, N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding @@ -310,20 +326,20 @@ function braid(fs_src::OuterTreeIterator{I,N,0}, levels::NTuple{N,Int}, return fs_dst, U end -function braid(fs_src::OuterTreeIterator{I}, levels::Index2Tuple, - p::Index2Tuple{N₁,N₂}) where {I,N₁,N₂} +function braid( + fs_src::OuterTreeIterator{I}, levels::Index2Tuple, p::Index2Tuple{N₁, N₂} + ) where {I, N₁, N₂} @assert numind(fs_src) == N₁ + N₂ @assert numout(fs_src) == length(levels[1]) && numin(fs_src) == length(levels[2]) @assert TupleTools.isperm((p[1]..., p[2]...)) return _fsbraid((fs_src, levels, p)) end -const _FSBraidKey{I,N₁,N₂} = Tuple{<:OuterTreeIterator{I},Index2Tuple,Index2Tuple{N₁,N₂}} +const _FSBraidKey{I, N₁, N₂} = Tuple{<:OuterTreeIterator{I}, Index2Tuple, Index2Tuple{N₁, N₂}} -@cached function _fsbraid(key::_FSBraidKey{I,N₁,N₂})::Tuple{OuterTreeIterator{I,N₁,N₂}, - Matrix{sectorscalartype(I)}} where {I, - N₁, - N₂} +@cached function _fsbraid( + key::_FSBraidKey{I, N₁, N₂} + )::Tuple{OuterTreeIterator{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} fs_src, (l1, l2), (p1, p2) = key p = linearizepermutation(p1, p2, numout(fs_src), numin(fs_src)) diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index d8f3f9cfd..6eef114cf 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -62,39 +62,26 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) fusionstructure_dst = structure_dst.fusiontreestructure structure_src = fusionblockstructure(Vsrc) fusionstructure_src = structure_src.fusiontreestructure - I = sectortype(Vsrc) - - uncoupleds_src = map(structure_src.fusiontreelist) do (f₁, f₂) - return TupleTools.vcat(f₁.uncoupled, dual.(f₂.uncoupled)) - end - uncoupleds_src_unique = unique(uncoupleds_src) - - uncoupleds_dst = map(structure_dst.fusiontreelist) do (f₁, f₂) - return TupleTools.vcat(f₁.uncoupled, dual.(f₂.uncoupled)) - end + I = sectortype(Vsrc) T = sectorscalartype(I) N = numind(Vdst) - L = length(uncoupleds_src_unique) - data = Vector{_GenericTransformerData{T, N}}(undef, L) + data = Vector{_GenericTransformerData{T, N}}() - # TODO: this can be multithreaded - for (i, uncoupled) in enumerate(uncoupleds_src_unique) - inds_src = findall(==(uncoupled), uncoupleds_src) - fusiontrees_outer_src = structure_src.fusiontreelist[inds_src] + isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) + for cod_uncoupled_src in sectors(codomain(Vsrc)), + dom_uncoupled_src in sectors(domain(Vsrc)) - uncoupled_dst = TupleTools.getindices(uncoupled, (p[1]..., p[2]...)) - inds_dst = findall(==(uncoupled_dst), uncoupleds_dst) + fs_src = OuterTreeIterator((cod_uncoupled_src, dom_uncoupled_src), isdual_src) + trees_src = fusiontrees(fs_src) + isempty(trees_src) && continue - fusiontrees_outer_dst = structure_dst.fusiontreelist[inds_dst] + fs_dst, U = transform(fs_src) + matrix = copy(transpose(U)) # TODO: should we avoid this - matrix = zeros(sectorscalartype(I), length(inds_dst), length(inds_src)) - for (row, (f₁, f₂)) in enumerate(fusiontrees_outer_src) - for ((f₃, f₄), coeff) in transform(f₁, f₂) - col = findfirst(==((f₃, f₄)), fusiontrees_outer_dst)::Int - matrix[row, col] = coeff - end - end + inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) + trees_dst = fusiontrees(fs_dst) + inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) # size is shared between blocks, so repack: # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) @@ -106,7 +93,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) sz = size(matrix), sparsity = count(!iszero, matrix) / length(matrix) ) - data[i] = (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src)) + push!(data, (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src))) end transformer = GenericTreeTransformer{T, N}(data) @@ -171,7 +158,7 @@ end # braid is special because it has levels function treebraider(::AbstractTensorMap, ::AbstractTensorMap, p::Index2Tuple, levels) - return fusiontreetransform((f1, f2)) = braid((f1, f2), levels, p) + return fusiontreetransform(f) = braid(f, levels, p) end function treebraider(tdst::TensorMap, tsrc::TensorMap, p::Index2Tuple, levels) return treebraider(space(tdst), space(tsrc), p, levels) @@ -179,7 +166,7 @@ end @cached function treebraider( Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, levels )::treetransformertype(Vdst, Vsrc) - fusiontreebraider((f1, f2)) = braid((f1, f2), levels, p) + fusiontreebraider(f) = braid(f, levels, p) return TreeTransformer(fusiontreebraider, p, Vdst, Vsrc) end @@ -187,7 +174,7 @@ for (transform, treetransformer) in ((:permute, :treepermuter), (:transpose, :treetransposer)) @eval begin function $treetransformer(::AbstractTensorMap, ::AbstractTensorMap, p::Index2Tuple) - return fusiontreetransform(f1, f2) = $transform((f1, f2), p) + return fusiontreetransform(f) = $transform(f, p) end function $treetransformer(tdst::TensorMap, tsrc::TensorMap, p::Index2Tuple) return $treetransformer(space(tdst), space(tsrc), p) @@ -195,7 +182,7 @@ for (transform, treetransformer) in @cached function $treetransformer( Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple )::treetransformertype(Vdst, Vsrc) - fusiontreetransform((f1, f2)) = $transform((f1, f2), p) + fusiontreetransform(f) = $transform(f, p) return TreeTransformer(fusiontreetransform, p, Vdst, Vsrc) end end From 610e19572fdd2fc469811b69f2cf282ae8a67034 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 22:17:00 -0400 Subject: [PATCH 04/39] fix arg order `braid` --- src/fusiontrees/manipulations.jl | 24 ++++++++++++------------ src/fusiontrees/uncouplediterator.jl | 16 ++++++++-------- src/tensors/treetransformers.jl | 4 ++-- test/symmetries/fusiontrees.jl | 10 +++++----- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index d95502fdf..7ff834692 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -942,7 +942,7 @@ end # braid fusion tree """ - braid(f::FusionTree{<:Sector, N}, levels::NTuple{N, Int}, p::NTuple{N, Int}) + braid(f::FusionTree{<:Sector, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) -> <:AbstractDict{typeof(t), <:Number} Perform a braiding of the uncoupled indices of the fusion tree `f` and return the result as @@ -955,7 +955,7 @@ that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` an ``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid(f::FusionTree{I, N}, levels::NTuple{N, Int}, p::NTuple{N, Int}) where {I, N} +function braid(f::FusionTree{I, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) where {I, N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding coeff = one(sectorscalartype(I)) @@ -1004,13 +1004,13 @@ as a `<:AbstractDict` of output trees and corresponding coefficients; this requi """ function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I, N} @assert BraidingStyle(I) isa SymmetricBraiding - return braid(f, ntuple(identity, Val(N)), p) + return braid(f, p, ntuple(identity, Val(N))) end # braid double fusion tree """ - braid((f₁, f₂)::FusionTreePair{I}, (levels1, levels2)::Index2Tuple, - (p1, p2)::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} + braid((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁,N₂}, + (levels1, levels2)::Index2Tuple) where {I,N₁,N₂} -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} Input is a fusion-splitting tree pair that describes the fusion of a set of incoming @@ -1026,23 +1026,23 @@ levels[j]`. This does not allow to encode the most general braid, but a general be obtained by combining such operations. """ function braid( - (f₁, f₂)::FusionTreePair{I}, (levels1, levels2)::Index2Tuple, - (p1, p2)::Index2Tuple{N₁, N₂} + (f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}, + (levels1, levels2)::Index2Tuple ) where {I, N₁, N₂} @assert length(f₁) + length(f₂) == N₁ + N₂ @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) - return fsbraid(((f₁, f₂), (levels1, levels2), (p1, p2))) + return fsbraid(((f₁, f₂), (p1, p2), (levels1, levels2))) end -const FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreePair{I}, Index2Tuple, Index2Tuple{N₁, N₂}} +const FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreePair{I}, Index2Tuple{N₁, N₂}, Index2Tuple} @cached function fsbraid(key::FSBraidKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I, N₁, N₂} - ((f₁, f₂), (l1, l2), (p1, p2)) = key + ((f₁, f₂), (p1, p2), (l1, l2)) = key p = linearizepermutation(p1, p2, length(f₁), length(f₂)) levels = (l1..., reverse(l2)...) local newtrees for ((f, f0), coeff1) in repartition((f₁, f₂), N₁ + N₂) - for (f′, coeff2) in braid(f, levels, p) + for (f′, coeff2) in braid(f, p, levels) for ((f₁′, f₂′), coeff3) in repartition((f′, f0), N₁) if @isdefined newtrees newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + @@ -1079,5 +1079,5 @@ function permute((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N @assert BraidingStyle(I) isa SymmetricBraiding levels1 = ntuple(identity, length(f₁)) levels2 = length(f₁) .+ ntuple(identity, length(f₂)) - return braid((f₁, f₂), (levels1, levels2), (p1, p2)) + return braid((f₁, f₂), (p1, p2), (levels1, levels2)) end diff --git a/src/fusiontrees/uncouplediterator.jl b/src/fusiontrees/uncouplediterator.jl index e00d8eed5..332f8c70c 100644 --- a/src/fusiontrees/uncouplediterator.jl +++ b/src/fusiontrees/uncouplediterator.jl @@ -293,7 +293,7 @@ function artin_braid(fs_src::OuterTreeIterator{I, N, 0}, i; inv::Bool = false) w end function braid( - fs_src::OuterTreeIterator{I, N, 0}, levels::NTuple{N, Int}, p::NTuple{N, Int} + fs_src::OuterTreeIterator{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{N, Int} ) where {I, N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) @@ -308,7 +308,7 @@ function braid( U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) for (col, (f₁, f₂)) in enumerate(trees_src) - for (f₁′, c) in braid(f₁, levels, p) + for (f₁′, c) in braid(f₁, p, levels) row = indexmap[(f₁′, f₂)] U[row, col] = c end @@ -327,26 +327,26 @@ function braid( end function braid( - fs_src::OuterTreeIterator{I}, levels::Index2Tuple, p::Index2Tuple{N₁, N₂} + fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁, N₂}, levels::Index2Tuple ) where {I, N₁, N₂} @assert numind(fs_src) == N₁ + N₂ @assert numout(fs_src) == length(levels[1]) && numin(fs_src) == length(levels[2]) @assert TupleTools.isperm((p[1]..., p[2]...)) - return _fsbraid((fs_src, levels, p)) + return _fsbraid((fs_src, p, levels)) end -const _FSBraidKey{I, N₁, N₂} = Tuple{<:OuterTreeIterator{I}, Index2Tuple, Index2Tuple{N₁, N₂}} +const _FSBraidKey{I, N₁, N₂} = Tuple{<:OuterTreeIterator{I}, Index2Tuple{N₁, N₂}, Index2Tuple} @cached function _fsbraid( key::_FSBraidKey{I, N₁, N₂} )::Tuple{OuterTreeIterator{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} - fs_src, (l1, l2), (p1, p2) = key + fs_src, (p1, p2), (l1, l2) = key p = linearizepermutation(p1, p2, numout(fs_src), numin(fs_src)) levels = (l1..., reverse(l2)...) fs_dst, U = repartition(fs_src, numind(fs_src)) - fs_dst, U_tmp = braid(fs_dst, levels, p) + fs_dst, U_tmp = braid(fs_dst, p, levels) U = U_tmp * U fs_dst, U_tmp = repartition(fs_dst, N₁) U = U_tmp * U @@ -365,5 +365,5 @@ function permute(fs_src::OuterTreeIterator{I}, p::Index2Tuple) where {I} @assert BraidingStyle(I) isa SymmetricBraiding levels1 = ntuple(identity, numout(fs_src)) levels2 = numout(fs_src) .+ ntuple(identity, numin(fs_src)) - return braid(fs_src, (levels1, levels2), p) + return braid(fs_src, p, (levels1, levels2)) end diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 6eef114cf..fe86c5296 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -158,7 +158,7 @@ end # braid is special because it has levels function treebraider(::AbstractTensorMap, ::AbstractTensorMap, p::Index2Tuple, levels) - return fusiontreetransform(f) = braid(f, levels, p) + return fusiontreetransform(f) = braid(f, p, levels) end function treebraider(tdst::TensorMap, tsrc::TensorMap, p::Index2Tuple, levels) return treebraider(space(tdst), space(tsrc), p, levels) @@ -166,7 +166,7 @@ end @cached function treebraider( Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, levels )::treetransformertype(Vdst, Vsrc) - fusiontreebraider(f) = braid(f, levels, p) + fusiontreebraider(f) = braid(f, p, levels) return TreeTransformer(fusiontreebraider, p, Vdst, Vsrc) end diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl index ff2c69683..f5113f2f0 100644 --- a/test/symmetries/fusiontrees.jl +++ b/test/symmetries/fusiontrees.jl @@ -99,13 +99,13 @@ using .TestSetup @test c′ == one(c′) return t′ end - braid_i_to_1 = braid(f1, levels, (i, (1:(i - 1))..., ((i + 1):N)...)) + braid_i_to_1 = braid(f1, (i, (1:(i - 1))..., ((i + 1):N)...), levels) trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) trees3 = empty(trees2) p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) for (t, coeff) in trees2 - for (t′, coeff′) in braid(t, levels, p) + for (t′, coeff′) in braid(t, p, levels) trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ end end @@ -285,11 +285,11 @@ using .TestSetup ip = invperm(p) levels = ntuple(identity, N) - d = @constinferred braid(f, levels, p) + d = @constinferred braid(f, p, levels) d2 = Dict{typeof(f), valtype(d)}() levels2 = p for (f2, coeff) in d - for (f1, coeff2) in braid(f2, levels2, ip) + for (f1, coeff2) in braid(f2, ip, levels2) d2[f1] = get(d2, f1, zero(coeff)) + coeff2 * coeff end end @@ -348,7 +348,7 @@ using .TestSetup perm = ((N .+ (1:N))..., (1:N)...) levels = ntuple(identity, 2 * N) for (t, coeff) in trees1 - for (t′, coeff′) in braid(t, levels, perm) + for (t′, coeff′) in braid(t, perm, levels) trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ end end From 42fa59be696001bad23e15dafc8076a5263ed03e Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 31 Jul 2025 12:04:58 -0400 Subject: [PATCH 05/39] refactor in terms of FusionTreeBlock --- src/fusiontrees/fusiontreeblocks.jl | 321 ++++++++++++++++++++++++++++ src/fusiontrees/fusiontrees.jl | 2 +- src/tensors/treetransformers.jl | 2 +- 3 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 src/fusiontrees/fusiontreeblocks.jl diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl new file mode 100644 index 000000000..6d83b9065 --- /dev/null +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -0,0 +1,321 @@ +struct FusionTreeBlock{I, N₁, N₂, F <: FusionTreePair{I, N₁, N₂}} + trees::Vector{F} +end + +function FusionTreeBlock( + uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, + isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}} + ) where {I <: Sector, N₁, N₂} + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + trees = Vector{Tuple{F₁, F₂}}(undef, 0) + + cleft = N₁ == 0 ? (one(I),) : ⊗(uncoupled[1]...) + cright = N₂ == 0 ? (one(I),) : ⊗(uncoupled[2]...) + cs = sort!(collect(intersect(cleft, cright))) + for c in cs + for f₁ in fusiontrees(uncoupled[1], c, isdual[1]), + f₂ in fusiontrees(uncoupled[2], c, isdual[2]) + + push!(trees, (f₁, f₂)) + end + end + return FusionTreeBlock(trees) +end + +Base.@constprop :aggressive function Base.getproperty(block::FusionTreeBlock, prop::Symbol) + if prop === :uncoupled + f₁, f₂ = first(block.trees) + return f₁.uncoupled, f₂.uncoupled + elseif prop === :isdual + f₁, f₂ = first(block.trees) + return f₁.isdual, f₂.isdual + else + return getfield(block, prop) + end +end + +Base.propertynames(::FusionTreeBlock, private::Bool = false) = (:trees, :uncoupled, :isdual) + +sectortype(::Type{<:FusionTreeBlock{I}}) where {I} = I +numout(fs::FusionTreeBlock) = numout(typeof(fs)) +numout(::Type{<:FusionTreeBlock{I, N₁}}) where {I, N₁} = N₁ +numin(fs::FusionTreeBlock) = numin(typeof(fs)) +numin(::Type{<:FusionTreeBlock{I, N₁, N₂}}) where {I, N₁, N₂} = N₂ +numind(fs::FusionTreeBlock) = numind(typeof(fs)) +numind(::Type{T}) where {T <: FusionTreeBlock} = numin(T) + numout(T) + +fusiontrees(block::FusionTreeBlock) = block.trees +Base.length(block::FusionTreeBlock) = length(fusiontrees(block)) + +# Manipulations +# ------------- +function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} + U = zeros(sectorscalartype(I), length(dst), length(src)) + indexmap = Dict(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + for (col, f) in enumerate(fusiontrees(src)) + for (f′, c) in transform(f) + row = indexmap[f′] + U[row, col] = c + end + end + return U +end + +function bendright(src::FusionTreeBlock) + uncoupled_dst = ( + TupleTools.front(src.uncoupled[1]), + (src.uncoupled[2]..., dual(src.uncoupled[1][end])), + ) + isdual_dst = ( + TupleTools.front(src.isdual[1]), + (src.isdual[2]..., !(src.isdual[1][end])), + ) + dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + + U = transformation_matrix(bendright, dst, src) + return dst, U +end + +# TODO: verify if this can be computed through an adjoint +function bendleft(src::FusionTreeBlock) + uncoupled_dst = ( + (src.uncoupled[1]..., dual(src.uncoupled[2][end])), + TupleTools.front(src.uncoupled[2]), + ) + isdual_dst = ( + (src.isdual[1]..., !(src.isdual[2][end])), + TupleTools.front(src.isdual[2]), + ) + dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + + U = transformation_matrix(bendleft, dst, src) + return dst, U +end + +function foldright(src::FusionTreeBlock) + uncoupled_dst = ( + Base.tail(src.uncoupled[1]), + (dual(first(src.uncoupled[1])), src.uncoupled[2]...), + ) + isdual_dst = ( + Base.tail(src.isdual[1]), + (!first(src.isdual[1]), src.isdual[2]...), + ) + dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + + U = transformation_matrix(foldright, dst, src) + return dst, U +end + +# TODO: verify if this can be computed through an adjoint +function foldleft(src::FusionTreeBlock) + uncoupled_dst = ( + (dual(first(src.uncoupled[2])), src.uncoupled[1]...), + Base.tail(src.uncoupled[2]), + ) + isdual_dst = ( + (!first(src.isdual[2]), src.isdual[1]...), + Base.tail(src.isdual[2]), + ) + dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + + U = transformation_matrix(foldleft, dst, src) + return dst, U +end + +function cycleclockwise(src::FusionTreeBlock) + if numout(src) > 0 + tmp, U₁ = foldright(src) + dst, U₂ = bendleft(tmp) + else + tmp, U₁ = bendleft(src) + dst, U₂ = foldright(tmp) + end + return dst, U₂ * U₁ +end + +function cycleanticlockwise(src::FusionTreeBlock) + if numin(src) > 0 + tmp, U₁ = foldleft(src) + dst, U₂ = bendright(tmp) + else + tmp, U₁ = bendright(src) + dst, U₂ = foldleft(tmp) + end + return dst, U₂ * U₁ +end + +@inline function repartition(src::FusionTreeBlock, N::Int) + @assert 0 <= N <= numind(src) + return _recursive_repartition(src, Val(N)) +end + +function _repartition_type(I, N, N₁, N₂) + return Tuple{FusionTreeBlock{I, N, N₁ + N₂ - N}, Matrix{sectorscalartype(I)}} +end +function _recursive_repartition( + src::FusionTreeBlock{I, N₁, N₂}, + ::Val{N} + )::_repartition_type(I, N, N₁, N₂) where {I, N₁, N₂, N} + if N == N₁ + dst = src + U = zeros(sectorscalartype(I), length(dst), length(src)) + copyto!(U, LinearAlgebra.I) + return dst, U + end + + N == N₁ - 1 && return bendright(src) + N == N₁ + 1 && return bendleft(src) + + tmp, U₁ = N < N₁ ? bendright(src) : bendleft(src) + dst, U₂ = _recursive_repartition(tmp, Val(N)) + return dst, U₂ * U₁ +end + +function Base.transpose(src::FusionTreeBlock, p::Index2Tuple{N₁, N₂}) where {N₁, N₂} + N = N₁ + N₂ + @assert numind(src) == N + p′ = linearizepermutation(p..., numout(src), numin(src)) + @assert iscyclicpermutation(p′) + return _fstranspose((src, p)) +end + +const _FSTransposeKey{I, N₁, N₂} = Tuple{<:FusionTreeBlock{I}, Index2Tuple{N₁, N₂}} + +@cached function _fstranspose( + key::_FSTransposeKey{I, N₁, N₂} + )::Tuple{FusionTreeBlock{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} + src, (p1, p2) = key + + N = N₁ + N₂ + p = linearizepermutation(p1, p2, numout(src), numin(src)) + + dst, U = repartition(src, N₁) + length(p) == 0 && return dst, U + i1 = findfirst(==(1), p)::Int + i1 == 1 && return dst, U + + Nhalf = N >> 1 + while 1 < i1 ≤ Nhalf + dst, U_tmp = cycleanticlockwise(dst) + U = U_tmp * U + i1 -= 1 + end + while Nhalf < i1 + dst, U_tmp = cycleclockwise(dst) + U = U_tmp * U + i1 = mod1(i1 + 1, N) + end + + return dst, U +end + +function CacheStyle(::typeof(_fstranspose), k::_FSTransposeKey{I}) where {I} + if FusionStyle(I) == UniqueFusion() + return NoCache() + else + return GlobalLRUCache() + end +end + +function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where {I, N} + 1 <= i < N || + throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) + + uncoupled = src.uncoupled[1] + uncoupled′ = TupleTools.setindex(uncoupled, uncoupled[i + 1], i) + uncoupled′ = TupleTools.setindex(uncoupled′, uncoupled[i], i + 1) + isdual = src.isdual[1] + isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) + isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) + dst = FusionTreeBlock((uncoupled′, ()), (isdual′, ())) + + # TODO: do we want to rewrite `artin_braid` to take double trees instead? + U = transformation_matrix(dst, src) do (f₁, f₂) + return ((f₁′, f₂) => c for (f₁′, c) in artin_braid(f₁, i; inv)) + end + return dst, U +end + +function braid(src::FusionTreeBlock{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{N, Int}) where {I, N} + TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) + + if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding + uncoupled′ = TupleTools._permute(src.uncoupled[1], p) + isdual′ = TupleTools._permute(src.isdual[1], p) + dst = FusionTreeBlock(uncoupled′, isdual′) + U = transformation_matrix(dst, src) do (f₁, f₂) + return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) + end + else + dst, U = repartition(src, N) # TODO: can we avoid this? + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + dst, U_tmp = artin_braid(dst, s; inv) + U = U_tmp * U + end + end + return dst, U +end + +function braid(src::FusionTreeBlock{I}, p::Index2Tuple{N₁, N₂}, levels::Index2Tuple) where {I, N₁, N₂} + @assert numind(src) == N₁ + N₂ + @assert numout(src) == length(levels[1]) && numin(src) == length(levels[2]) + @assert TupleTools.isperm((p[1]..., p[2]...)) + return _fsbraid((src, p, levels)) +end + +const _FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreeBlock{I}, Index2Tuple{N₁, N₂}, Index2Tuple} + +@cached function _fsbraid( + key::_FSBraidKey{I, N₁, N₂} + )::Tuple{FusionTreeBlock{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} + src, (p1, p2), (l1, l2) = key + + p = linearizepermutation(p1, p2, numout(src), numin(src)) + levels = (l1..., reverse(l2)...) + + dst, U = repartition(src, numind(src)) + + if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding + uncoupled′ = TupleTools._permute(dst.uncoupled[1], p) + isdual′ = TupleTools._permute(dst.isdual[1], p) + + dst′ = FusionTreeBlock(uncoupled′, isdual′) + U_tmp = transformation_matrix(dst′, dst) do (f₁, f₂) + return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) + end + dst = dst′ + U = U_tmp * U + else + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + dst, U_tmp = artin_braid(dst, s; inv) + U = U_tmp * U + end + end + + if N₂ == 0 + return dst, U + else + dst, U_tmp = repartition(dst, N₁) + U = U_tmp * U + return dst, U + end +end + +function CacheStyle(::typeof(_fsbraid), k::_FSBraidKey{I}) where {I} + if FusionStyle(I) isa UniqueFusion + return NoCache() + else + return GlobalLRUCache() + end +end + +function permute(src::FusionTreeBlock{I}, p::Index2Tuple) where {I} + @assert BraidingStyle(I) isa SymmetricBraiding + levels1 = ntuple(identity, numout(src)) + levels2 = numout(src) .+ ntuple(identity, numin(src)) + return braid(src, p, (levels1, levels2)) +end diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index defddf1ca..e20264a24 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -249,7 +249,7 @@ end # Fusion tree iterators include("iterator.jl") -include("uncouplediterator.jl") +include("fusiontreeblocks.jl") # Manipulate fusion trees include("manipulations.jl") diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index fe86c5296..9e45bb60b 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -72,7 +72,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) for cod_uncoupled_src in sectors(codomain(Vsrc)), dom_uncoupled_src in sectors(domain(Vsrc)) - fs_src = OuterTreeIterator((cod_uncoupled_src, dom_uncoupled_src), isdual_src) + fs_src = FusionTreeBlock((cod_uncoupled_src, dom_uncoupled_src), isdual_src) trees_src = fusiontrees(fs_src) isempty(trees_src) && continue From 7cd9fbc4798f0d3d0ba5fe775c2e8d1b345bd9fa Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 31 Jul 2025 19:21:13 -0400 Subject: [PATCH 06/39] Fix unbound type parameter --- src/fusiontrees/fusiontreeblocks.jl | 16 ++++++++-------- src/tensors/treetransformers.jl | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 6d83b9065..777504cf5 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -2,7 +2,7 @@ struct FusionTreeBlock{I, N₁, N₂, F <: FusionTreePair{I, N₁, N₂}} trees::Vector{F} end -function FusionTreeBlock( +function FusionTreeBlock{I}( uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}} ) where {I <: Sector, N₁, N₂} @@ -71,7 +71,7 @@ function bendright(src::FusionTreeBlock) TupleTools.front(src.isdual[1]), (src.isdual[2]..., !(src.isdual[1][end])), ) - dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) U = transformation_matrix(bendright, dst, src) return dst, U @@ -87,7 +87,7 @@ function bendleft(src::FusionTreeBlock) (src.isdual[1]..., !(src.isdual[2][end])), TupleTools.front(src.isdual[2]), ) - dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) U = transformation_matrix(bendleft, dst, src) return dst, U @@ -102,7 +102,7 @@ function foldright(src::FusionTreeBlock) Base.tail(src.isdual[1]), (!first(src.isdual[1]), src.isdual[2]...), ) - dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) U = transformation_matrix(foldright, dst, src) return dst, U @@ -118,7 +118,7 @@ function foldleft(src::FusionTreeBlock) (!first(src.isdual[2]), src.isdual[1]...), Base.tail(src.isdual[2]), ) - dst = FusionTreeBlock(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) U = transformation_matrix(foldleft, dst, src) return dst, U @@ -229,7 +229,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where isdual = src.isdual[1] isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) - dst = FusionTreeBlock((uncoupled′, ()), (isdual′, ())) + dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ())) # TODO: do we want to rewrite `artin_braid` to take double trees instead? U = transformation_matrix(dst, src) do (f₁, f₂) @@ -244,7 +244,7 @@ function braid(src::FusionTreeBlock{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{ if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding uncoupled′ = TupleTools._permute(src.uncoupled[1], p) isdual′ = TupleTools._permute(src.isdual[1], p) - dst = FusionTreeBlock(uncoupled′, isdual′) + dst = FusionTreeBlock{I}(uncoupled′, isdual′) U = transformation_matrix(dst, src) do (f₁, f₂) return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) end @@ -282,7 +282,7 @@ const _FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreeBlock{I}, Index2Tuple{N₁, uncoupled′ = TupleTools._permute(dst.uncoupled[1], p) isdual′ = TupleTools._permute(dst.isdual[1], p) - dst′ = FusionTreeBlock(uncoupled′, isdual′) + dst′ = FusionTreeBlock{I}(uncoupled′, isdual′) U_tmp = transformation_matrix(dst′, dst) do (f₁, f₂) return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) end diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 9e45bb60b..a5597eee4 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -72,7 +72,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) for cod_uncoupled_src in sectors(codomain(Vsrc)), dom_uncoupled_src in sectors(domain(Vsrc)) - fs_src = FusionTreeBlock((cod_uncoupled_src, dom_uncoupled_src), isdual_src) + fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) trees_src = fusiontrees(fs_src) isempty(trees_src) && continue From 59f8188cf299b0a4a6df43f900014d04e4edf4b2 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 31 Jul 2025 19:22:27 -0400 Subject: [PATCH 07/39] refactor repartition to unroll loop --- src/fusiontrees/fusiontreeblocks.jl | 54 +++++++++++++++++++---------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 777504cf5..2ec718746 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -148,29 +148,45 @@ end @inline function repartition(src::FusionTreeBlock, N::Int) @assert 0 <= N <= numind(src) - return _recursive_repartition(src, Val(N)) + return repartition(src, Val(N)) end -function _repartition_type(I, N, N₁, N₂) - return Tuple{FusionTreeBlock{I, N, N₁ + N₂ - N}, Matrix{sectorscalartype(I)}} -end -function _recursive_repartition( - src::FusionTreeBlock{I, N₁, N₂}, - ::Val{N} - )::_repartition_type(I, N, N₁, N₂) where {I, N₁, N₂, N} - if N == N₁ - dst = src - U = zeros(sectorscalartype(I), length(dst), length(src)) - copyto!(U, LinearAlgebra.I) - return dst, U - end +#= +Using a generated function here to ensure type stability by unrolling the loops: +```julia +dst, U = bendleft/right(src) - N == N₁ - 1 && return bendright(src) - N == N₁ + 1 && return bendleft(src) +# repeat the following 2 lines N - 1 times +dst, Utmp = bendleft/right(dst) +U = Utmp * U - tmp, U₁ = N < N₁ ? bendright(src) : bendleft(src) - dst, U₂ = _recursive_repartition(tmp, Val(N)) - return dst, U₂ * U₁ +return dst, U +``` +=# +@generated function repartition(src::FusionTreeBlock, ::Val{N}) where {N} + return _repartition_body(numout(src) - N) +end +function _repartition_body(N) + if N == 0 + ex = quote + T = sectorscalartype(sectortype(src)) + U = copyto!(zeros(T, length(src), length(src)), LinearAlgebra.I) + return src, U + end + else + f = N < 0 ? bendleft : bendright + ex_rep = Expr(:block) + for _ in 1:(abs(N) - 1) + push!(ex_rep.args, :((dst, Utmp) = $f(dst))) + push!(ex_rep.args, :(U = Utmp * U)) + end + ex = quote + dst, U = $f(src) + $ex_rep + return dst, U + end + end + return ex end function Base.transpose(src::FusionTreeBlock, p::Index2Tuple{N₁, N₂}) where {N₁, N₂} From 260688b7c0cfdcbf0b6cac923a1493d0041edb76 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Aug 2025 14:15:52 +0200 Subject: [PATCH 08/39] dont depend on intricate scoping rules --- src/fusiontrees/fusiontreeblocks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 2ec718746..38f454205 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -262,7 +262,7 @@ function braid(src::FusionTreeBlock{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{ isdual′ = TupleTools._permute(src.isdual[1], p) dst = FusionTreeBlock{I}(uncoupled′, isdual′) U = transformation_matrix(dst, src) do (f₁, f₂) - return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) + return ((f₁′, f₂) => c for (f₁′, c) in braid(f₁, p, levels)) end else dst, U = repartition(src, N) # TODO: can we avoid this? From cb8ab0d7257e9668c5abbff34b5ed63c260e596c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Aug 2025 14:16:16 +0200 Subject: [PATCH 09/39] Refactor bendright to avoid extra dictionary --- src/fusiontrees/fusiontreeblocks.jl | 50 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 38f454205..f3da8c27d 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -71,9 +71,55 @@ function bendright(src::FusionTreeBlock) TupleTools.front(src.isdual[1]), (src.isdual[2]..., !(src.isdual[1][end])), ) - dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) + I = sectortype(src) + N₁ = numout(src) + N₂ = numin(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) + c = f₁.coupled + a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : + (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + b = f₁.uncoupled[N₁] + + uncoupled1 = TupleTools.front(f₁.uncoupled) + isdual1 = TupleTools.front(f₁.isdual) + inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () + vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () + f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) + + uncoupled2 = (f₂.uncoupled..., dual(b)) + isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) + inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () + + coeff₀ = sqrtdim(c) * invsqrtdim(a) + if f₁.isdual[N₁] + coeff₀ *= conj(frobeniusschur(dual(b))) + end + if FusionStyle(I) isa MultiplicityFreeFusion + coeff = coeff₀ * Bsymbol(a, b, c) + vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[(f₁′, f₂′)] + @inbounds U[row, col] = coeff + else + Bmat = Bsymbol(a, b, c) + μ = N₁ > 1 ? f₁.vertices[end] : 1 + for ν in axes(Bmat, 2) + coeff = coeff₀ * Bmat[μ, ν] + iszero(coeff) && continue + vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[(f₁′, f₂′)] + @inbounds U[row, col] = coeff + end + end + end - U = transformation_matrix(bendright, dst, src) return dst, U end From 1ae260832a09a6a97674d51d6441976a09450b76 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Aug 2025 14:24:30 +0200 Subject: [PATCH 10/39] Refactor bendleft to avoid extra dictionaries --- src/fusiontrees/fusiontreeblocks.jl | 53 +++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index f3da8c27d..715b591ab 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -123,7 +123,8 @@ function bendright(src::FusionTreeBlock) return dst, U end -# TODO: verify if this can be computed through an adjoint +# !! note that this is more or less a copy of bendright through +# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in bendleft(src) function bendleft(src::FusionTreeBlock) uncoupled_dst = ( (src.uncoupled[1]..., dual(src.uncoupled[2][end])), @@ -133,9 +134,55 @@ function bendleft(src::FusionTreeBlock) (src.isdual[1]..., !(src.isdual[2][end])), TupleTools.front(src.isdual[2]), ) - dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) + I = sectortype(src) + N₁ = numin(src) + N₂ = numout(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) + c = f₁.coupled + a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : + (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + b = f₁.uncoupled[N₁] + + uncoupled1 = TupleTools.front(f₁.uncoupled) + isdual1 = TupleTools.front(f₁.isdual) + inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () + vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () + f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) + + uncoupled2 = (f₂.uncoupled..., dual(b)) + isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) + inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () + + coeff₀ = sqrtdim(c) * invsqrtdim(a) + if f₁.isdual[N₁] + coeff₀ *= conj(frobeniusschur(dual(b))) + end + if FusionStyle(I) isa MultiplicityFreeFusion + coeff = coeff₀ * Bsymbol(a, b, c) + vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[(f₂′, f₁′)] + @inbounds U[row, col] = conj(coeff) + else + Bmat = Bsymbol(a, b, c) + μ = N₁ > 1 ? f₁.vertices[end] : 1 + for ν in axes(Bmat, 2) + coeff = coeff₀ * Bmat[μ, ν] + iszero(coeff) && continue + vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[(f₂′, f₁′)] + @inbounds U[row, col] = conj(coeff) + end + end + end - U = transformation_matrix(bendleft, dst, src) return dst, U end From 6edf83cfd4d8d99a6a01c872dc613ddf5b9f1c7c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Aug 2025 14:32:35 +0200 Subject: [PATCH 11/39] Refactor foldright to avoid extra dictionaries --- src/fusiontrees/fusiontreeblocks.jl | 63 ++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 715b591ab..a4e4f0da3 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -191,13 +191,66 @@ function foldright(src::FusionTreeBlock) Base.tail(src.uncoupled[1]), (dual(first(src.uncoupled[1])), src.uncoupled[2]...), ) - isdual_dst = ( - Base.tail(src.isdual[1]), - (!first(src.isdual[1]), src.isdual[2]...), - ) + isdual_dst = (Base.tail(src.isdual[1]), (!first(src.isdual[1]), src.isdual[2]...)) + I = sectortype(src) + N₁ = numout(src) + N₂ = numin(src) + @assert N₁ > 0 dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) - U = transformation_matrix(foldright, dst, src) + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) + # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) + a = f₁.uncoupled[1] + isduala = f₁.isdual[1] + factor = sqrtdim(a) + if !isduala + factor *= conj(frobeniusschur(a)) + end + c1 = dual(a) + c2 = f₁.coupled + uncoupled = Base.tail(f₁.uncoupled) + isdual = Base.tail(f₁.isdual) + if FusionStyle(I) isa UniqueFusion + c = first(c1 ⊗ c2) + fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) + fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) + row = indexmap[(fl, fr)] + @inbounds U[row, col] = factor + else + if N₁ == 1 + cset = (leftone(c1),) # or rightone(a) + elseif N₁ == 2 + cset = (f₁.uncoupled[2],) + else + cset = ⊗(Base.tail(f₁.uncoupled)...) + end + for c in c1 ⊗ c2 + c ∈ cset || continue + for μ in 1:Nsymbol(c1, c2, c) + fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + for (fl′, coeff1) in insertat(fc, 2, f₁) + N₁ > 1 && !isone(fl′.innerlines[1]) && continue + coupled = fl′.coupled + uncoupled = Base.tail(Base.tail(fl′.uncoupled)) + isdual = Base.tail(Base.tail(fl′.isdual)) + inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) + vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) + fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) + for (fr, coeff2) in insertat(fc, 2, f₂) + coeff = factor * coeff1 * conj(coeff2) + row = indexmap[(fl, fr)] + @inbounds U[row, col] = coeff + end + end + end + end + end + end + return dst, U end From 10c04ae50bf0a8cc36255fa5c69742be608b9c49 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Aug 2025 14:54:54 +0200 Subject: [PATCH 12/39] Refactor foldleft to avoid extra dictionaries --- src/fusiontrees/fusiontreeblocks.jl | 61 +++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index a4e4f0da3..e66cab42b 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -254,7 +254,8 @@ function foldright(src::FusionTreeBlock) return dst, U end -# TODO: verify if this can be computed through an adjoint +# !! note that this is more or less a copy of foldright through +# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in foldright(src) function foldleft(src::FusionTreeBlock) uncoupled_dst = ( (dual(first(src.uncoupled[2])), src.uncoupled[1]...), @@ -264,9 +265,63 @@ function foldleft(src::FusionTreeBlock) (!first(src.isdual[2]), src.isdual[1]...), Base.tail(src.isdual[2]), ) - dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) + I = sectortype(src) + N₁ = numin(src) + N₂ = numout(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + U = zeros(sectorscalartype(I), length(dst), length(src)) - U = transformation_matrix(foldleft, dst, src) + for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) + # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) + a = f₁.uncoupled[1] + isduala = f₁.isdual[1] + factor = sqrtdim(a) + if !isduala + factor *= conj(frobeniusschur(a)) + end + c1 = dual(a) + c2 = f₁.coupled + uncoupled = Base.tail(f₁.uncoupled) + isdual = Base.tail(f₁.isdual) + if FusionStyle(I) isa UniqueFusion + c = first(c1 ⊗ c2) + fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) + fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) + row = indexmap[(fr, fl)] + @inbounds U[row, col] = conj(factor) + else + if N₁ == 1 + cset = (leftone(c1),) # or rightone(a) + elseif N₁ == 2 + cset = (f₁.uncoupled[2],) + else + cset = ⊗(Base.tail(f₁.uncoupled)...) + end + for c in c1 ⊗ c2 + c ∈ cset || continue + for μ in 1:Nsymbol(c1, c2, c) + fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + for (fl′, coeff1) in insertat(fc, 2, f₁) + N₁ > 1 && !isone(fl′.innerlines[1]) && continue + coupled = fl′.coupled + uncoupled = Base.tail(Base.tail(fl′.uncoupled)) + isdual = Base.tail(Base.tail(fl′.isdual)) + inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) + vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) + fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) + for (fr, coeff2) in insertat(fc, 2, f₂) + coeff = factor * coeff1 * conj(coeff2) + row = indexmap[(fr, fl)] + @inbounds U[row, col] = conj(coeff) + end + end + end + end + end + end return dst, U end From 2cfe587c762a0fb8988aa5577f58e321dabae48d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Aug 2025 14:56:22 +0200 Subject: [PATCH 13/39] remove unused variable --- src/fusiontrees/manipulations.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 7ff834692..7e59662b4 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -356,7 +356,6 @@ function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) return fusiontreedict(I)((fl, fr) => factor) else - hasmultiplicities = FusionStyle(a) isa GenericFusion local newtrees if N₁ == 1 cset = (leftunit(c1),) # or rightunit(a) From cf52331e39040229f7894e7cd9293de85a0e52e8 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 14 Aug 2025 09:48:56 +0200 Subject: [PATCH 14/39] some docs fixes --- docs/make.jl | 1 + docs/src/man/sectors.md | 6 +++--- src/fusiontrees/manipulations.jl | 20 ++++++++------------ src/tensors/linalg.jl | 2 -- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 057981577..55234c2c9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,7 @@ using Documenter using Random using TensorKit +using TensorKit: FusionTreePair, Index2Tuple using TensorKit.TensorKitSectors using TensorKit.MatrixAlgebraKit using DocumenterInterLinks diff --git a/docs/src/man/sectors.md b/docs/src/man/sectors.md index 73c1630a0..b2fea3a6c 100644 --- a/docs/src/man/sectors.md +++ b/docs/src/man/sectors.md @@ -1159,7 +1159,7 @@ the splitting tree. The `FusionTree` interface to duality and line bending is given by -[`repartition(f1::FusionTree{I,N₁}, f2::FusionTree{I,N₂}, N::Int)`](@ref repartition) +[`repartition(f1::FusionTreePair{I,N₁,N₂}, N::Int)`](@ref repartition) which takes a splitting tree `f1` with `N₁` outgoing sectors, a fusion tree `f2` with `N₂` incoming sectors, and applies line bending such that the resulting splitting and fusion @@ -1184,7 +1184,7 @@ With this basic function, we can now perform arbitrary combinations of braids or permutations with line bendings, to completely reshuffle where sectors appear. The interface provided for this is given by -[`braid(f1::FusionTree{I,N₁}, f2::FusionTree{I,N₂}, levels1::NTuple{N₁,Int}, levels2::NTuple{N₂,Int}, p1::NTuple{N₁′,Int}, p2::NTuple{N₂′,Int})`](@ref braid(::FusionTree{I}, ::FusionTree{I}, ::IndexTuple, ::IndexTuple, ::IndexTuple{N₁}, ::IndexTuple{N₂}) where {I<:Sector,N₁,N₂}) +[`braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple)`](@ref braid(::TensorKit.FusionTreePair, ::Index2Tuple, ::Index2Tuple)) where we now have splitting tree `f1` with `N₁` outgoing sectors, a fusion tree `f2` with `N₂` incoming sectors, `levels1` and `levels2` assign a level or depth to the corresponding @@ -1211,7 +1211,7 @@ As before, there is a simplified interface for the case where `BraidingStyle(I) isa SymmetricBraiding` and the levels are not needed. This is simply given by -[`permute(f1::FusionTree{I,N₁}, f2::FusionTree{I,N₂}, p1::NTuple{N₁′,Int}, p2::NTuple{N₂′,Int})`](@ref permute(::FusionTree{I}, ::FusionTree{I}, ::IndexTuple{N₁}, ::IndexTuple{N₂}) where {I<:Sector,N₁,N₂}) +[`permute((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple)`](@ref permute(::FusionTreePair, ::Index2Tuple)) The `braid` and `permute` routines for double fusion trees will be the main access point for corresponding manipulations on tensors. As a consequence, results from this routine are diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 7e59662b4..d99bfcab3 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -489,9 +489,9 @@ outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical cou repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have `N` outgoing sectors. """ -@inline function repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, N::Int) where {I, N₁, N₂} +@inline function repartition((f₁, f₂)::FusionTreePair, N::Int) f₁.coupled == f₂.coupled || throw(SectorMismatch()) - @assert 0 <= N <= N₁ + N₂ + @assert 0 <= N <= length(f₁) + length(f₂) return _recursive_repartition((f₁, f₂), Val(N)) end @@ -1008,8 +1008,7 @@ end # braid double fusion tree """ - braid((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁,N₂}, - (levels1, levels2)::Index2Tuple) where {I,N₁,N₂} + braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} Input is a fusion-splitting tree pair that describes the fusion of a set of incoming @@ -1024,11 +1023,8 @@ respectively, which determines how indices braid. In particular, if `i` and `j` levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid( - (f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}, - (levels1, levels2)::Index2Tuple - ) where {I, N₁, N₂} - @assert length(f₁) + length(f₂) == N₁ + N₂ +function braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) + @assert length(f₁) + length(f₂) == length(p1) + length(p2) @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) return fsbraid(((f₁, f₂), (p1, p2), (levels1, levels2))) @@ -1064,7 +1060,7 @@ function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I <: Sector} end """ - permute((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} + permute((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple) -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} Input is a double fusion tree that describes the fusion of a set of incoming uncoupled @@ -1074,8 +1070,8 @@ outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors `p2` become incoming. """ -function permute((f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} - @assert BraidingStyle(I) isa SymmetricBraiding +function permute((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple) + @assert BraidingStyle(sectortype(f₁)) isa SymmetricBraiding levels1 = ntuple(identity, length(f₁)) levels2 = length(f₁) .+ ntuple(identity, length(f₂)) return braid((f₁, f₂), (p1, p2), (levels1, levels2)) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index d09609edc..592a40a5b 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -73,8 +73,6 @@ end Construct the identity endomorphism on space `V`, i.e. return a `t::TensorMap` with `domain(t) == codomain(t) == V`, where either `scalartype(t) = T` if `T` is a `Number` type or `storagetype(t) = T` if `T` is a `DenseVector` type. - -See also [`one!`](@ref). """ id, id! id(V::TensorSpace) = id(Float64, V) From 47cbe1124df9b584b48a1fbba9b77f5e56b66fe2 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 14 Aug 2025 10:02:04 +0200 Subject: [PATCH 15/39] Avoid using `one(I)` --- src/fusiontrees/fusiontreeblocks.jl | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index e66cab42b..1b1601565 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -10,9 +10,16 @@ function FusionTreeBlock{I}( F₂ = fusiontreetype(I, N₂) trees = Vector{Tuple{F₁, F₂}}(undef, 0) - cleft = N₁ == 0 ? (one(I),) : ⊗(uncoupled[1]...) - cright = N₂ == 0 ? (one(I),) : ⊗(uncoupled[2]...) - cs = sort!(collect(intersect(cleft, cright))) + if N₁ == N₂ == 0 + return FusionTreeBlock(trees) + elseif N₁ == 0 + cs = sort!(collect(filter(isone, ⊗(uncoupled[2]...)))) + elseif N₂ == 0 + cs = sort!(collect(filter(isone, ⊗(uncoupled[1]...)))) + else + cs = sort!(collect(intersect(⊗(uncoupled[1]...), ⊗(uncoupled[2]...)))) + end + for c in cs for f₁ in fusiontrees(uncoupled[1], c, isdual[1]), f₂ in fusiontrees(uncoupled[2], c, isdual[2]) From 83d9a2a93671b4c58ad0b44150bd8042a4aa7146 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 14 Aug 2025 10:26:19 +0200 Subject: [PATCH 16/39] format --- src/fusiontrees/manipulations.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index d99bfcab3..79b3b125e 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -1023,7 +1023,8 @@ respectively, which determines how indices braid. In particular, if `i` and `j` levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) +function braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, + (levels1, levels2)::Index2Tuple) @assert length(f₁) + length(f₂) == length(p1) + length(p2) @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) From ecdaa9412273613627765ba3992b11c10946f918 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 15 Aug 2025 11:59:56 +0200 Subject: [PATCH 17/39] Move independent computations out of loop --- src/fusiontrees/fusiontreeblocks.jl | 6 ++++-- src/fusiontrees/manipulations.jl | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 1b1601565..d353aedb4 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -239,6 +239,7 @@ function foldright(src::FusionTreeBlock) c ∈ cset || continue for μ in 1:Nsymbol(c1, c2, c) fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + frs_coeffs = insertat(fc, 2, f₂) for (fl′, coeff1) in insertat(fc, 2, f₁) N₁ > 1 && !isone(fl′.innerlines[1]) && continue coupled = fl′.coupled @@ -247,7 +248,7 @@ function foldright(src::FusionTreeBlock) inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in insertat(fc, 2, f₂) + for (fr, coeff2) in frs_coeffs coeff = factor * coeff1 * conj(coeff2) row = indexmap[(fl, fr)] @inbounds U[row, col] = coeff @@ -311,6 +312,7 @@ function foldleft(src::FusionTreeBlock) c ∈ cset || continue for μ in 1:Nsymbol(c1, c2, c) fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + fr_coeffs = insertat(fc, 2, f₂) for (fl′, coeff1) in insertat(fc, 2, f₁) N₁ > 1 && !isone(fl′.innerlines[1]) && continue coupled = fl′.coupled @@ -319,7 +321,7 @@ function foldleft(src::FusionTreeBlock) inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in insertat(fc, 2, f₂) + for (fr, coeff2) in fr_coeffs coeff = factor * coeff1 * conj(coeff2) row = indexmap[(fr, fl)] @inbounds U[row, col] = conj(coeff) diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index 79b3b125e..fadf6befa 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -368,6 +368,7 @@ function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, c ∈ cset || continue for μ in 1:Nsymbol(c1, c2, c) fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + fr_coeffs = insertat(fc, 2, f₂) for (fl′, coeff1) in insertat(fc, 2, f₁) N₁ > 1 && !isunit(fl′.innerlines[1]) && continue coupled = fl′.coupled @@ -376,7 +377,7 @@ function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in insertat(fc, 2, f₂) + for (fr, coeff2) in fr_coeffs coeff = factor * coeff1 * conj(coeff2) if (@isdefined newtrees) newtrees[(fl, fr)] = get(newtrees, (fl, fr), zero(coeff)) + From fdcaddcc260a29b681293ef7ee65bdb65198f708 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 15 Aug 2025 12:12:19 +0200 Subject: [PATCH 18/39] add utility fusiontreetype --- src/fusiontrees/fusiontrees.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index e20264a24..0ea90a3e0 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -158,6 +158,9 @@ function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} FusionTree{I, N, N - 2, N - 1} end end +function fusiontreetype(::Type{I}, N₁::Int, N₂::Int) where {I<:Sector} + return Tuple{fusiontreetype(I, N₁),fusiontreetype(I, N₂)} +end # converting to actual array function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, 0}) where {I} From fb8d815ded44a7ec4f4544dc8ae2a0e36d50e3d8 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 15 Aug 2025 12:12:32 +0200 Subject: [PATCH 19/39] add multithreaded treetransformer implementation --- src/fusiontrees/fusiontrees.jl | 4 +- src/fusiontrees/manipulations.jl | 3 +- src/tensors/treetransformers.jl | 110 ++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 0ea90a3e0..3019d4b53 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -158,8 +158,8 @@ function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} FusionTree{I, N, N - 2, N - 1} end end -function fusiontreetype(::Type{I}, N₁::Int, N₂::Int) where {I<:Sector} - return Tuple{fusiontreetype(I, N₁),fusiontreetype(I, N₂)} +function fusiontreetype(::Type{I}, N₁::Int, N₂::Int) where {I <: Sector} + return Tuple{fusiontreetype(I, N₁), fusiontreetype(I, N₂)} end # converting to actual array diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index fadf6befa..f3fe6ebb2 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -1024,8 +1024,7 @@ respectively, which determines how indices braid. In particular, if `i` and `j` levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, - (levels1, levels2)::Index2Tuple) +function braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) @assert length(f₁) + length(f₂) == length(p1) + length(p2) @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) @assert TupleTools.isperm((p1..., p2...)) diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index a5597eee4..8820e049a 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -66,34 +66,96 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) I = sectortype(Vsrc) T = sectorscalartype(I) N = numind(Vdst) - data = Vector{_GenericTransformerData{T, N}}() isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) - for cod_uncoupled_src in sectors(codomain(Vsrc)), - dom_uncoupled_src in sectors(domain(Vsrc)) - fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) - trees_src = fusiontrees(fs_src) - isempty(trees_src) && continue - - fs_dst, U = transform(fs_src) - matrix = copy(transpose(U)) # TODO: should we avoid this - - inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) - trees_dst = fusiontrees(fs_dst) - inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) - - # size is shared between blocks, so repack: - # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) - sz_src, newstructs_src = repack_transformer_structure(fusionstructure_src, inds_src) - sz_dst, newstructs_dst = repack_transformer_structure(fusionstructure_dst, inds_dst) - - @debug( - "Created recoupling block for uncoupled: $uncoupled", - sz = size(matrix), sparsity = count(!iszero, matrix) / length(matrix) - ) + nthreads = get_num_transformer_threads() + if nthreads > 1 + fusiontreeblocks = Vector{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}}() + for cod_uncoupled_src in sectors(codomain(Vsrc)), + dom_uncoupled_src in sectors(domain(Vsrc)) + + fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) + trees_src = fusiontrees(fs_src) + if !isempty(trees_src) + push!(fusiontreeblocks, fs_src) + end + end - push!(data, (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src))) + data = Vector{_GenericTransformerData{T, N}}(undef, length(fusiontreeblocks)) + counter = Threads.Atomic{Int}(1) + Threads.@sync for _ in 1:min(nthreads, length(fusiontreeblocks)) + Threads.@spawn begin + while true + local_counter = Threads.atomic_add!(counter, 1) + local_counter > nblocks && break + fs_src = fusiontreeblocks[local_counter] + fs_dst, U = transform(fs_src) + matrix = copy(transpose(U)) # TODO: should we avoid this + + inds_src = map( + Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src + ) + trees_dst = fusiontrees(fs_dst) + inds_dst = map( + Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst + ) + + # size is shared between blocks, so repack: + # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) + sz_src, newstructs_src = repack_transformer_structure( + fusionstructure_src, inds_src + ) + sz_dst, newstructs_dst = repack_transformer_structure( + fusionstructure_dst, inds_dst + ) + + @debug( + "Created recoupling block for uncoupled: $uncoupled", + sz = size(matrix), + sparsity = count(!iszero, matrix) / length(matrix) + ) + + data[local_counter] = ( + matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src), + ) + end + end + end + else + data = Vector{_GenericTransformerData{T, N}}() + + isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) + for cod_uncoupled_src in sectors(codomain(Vsrc)), + dom_uncoupled_src in sectors(domain(Vsrc)) + + fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) + trees_src = fusiontrees(fs_src) + isempty(trees_src) && continue + + fs_dst, U = transform(fs_src) + matrix = copy(transpose(U)) # TODO: should we avoid this + + inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) + trees_dst = fusiontrees(fs_dst) + inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) + + # size is shared between blocks, so repack: + # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) + sz_src, newstructs_src = repack_transformer_structure( + fusionstructure_src, inds_src + ) + sz_dst, newstructs_dst = repack_transformer_structure( + fusionstructure_dst, inds_dst + ) + + @debug( + "Created recoupling block for uncoupled: $uncoupled", + sz = size(matrix), sparsity = count(!iszero, matrix) / length(matrix) + ) + + push!(data, (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src))) + end end transformer = GenericTreeTransformer{T, N}(data) From f30305024307f1c0df9609032a925c33c45b588a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 15 Aug 2025 13:00:24 +0200 Subject: [PATCH 20/39] refactor treeindex_map --- src/fusiontrees/fusiontreeblocks.jl | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index d353aedb4..cf329e8a0 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -55,11 +55,17 @@ numind(::Type{T}) where {T <: FusionTreeBlock} = numin(T) + numout(T) fusiontrees(block::FusionTreeBlock) = block.trees Base.length(block::FusionTreeBlock) = length(fusiontrees(block)) +function treeindex_map(fs::FusionTreeBlock) + I = sectortype(fs) + return fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(fs))) +end + + # Manipulations # ------------- function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} U = zeros(sectorscalartype(I), length(dst), length(src)) - indexmap = Dict(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + indexmap = treeindex_map(dst) for (col, f) in enumerate(fusiontrees(src)) for (f′, c) in transform(f) row = indexmap[f′] @@ -84,7 +90,7 @@ function bendright(src::FusionTreeBlock) @assert N₁ > 0 dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) - indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) @@ -147,7 +153,7 @@ function bendleft(src::FusionTreeBlock) @assert N₁ > 0 dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) - indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) @@ -206,7 +212,7 @@ function foldright(src::FusionTreeBlock) dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) - indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) @@ -279,7 +285,7 @@ function foldleft(src::FusionTreeBlock) @assert N₁ > 0 dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) - indexmap = fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(dst))) + indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) From 77e484c41ed7cd6acda8b6fba57826b86f84520a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sat, 16 Aug 2025 18:06:26 +0200 Subject: [PATCH 21/39] Refactor artin_braid to avoid extra dicts --- src/fusiontrees/fusiontreeblocks.jl | 125 +++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 4 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index cf329e8a0..2f595467f 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -60,7 +60,6 @@ function treeindex_map(fs::FusionTreeBlock) return fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(fs))) end - # Manipulations # ------------- function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} @@ -463,10 +462,128 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ())) - # TODO: do we want to rewrite `artin_braid` to take double trees instead? - U = transformation_matrix(dst, src) do (f₁, f₂) - return ((f₁′, f₂) => c for (f₁′, c) in artin_braid(f₁, i; inv)) + indexmap = treeindex_map(dst) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f, f₂)) in enumerate(fusiontrees(src)) + a, b = uncoupled[i], uncoupled[i + 1] + uncoupled′ = TupleTools.setindex(uncoupled, b, i) + uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) + coupled′ = f.coupled + isdual′ = TupleTools.setindex(f.isdual, f.isdual[i], i + 1) + isdual′ = TupleTools.setindex(isdual′, f.isdual[i + 1], i) + inner = f.innerlines + inner_extended = (uncoupled[1], inner..., coupled′) + vertices = f.vertices + oneT = one(sectorscalartype(I)) + + if isone(uncoupled[i]) || isone(uncoupled[i + 1]) + # braiding with trivial sector: simple and always possible + inner′ = inner + vertices′ = vertices + if i > 1 # we also need to alter innerlines and vertices + inner′ = TupleTools.setindex(inner, + inner_extended[isone(a) ? (i + 1) : (i - 1)], + i - 1) + vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) + vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) + end + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + row = indexmap[(f′, f₂)] + @inbounds U[row, col] = oneT + continue + end + + BraidingStyle(I) isa NoBraiding && + throw(SectorMismatch("Cannot braid sectors $(uncoupled[i]) and $(uncoupled[i + 1])")) + + if i == 1 + c = N > 2 ? inner[1] : coupled′ + if FusionStyle(I) isa MultiplicityFreeFusion + R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) + row = indexmap[(f′, f₂)] + @inbounds U[row, col] = R + else # GenericFusion + μ = vertices[1] + Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) + local newtrees + for ν in axes(Rmat, 2) + R = oftype(oneT, Rmat[μ, ν]) + iszero(R) && continue + vertices′ = TupleTools.setindex(vertices, ν, 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) + row = indexmap[(f′, f₂)] + @inbounds U[row, col] = R + end + end + continue + end + # case i > 1: other naming convention + b = uncoupled[i] + d = uncoupled[i + 1] + a = inner_extended[i - 1] + c = inner_extended[i] + e = inner_extended[i + 1] + if FusionStyle(I) isa UniqueFusion + c′ = first(a ⊗ d) + coeff = oftype(oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * + Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * + conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + row = indexmap[(f′, f₂)] + @inbounds U[row, col] = coeff + elseif FusionStyle(I) isa SimpleFusion + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs + coeff = oftype(oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * + Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * + conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end) + iszero(coeff) && continue + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + row = indexmap[(f′, f₂)] + @inbounds U[row, col] = coeff + end + else # GenericFusion + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs + Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) + Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) + Fmat = Fsymbol(d, a, b, e, c′, c) + μ = vertices[i - 1] + ν = vertices[i] + for σ in 1:Nsymbol(a, d, c′) + for λ in 1:Nsymbol(c′, b, e) + coeff = zero(oneT) + for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) + coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * + conj(Rmat2[σ, κ]) + end + iszero(coeff) && continue + vertices′ = TupleTools.setindex(vertices, σ, i - 1) + vertices′ = TupleTools.setindex(vertices′, λ, i) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + row = indexmap[(f′, f₂)] + @inbounds U[row, col] = coeff + end + end + end + end end + return dst, U end From cea4e1cd892c76658375ba1f093dc2003816f07b Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sat, 16 Aug 2025 18:07:38 +0200 Subject: [PATCH 22/39] type stability improvements --- src/fusiontrees/fusiontreeblocks.jl | 42 ++++++++++++++--------------- src/fusiontrees/fusiontrees.jl | 4 +-- src/fusiontrees/manipulations.jl | 6 +++-- src/tensors/treetransformers.jl | 34 +++++++++-------------- 4 files changed, 40 insertions(+), 46 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 2f595467f..04c446492 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -482,9 +482,9 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where inner′ = inner vertices′ = vertices if i > 1 # we also need to alter innerlines and vertices - inner′ = TupleTools.setindex(inner, - inner_extended[isone(a) ? (i + 1) : (i - 1)], - i - 1) + inner′ = TupleTools.setindex( + inner, inner_extended[isone(a) ? (i + 1) : (i - 1)], i - 1 + ) vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) end @@ -527,14 +527,14 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where e = inner_extended[i + 1] if FusionStyle(I) isa UniqueFusion c′ = first(a ⊗ d) - coeff = oftype(oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * - Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * - conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) row = indexmap[(f′, f₂)] @@ -542,14 +542,14 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where elseif FusionStyle(I) isa SimpleFusion cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) for c′ in cs - coeff = oftype(oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * - Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * - conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) iszero(coeff) && continue inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) @@ -569,7 +569,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where coeff = zero(oneT) for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * - conj(Rmat2[σ, κ]) + conj(Rmat2[σ, κ]) end iszero(coeff) && continue vertices′ = TupleTools.setindex(vertices, σ, i - 1) @@ -619,7 +619,7 @@ const _FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreeBlock{I}, Index2Tuple{N₁, @cached function _fsbraid( key::_FSBraidKey{I, N₁, N₂} - )::Tuple{FusionTreeBlock{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} + )::Tuple{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} src, (p1, p2), (l1, l2) = key p = linearizepermutation(p1, p2, numout(src), numin(src)) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 3019d4b53..f4e6c1a48 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -149,7 +149,7 @@ end Base.:(==)(f₁::FusionTree, f₂::FusionTree) = false # Facilitate getting correct fusion tree types -function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} +Base.@assume_effects :foldable function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} return if N === 0 FusionTree{I, 0, 0, 0} elseif N === 1 @@ -158,7 +158,7 @@ function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} FusionTree{I, N, N - 2, N - 1} end end -function fusiontreetype(::Type{I}, N₁::Int, N₂::Int) where {I <: Sector} +Base.@assume_effects :foldable function fusiontreetype(::Type{I}, N₁::Int, N₂::Int) where {I <: Sector} return Tuple{fusiontreetype(I, N₁), fusiontreetype(I, N₂)} end diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl index f3fe6ebb2..ae87acaab 100644 --- a/src/fusiontrees/manipulations.jl +++ b/src/fusiontrees/manipulations.jl @@ -890,7 +890,8 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} return fusiontreedict(I)(f′ => coeff) elseif FusionStyle(I) isa SimpleFusion local newtrees - for c′ in intersect(a ⊗ d, e ⊗ dual(b)) + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs coeff = oftype( oneT, if inv @@ -911,7 +912,8 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} return newtrees else # GenericFusion local newtrees - for c′ in intersect(a ⊗ d, e ⊗ dual(b)) + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) Fmat = Fsymbol(d, a, b, e, c′, c) diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 8820e049a..f1c1af87b 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -66,9 +66,13 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) I = sectortype(Vsrc) T = sectorscalartype(I) N = numind(Vdst) + N₁ = numout(Vsrc) + N₂ = numin(Vsrc) isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) + data = Vector{_GenericTransformerData{T, N}}() + nthreads = get_num_transformer_threads() if nthreads > 1 fusiontreeblocks = Vector{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}}() @@ -82,7 +86,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) end end - data = Vector{_GenericTransformerData{T, N}}(undef, length(fusiontreeblocks)) + resize!(data, length(fusiontreeblocks)) counter = Threads.Atomic{Int}(1) Threads.@sync for _ in 1:min(nthreads, length(fusiontreeblocks)) Threads.@spawn begin @@ -110,21 +114,15 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) fusionstructure_dst, inds_dst ) - @debug( - "Created recoupling block for uncoupled: $uncoupled", - sz = size(matrix), - sparsity = count(!iszero, matrix) / length(matrix) - ) - - data[local_counter] = ( - matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src), + data1[local_counter] = ( + matrix, (sz_dst, newstructs_dst), + (sz_src, newstructs_src), ) end end end + transformer = GenericTreeTransformer{T, N}(data) else - data = Vector{_GenericTransformerData{T, N}}() - isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) for cod_uncoupled_src in sectors(codomain(Vsrc)), dom_uncoupled_src in sectors(domain(Vsrc)) @@ -149,17 +147,11 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) fusionstructure_dst, inds_dst ) - @debug( - "Created recoupling block for uncoupled: $uncoupled", - sz = size(matrix), sparsity = count(!iszero, matrix) / length(matrix) - ) - push!(data, (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src))) end + transformer = GenericTreeTransformer{T, N}(data) end - transformer = GenericTreeTransformer{T, N}(data) - # sort by (approximate) weight to facilitate multi-threading strategies sort!(transformer) @@ -167,9 +159,9 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) @debug( "TreeTransformer for $Vsrc to $Vdst via $p", - nblocks = length(data), - sz_median = size(data[cld(end, 2)][1], 1), - sz_max = size(data[1][1], 1), + nblocks = length(transformer.data), + sz_median = size(transformer.data[cld(end, 2)][1], 1), + sz_max = size(transformer.data[1][1], 1), Δt ) From a77dde56e46e136da4259c0d944ed6bf7eba64bb Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 17 Aug 2025 09:36:20 +0200 Subject: [PATCH 23/39] fix multithreaded implementation --- src/tensors/treetransformers.jl | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index f1c1af87b..9ea4c7375 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -85,10 +85,11 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) push!(fusiontreeblocks, fs_src) end end + nblocks = length(fusiontreeblocks) - resize!(data, length(fusiontreeblocks)) + resize!(data, nblocks) counter = Threads.Atomic{Int}(1) - Threads.@sync for _ in 1:min(nthreads, length(fusiontreeblocks)) + Threads.@sync for _ in 1:min(nthreads, nblocks) Threads.@spawn begin while true local_counter = Threads.atomic_add!(counter, 1) @@ -97,13 +98,10 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) fs_dst, U = transform(fs_src) matrix = copy(transpose(U)) # TODO: should we avoid this - inds_src = map( - Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src - ) + trees_src = fusiontrees(fs_src) + inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) trees_dst = fusiontrees(fs_dst) - inds_dst = map( - Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst - ) + inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) # size is shared between blocks, so repack: # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) @@ -114,10 +112,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) fusionstructure_dst, inds_dst ) - data1[local_counter] = ( - matrix, (sz_dst, newstructs_dst), - (sz_src, newstructs_src), - ) + data[local_counter] = (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src)) end end end From f70405cd7bc5a939b7cd55b281752315f207995d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 17 Aug 2025 09:37:12 +0200 Subject: [PATCH 24/39] speed up hashing by hashing less things --- src/fusiontrees/fusiontreeblocks.jl | 44 ++++++++++++++++++----------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 04c446492..168a35acf 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -55,9 +55,22 @@ numind(::Type{T}) where {T <: FusionTreeBlock} = numin(T) + numout(T) fusiontrees(block::FusionTreeBlock) = block.trees Base.length(block::FusionTreeBlock) = length(fusiontrees(block)) +# Within one block, all values of uncoupled and isdual are equal, so avoid hashing these +function treeindex_data((f₁, f₂)) + I = sectortype(f₁) + if FusionStyle(I) isa GenericFusion + return (f₁.coupled, f₁.innerlines..., f₂.innerlines...), + (f₁.vertices..., f₂.vertices...) + elseif FusionStyle(I) isa MultipleFusion + return (f₁.coupled, f₁.innerlines..., f₂.innerlines...) + else # there should be only a single element anyways + return false + end +end function treeindex_map(fs::FusionTreeBlock) I = sectortype(fs) - return fusiontreedict(I)(f => ind for (ind, f) in enumerate(fusiontrees(fs))) + return fusiontreedict(I)(treeindex_data(f) => ind + for (ind, f) in enumerate(fusiontrees(fs))) end # Manipulations @@ -116,7 +129,7 @@ function bendright(src::FusionTreeBlock) coeff = coeff₀ * Bsymbol(a, b, c) vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[(f₁′, f₂′)] + row = indexmap[treeindex_data((f₁′, f₂′))] @inbounds U[row, col] = coeff else Bmat = Bsymbol(a, b, c) @@ -179,7 +192,7 @@ function bendleft(src::FusionTreeBlock) coeff = coeff₀ * Bsymbol(a, b, c) vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[(f₂′, f₁′)] + row = indexmap[treeindex_data((f₂′, f₁′))] @inbounds U[row, col] = conj(coeff) else Bmat = Bsymbol(a, b, c) @@ -189,7 +202,7 @@ function bendleft(src::FusionTreeBlock) iszero(coeff) && continue vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[(f₂′, f₁′)] + row = indexmap[treeindex_data((f₂′, f₁′))] @inbounds U[row, col] = conj(coeff) end end @@ -208,7 +221,7 @@ function foldright(src::FusionTreeBlock) N₁ = numout(src) N₂ = numin(src) @assert N₁ > 0 - dst = FusionTreeBlock{sectortype(src)}(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) indexmap = treeindex_map(dst) @@ -230,7 +243,7 @@ function foldright(src::FusionTreeBlock) c = first(c1 ⊗ c2) fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) - row = indexmap[(fl, fr)] + row = indexmap[treeindex_data((fl, fr))] @inbounds U[row, col] = factor else if N₁ == 1 @@ -255,7 +268,7 @@ function foldright(src::FusionTreeBlock) fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) for (fr, coeff2) in frs_coeffs coeff = factor * coeff1 * conj(coeff2) - row = indexmap[(fl, fr)] + row = indexmap[treeindex_data((fl, fr))] @inbounds U[row, col] = coeff end end @@ -303,7 +316,7 @@ function foldleft(src::FusionTreeBlock) c = first(c1 ⊗ c2) fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) - row = indexmap[(fr, fl)] + row = indexmap[treeindex_data((fr, fl))] @inbounds U[row, col] = conj(factor) else if N₁ == 1 @@ -328,7 +341,7 @@ function foldleft(src::FusionTreeBlock) fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) for (fr, coeff2) in fr_coeffs coeff = factor * coeff1 * conj(coeff2) - row = indexmap[(fr, fl)] + row = indexmap[treeindex_data((fr, fl))] @inbounds U[row, col] = conj(coeff) end end @@ -489,7 +502,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) end f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - row = indexmap[(f′, f₂)] + row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = oneT continue end @@ -502,18 +515,17 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where if FusionStyle(I) isa MultiplicityFreeFusion R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) - row = indexmap[(f′, f₂)] + row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = R else # GenericFusion μ = vertices[1] Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) - local newtrees for ν in axes(Rmat, 2) R = oftype(oneT, Rmat[μ, ν]) iszero(R) && continue vertices′ = TupleTools.setindex(vertices, ν, 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) - row = indexmap[(f′, f₂)] + row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = R end end @@ -537,7 +549,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where ) inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - row = indexmap[(f′, f₂)] + row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = coeff elseif FusionStyle(I) isa SimpleFusion cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) @@ -553,7 +565,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where iszero(coeff) && continue inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - row = indexmap[(f′, f₂)] + row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = coeff end else # GenericFusion @@ -576,7 +588,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where vertices′ = TupleTools.setindex(vertices′, λ, i) inner′ = TupleTools.setindex(inner, c′, i - 1) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - row = indexmap[(f′, f₂)] + row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = coeff end end From 6210d0435de5b02dea45c14e36b0709ac71b687a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 17 Aug 2025 09:37:39 +0200 Subject: [PATCH 25/39] Slight refactor of artin_braid --- src/fusiontrees/fusiontreeblocks.jl | 40 +++++++++++++++-------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 168a35acf..ba2b7e502 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -468,30 +468,26 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) uncoupled = src.uncoupled[1] - uncoupled′ = TupleTools.setindex(uncoupled, uncoupled[i + 1], i) - uncoupled′ = TupleTools.setindex(uncoupled′, uncoupled[i], i + 1) + a, b = uncoupled[i], uncoupled[i + 1] + uncoupled′ = TupleTools.setindex(uncoupled, b, i) + uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) + coupled′ = rightone(src.uncoupled[1][N]) + isdual = src.isdual[1] isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ())) + oneT = one(sectorscalartype(I)) + indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) - for (col, (f, f₂)) in enumerate(fusiontrees(src)) - a, b = uncoupled[i], uncoupled[i + 1] - uncoupled′ = TupleTools.setindex(uncoupled, b, i) - uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) - coupled′ = f.coupled - isdual′ = TupleTools.setindex(f.isdual, f.isdual[i], i + 1) - isdual′ = TupleTools.setindex(isdual′, f.isdual[i + 1], i) - inner = f.innerlines - inner_extended = (uncoupled[1], inner..., coupled′) - vertices = f.vertices - oneT = one(sectorscalartype(I)) - - if isone(uncoupled[i]) || isone(uncoupled[i + 1]) - # braiding with trivial sector: simple and always possible + if isone(a) || isone(b) # braiding with trivial sector: simple and always possible + for (col, (f, f₂)) in enumerate(fusiontrees(src)) + inner = f.innerlines + inner_extended = (uncoupled[1], inner..., coupled′) + vertices = f.vertices inner′ = inner vertices′ = vertices if i > 1 # we also need to alter innerlines and vertices @@ -504,11 +500,17 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = oneT - continue end + return dst, U + end - BraidingStyle(I) isa NoBraiding && - throw(SectorMismatch("Cannot braid sectors $(uncoupled[i]) and $(uncoupled[i + 1])")) + BraidingStyle(I) isa NoBraiding && + throw(SectorMismatch(lazy"Cannot braid sectors $a and $b")) + + for (col, (f, f₂)) in enumerate(fusiontrees(src)) + inner = f.innerlines + inner_extended = (uncoupled[1], inner..., coupled′) + vertices = f.vertices if i == 1 c = N > 2 ? inner[1] : coupled′ From bf9e6d91f30725977ad711337d9ef0c6d97d0c70 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 17 Aug 2025 09:54:03 +0200 Subject: [PATCH 26/39] reduce allocations with sizehints --- src/fusiontrees/fusiontreeblocks.jl | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index ba2b7e502..c5a580035 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -4,11 +4,12 @@ end function FusionTreeBlock{I}( uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, - isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}} + isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}}; + sizehint::Int = 0 ) where {I <: Sector, N₁, N₂} - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) - trees = Vector{Tuple{F₁, F₂}}(undef, 0) + F = fusiontreetype(I, N₁, N₂) + trees = Vector{F}(undef, 0) + sizehint > 0 && sizehint!(trees, sizehint) if N₁ == N₂ == 0 return FusionTreeBlock(trees) @@ -60,7 +61,7 @@ function treeindex_data((f₁, f₂)) I = sectortype(f₁) if FusionStyle(I) isa GenericFusion return (f₁.coupled, f₁.innerlines..., f₂.innerlines...), - (f₁.vertices..., f₂.vertices...) + (f₁.vertices..., f₂.vertices...) elseif FusionStyle(I) isa MultipleFusion return (f₁.coupled, f₁.innerlines..., f₂.innerlines...) else # there should be only a single element anyways @@ -69,8 +70,7 @@ function treeindex_data((f₁, f₂)) end function treeindex_map(fs::FusionTreeBlock) I = sectortype(fs) - return fusiontreedict(I)(treeindex_data(f) => ind - for (ind, f) in enumerate(fusiontrees(fs))) + return fusiontreedict(I)(treeindex_data(f) => ind for (ind, f) in enumerate(fusiontrees(fs))) end # Manipulations @@ -101,7 +101,7 @@ function bendright(src::FusionTreeBlock) N₂ = numin(src) @assert N₁ > 0 - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) @@ -164,7 +164,7 @@ function bendleft(src::FusionTreeBlock) N₂ = numout(src) @assert N₁ > 0 - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) @@ -221,9 +221,8 @@ function foldright(src::FusionTreeBlock) N₁ = numout(src) N₂ = numin(src) @assert N₁ > 0 - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) @@ -296,7 +295,7 @@ function foldleft(src::FusionTreeBlock) N₂ = numout(src) @assert N₁ > 0 - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst) + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) indexmap = treeindex_map(dst) U = zeros(sectorscalartype(I), length(dst), length(src)) @@ -476,7 +475,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where isdual = src.isdual[1] isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) - dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ())) + dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ()); sizehint = length(src)) oneT = one(sectorscalartype(I)) @@ -607,7 +606,7 @@ function braid(src::FusionTreeBlock{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{ if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding uncoupled′ = TupleTools._permute(src.uncoupled[1], p) isdual′ = TupleTools._permute(src.isdual[1], p) - dst = FusionTreeBlock{I}(uncoupled′, isdual′) + dst = FusionTreeBlock{I}(uncoupled′, isdual′; sizehint = length(src)) U = transformation_matrix(dst, src) do (f₁, f₂) return ((f₁′, f₂) => c for (f₁′, c) in braid(f₁, p, levels)) end From f5292d1b19da002b1539bc6bad7168eeb61d15c4 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 17 Aug 2025 10:18:22 +0200 Subject: [PATCH 27/39] separate treemanipulation threads --- src/TensorKit.jl | 13 +++++++++++++ src/tensors/treetransformers.jl | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 74d23c8ce..06c6b8998 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -210,6 +210,19 @@ function set_num_transformer_threads(n::Int) return TRANSFORMER_THREADS[] = n end +const TREEMANIPULATION_THREADS = Ref(1) + +get_num_manipulation_threads() = TREEMANIPULATION_THREADS[] + +function set_num_manipulation_threads(n::Int) + N = Base.Threads.nthreads() + if n > N + n = N + Strided._set_num_threads_warn(n) + end + return TREEMANIPULATION_THREADS[] = n +end + # Definitions and methods for tensors #------------------------------------- # general definitions diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 9ea4c7375..78ce43d18 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -73,7 +73,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) data = Vector{_GenericTransformerData{T, N}}() - nthreads = get_num_transformer_threads() + nthreads = get_num_manipulation_threads() if nthreads > 1 fusiontreeblocks = Vector{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}}() for cod_uncoupled_src in sectors(codomain(Vsrc)), From bc48a057c91838fade3a83533e2d3b77e10eeaf7 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 12 Nov 2025 09:38:55 -0500 Subject: [PATCH 28/39] update `leftunit` and `rightunit` --- src/fusiontrees/fusiontreeblocks.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index c5a580035..78cc5c443 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -107,7 +107,7 @@ function bendright(src::FusionTreeBlock) for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) c = f₁.coupled - a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : + a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) b = f₁.uncoupled[N₁] @@ -170,7 +170,7 @@ function bendleft(src::FusionTreeBlock) for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) c = f₁.coupled - a = N₁ == 1 ? leftone(f₁.uncoupled[1]) : + a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) b = f₁.uncoupled[N₁] @@ -246,7 +246,7 @@ function foldright(src::FusionTreeBlock) @inbounds U[row, col] = factor else if N₁ == 1 - cset = (leftone(c1),) # or rightone(a) + cset = (leftunit(c1),) # or rightunit(a) elseif N₁ == 2 cset = (f₁.uncoupled[2],) else @@ -319,7 +319,7 @@ function foldleft(src::FusionTreeBlock) @inbounds U[row, col] = conj(factor) else if N₁ == 1 - cset = (leftone(c1),) # or rightone(a) + cset = (leftunit(c1),) # or rightunit(a) elseif N₁ == 2 cset = (f₁.uncoupled[2],) else @@ -470,7 +470,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where a, b = uncoupled[i], uncoupled[i + 1] uncoupled′ = TupleTools.setindex(uncoupled, b, i) uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) - coupled′ = rightone(src.uncoupled[1][N]) + coupled′ = rightunit(src.uncoupled[1][N]) isdual = src.isdual[1] isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) From 68cef168b869eb6f055a8b15d7f71c7595faf116 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 12 Nov 2025 09:39:03 -0500 Subject: [PATCH 29/39] add missing `treeindex_data` --- src/fusiontrees/fusiontreeblocks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 78cc5c443..455df5c8d 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -139,7 +139,7 @@ function bendright(src::FusionTreeBlock) iszero(coeff) && continue vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[(f₁′, f₂′)] + row = indexmap[treeindex_data((f₁′, f₂′))] @inbounds U[row, col] = coeff end end From c3ded84a3fa46ec3a398937fc1861a6968e955e7 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 12 Nov 2025 09:56:44 -0500 Subject: [PATCH 30/39] update `frobeniusschur` to `frobenius_schur_phase` --- src/fusiontrees/fusiontreeblocks.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl index 455df5c8d..c10e820e5 100644 --- a/src/fusiontrees/fusiontreeblocks.jl +++ b/src/fusiontrees/fusiontreeblocks.jl @@ -123,7 +123,7 @@ function bendright(src::FusionTreeBlock) coeff₀ = sqrtdim(c) * invsqrtdim(a) if f₁.isdual[N₁] - coeff₀ *= conj(frobeniusschur(dual(b))) + coeff₀ *= conj(frobenius_schur_phase(dual(b))) end if FusionStyle(I) isa MultiplicityFreeFusion coeff = coeff₀ * Bsymbol(a, b, c) @@ -186,7 +186,7 @@ function bendleft(src::FusionTreeBlock) coeff₀ = sqrtdim(c) * invsqrtdim(a) if f₁.isdual[N₁] - coeff₀ *= conj(frobeniusschur(dual(b))) + coeff₀ *= conj(frobenius_schur_phase(dual(b))) end if FusionStyle(I) isa MultiplicityFreeFusion coeff = coeff₀ * Bsymbol(a, b, c) @@ -232,7 +232,7 @@ function foldright(src::FusionTreeBlock) isduala = f₁.isdual[1] factor = sqrtdim(a) if !isduala - factor *= conj(frobeniusschur(a)) + factor *= conj(frobenius_schur_phase(a)) end c1 = dual(a) c2 = f₁.coupled @@ -305,7 +305,7 @@ function foldleft(src::FusionTreeBlock) isduala = f₁.isdual[1] factor = sqrtdim(a) if !isduala - factor *= conj(frobeniusschur(a)) + factor *= conj(frobenius_schur_phase(a)) end c1 = dual(a) c2 = f₁.coupled From 02c0a056042238dcdff943d9e7289aff6624b441 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 14 Nov 2025 14:07:57 -0500 Subject: [PATCH 31/39] remove stray file --- src/fusiontrees/uncouplediterator.jl | 369 --------------------------- 1 file changed, 369 deletions(-) delete mode 100644 src/fusiontrees/uncouplediterator.jl diff --git a/src/fusiontrees/uncouplediterator.jl b/src/fusiontrees/uncouplediterator.jl deleted file mode 100644 index 332f8c70c..000000000 --- a/src/fusiontrees/uncouplediterator.jl +++ /dev/null @@ -1,369 +0,0 @@ -struct OuterTreeIterator{I <: Sector, N₁, N₂} - uncoupled::Tuple{NTuple{N₁, I}, NTuple{N₂, I}} - isdual::Tuple{NTuple{N₁, Bool}, NTuple{N₂, Bool}} -end - -sectortype(::Type{<:OuterTreeIterator{I}}) where {I} = I -numout(fs::OuterTreeIterator) = numout(typeof(fs)) -numout(::Type{<:OuterTreeIterator{I, N₁}}) where {I, N₁} = N₁ -numin(fs::OuterTreeIterator) = numin(typeof(fs)) -numin(::Type{<:OuterTreeIterator{I, N₁, N₂}}) where {I, N₁, N₂} = N₂ -numind(fs::OuterTreeIterator) = numind(typeof(fs)) -numind(::Type{T}) where {T <: OuterTreeIterator} = numin(T) + numout(T) - -# TODO: should we make this an actual iterator? -function fusiontrees(iter::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) - - trees = Vector{Tuple{F₁, F₂}}(undef, 0) - for c in blocksectors(iter), f₁ in fusiontrees(iter.uncoupled[1], c, iter.isdual[1]), - f₂ in fusiontrees(iter.uncoupled[2], c, iter.isdual[2]) - - push!(trees, (f₁, f₂)) - end - return trees -end - -# TODO: better implementation -Base.length(iter::OuterTreeIterator) = length(fusiontrees(iter)) - -function blocksectors(iter::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - I == Trivial && return (Trivial(),) - - bs_codomain = Vector{I}() - if N₁ == 0 - push!(bs_codomain, one(I)) - elseif N₁ == 1 - push!(bs_codomain, only(iter.uncoupled[1])) - else - for c in ⊗(iter.uncoupled[1]...) - if !(c in bs_codomain) - push!(bs_codomain, c) - end - end - end - - bs_domain = Vector{I}() - if N₂ == 0 - push!(bs_domain, one(I)) - elseif N₂ == 1 - push!(bs_domain, only(iter.uncoupled[2])) - else - for c in ⊗(iter.uncoupled[2]...) - if !(c in bs_domain) - push!(bs_domain, c) - end - end - end - - return sort!(collect(intersect(bs_codomain, bs_domain))) -end - -# Manipulations -# ------------- - -function bendright(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - uncoupled_dst = ( - TupleTools.front(fs_src.uncoupled[1]), - (fs_src.uncoupled[2]..., dual(fs_src.uncoupled[1][end])), - ) - isdual_dst = ( - TupleTools.front(fs_src.isdual[1]), - (fs_src.isdual[2]..., !(fs_src.isdual[1][end])), - ) - fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) - - trees_src = fusiontrees(fs_src) - trees_dst = fusiontrees(fs_dst) - indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) - U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) - - for (col, f) in enumerate(trees_src) - for (f′, c) in bendright(f) - row = indexmap[f′] - U[row, col] = c - end - end - - return fs_dst, U -end - -# TODO: verify if this can be computed through an adjoint -function bendleft(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - uncoupled_dst = ( - (fs_src.uncoupled[1]..., dual(fs_src.uncoupled[2][end])), - TupleTools.front(fs_src.uncoupled[2]), - ) - isdual_dst = ( - (fs_src.isdual[1]..., !(fs_src.isdual[2][end])), - TupleTools.front(fs_src.isdual[2]), - ) - fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) - - trees_src = fusiontrees(fs_src) - trees_dst = fusiontrees(fs_dst) - indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) - U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) - - for (col, f) in enumerate(trees_src) - for (f′, c) in bendleft(f) - row = indexmap[f′] - U[row, col] = c - end - end - - return fs_dst, U -end - -function foldright(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - uncoupled_dst = ( - Base.tail(fs_src.uncoupled[1]), - (dual(first(fs_src.uncoupled[1])), fs_src.uncoupled[2]...), - ) - isdual_dst = ( - Base.tail(fs_src.isdual[1]), - (!first(fs_src.isdual[1]), fs_src.isdual[2]...), - ) - fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) - - trees_src = fusiontrees(fs_src) - trees_dst = fusiontrees(fs_dst) - indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) - U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) - - for (col, f) in enumerate(trees_src) - for (f′, c) in foldright(f) - row = indexmap[f′] - U[row, col] = c - end - end - - return fs_dst, U -end - -# TODO: verify if this can be computed through an adjoint -function foldleft(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - uncoupled_dst = ( - (dual(first(fs_src.uncoupled[2])), fs_src.uncoupled[1]...), - Base.tail(fs_src.uncoupled[2]), - ) - isdual_dst = ( - (!first(fs_src.isdual[2]), fs_src.isdual[1]...), - Base.tail(fs_src.isdual[2]), - ) - fs_dst = OuterTreeIterator(uncoupled_dst, isdual_dst) - - trees_src = fusiontrees(fs_src) - trees_dst = fusiontrees(fs_dst) - indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) - U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) - - for (col, f) in enumerate(trees_src) - for (f′, c) in foldleft(f) - row = indexmap[f′] - U[row, col] = c - end - end - - return fs_dst, U -end - -function cycleclockwise(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - if N₁ > 0 - fs_tmp, U₁ = foldright(fs_src) - fs_dst, U₂ = bendleft(fs_tmp) - else - fs_tmp, U₁ = bendleft(fs_src) - fs_dst, U₂ = foldright(fs_tmp) - end - return fs_dst, U₂ * U₁ -end - -function cycleanticlockwise(fs_src::OuterTreeIterator{I, N₁, N₂}) where {I, N₁, N₂} - if N₂ > 0 - fs_tmp, U₁ = foldleft(fs_src) - fs_dst, U₂ = bendright(fs_tmp) - else - fs_tmp, U₁ = bendright(fs_src) - fs_dst, U₂ = foldleft(fs_tmp) - end - return fs_dst, U₂ * U₁ -end - -@inline function repartition(fs_src::OuterTreeIterator{I, N₁, N₂}, N::Int) where {I, N₁, N₂} - @assert 0 <= N <= N₁ + N₂ - return _recursive_repartition(fs_src, Val(N)) -end - -function _repartition_type(I, N, N₁, N₂) - return Tuple{OuterTreeIterator{I, N, N₁ + N₂ - N}, Matrix{sectorscalartype(I)}} -end -function _recursive_repartition( - fs_src::OuterTreeIterator{I, N₁, N₂}, ::Val{N} - )::_repartition_type(I, N, N₁, N₂) where {I, N₁, N₂, N} - if N == N₁ - fs_dst = fs_src - U = zeros(sectorscalartype(I), length(fs_dst), length(fs_src)) - copyto!(U, LinearAlgebra.I) - return fs_dst, U - end - - N == N₁ - 1 && return bendright(fs_src) - N == N₁ + 1 && return bendleft(fs_src) - - fs_tmp, U₁ = N < N₁ ? bendright(fs_src) : bendleft(fs_src) - fs_dst, U₂ = _recursive_repartition(fs_tmp, Val(N)) - return fs_dst, U₂ * U₁ -end - -function Base.transpose(fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} - N = N₁ + N₂ - @assert numind(fs_src) == N - p′ = linearizepermutation(p..., numout(fs_src), numin(fs_src)) - @assert iscyclicpermutation(p′) - return _fstranspose((fs_src, p)) -end - -const _FSTransposeKey{I, N₁, N₂} = Tuple{<:OuterTreeIterator{I}, Index2Tuple{N₁, N₂}} - -@cached function _fstranspose( - key::_FSTransposeKey{I, N₁, N₂} - )::Tuple{OuterTreeIterator{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} - fs_src, (p1, p2) = key - - N = N₁ + N₂ - p = linearizepermutation(p1, p2, numout(fs_src), numin(fs_src)) - - fs_dst, U = repartition(fs_src, N₁) - length(p) == 0 && return fs_dst, U - i1 = findfirst(==(1), p)::Int - i1 == 1 && return fs_dst, U - - Nhalf = N >> 1 - while 1 < i1 ≤ Nhalf - fs_dst, U_tmp = cycleanticlockwise(fs_dst) - U = U_tmp * U - i1 -= 1 - end - while Nhalf < i1 - fs_dst, U_tmp = cycleclockwise(fs_dst) - U = U_tmp * U - i1 = mod1(i1 + 1, N) - end - - return fs_dst, U -end - -function CacheStyle(::typeof(_fstranspose), k::_FSTransposeKey{I}) where {I} - if FusionStyle(I) == UniqueFusion() - return NoCache() - else - return GlobalLRUCache() - end -end - -function artin_braid(fs_src::OuterTreeIterator{I, N, 0}, i; inv::Bool = false) where {I, N} - 1 <= i < N || - throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) - - uncoupled = fs_src.uncoupled[1] - uncoupled′ = TupleTools.setindex(uncoupled, uncoupled[i + 1], i) - uncoupled′ = TupleTools.setindex(uncoupled′, uncoupled[i], i + 1) - - isdual = fs_src.isdual[1] - isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) - isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) - - fs_dst = OuterTreeIterator((uncoupled′, ()), (isdual′, ())) - - trees_src = fusiontrees(fs_src) - trees_dst = fusiontrees(fs_dst) - indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) - U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) - - for (col, (f₁, f₂)) in enumerate(trees_src) - for (f₁′, c) in artin_braid(f₁, i; inv) - row = indexmap[(f₁′, f₂)] - U[row, col] = c - end - end - - return fs_dst, U -end - -function braid( - fs_src::OuterTreeIterator{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{N, Int} - ) where {I, N} - TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) - - if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding - uncoupled′ = TupleTools._permute(fs_src.uncoupled[1], p) - isdual′ = TupleTools._permute(fs_src.isdual[1], p) - fs_dst = OuterTreeIterator(uncoupled′, isdual′) - - trees_src = fusiontrees(fs_src) - trees_dst = fusiontrees(fs_dst) - indexmap = Dict(f => ind for (ind, f) in enumerate(trees_dst)) - U = zeros(sectorscalartype(I), length(trees_dst), length(trees_src)) - - for (col, (f₁, f₂)) in enumerate(trees_src) - for (f₁′, c) in braid(f₁, p, levels) - row = indexmap[(f₁′, f₂)] - U[row, col] = c - end - end - - return fs_dst, U - end - - fs_dst, U = repartition(fs_src, N) # TODO: can we avoid this? - for s in permutation2swaps(p) - inv = levels[s] > levels[s + 1] - fs_dst, U_tmp = artin_braid(fs_dst, s; inv) - U = U_tmp * U - end - return fs_dst, U -end - -function braid( - fs_src::OuterTreeIterator{I}, p::Index2Tuple{N₁, N₂}, levels::Index2Tuple - ) where {I, N₁, N₂} - @assert numind(fs_src) == N₁ + N₂ - @assert numout(fs_src) == length(levels[1]) && numin(fs_src) == length(levels[2]) - @assert TupleTools.isperm((p[1]..., p[2]...)) - return _fsbraid((fs_src, p, levels)) -end - -const _FSBraidKey{I, N₁, N₂} = Tuple{<:OuterTreeIterator{I}, Index2Tuple{N₁, N₂}, Index2Tuple} - -@cached function _fsbraid( - key::_FSBraidKey{I, N₁, N₂} - )::Tuple{OuterTreeIterator{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} - fs_src, (p1, p2), (l1, l2) = key - - p = linearizepermutation(p1, p2, numout(fs_src), numin(fs_src)) - levels = (l1..., reverse(l2)...) - - fs_dst, U = repartition(fs_src, numind(fs_src)) - fs_dst, U_tmp = braid(fs_dst, p, levels) - U = U_tmp * U - fs_dst, U_tmp = repartition(fs_dst, N₁) - U = U_tmp * U - return fs_dst, U -end - -function CacheStyle(::typeof(_fsbraid), k::_FSBraidKey{I}) where {I} - if FusionStyle(I) isa UniqueFusion - return NoCache() - else - return GlobalLRUCache() - end -end - -function permute(fs_src::OuterTreeIterator{I}, p::Index2Tuple) where {I} - @assert BraidingStyle(I) isa SymmetricBraiding - levels1 = ntuple(identity, numout(fs_src)) - levels2 = numout(fs_src) .+ ntuple(identity, numin(fs_src)) - return braid(fs_src, p, (levels1, levels2)) -end From 9dc27c49723fd3757619e176d11fd3ddf7b526d9 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 16 Nov 2025 09:27:36 -0500 Subject: [PATCH 32/39] some QOL additions --- src/fusiontrees/fusiontrees.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index f4e6c1a48..0a5340929 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -105,18 +105,33 @@ function FusionTree( end FusionTree(uncoupled::Tuple{I, Vararg{I}}) where {I <: Sector} = FusionTree(uncoupled, unit(I)) +""" + FusionTreePair{I, N₁, N₂} + +Type alias for a fusion-splitting tree pair of sectortype `I`, with `N₁` splitting legs and +`N₂` fusion legs. +""" const FusionTreePair{I, N₁, N₂} = Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}} # Properties sectortype(::Type{<:FusionTree{I}}) where {I <: Sector} = I +sectortype(::Type{<:FusionTreePair{I}}) where {I <: Sector} = I FusionStyle(::Type{<:FusionTree{I}}) where {I <: Sector} = FusionStyle(I) +FusionStyle(::Type{<:FusionTreePair{I}}) where {I <: Sector} = FusionStyle(I) BraidingStyle(::Type{<:FusionTree{I}}) where {I <: Sector} = BraidingStyle(I) +BraidingStyle(::Type{<:FusionTreePair{I}}) where {I <: Sector} = BraidingStyle(I) Base.length(::Type{<:FusionTree{<:Sector, N}}) where {N} = N FusionStyle(f::FusionTree) = FusionStyle(typeof(f)) +FusionStyle(f::FusionTreePair) = FusionStyle(typeof(f)) BraidingStyle(f::FusionTree) = BraidingStyle(typeof(f)) +BraidingStyle(f::FusionTreePair) = BraidingStyle(typeof(f)) Base.length(f::FusionTree) = length(typeof(f)) +# Note: cannot define the following since FusionTreePair is a const for a Tuple +# Base.length(::Type{<:FusionTreePair{<:Sector, N₁, N₂}}) where {N₁, N₂} = N₁ + N₂ +# Base.length(f::FusionTreePair) = length(typeof(f)) + # Hashing, important for using fusion trees as key in a dictionary function Base.hash(f::FusionTree{I}, h::UInt) where {I} h = hash(f.isdual, hash(f.coupled, hash(f.uncoupled, h))) From ed005242f618cf625d29014152379985fc84c6c5 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 16 Nov 2025 09:48:17 -0500 Subject: [PATCH 33/39] some reorganization --- src/auxiliary/auxiliary.jl | 14 + src/fusiontrees/basic_manipulations.jl | 273 ++++++ src/fusiontrees/braiding_manipulations.jl | 482 +++++++++ src/fusiontrees/duality_manipulations.jl | 896 +++++++++++++++++ src/fusiontrees/fusiontreeblocks.jl | 683 ------------- src/fusiontrees/fusiontrees.jl | 111 ++- src/fusiontrees/manipulations.jl | 1081 --------------------- 7 files changed, 1764 insertions(+), 1776 deletions(-) create mode 100644 src/fusiontrees/basic_manipulations.jl create mode 100644 src/fusiontrees/braiding_manipulations.jl create mode 100644 src/fusiontrees/duality_manipulations.jl delete mode 100644 src/fusiontrees/fusiontreeblocks.jl delete mode 100644 src/fusiontrees/manipulations.jl diff --git a/src/auxiliary/auxiliary.jl b/src/auxiliary/auxiliary.jl index 4b97603ca..23436bf1d 100644 --- a/src/auxiliary/auxiliary.jl +++ b/src/auxiliary/auxiliary.jl @@ -27,6 +27,20 @@ function permutation2swaps(perm) return swaps end +# one-argument version: check whether `p` is a cyclic permutation (of `1:length(p)`) +function iscyclicpermutation(p) + N = length(p) + @inbounds for i in 1:N + p[mod1(i + 1, N)] == mod1(p[i] + 1, N) || return false + end + return true +end +# two-argument version: check whether `v1` is a cyclic permutation of `v2` +function iscyclicpermutation(v1, v2) + length(v1) == length(v2) || return false + return iscyclicpermutation(indexin(v1, v2)) +end + _kron(A, B, C, D...) = _kron(_kron(A, B), C, D...) function _kron(A, B) sA = size(A) diff --git a/src/fusiontrees/basic_manipulations.jl b/src/fusiontrees/basic_manipulations.jl new file mode 100644 index 000000000..3f2ca74a7 --- /dev/null +++ b/src/fusiontrees/basic_manipulations.jl @@ -0,0 +1,273 @@ +# BASIC MANIPULATIONS: +#---------------------------------------------- +# -> rewrite generic fusion tree in basis of fusion trees in standard form +# -> only depend on Fsymbol + +""" + insertat(f::FusionTree{I, N₁}, i::Int, f₂::FusionTree{I, N₂}) + -> <:AbstractDict{<:FusionTree{I, N₁+N₂-1}, <:Number} + +Attach a fusion tree `f₂` to the uncoupled leg `i` of the fusion tree `f₁` and bring it +into a linear combination of fusion trees in standard form. This requires that +`f₂.coupled == f₁.uncoupled[i]` and `f₁.isdual[i] == false`. +""" +function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I, 0}) where {I} + # this actually removes uncoupled line i, which should be trivial + (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || + throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) + coeff = one(sectorscalartype(I)) + + uncoupled = TupleTools.deleteat(f₁.uncoupled, i) + coupled = f₁.coupled + isdual = TupleTools.deleteat(f₁.isdual, i) + if length(uncoupled) <= 2 + inner = () + else + inner = TupleTools.deleteat(f₁.innerlines, max(1, i - 2)) + end + if length(uncoupled) <= 1 + vertices = () + else + vertices = TupleTools.deleteat(f₁.vertices, max(1, i - 1)) + end + f = FusionTree(uncoupled, coupled, isdual, inner, vertices) + return fusiontreedict(I)(f => coeff) +end +function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 1}) where {I} + # identity operation + (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || + throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) + coeff = one(sectorscalartype(I)) + isdual′ = TupleTools.setindex(f₁.isdual, f₂.isdual[1], i) + f = FusionTree{I}(f₁.uncoupled, f₁.coupled, isdual′, f₁.innerlines, f₁.vertices) + return fusiontreedict(I)(f => coeff) +end +function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 2}) where {I} + # elementary building block, + (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || + throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) + uncoupled = f₁.uncoupled + coupled = f₁.coupled + inner = f₁.innerlines + b, c = f₂.uncoupled + isdual = f₁.isdual + isdualb, isdualc = f₂.isdual + if i == 1 + uncoupled′ = (b, c, tail(uncoupled)...) + isdual′ = (isdualb, isdualc, tail(isdual)...) + inner′ = (uncoupled[1], inner...) + vertices′ = (f₂.vertices..., f₁.vertices...) + coeff = one(sectorscalartype(I)) + f′ = FusionTree(uncoupled′, coupled, isdual′, inner′, vertices′) + return fusiontreedict(I)(f′ => coeff) + end + uncoupled′ = TupleTools.insertafter(TupleTools.setindex(uncoupled, b, i), i, (c,)) + isdual′ = TupleTools.insertafter(TupleTools.setindex(isdual, isdualb, i), i, (isdualc,)) + inner_extended = (uncoupled[1], inner..., coupled) + a = inner_extended[i - 1] + d = inner_extended[i] + e′ = uncoupled[i] + if FusionStyle(I) isa MultiplicityFreeFusion + local newtrees + for e in a ⊗ b + coeff = conj(Fsymbol(a, b, c, d, e, e′)) + iszero(coeff) && continue + inner′ = TupleTools.insertafter(inner, i - 2, (e,)) + f′ = FusionTree(uncoupled′, coupled, isdual′, inner′) + if @isdefined newtrees + push!(newtrees, f′ => coeff) + else + newtrees = fusiontreedict(I)(f′ => coeff) + end + end + return newtrees + else + local newtrees + κ = f₂.vertices[1] + λ = f₁.vertices[i - 1] + for e in a ⊗ b + inner′ = TupleTools.insertafter(inner, i - 2, (e,)) + Fmat = Fsymbol(a, b, c, d, e, e′) + for μ in axes(Fmat, 1), ν in axes(Fmat, 2) + coeff = conj(Fmat[μ, ν, κ, λ]) + iszero(coeff) && continue + vertices′ = TupleTools.setindex(f₁.vertices, ν, i - 1) + vertices′ = TupleTools.insertafter(vertices′, i - 2, (μ,)) + f′ = FusionTree(uncoupled′, coupled, isdual′, inner′, vertices′) + if @isdefined newtrees + push!(newtrees, f′ => coeff) + else + newtrees = fusiontreedict(I)(f′ => coeff) + end + end + end + return newtrees + end +end +function insertat(f₁::FusionTree{I, N₁}, i, f₂::FusionTree{I, N₂}) where {I, N₁, N₂} + F = fusiontreetype(I, N₁ + N₂ - 1) + (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || + throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) + T = sectorscalartype(I) + coeff = one(T) + if length(f₁) == 1 + return fusiontreedict(I){F, T}(f₂ => coeff) + end + if i == 1 + uncoupled = (f₂.uncoupled..., tail(f₁.uncoupled)...) + isdual = (f₂.isdual..., tail(f₁.isdual)...) + inner = (f₂.innerlines..., f₂.coupled, f₁.innerlines...) + vertices = (f₂.vertices..., f₁.vertices...) + coupled = f₁.coupled + f′ = FusionTree(uncoupled, coupled, isdual, inner, vertices) + return fusiontreedict(I){F, T}(f′ => coeff) + else # recursive definition + N2 = length(f₂) + f₂′, f₂′′ = split(f₂, N2 - 1) + local newtrees::fusiontreedict(I){F, T} + for (f, coeff) in insertat(f₁, i, f₂′′) + for (f′, coeff′) in insertat(f, i, f₂′) + if @isdefined newtrees + coeff′′ = coeff * coeff′ + newtrees[f′] = get(newtrees, f′, zero(coeff′′)) + coeff′′ + else + newtrees = fusiontreedict(I){F, T}(f′ => coeff * coeff′) + end + end + end + return newtrees + end +end + +""" + split(f::FusionTree{I, N}, M::Int) + -> (::FusionTree{I, M}, ::FusionTree{I, N-M+1}) + +Split a fusion tree into two. The first tree has as uncoupled sectors the first `M` +uncoupled sectors of the input tree `f`, whereas its coupled sector corresponds to the +internal sector between uncoupled sectors `M` and `M+1` of the original tree `f`. The +second tree has as first uncoupled sector that same internal sector of `f`, followed by +remaining `N-M` uncoupled sectors of `f`. It couples to the same sector as `f`. This +operation is the inverse of `insertat` in the sense that if +`f₁, f₂ = split(t, M) ⇒ f == insertat(f₂, 1, f₁)`. +""" +@inline function split(f::FusionTree{I, N}, M::Int) where {I, N} + if M > N || M < 0 + throw(ArgumentError("M should be between 0 and N = $N")) + elseif M === N + (f, FusionTree{I}((f.coupled,), f.coupled, (false,), (), ())) + elseif M === 1 + isdual1 = (f.isdual[1],) + isdual2 = TupleTools.setindex(f.isdual, false, 1) + f₁ = FusionTree{I}((f.uncoupled[1],), f.uncoupled[1], isdual1, (), ()) + f₂ = FusionTree{I}(f.uncoupled, f.coupled, isdual2, f.innerlines, f.vertices) + return f₁, f₂ + elseif M === 0 + u = leftunit(f.uncoupled[1]) + f₁ = FusionTree{I}((), u, (), ()) + uncoupled2 = (u, f.uncoupled...) + coupled2 = f.coupled + isdual2 = (false, f.isdual...) + innerlines2 = N >= 2 ? (f.uncoupled[1], f.innerlines...) : () + if FusionStyle(I) isa GenericFusion + vertices2 = (1, f.vertices...) + return f₁, FusionTree{I}(uncoupled2, coupled2, isdual2, innerlines2, vertices2) + else + return f₁, FusionTree{I}(uncoupled2, coupled2, isdual2, innerlines2) + end + else + uncoupled1 = ntuple(n -> f.uncoupled[n], M) + isdual1 = ntuple(n -> f.isdual[n], M) + innerlines1 = ntuple(n -> f.innerlines[n], max(0, M - 2)) + coupled1 = f.innerlines[M - 1] + vertices1 = ntuple(n -> f.vertices[n], M - 1) + + uncoupled2 = ntuple(N - M + 1) do n + return n == 1 ? f.innerlines[M - 1] : f.uncoupled[M + n - 1] + end + isdual2 = ntuple(N - M + 1) do n + return n == 1 ? false : f.isdual[M + n - 1] + end + innerlines2 = ntuple(n -> f.innerlines[M - 1 + n], N - M - 1) + coupled2 = f.coupled + vertices2 = ntuple(n -> f.vertices[M - 1 + n], N - M) + + f₁ = FusionTree{I}(uncoupled1, coupled1, isdual1, innerlines1, vertices1) + f₂ = FusionTree{I}(uncoupled2, coupled2, isdual2, innerlines2, vertices2) + return f₁, f₂ + end +end + +""" + merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I, μ = 1) + -> <:AbstractDict{<:FusionTree{I, N₁+N₂}, <:Number} + +Merge two fusion trees together to a linear combination of fusion trees whose uncoupled +sectors are those of `f₁` followed by those of `f₂`, and where the two coupled sectors of +`f₁` and `f₂` are further fused to `c`. In case of +`FusionStyle(I) == GenericFusion()`, also a degeneracy label `μ` for the fusion of +the coupled sectors of `f₁` and `f₂` to `c` needs to be specified. +""" +function merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I) where {I, N₁, N₂} + if FusionStyle(I) isa GenericFusion + throw(ArgumentError("vertex label for merging required")) + end + return merge(f₁, f₂, c, 1) +end +function merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I, μ) where {I, N₁, N₂} + if !(c in f₁.coupled ⊗ f₂.coupled) + throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) + end + if μ > Nsymbol(f₁.coupled, f₂.coupled, c) + throw(ArgumentError("invalid fusion vertex label $μ")) + end + f₀ = FusionTree{I}((f₁.coupled, f₂.coupled), c, (false, false), (), (μ,)) + f, coeff = first(insertat(f₀, 1, f₁)) # takes fast path, single output + @assert coeff == one(coeff) + return insertat(f, N₁ + 1, f₂) +end +function merge(f₁::FusionTree{I, 0}, f₂::FusionTree{I, 0}, c::I, μ) where {I} + Nsymbol(f₁.coupled, f₂.coupled, c) == μ == 1 || + throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) + return fusiontreedict(I)(f₁ => Fsymbol(c, c, c, c, c, c)[1, 1, 1, 1]) +end + +# flip a duality flag of a fusion tree +function flip((f₁, f₂)::FusionTreePair{I, N₁, N₂}, i::Int; inv::Bool = false) where {I, N₁, N₂} + @assert 0 < i ≤ N₁ + N₂ + if i ≤ N₁ + a = f₁.uncoupled[i] + χₐ = frobenius_schur_phase(a) + θₐ = twist(a) + if !inv + factor = f₁.isdual[i] ? χₐ * θₐ : one(θₐ) + else + factor = f₁.isdual[i] ? one(θₐ) : χₐ * conj(θₐ) + end + isdual′ = TupleTools.setindex(f₁.isdual, !f₁.isdual[i], i) + f₁′ = FusionTree{I}(f₁.uncoupled, f₁.coupled, isdual′, f₁.innerlines, f₁.vertices) + return SingletonDict((f₁′, f₂) => factor) + else + i -= N₁ + a = f₂.uncoupled[i] + χₐ = frobenius_schur_phase(a) + θₐ = twist(a) + if !inv + factor = f₂.isdual[i] ? χₐ * one(θₐ) : θₐ + else + factor = f₂.isdual[i] ? conj(θₐ) : χₐ * one(θₐ) + end + isdual′ = TupleTools.setindex(f₂.isdual, !f₂.isdual[i], i) + f₂′ = FusionTree{I}(f₂.uncoupled, f₂.coupled, isdual′, f₂.innerlines, f₂.vertices) + return SingletonDict((f₁, f₂′) => factor) + end +end +function flip((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ind; inv::Bool = false) where {I, N₁, N₂} + f₁′, f₂′ = f₁, f₂ + factor = one(sectorscalartype(I)) + for i in ind + (f₁′, f₂′), s = only(flip((f₁′, f₂′), i; inv)) + factor *= s + end + return SingletonDict((f₁′, f₂′) => factor) +end diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl new file mode 100644 index 000000000..b4a93f8d8 --- /dev/null +++ b/src/fusiontrees/braiding_manipulations.jl @@ -0,0 +1,482 @@ +# BRAIDING MANIPULATIONS: +#----------------------------------------------- +# -> manipulations that depend on a braiding +# -> requires both Fsymbol and Rsymbol +""" + artin_braid(f::FusionTree, i; inv::Bool = false) -> <:AbstractDict{typeof(f), <:Number} + +Perform an elementary braid (Artin generator) of neighbouring uncoupled indices `i` and +`i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and +corresponding coefficients. + +The keyword `inv` determines whether index `i` will braid above or below index `i+1`, i.e. +applying `artin_braid(f′, i; inv = true)` to all the outputs `f′` of +`artin_braid(f, i; inv = false)` and collecting the results should yield a single fusion +tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword has no effect +if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. +""" +function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} + 1 <= i < N || + throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) + uncoupled = f.uncoupled + a, b = uncoupled[i], uncoupled[i + 1] + uncoupled′ = TupleTools.setindex(uncoupled, b, i) + uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) + coupled′ = f.coupled + isdual′ = TupleTools.setindex(f.isdual, f.isdual[i], i + 1) + isdual′ = TupleTools.setindex(isdual′, f.isdual[i + 1], i) + inner = f.innerlines + inner_extended = (uncoupled[1], inner..., coupled′) + vertices = f.vertices + oneT = one(sectorscalartype(I)) + + if isunit(a) || isunit(b) + # braiding with trivial sector: simple and always possible + inner′ = inner + vertices′ = vertices + if i > 1 # we also need to alter innerlines and vertices + inner′ = TupleTools.setindex( + inner, + inner_extended[isunit(a) ? (i + 1) : (i - 1)], + i - 1 + ) + vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) + vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) + end + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + return fusiontreedict(I)(f′ => oneT) + end + + BraidingStyle(I) isa NoBraiding && + throw(SectorMismatch("Cannot braid sectors $(uncoupled[i]) and $(uncoupled[i + 1])")) + + if i == 1 + c = N > 2 ? inner[1] : coupled′ + if FusionStyle(I) isa MultiplicityFreeFusion + R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) + return fusiontreedict(I)(f′ => R) + else # GenericFusion + μ = vertices[1] + Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) + local newtrees + for ν in axes(Rmat, 2) + R = oftype(oneT, Rmat[μ, ν]) + iszero(R) && continue + vertices′ = TupleTools.setindex(vertices, ν, 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) + if (@isdefined newtrees) + push!(newtrees, f′ => R) + else + newtrees = fusiontreedict(I)(f′ => R) + end + end + return newtrees + end + end + # case i > 1: other naming convention + b = uncoupled[i] + d = uncoupled[i + 1] + a = inner_extended[i - 1] + c = inner_extended[i] + e = inner_extended[i + 1] + if FusionStyle(I) isa UniqueFusion + c′ = first(a ⊗ d) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + return fusiontreedict(I)(f′ => coeff) + elseif FusionStyle(I) isa SimpleFusion + local newtrees + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) + iszero(coeff) && continue + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + if (@isdefined newtrees) + push!(newtrees, f′ => coeff) + else + newtrees = fusiontreedict(I)(f′ => coeff) + end + end + return newtrees + else # GenericFusion + local newtrees + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs + Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) + Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) + Fmat = Fsymbol(d, a, b, e, c′, c) + μ = vertices[i - 1] + ν = vertices[i] + for σ in 1:Nsymbol(a, d, c′) + for λ in 1:Nsymbol(c′, b, e) + coeff = zero(oneT) + for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) + coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * conj(Rmat2[σ, κ]) + end + iszero(coeff) && continue + vertices′ = TupleTools.setindex(vertices, σ, i - 1) + vertices′ = TupleTools.setindex(vertices′, λ, i) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + if (@isdefined newtrees) + push!(newtrees, f′ => coeff) + else + newtrees = fusiontreedict(I)(f′ => coeff) + end + end + end + end + return newtrees + end +end + +function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where {I, N} + 1 <= i < N || + throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) + + uncoupled = src.uncoupled[1] + a, b = uncoupled[i], uncoupled[i + 1] + uncoupled′ = TupleTools.setindex(uncoupled, b, i) + uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) + coupled′ = rightunit(src.uncoupled[1][N]) + + isdual = src.isdual[1] + isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) + isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) + dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ()); sizehint = length(src)) + + oneT = one(sectorscalartype(I)) + + indexmap = treeindex_map(dst) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + if isone(a) || isone(b) # braiding with trivial sector: simple and always possible + for (col, (f, f₂)) in enumerate(fusiontrees(src)) + inner = f.innerlines + inner_extended = (uncoupled[1], inner..., coupled′) + vertices = f.vertices + inner′ = inner + vertices′ = vertices + if i > 1 # we also need to alter innerlines and vertices + inner′ = TupleTools.setindex( + inner, inner_extended[isone(a) ? (i + 1) : (i - 1)], i - 1 + ) + vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) + vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) + end + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + row = indexmap[treeindex_data((f′, f₂))] + @inbounds U[row, col] = oneT + end + return dst, U + end + + BraidingStyle(I) isa NoBraiding && + throw(SectorMismatch(lazy"Cannot braid sectors $a and $b")) + + for (col, (f, f₂)) in enumerate(fusiontrees(src)) + inner = f.innerlines + inner_extended = (uncoupled[1], inner..., coupled′) + vertices = f.vertices + + if i == 1 + c = N > 2 ? inner[1] : coupled′ + if FusionStyle(I) isa MultiplicityFreeFusion + R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) + row = indexmap[treeindex_data((f′, f₂))] + @inbounds U[row, col] = R + else # GenericFusion + μ = vertices[1] + Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) + for ν in axes(Rmat, 2) + R = oftype(oneT, Rmat[μ, ν]) + iszero(R) && continue + vertices′ = TupleTools.setindex(vertices, ν, 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) + row = indexmap[treeindex_data((f′, f₂))] + @inbounds U[row, col] = R + end + end + continue + end + # case i > 1: other naming convention + b = uncoupled[i] + d = uncoupled[i + 1] + a = inner_extended[i - 1] + c = inner_extended[i] + e = inner_extended[i + 1] + if FusionStyle(I) isa UniqueFusion + c′ = first(a ⊗ d) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + row = indexmap[treeindex_data((f′, f₂))] + @inbounds U[row, col] = coeff + elseif FusionStyle(I) isa SimpleFusion + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) + end + ) + iszero(coeff) && continue + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + row = indexmap[treeindex_data((f′, f₂))] + @inbounds U[row, col] = coeff + end + else # GenericFusion + cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) + for c′ in cs + Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) + Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) + Fmat = Fsymbol(d, a, b, e, c′, c) + μ = vertices[i - 1] + ν = vertices[i] + for σ in 1:Nsymbol(a, d, c′) + for λ in 1:Nsymbol(c′, b, e) + coeff = zero(oneT) + for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) + coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * + conj(Rmat2[σ, κ]) + end + iszero(coeff) && continue + vertices′ = TupleTools.setindex(vertices, σ, i - 1) + vertices′ = TupleTools.setindex(vertices′, λ, i) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + row = indexmap[treeindex_data((f′, f₂))] + @inbounds U[row, col] = coeff + end + end + end + end + end + + return dst, U +end + +# braid fusion tree +""" + braid(f::FusionTree{<:Sector, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) + -> <:AbstractDict{typeof(t), <:Number} + +Perform a braiding of the uncoupled indices of the fusion tree `f` and return the result as +a `<:AbstractDict` of output trees and corresponding coefficients. The braiding is +determined by specifying that the new sector at position `k` corresponds to the sector that +was originally at the position `i = p[k]`, and assigning to every index `i` of the original +fusion tree a distinct level or depth `levels[i]`. This permutation is then decomposed into +elementary swaps between neighbouring indices, where the swaps are applied as braids such +that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` and +``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general +braid, but a general braid can be obtained by combining such operations. +""" +function braid(f::FusionTree{I, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) where {I, N} + TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) + if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding + coeff = one(sectorscalartype(I)) + for i in 1:N + for j in 1:(i - 1) + if p[j] > p[i] + a, b = f.uncoupled[p[j]], f.uncoupled[p[i]] + coeff *= Rsymbol(a, b, first(a ⊗ b)) + end + end + end + uncoupled′ = TupleTools._permute(f.uncoupled, p) + coupled′ = f.coupled + isdual′ = TupleTools._permute(f.isdual, p) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′) + return fusiontreedict(I)(f′ => coeff) + else + T = sectorscalartype(I) + coeff = one(T) + trees = FusionTreeDict(f => coeff) + newtrees = empty(trees) + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + for (f, c) in trees + for (f′, c′) in artin_braid(f, s; inv) + newtrees[f′] = get(newtrees, f′, zero(coeff)) + c * c′ + end + end + l = levels[s] + levels = TupleTools.setindex(levels, levels[s + 1], s) + levels = TupleTools.setindex(levels, l, s + 1) + trees, newtrees = newtrees, trees + empty!(newtrees) + end + return trees + end +end + +# permute fusion tree +""" + permute(f::FusionTree, p::NTuple{N, Int}) -> <:AbstractDict{typeof(t), <:Number} + +Perform a permutation of the uncoupled indices of the fusion tree `f` and returns the result +as a `<:AbstractDict` of output trees and corresponding coefficients; this requires that +`BraidingStyle(sectortype(f)) isa SymmetricBraiding`. +""" +function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I, N} + @assert BraidingStyle(I) isa SymmetricBraiding + return braid(f, p, ntuple(identity, Val(N))) +end + +# braid double fusion tree +""" + braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) + -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} + +Input is a fusion-splitting tree pair that describes the fusion of a set of incoming +uncoupled sectors to a set of outgoing uncoupled sectors, represented using the splitting +tree `f₁` and fusion tree `f₂`, such that the incoming sectors `f₂.uncoupled` are fused to +`f₁.coupled == f₂.coupled` and then to the outgoing sectors `f₁.uncoupled`. Compute new +trees and corresponding coefficients obtained from repartitioning and braiding the tree such +that sectors `p1` become outgoing and sectors `p2` become incoming. The uncoupled indices in +splitting tree `f₁` and fusion tree `f₂` have levels (or depths) `levels1` and `levels2` +respectively, which determines how indices braid. In particular, if `i` and `j` cross, +``τ_{i,j}`` is applied if `levels[i] < levels[j]` and ``τ_{j,i}^{-1}`` if `levels[i] > +levels[j]`. This does not allow to encode the most general braid, but a general braid can +be obtained by combining such operations. +""" +function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, levels::Index2Tuple) + @assert numind(src) == length(p[1]) + length(p[2]) + @assert numout(src) == length(levels[1]) && numin(src) == length(levels[2]) + @assert TupleTools.isperm((p[1]..., p[2]...)) + return fsbraid((src, p, levels)) +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} + +Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSPBraidKey{I, N₁, N₂}} + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + E = sectorscalartype(I) + return fusiontreedict(I){Tuple{F₁, F₂}, E} +end +Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSBBraidKey{I, N₁, N₂}} + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + E = sectorscalartype(I) + return Tuple{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} +end + +@cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSPBraidKey{I, N₁, N₂}} + ((f₁, f₂), (p1, p2), (l1, l2)) = key + p = linearizepermutation(p1, p2, length(f₁), length(f₂)) + levels = (l1..., reverse(l2)...) + local newtrees + for ((f, f0), coeff1) in repartition((f₁, f₂), N₁ + N₂) + for (f′, coeff2) in braid(f, p, levels) + for ((f₁′, f₂′), coeff3) in repartition((f′, f0), N₁) + if @isdefined newtrees + newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + + coeff1 * coeff2 * coeff3 + else + newtrees = fusiontreedict(I)((f₁′, f₂′) => coeff1 * coeff2 * coeff3) + end + end + end + end + return newtrees +end + +function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} + U = zeros(sectorscalartype(I), length(dst), length(src)) + indexmap = treeindex_map(dst) + for (col, f) in enumerate(fusiontrees(src)) + for (f′, c) in transform(f) + row = indexmap[f′] + U[row, col] = c + end + end + return U +end +@cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSBBraidKey{I, N₁, N₂}} + src, (p1, p2), (l1, l2) = key + + p = linearizepermutation(p1, p2, numout(src), numin(src)) + levels = (l1..., reverse(l2)...) + + dst, U = repartition(src, numind(src)) + + if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding + uncoupled′ = TupleTools._permute(dst.uncoupled[1], p) + isdual′ = TupleTools._permute(dst.isdual[1], p) + + dst′ = FusionTreeBlock{I}(uncoupled′, isdual′) + U_tmp = transformation_matrix(dst′, dst) do (f₁, f₂) + return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) + end + dst = dst′ + U = U_tmp * U + else + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + dst, U_tmp = artin_braid(dst, s; inv) + U = U_tmp * U + end + end + + if N₂ == 0 + return dst, U + else + dst, U_tmp = repartition(dst, N₁) + U = U_tmp * U + return dst, U + end +end + +CacheStyle(::typeof(fsbraid), k::FSPBraidKey{I}) where {I} = + FusionStyle(I) isa UniqueFusion ? NoCache() : GlobalLRUCache() +CacheStyle(::typeof(fsbraid), k::FSBBraidKey{I}) where {I} = + FusionStyle(I) isa UniqueFusion ? NoCache() : GlobalLRUCache() + +""" + permute((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple) + -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} + +Input is a double fusion tree that describes the fusion of a set of incoming uncoupled +sectors to a set of outgoing uncoupled sectors, represented using the individual trees of +outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled sector +`t1.coupled == t2.coupled`). Computes new trees and corresponding coefficients obtained from +repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors +`p2` become incoming. +""" +function permute(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple) + @assert BraidingStyle(src) isa SymmetricBraiding + levels1 = ntuple(identity, numout(src)) + levels2 = numout(src) .+ ntuple(identity, numin(src)) + return braid(src, p, (levels1, levels2)) +end diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl new file mode 100644 index 000000000..a679be085 --- /dev/null +++ b/src/fusiontrees/duality_manipulations.jl @@ -0,0 +1,896 @@ +# ELEMENTARY DUALITY MANIPULATIONS: A- and B-moves +#--------------------------------------------------------- +# -> elementary manipulations that depend on the duality (rigidity) and pivotal structure +# -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) +# -> B-move (bendleft, bendright) is simple in standard basis +# -> A-move (foldleft, foldright) is complicated, needs to be reexpressed in standard form + +@doc """ + bendright(src) -> dst, coeffs + +Map the final splitting vertex `a ⊗ b ← c` of each tree in `src` to a (linear combination of) +fusion vertices `a ← c ⊗ dual(b)` in `dst`. + +``` + ╰─┬─╯ | | | ╰─┬─╯ | | | + ╰─┬─╯ | | ╰─┬─╯ | | + ╰ ⋯ ┬╯ | ╰ ⋯ ┬╯ | + | | → ╰─┬─╯ + ╭ ⋯ ┴╮ | ╭ ⋯ ╯ + ╭─┴─╮ | | ╭─┴─╮ + ╭─┴─╮ | ╰─╯ ╭─┴─╮ | +``` + +See also [`bendleft`](@ref). +""" bendright + +function bendright(src::FusionTreePair) + I, N₁, N₂ = sectortype(src), numout(src), numin(src) + f₁, f₂ = src + @assert N₁ > 0 + c = f₁.coupled + a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : + (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + b = f₁.uncoupled[N₁] + + uncoupled1 = TupleTools.front(f₁.uncoupled) + isdual1 = TupleTools.front(f₁.isdual) + inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () + vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () + f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) + + uncoupled2 = (f₂.uncoupled..., dual(b)) + isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) + inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () + + coeff₀ = sqrtdim(c) * invsqrtdim(a) + if f₁.isdual[N₁] + coeff₀ *= conj(frobenius_schur_phase(dual(b))) + end + if FusionStyle(I) isa MultiplicityFreeFusion + coeff = coeff₀ * Bsymbol(a, b, c) + vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + return SingletonDict((f₁′, f₂′) => coeff) + else + local newtrees + Bmat = Bsymbol(a, b, c) + μ = N₁ > 1 ? f₁.vertices[end] : 1 + for ν in axes(Bmat, 2) + coeff = coeff₀ * Bmat[μ, ν] + iszero(coeff) && continue + vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + if @isdefined newtrees + push!(newtrees, (f₁′, f₂′) => coeff) + else + newtrees = FusionTreeDict((f₁′, f₂′) => coeff) + end + end + return newtrees + end +end +function bendright(src::FusionTreeBlock) + uncoupled_dst = ( + TupleTools.front(src.uncoupled[1]), + (src.uncoupled[2]..., dual(src.uncoupled[1][end])), + ) + isdual_dst = ( + TupleTools.front(src.isdual[1]), + (src.isdual[2]..., !(src.isdual[1][end])), + ) + I = sectortype(src) + N₁ = numout(src) + N₂ = numin(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) + indexmap = treeindex_map(dst) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) + c = f₁.coupled + a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : + (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + b = f₁.uncoupled[N₁] + + uncoupled1 = TupleTools.front(f₁.uncoupled) + isdual1 = TupleTools.front(f₁.isdual) + inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () + vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () + f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) + + uncoupled2 = (f₂.uncoupled..., dual(b)) + isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) + inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () + + coeff₀ = sqrtdim(c) * invsqrtdim(a) + if f₁.isdual[N₁] + coeff₀ *= conj(frobenius_schur_phase(dual(b))) + end + if FusionStyle(I) isa MultiplicityFreeFusion + coeff = coeff₀ * Bsymbol(a, b, c) + vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[treeindex_data((f₁′, f₂′))] + @inbounds U[row, col] = coeff + else + Bmat = Bsymbol(a, b, c) + μ = N₁ > 1 ? f₁.vertices[end] : 1 + for ν in axes(Bmat, 2) + coeff = coeff₀ * Bmat[μ, ν] + iszero(coeff) && continue + vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[treeindex_data((f₁′, f₂′))] + @inbounds U[row, col] = coeff + end + end + end + + return dst, U +end + +@doc """ + bendleft(src) -> dst, coeffs + +Map the final fusion vertex `a ← c ⊗ dual(b)` of each tree in `src` to a (linear combination of) +splitting vertices `a ⊗ b ← c` in `dst`. + +``` + ╰─┬─╯ | ╭─╮ ╰─┬─╯ | + ╰─┬─╯ | | ╰─┬─╯ + ╰ ⋯ ┬╯ | ╰ ⋯ ╮ + | | → ╭─┴─╮ + ╭ ⋯ ┴╮ | ╭ ⋯ ┴╮ | + ╭─┴─╮ | | ╭─┴─╮ | | + ╭─┴─╮ | | | ╭─┴─╮ | | | +``` + +See also [`bendright`](@ref). +""" bendleft + +function bendleft((f₁, f₂)::FusionTreePair{I}) where {I} + return fusiontreedict(I)( + (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in bendright((f₂, f₁)) + ) +end + +# !! note that this is more or less a copy of bendright through +# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in bendleft(src) +function bendleft(src::FusionTreeBlock) + uncoupled_dst = ( + (src.uncoupled[1]..., dual(src.uncoupled[2][end])), + TupleTools.front(src.uncoupled[2]), + ) + isdual_dst = ( + (src.isdual[1]..., !(src.isdual[2][end])), + TupleTools.front(src.isdual[2]), + ) + I = sectortype(src) + N₁ = numin(src) + N₂ = numout(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) + indexmap = treeindex_map(dst) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) + c = f₁.coupled + a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : + (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + b = f₁.uncoupled[N₁] + + uncoupled1 = TupleTools.front(f₁.uncoupled) + isdual1 = TupleTools.front(f₁.isdual) + inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () + vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () + f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) + + uncoupled2 = (f₂.uncoupled..., dual(b)) + isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) + inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () + + coeff₀ = sqrtdim(c) * invsqrtdim(a) + if f₁.isdual[N₁] + coeff₀ *= conj(frobenius_schur_phase(dual(b))) + end + if FusionStyle(I) isa MultiplicityFreeFusion + coeff = coeff₀ * Bsymbol(a, b, c) + vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[treeindex_data((f₂′, f₁′))] + @inbounds U[row, col] = conj(coeff) + else + Bmat = Bsymbol(a, b, c) + μ = N₁ > 1 ? f₁.vertices[end] : 1 + for ν in axes(Bmat, 2) + coeff = coeff₀ * Bmat[μ, ν] + iszero(coeff) && continue + vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () + f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) + row = indexmap[treeindex_data((f₂′, f₁′))] + @inbounds U[row, col] = conj(coeff) + end + end + end + + return dst, U +end + + +# change to N₁ - 1, N₂ + 1 +function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} + # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) + @assert N₁ > 0 + a = f₁.uncoupled[1] + isduala = f₁.isdual[1] + factor = sqrtdim(a) + if !isduala + factor *= conj(frobenius_schur_phase(a)) + end + c1 = dual(a) + c2 = f₁.coupled + uncoupled = Base.tail(f₁.uncoupled) + isdual = Base.tail(f₁.isdual) + if FusionStyle(I) isa UniqueFusion + c = first(c1 ⊗ c2) + fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) + fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) + return fusiontreedict(I)((fl, fr) => factor) + else + local newtrees + if N₁ == 1 + cset = (leftunit(c1),) # or rightunit(a) + elseif N₁ == 2 + cset = (f₁.uncoupled[2],) + else + cset = ⊗(Base.tail(f₁.uncoupled)...) + end + for c in c1 ⊗ c2 + c ∈ cset || continue + for μ in 1:Nsymbol(c1, c2, c) + fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + fr_coeffs = insertat(fc, 2, f₂) + for (fl′, coeff1) in insertat(fc, 2, f₁) + N₁ > 1 && !isunit(fl′.innerlines[1]) && continue + coupled = fl′.coupled + uncoupled = Base.tail(Base.tail(fl′.uncoupled)) + isdual = Base.tail(Base.tail(fl′.isdual)) + inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) + vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) + fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) + for (fr, coeff2) in fr_coeffs + coeff = factor * coeff1 * conj(coeff2) + if (@isdefined newtrees) + newtrees[(fl, fr)] = get(newtrees, (fl, fr), zero(coeff)) + + coeff + else + newtrees = fusiontreedict(I)((fl, fr) => coeff) + end + end + end + end + end + return newtrees + end +end + +function foldright(src::FusionTreeBlock) + uncoupled_dst = ( + Base.tail(src.uncoupled[1]), + (dual(first(src.uncoupled[1])), src.uncoupled[2]...), + ) + isdual_dst = (Base.tail(src.isdual[1]), (!first(src.isdual[1]), src.isdual[2]...)) + I = sectortype(src) + N₁ = numout(src) + N₂ = numin(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) + indexmap = treeindex_map(dst) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) + # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) + a = f₁.uncoupled[1] + isduala = f₁.isdual[1] + factor = sqrtdim(a) + if !isduala + factor *= conj(frobenius_schur_phase(a)) + end + c1 = dual(a) + c2 = f₁.coupled + uncoupled = Base.tail(f₁.uncoupled) + isdual = Base.tail(f₁.isdual) + if FusionStyle(I) isa UniqueFusion + c = first(c1 ⊗ c2) + fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) + fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) + row = indexmap[treeindex_data((fl, fr))] + @inbounds U[row, col] = factor + else + if N₁ == 1 + cset = (leftunit(c1),) # or rightunit(a) + elseif N₁ == 2 + cset = (f₁.uncoupled[2],) + else + cset = ⊗(Base.tail(f₁.uncoupled)...) + end + for c in c1 ⊗ c2 + c ∈ cset || continue + for μ in 1:Nsymbol(c1, c2, c) + fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + frs_coeffs = insertat(fc, 2, f₂) + for (fl′, coeff1) in insertat(fc, 2, f₁) + N₁ > 1 && !isone(fl′.innerlines[1]) && continue + coupled = fl′.coupled + uncoupled = Base.tail(Base.tail(fl′.uncoupled)) + isdual = Base.tail(Base.tail(fl′.isdual)) + inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) + vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) + fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) + for (fr, coeff2) in frs_coeffs + coeff = factor * coeff1 * conj(coeff2) + row = indexmap[treeindex_data((fl, fr))] + @inbounds U[row, col] = coeff + end + end + end + end + end + end + + return dst, U +end + +# change to N₁ + 1, N₂ - 1 +function foldleft((f₁, f₂)::FusionTreePair{I}) where {I} + # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b + return fusiontreedict(I)( + (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in foldright((f₂, f₁)) + ) +end + +# !! note that this is more or less a copy of foldright through +# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in foldright(src) +function foldleft(src::FusionTreeBlock) + uncoupled_dst = ( + (dual(first(src.uncoupled[2])), src.uncoupled[1]...), + Base.tail(src.uncoupled[2]), + ) + isdual_dst = ( + (!first(src.isdual[2]), src.isdual[1]...), + Base.tail(src.isdual[2]), + ) + I = sectortype(src) + N₁ = numin(src) + N₂ = numout(src) + @assert N₁ > 0 + + dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) + indexmap = treeindex_map(dst) + U = zeros(sectorscalartype(I), length(dst), length(src)) + + for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) + # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) + a = f₁.uncoupled[1] + isduala = f₁.isdual[1] + factor = sqrtdim(a) + if !isduala + factor *= conj(frobenius_schur_phase(a)) + end + c1 = dual(a) + c2 = f₁.coupled + uncoupled = Base.tail(f₁.uncoupled) + isdual = Base.tail(f₁.isdual) + if FusionStyle(I) isa UniqueFusion + c = first(c1 ⊗ c2) + fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) + fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) + row = indexmap[treeindex_data((fr, fl))] + @inbounds U[row, col] = conj(factor) + else + if N₁ == 1 + cset = (leftunit(c1),) # or rightunit(a) + elseif N₁ == 2 + cset = (f₁.uncoupled[2],) + else + cset = ⊗(Base.tail(f₁.uncoupled)...) + end + for c in c1 ⊗ c2 + c ∈ cset || continue + for μ in 1:Nsymbol(c1, c2, c) + fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) + fr_coeffs = insertat(fc, 2, f₂) + for (fl′, coeff1) in insertat(fc, 2, f₁) + N₁ > 1 && !isone(fl′.innerlines[1]) && continue + coupled = fl′.coupled + uncoupled = Base.tail(Base.tail(fl′.uncoupled)) + isdual = Base.tail(Base.tail(fl′.isdual)) + inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) + vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) + fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) + for (fr, coeff2) in fr_coeffs + coeff = factor * coeff1 * conj(coeff2) + row = indexmap[treeindex_data((fr, fl))] + @inbounds U[row, col] = conj(coeff) + end + end + end + end + end + end + return dst, U +end + +# clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft +function cycleclockwise((f₁, f₂)::FusionTreePair{I}) where {I} + local newtrees + if length(f₁) > 0 + for ((f1a, f2a), coeffa) in foldright((f₁, f₂)) + for ((f1b, f2b), coeffb) in bendleft((f1a, f2a)) + coeff = coeffa * coeffb + if (@isdefined newtrees) + newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff + else + newtrees = fusiontreedict(I)((f1b, f2b) => coeff) + end + end + end + else + for ((f1a, f2a), coeffa) in bendleft((f₁, f₂)) + for ((f1b, f2b), coeffb) in foldright((f1a, f2a)) + coeff = coeffa * coeffb + if (@isdefined newtrees) + newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff + else + newtrees = fusiontreedict(I)((f1b, f2b) => coeff) + end + end + end + end + return newtrees +end +function cycleclockwise(src::FusionTreeBlock) + if numout(src) > 0 + tmp, U₁ = foldright(src) + dst, U₂ = bendleft(tmp) + else + tmp, U₁ = bendleft(src) + dst, U₂ = foldright(tmp) + end + return dst, U₂ * U₁ +end + +# anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright +function cycleanticlockwise((f₁, f₂)::FusionTreePair{I}) where {I} + local newtrees + if length(f₂) > 0 + for ((f1a, f2a), coeffa) in foldleft((f₁, f₂)) + for ((f1b, f2b), coeffb) in bendright((f1a, f2a)) + coeff = coeffa * coeffb + if (@isdefined newtrees) + newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff + else + newtrees = fusiontreedict(I)((f1b, f2b) => coeff) + end + end + end + else + for ((f1a, f2a), coeffa) in bendright((f₁, f₂)) + for ((f1b, f2b), coeffb) in foldleft((f1a, f2a)) + coeff = coeffa * coeffb + if (@isdefined newtrees) + newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff + else + newtrees = fusiontreedict(I)((f1b, f2b) => coeff) + end + end + end + end + return newtrees +end +function cycleanticlockwise(src::FusionTreeBlock) + if numin(src) > 0 + tmp, U₁ = foldleft(src) + dst, U₂ = bendright(tmp) + else + tmp, U₁ = bendright(src) + dst, U₂ = foldleft(tmp) + end + return dst, U₂ * U₁ +end + +# COMPOSITE DUALITY MANIPULATIONS PART 1: Repartition and transpose +#------------------------------------------------------------------- +# -> composite manipulations that depend on the duality (rigidity) and pivotal structure +# -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) +# -> transpose expressed as cyclic permutation + +# repartition double fusion tree +""" + repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, N::Int) where {I, N₁, N₂} + -> <:AbstractDict{<:FusionTreePair{I, N, N₁+N₂-N}}, <:Number} + +Input is a double fusion tree that describes the fusion of a set of incoming uncoupled +sectors to a set of outgoing uncoupled sectors, represented using the individual trees of +outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical coupled sector +`f₁.coupled == f₂.coupled`). Computes new trees and corresponding coefficients obtained from +repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to +have `N` outgoing sectors. +""" +@inline function repartition((f₁, f₂)::FusionTreePair, N::Int) + f₁.coupled == f₂.coupled || throw(SectorMismatch()) + @assert 0 <= N <= length(f₁) + length(f₂) + return _recursive_repartition((f₁, f₂), Val(N)) +end + +function _recursive_repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ::Val{N}) where {I, N₁, N₂, N} + # recursive definition is only way to get correct number of loops for + # GenericFusion, but is too complex for type inference to handle, so we + # precompute the parameters of the return type + F₁ = fusiontreetype(I, N) + F₂ = fusiontreetype(I, N₁ + N₂ - N) + FF = Tuple{F₁, F₂} + T = sectorscalartype(I) + coeff = one(T) + if N == N₁ + return fusiontreedict(I){Tuple{F₁, F₂}, T}((f₁, f₂) => coeff) + else + local newtrees::fusiontreedict(I){Tuple{F₁, F₂}, T} + for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright((f₁, f₂)) : bendleft((f₁, f₂))) + for ((f₁′′, f₂′′), coeff2) in _recursive_repartition((f₁′, f₂′), Val(N)) + if (@isdefined newtrees) + push!(newtrees, (f₁′′, f₂′′) => coeff1 * coeff2) + else + newtrees = fusiontreedict(I){FF, T}((f₁′′, f₂′′) => coeff1 * coeff2) + end + end + end + return newtrees + end +end +@inline function repartition(src::FusionTreeBlock, N::Int) + @assert 0 <= N <= numind(src) + return repartition(src, Val(N)) +end + +#= +Using a generated function here to ensure type stability by unrolling the loops: +```julia +dst, U = bendleft/right(src) + +# repeat the following 2 lines N - 1 times +dst, Utmp = bendleft/right(dst) +U = Utmp * U + +return dst, U +``` +=# +function _repartition_body(N) + if N == 0 + ex = quote + T = sectorscalartype(sectortype(src)) + U = copyto!(zeros(T, length(src), length(src)), LinearAlgebra.I) + return src, U + end + else + f = N < 0 ? bendleft : bendright + ex_rep = Expr(:block) + for _ in 1:(abs(N) - 1) + push!(ex_rep.args, :((dst, Utmp) = $f(dst))) + push!(ex_rep.args, :(U = Utmp * U)) + end + ex = quote + dst, U = $f(src) + $ex_rep + return dst, U + end + end + return ex +end +@generated function repartition(src::FusionTreeBlock, ::Val{N}) where {N} + 1 + 1 + return _repartition_body(numout(src) - N) +end + +""" + transpose((f₁, f₂)::FusionTreePair{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} + -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} + +Input is a double fusion tree that describes the fusion of a set of incoming uncoupled +sectors to a set of outgoing uncoupled sectors, represented using the individual trees of +outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled sector +`t1.coupled == t2.coupled`). Computes new trees and corresponding coefficients obtained from +repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors +`p2` become incoming. +""" +function Base.transpose(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple) + numind(src) == length(p[1]) + length(p[2]) || throw(ArgumentError("invalid permutation p")) + p′ = linearizepermutation(p..., numout(src), numin(src)) + iscyclicpermutation(p′) || throw(ArgumentError("invalid permutation p")) + return fstranspose((src, p)) +end + +const FSPTransposeKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁, N₂}} +const FSBTransposeKey{I, N₁, N₂} = Tuple{FusionTreeBlock{I}, Index2Tuple{N₁, N₂}} + +Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSPTransposeKey{I, N₁, N₂}} + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + E = sectorscalartype(I) + return fusiontreedict(I){Tuple{F₁, F₂}, E} +end +Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSBTransposeKey{I, N₁, N₂}} + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + E = sectorscalartype(I) + return Tuple{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} +end + +@cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSPTransposeKey{I, N₁, N₂}} + (f₁, f₂), (p1, p2) = key + N = N₁ + N₂ + p = linearizepermutation(p1, p2, length(f₁), length(f₂)) + newtrees = repartition((f₁, f₂), N₁) + length(p) == 0 && return newtrees + i1 = findfirst(==(1), p) + @assert i1 !== nothing + i1 == 1 && return newtrees + Nhalf = N >> 1 + while 1 < i1 <= Nhalf + local newtrees′ + for ((f1a, f2a), coeffa) in newtrees + for ((f1b, f2b), coeffb) in cycleanticlockwise((f1a, f2a)) + coeff = coeffa * coeffb + if (@isdefined newtrees′) + newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff + else + newtrees′ = fusiontreedict(I)((f1b, f2b) => coeff) + end + end + end + newtrees = newtrees′ + i1 -= 1 + end + while Nhalf < i1 + local newtrees′ + for ((f1a, f2a), coeffa) in newtrees + for ((f1b, f2b), coeffb) in cycleclockwise((f1a, f2a)) + coeff = coeffa * coeffb + if (@isdefined newtrees′) + newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff + else + newtrees′ = fusiontreedict(I)((f1b, f2b) => coeff) + end + end + end + newtrees = newtrees′ + i1 = mod1(i1 + 1, N) + end + return newtrees +end +@cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSBTransposeKey{I, N₁, N₂}} + src, (p1, p2) = key + + N = N₁ + N₂ + p = linearizepermutation(p1, p2, numout(src), numin(src)) + + dst, U = repartition(src, N₁) + length(p) == 0 && return dst, U + i1 = findfirst(==(1), p)::Int + i1 == 1 && return dst, U + + Nhalf = N >> 1 + while 1 < i1 ≤ Nhalf + dst, U_tmp = cycleanticlockwise(dst) + U = U_tmp * U + i1 -= 1 + end + while Nhalf < i1 + dst, U_tmp = cycleclockwise(dst) + U = U_tmp * U + i1 = mod1(i1 + 1, N) + end + + return dst, U +end + +CacheStyle(::typeof(fstranspose), k::FSPTransposeKey{I}) where {I} = + FusionStyle(I) isa UniqueFusion ? NoCache() : GlobalLRUCache() +CacheStyle(::typeof(fstranspose), k::FSBTransposeKey{I}) where {I} = + FusionStyle(I) isa UniqueFusion ? NoCache() : GlobalLRUCache() + +# COMPOSITE DUALITY MANIPULATIONS PART 2: Planar traces +#------------------------------------------------------------------- +# -> composite manipulations that depend on the duality (rigidity) and pivotal structure +# -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) + +function planar_trace( + (f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}, (q1, q2)::Index2Tuple{N₃, N₃} + ) where {I, N₁, N₂, N₃} + N = N₁ + N₂ + 2N₃ + @assert length(f₁) + length(f₂) == N + if N₃ == 0 + return transpose((f₁, f₂), (p1, p2)) + end + + linearindex = ( + ntuple(identity, Val(length(f₁)))..., + reverse(length(f₁) .+ ntuple(identity, Val(length(f₂))))..., + ) + + q1′ = TupleTools.getindices(linearindex, q1) + q2′ = TupleTools.getindices(linearindex, q2) + p1′, p2′ = let q′ = (q1′..., q2′...) + ( + map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p1)), + map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p2)), + ) + end + + T = sectorscalartype(I) + F₁ = fusiontreetype(I, N₁) + F₂ = fusiontreetype(I, N₂) + newtrees = FusionTreeDict{Tuple{F₁, F₂}, T}() + for ((f₁′, f₂′), coeff′) in repartition((f₁, f₂), N) + for (f₁′′, coeff′′) in planar_trace(f₁′, (q1′, q2′)) + for (f12′′′, coeff′′′) in transpose((f₁′′, f₂′), (p1′, p2′)) + coeff = coeff′ * coeff′′ * coeff′′′ + if !iszero(coeff) + newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff + end + end + end + end + return newtrees +end + +""" + planar_trace(f::FusionTree{I,N}, (q1, q2)::Index2Tuple{N₃,N₃}) where {I,N,N₃} + -> <:AbstractDict{FusionTree{I,N-2*N₃}, <:Number} + +Perform a planar trace of the uncoupled indices of the fusion tree `f` at `q1` with those at +`q2`, where `q1[i]` is connected to `q2[i]` for all `i`. The result is returned as a dictionary +of output trees and corresponding coefficients. +""" +function planar_trace(f::FusionTree{I, N}, (q1, q2)::Index2Tuple{N₃, N₃}) where {I, N, N₃} + T = sectorscalartype(I) + F = fusiontreetype(I, N - 2 * N₃) + newtrees = FusionTreeDict{F, T}() + N₃ === 0 && return push!(newtrees, f => one(T)) + + for (i, j) in zip(q1, q2) + (f.uncoupled[i] == dual(f.uncoupled[j]) && f.isdual[i] != f.isdual[j]) || + return newtrees + end + k = 1 + local i, j + while k <= N₃ + if mod1(q1[k] + 1, N) == q2[k] + i = q1[k] + j = q2[k] + break + elseif mod1(q2[k] + 1, N) == q1[k] + i = q2[k] + j = q1[k] + break + else + k += 1 + end + end + k > N₃ && throw(ArgumentError("Not a planar trace")) + + q1′ = let i = i, j = j + map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q1, k)) + end + q2′ = let i = i, j = j + map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q2, k)) + end + for (f′, coeff′) in elementary_trace(f, i) + for (f′′, coeff′′) in planar_trace(f′, (q1′, q2′)) + coeff = coeff′ * coeff′′ + if !iszero(coeff) + newtrees[f′′] = get(newtrees, f′′, zero(coeff)) + coeff + end + end + end + return newtrees +end + +# trace two neighbouring indices of a single fusion tree +""" + elementary_trace(f::FusionTree{I,N}, i) where {I,N} -> <:AbstractDict{FusionTree{I,N-2}, <:Number} + +Perform an elementary trace of neighbouring uncoupled indices `i` and +`i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and +corresponding coefficients. +""" +function elementary_trace(f::FusionTree{I, N}, i) where {I, N} + (N > 1 && 1 <= i <= N) || + throw(ArgumentError("Cannot trace outputs i=$i and i+1 out of only $N outputs")) + i < N || isunit(f.coupled) || + throw(ArgumentError("Cannot trace outputs i=$N and 1 of fusion tree that couples to non-trivial sector")) + + T = sectorscalartype(I) + F = fusiontreetype(I, N - 2) + newtrees = FusionTreeDict{F, T}() + + j = mod1(i + 1, N) + b = f.uncoupled[i] + b′ = f.uncoupled[j] + # if trace is zero, return empty dict + (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees + if i < N + inner_extended = (leftunit(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., f.coupled) + a = inner_extended[i] + d = inner_extended[i + 2] + a == d || return newtrees + uncoupled′ = TupleTools.deleteat(TupleTools.deleteat(f.uncoupled, i + 1), i) + isdual′ = TupleTools.deleteat(TupleTools.deleteat(f.isdual, i + 1), i) + coupled′ = f.coupled + if N <= 4 + inner′ = () + else + inner′ = i <= 2 ? Base.tail(Base.tail(f.innerlines)) : + TupleTools.deleteat(TupleTools.deleteat(f.innerlines, i - 1), i - 2) + end + if N <= 3 + vertices′ = () + else + vertices′ = i <= 2 ? Base.tail(Base.tail(f.vertices)) : + TupleTools.deleteat(TupleTools.deleteat(f.vertices, i), i - 1) + end + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) + coeff = sqrtdim(b) + if i > 1 + c = f.innerlines[i - 1] + if FusionStyle(I) isa MultiplicityFreeFusion + coeff *= Fsymbol(a, b, dual(b), a, c, rightunit(a)) + else + μ = f.vertices[i - 1] + ν = f.vertices[i] + coeff *= Fsymbol(a, b, dual(b), a, c, rightunit(a))[μ, ν, 1, 1] + end + end + if f.isdual[i] + coeff *= frobenius_schur_phase(b) + end + push!(newtrees, f′ => coeff) + return newtrees + else # i == N + unit = leftunit(b) + if N == 2 + f′ = FusionTree{I}((), unit, (), (), ()) + coeff = sqrtdim(b) + if !(f.isdual[N]) + coeff *= conj(frobenius_schur_phase(b)) + end + push!(newtrees, f′ => coeff) + return newtrees + end + uncoupled_ = TupleTools.front(f.uncoupled) + inner_ = TupleTools.front(f.innerlines) + coupled_ = f.innerlines[end] + isdual_ = TupleTools.front(f.isdual) + vertices_ = TupleTools.front(f.vertices) + f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) + fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) + for (f_′, coeff) in merge(fs, f_, unit, 1) + f_′.innerlines[1] == unit || continue + uncoupled′ = Base.tail(Base.tail(f_′.uncoupled)) + isdual′ = Base.tail(Base.tail(f_′.isdual)) + inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) + vertices′ = N <= 3 ? () : Base.tail(Base.tail(f_′.vertices)) + f′ = FusionTree(uncoupled′, unit, isdual′, inner′, vertices′) + coeff *= sqrtdim(b) + if !(f.isdual[N]) + coeff *= conj(frobenius_schur_phase(b)) + end + newtrees[f′] = get(newtrees, f′, zero(coeff)) + coeff + end + return newtrees + end +end diff --git a/src/fusiontrees/fusiontreeblocks.jl b/src/fusiontrees/fusiontreeblocks.jl deleted file mode 100644 index c10e820e5..000000000 --- a/src/fusiontrees/fusiontreeblocks.jl +++ /dev/null @@ -1,683 +0,0 @@ -struct FusionTreeBlock{I, N₁, N₂, F <: FusionTreePair{I, N₁, N₂}} - trees::Vector{F} -end - -function FusionTreeBlock{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) - - if N₁ == N₂ == 0 - return FusionTreeBlock(trees) - elseif N₁ == 0 - cs = sort!(collect(filter(isone, ⊗(uncoupled[2]...)))) - elseif N₂ == 0 - cs = sort!(collect(filter(isone, ⊗(uncoupled[1]...)))) - else - cs = sort!(collect(intersect(⊗(uncoupled[1]...), ⊗(uncoupled[2]...)))) - end - - for c in cs - for f₁ in fusiontrees(uncoupled[1], c, isdual[1]), - f₂ in fusiontrees(uncoupled[2], c, isdual[2]) - - push!(trees, (f₁, f₂)) - end - end - return FusionTreeBlock(trees) -end - -Base.@constprop :aggressive function Base.getproperty(block::FusionTreeBlock, prop::Symbol) - if prop === :uncoupled - f₁, f₂ = first(block.trees) - return f₁.uncoupled, f₂.uncoupled - elseif prop === :isdual - f₁, f₂ = first(block.trees) - return f₁.isdual, f₂.isdual - else - return getfield(block, prop) - end -end - -Base.propertynames(::FusionTreeBlock, private::Bool = false) = (:trees, :uncoupled, :isdual) - -sectortype(::Type{<:FusionTreeBlock{I}}) where {I} = I -numout(fs::FusionTreeBlock) = numout(typeof(fs)) -numout(::Type{<:FusionTreeBlock{I, N₁}}) where {I, N₁} = N₁ -numin(fs::FusionTreeBlock) = numin(typeof(fs)) -numin(::Type{<:FusionTreeBlock{I, N₁, N₂}}) where {I, N₁, N₂} = N₂ -numind(fs::FusionTreeBlock) = numind(typeof(fs)) -numind(::Type{T}) where {T <: FusionTreeBlock} = numin(T) + numout(T) - -fusiontrees(block::FusionTreeBlock) = block.trees -Base.length(block::FusionTreeBlock) = length(fusiontrees(block)) - -# Within one block, all values of uncoupled and isdual are equal, so avoid hashing these -function treeindex_data((f₁, f₂)) - I = sectortype(f₁) - if FusionStyle(I) isa GenericFusion - return (f₁.coupled, f₁.innerlines..., f₂.innerlines...), - (f₁.vertices..., f₂.vertices...) - elseif FusionStyle(I) isa MultipleFusion - return (f₁.coupled, f₁.innerlines..., f₂.innerlines...) - else # there should be only a single element anyways - return false - end -end -function treeindex_map(fs::FusionTreeBlock) - I = sectortype(fs) - return fusiontreedict(I)(treeindex_data(f) => ind for (ind, f) in enumerate(fusiontrees(fs))) -end - -# Manipulations -# ------------- -function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} - U = zeros(sectorscalartype(I), length(dst), length(src)) - indexmap = treeindex_map(dst) - for (col, f) in enumerate(fusiontrees(src)) - for (f′, c) in transform(f) - row = indexmap[f′] - U[row, col] = c - end - end - return U -end - -function bendright(src::FusionTreeBlock) - uncoupled_dst = ( - TupleTools.front(src.uncoupled[1]), - (src.uncoupled[2]..., dual(src.uncoupled[1][end])), - ) - isdual_dst = ( - TupleTools.front(src.isdual[1]), - (src.isdual[2]..., !(src.isdual[1][end])), - ) - I = sectortype(src) - N₁ = numout(src) - N₂ = numin(src) - @assert N₁ > 0 - - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) - indexmap = treeindex_map(dst) - U = zeros(sectorscalartype(I), length(dst), length(src)) - - for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) - c = f₁.coupled - a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : - (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) - b = f₁.uncoupled[N₁] - - uncoupled1 = TupleTools.front(f₁.uncoupled) - isdual1 = TupleTools.front(f₁.isdual) - inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () - vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () - f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) - - uncoupled2 = (f₂.uncoupled..., dual(b)) - isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) - inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () - - coeff₀ = sqrtdim(c) * invsqrtdim(a) - if f₁.isdual[N₁] - coeff₀ *= conj(frobenius_schur_phase(dual(b))) - end - if FusionStyle(I) isa MultiplicityFreeFusion - coeff = coeff₀ * Bsymbol(a, b, c) - vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[treeindex_data((f₁′, f₂′))] - @inbounds U[row, col] = coeff - else - Bmat = Bsymbol(a, b, c) - μ = N₁ > 1 ? f₁.vertices[end] : 1 - for ν in axes(Bmat, 2) - coeff = coeff₀ * Bmat[μ, ν] - iszero(coeff) && continue - vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[treeindex_data((f₁′, f₂′))] - @inbounds U[row, col] = coeff - end - end - end - - return dst, U -end - -# !! note that this is more or less a copy of bendright through -# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in bendleft(src) -function bendleft(src::FusionTreeBlock) - uncoupled_dst = ( - (src.uncoupled[1]..., dual(src.uncoupled[2][end])), - TupleTools.front(src.uncoupled[2]), - ) - isdual_dst = ( - (src.isdual[1]..., !(src.isdual[2][end])), - TupleTools.front(src.isdual[2]), - ) - I = sectortype(src) - N₁ = numin(src) - N₂ = numout(src) - @assert N₁ > 0 - - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) - indexmap = treeindex_map(dst) - U = zeros(sectorscalartype(I), length(dst), length(src)) - - for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) - c = f₁.coupled - a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : - (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) - b = f₁.uncoupled[N₁] - - uncoupled1 = TupleTools.front(f₁.uncoupled) - isdual1 = TupleTools.front(f₁.isdual) - inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () - vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () - f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) - - uncoupled2 = (f₂.uncoupled..., dual(b)) - isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) - inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () - - coeff₀ = sqrtdim(c) * invsqrtdim(a) - if f₁.isdual[N₁] - coeff₀ *= conj(frobenius_schur_phase(dual(b))) - end - if FusionStyle(I) isa MultiplicityFreeFusion - coeff = coeff₀ * Bsymbol(a, b, c) - vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[treeindex_data((f₂′, f₁′))] - @inbounds U[row, col] = conj(coeff) - else - Bmat = Bsymbol(a, b, c) - μ = N₁ > 1 ? f₁.vertices[end] : 1 - for ν in axes(Bmat, 2) - coeff = coeff₀ * Bmat[μ, ν] - iszero(coeff) && continue - vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - row = indexmap[treeindex_data((f₂′, f₁′))] - @inbounds U[row, col] = conj(coeff) - end - end - end - - return dst, U -end - -function foldright(src::FusionTreeBlock) - uncoupled_dst = ( - Base.tail(src.uncoupled[1]), - (dual(first(src.uncoupled[1])), src.uncoupled[2]...), - ) - isdual_dst = (Base.tail(src.isdual[1]), (!first(src.isdual[1]), src.isdual[2]...)) - I = sectortype(src) - N₁ = numout(src) - N₂ = numin(src) - @assert N₁ > 0 - - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) - indexmap = treeindex_map(dst) - U = zeros(sectorscalartype(I), length(dst), length(src)) - - for (col, (f₁, f₂)) in enumerate(fusiontrees(src)) - # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) - a = f₁.uncoupled[1] - isduala = f₁.isdual[1] - factor = sqrtdim(a) - if !isduala - factor *= conj(frobenius_schur_phase(a)) - end - c1 = dual(a) - c2 = f₁.coupled - uncoupled = Base.tail(f₁.uncoupled) - isdual = Base.tail(f₁.isdual) - if FusionStyle(I) isa UniqueFusion - c = first(c1 ⊗ c2) - fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) - fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) - row = indexmap[treeindex_data((fl, fr))] - @inbounds U[row, col] = factor - else - if N₁ == 1 - cset = (leftunit(c1),) # or rightunit(a) - elseif N₁ == 2 - cset = (f₁.uncoupled[2],) - else - cset = ⊗(Base.tail(f₁.uncoupled)...) - end - for c in c1 ⊗ c2 - c ∈ cset || continue - for μ in 1:Nsymbol(c1, c2, c) - fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) - frs_coeffs = insertat(fc, 2, f₂) - for (fl′, coeff1) in insertat(fc, 2, f₁) - N₁ > 1 && !isone(fl′.innerlines[1]) && continue - coupled = fl′.coupled - uncoupled = Base.tail(Base.tail(fl′.uncoupled)) - isdual = Base.tail(Base.tail(fl′.isdual)) - inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) - vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) - fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in frs_coeffs - coeff = factor * coeff1 * conj(coeff2) - row = indexmap[treeindex_data((fl, fr))] - @inbounds U[row, col] = coeff - end - end - end - end - end - end - - return dst, U -end - -# !! note that this is more or less a copy of foldright through -# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in foldright(src) -function foldleft(src::FusionTreeBlock) - uncoupled_dst = ( - (dual(first(src.uncoupled[2])), src.uncoupled[1]...), - Base.tail(src.uncoupled[2]), - ) - isdual_dst = ( - (!first(src.isdual[2]), src.isdual[1]...), - Base.tail(src.isdual[2]), - ) - I = sectortype(src) - N₁ = numin(src) - N₂ = numout(src) - @assert N₁ > 0 - - dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) - indexmap = treeindex_map(dst) - U = zeros(sectorscalartype(I), length(dst), length(src)) - - for (col, (f₂, f₁)) in enumerate(fusiontrees(src)) - # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) - a = f₁.uncoupled[1] - isduala = f₁.isdual[1] - factor = sqrtdim(a) - if !isduala - factor *= conj(frobenius_schur_phase(a)) - end - c1 = dual(a) - c2 = f₁.coupled - uncoupled = Base.tail(f₁.uncoupled) - isdual = Base.tail(f₁.isdual) - if FusionStyle(I) isa UniqueFusion - c = first(c1 ⊗ c2) - fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) - fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) - row = indexmap[treeindex_data((fr, fl))] - @inbounds U[row, col] = conj(factor) - else - if N₁ == 1 - cset = (leftunit(c1),) # or rightunit(a) - elseif N₁ == 2 - cset = (f₁.uncoupled[2],) - else - cset = ⊗(Base.tail(f₁.uncoupled)...) - end - for c in c1 ⊗ c2 - c ∈ cset || continue - for μ in 1:Nsymbol(c1, c2, c) - fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) - fr_coeffs = insertat(fc, 2, f₂) - for (fl′, coeff1) in insertat(fc, 2, f₁) - N₁ > 1 && !isone(fl′.innerlines[1]) && continue - coupled = fl′.coupled - uncoupled = Base.tail(Base.tail(fl′.uncoupled)) - isdual = Base.tail(Base.tail(fl′.isdual)) - inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) - vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) - fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in fr_coeffs - coeff = factor * coeff1 * conj(coeff2) - row = indexmap[treeindex_data((fr, fl))] - @inbounds U[row, col] = conj(coeff) - end - end - end - end - end - end - return dst, U -end - -function cycleclockwise(src::FusionTreeBlock) - if numout(src) > 0 - tmp, U₁ = foldright(src) - dst, U₂ = bendleft(tmp) - else - tmp, U₁ = bendleft(src) - dst, U₂ = foldright(tmp) - end - return dst, U₂ * U₁ -end - -function cycleanticlockwise(src::FusionTreeBlock) - if numin(src) > 0 - tmp, U₁ = foldleft(src) - dst, U₂ = bendright(tmp) - else - tmp, U₁ = bendright(src) - dst, U₂ = foldleft(tmp) - end - return dst, U₂ * U₁ -end - -@inline function repartition(src::FusionTreeBlock, N::Int) - @assert 0 <= N <= numind(src) - return repartition(src, Val(N)) -end - -#= -Using a generated function here to ensure type stability by unrolling the loops: -```julia -dst, U = bendleft/right(src) - -# repeat the following 2 lines N - 1 times -dst, Utmp = bendleft/right(dst) -U = Utmp * U - -return dst, U -``` -=# -@generated function repartition(src::FusionTreeBlock, ::Val{N}) where {N} - return _repartition_body(numout(src) - N) -end -function _repartition_body(N) - if N == 0 - ex = quote - T = sectorscalartype(sectortype(src)) - U = copyto!(zeros(T, length(src), length(src)), LinearAlgebra.I) - return src, U - end - else - f = N < 0 ? bendleft : bendright - ex_rep = Expr(:block) - for _ in 1:(abs(N) - 1) - push!(ex_rep.args, :((dst, Utmp) = $f(dst))) - push!(ex_rep.args, :(U = Utmp * U)) - end - ex = quote - dst, U = $f(src) - $ex_rep - return dst, U - end - end - return ex -end - -function Base.transpose(src::FusionTreeBlock, p::Index2Tuple{N₁, N₂}) where {N₁, N₂} - N = N₁ + N₂ - @assert numind(src) == N - p′ = linearizepermutation(p..., numout(src), numin(src)) - @assert iscyclicpermutation(p′) - return _fstranspose((src, p)) -end - -const _FSTransposeKey{I, N₁, N₂} = Tuple{<:FusionTreeBlock{I}, Index2Tuple{N₁, N₂}} - -@cached function _fstranspose( - key::_FSTransposeKey{I, N₁, N₂} - )::Tuple{FusionTreeBlock{I, N₁, N₂}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} - src, (p1, p2) = key - - N = N₁ + N₂ - p = linearizepermutation(p1, p2, numout(src), numin(src)) - - dst, U = repartition(src, N₁) - length(p) == 0 && return dst, U - i1 = findfirst(==(1), p)::Int - i1 == 1 && return dst, U - - Nhalf = N >> 1 - while 1 < i1 ≤ Nhalf - dst, U_tmp = cycleanticlockwise(dst) - U = U_tmp * U - i1 -= 1 - end - while Nhalf < i1 - dst, U_tmp = cycleclockwise(dst) - U = U_tmp * U - i1 = mod1(i1 + 1, N) - end - - return dst, U -end - -function CacheStyle(::typeof(_fstranspose), k::_FSTransposeKey{I}) where {I} - if FusionStyle(I) == UniqueFusion() - return NoCache() - else - return GlobalLRUCache() - end -end - -function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where {I, N} - 1 <= i < N || - throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) - - uncoupled = src.uncoupled[1] - a, b = uncoupled[i], uncoupled[i + 1] - uncoupled′ = TupleTools.setindex(uncoupled, b, i) - uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) - coupled′ = rightunit(src.uncoupled[1][N]) - - isdual = src.isdual[1] - isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1) - isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i) - dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ()); sizehint = length(src)) - - oneT = one(sectorscalartype(I)) - - indexmap = treeindex_map(dst) - U = zeros(sectorscalartype(I), length(dst), length(src)) - - if isone(a) || isone(b) # braiding with trivial sector: simple and always possible - for (col, (f, f₂)) in enumerate(fusiontrees(src)) - inner = f.innerlines - inner_extended = (uncoupled[1], inner..., coupled′) - vertices = f.vertices - inner′ = inner - vertices′ = vertices - if i > 1 # we also need to alter innerlines and vertices - inner′ = TupleTools.setindex( - inner, inner_extended[isone(a) ? (i + 1) : (i - 1)], i - 1 - ) - vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) - vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) - end - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - row = indexmap[treeindex_data((f′, f₂))] - @inbounds U[row, col] = oneT - end - return dst, U - end - - BraidingStyle(I) isa NoBraiding && - throw(SectorMismatch(lazy"Cannot braid sectors $a and $b")) - - for (col, (f, f₂)) in enumerate(fusiontrees(src)) - inner = f.innerlines - inner_extended = (uncoupled[1], inner..., coupled′) - vertices = f.vertices - - if i == 1 - c = N > 2 ? inner[1] : coupled′ - if FusionStyle(I) isa MultiplicityFreeFusion - R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) - row = indexmap[treeindex_data((f′, f₂))] - @inbounds U[row, col] = R - else # GenericFusion - μ = vertices[1] - Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) - for ν in axes(Rmat, 2) - R = oftype(oneT, Rmat[μ, ν]) - iszero(R) && continue - vertices′ = TupleTools.setindex(vertices, ν, 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) - row = indexmap[treeindex_data((f′, f₂))] - @inbounds U[row, col] = R - end - end - continue - end - # case i > 1: other naming convention - b = uncoupled[i] - d = uncoupled[i + 1] - a = inner_extended[i - 1] - c = inner_extended[i] - e = inner_extended[i + 1] - if FusionStyle(I) isa UniqueFusion - c′ = first(a ⊗ d) - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - row = indexmap[treeindex_data((f′, f₂))] - @inbounds U[row, col] = coeff - elseif FusionStyle(I) isa SimpleFusion - cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) - for c′ in cs - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) - iszero(coeff) && continue - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - row = indexmap[treeindex_data((f′, f₂))] - @inbounds U[row, col] = coeff - end - else # GenericFusion - cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) - for c′ in cs - Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) - Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) - Fmat = Fsymbol(d, a, b, e, c′, c) - μ = vertices[i - 1] - ν = vertices[i] - for σ in 1:Nsymbol(a, d, c′) - for λ in 1:Nsymbol(c′, b, e) - coeff = zero(oneT) - for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) - coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * - conj(Rmat2[σ, κ]) - end - iszero(coeff) && continue - vertices′ = TupleTools.setindex(vertices, σ, i - 1) - vertices′ = TupleTools.setindex(vertices′, λ, i) - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - row = indexmap[treeindex_data((f′, f₂))] - @inbounds U[row, col] = coeff - end - end - end - end - end - - return dst, U -end - -function braid(src::FusionTreeBlock{I, N, 0}, p::NTuple{N, Int}, levels::NTuple{N, Int}) where {I, N} - TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) - - if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding - uncoupled′ = TupleTools._permute(src.uncoupled[1], p) - isdual′ = TupleTools._permute(src.isdual[1], p) - dst = FusionTreeBlock{I}(uncoupled′, isdual′; sizehint = length(src)) - U = transformation_matrix(dst, src) do (f₁, f₂) - return ((f₁′, f₂) => c for (f₁′, c) in braid(f₁, p, levels)) - end - else - dst, U = repartition(src, N) # TODO: can we avoid this? - for s in permutation2swaps(p) - inv = levels[s] > levels[s + 1] - dst, U_tmp = artin_braid(dst, s; inv) - U = U_tmp * U - end - end - return dst, U -end - -function braid(src::FusionTreeBlock{I}, p::Index2Tuple{N₁, N₂}, levels::Index2Tuple) where {I, N₁, N₂} - @assert numind(src) == N₁ + N₂ - @assert numout(src) == length(levels[1]) && numin(src) == length(levels[2]) - @assert TupleTools.isperm((p[1]..., p[2]...)) - return _fsbraid((src, p, levels)) -end - -const _FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreeBlock{I}, Index2Tuple{N₁, N₂}, Index2Tuple} - -@cached function _fsbraid( - key::_FSBraidKey{I, N₁, N₂} - )::Tuple{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}, Matrix{sectorscalartype(I)}} where {I, N₁, N₂} - src, (p1, p2), (l1, l2) = key - - p = linearizepermutation(p1, p2, numout(src), numin(src)) - levels = (l1..., reverse(l2)...) - - dst, U = repartition(src, numind(src)) - - if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding - uncoupled′ = TupleTools._permute(dst.uncoupled[1], p) - isdual′ = TupleTools._permute(dst.isdual[1], p) - - dst′ = FusionTreeBlock{I}(uncoupled′, isdual′) - U_tmp = transformation_matrix(dst′, dst) do (f₁, f₂) - return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) - end - dst = dst′ - U = U_tmp * U - else - for s in permutation2swaps(p) - inv = levels[s] > levels[s + 1] - dst, U_tmp = artin_braid(dst, s; inv) - U = U_tmp * U - end - end - - if N₂ == 0 - return dst, U - else - dst, U_tmp = repartition(dst, N₁) - U = U_tmp * U - return dst, U - end -end - -function CacheStyle(::typeof(_fsbraid), k::_FSBraidKey{I}) where {I} - if FusionStyle(I) isa UniqueFusion - return NoCache() - else - return GlobalLRUCache() - end -end - -function permute(src::FusionTreeBlock{I}, p::Index2Tuple) where {I} - @assert BraidingStyle(I) isa SymmetricBraiding - levels1 = ntuple(identity, numout(src)) - levels2 = numout(src) .+ ntuple(identity, numin(src)) - return braid(src, p, (levels1, levels2)) -end diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 0a5340929..36b8e5ec5 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -113,24 +113,90 @@ Type alias for a fusion-splitting tree pair of sectortype `I`, with `N₁` split """ const FusionTreePair{I, N₁, N₂} = Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}} +""" + FusionTreeBlock{I, N₁, N₂, F <: FusionTreePair{I, N₁, N₂}} + +Collection of fusion-splitting tree pairs that share the same uncoupled sectors. +Mostly internal structure to speed up non-`UniqueFusion` fusiontree manipulation computations. +""" +struct FusionTreeBlock{I, N₁, N₂, F <: FusionTreePair{I, N₁, N₂}} + trees::Vector{F} +end + +function FusionTreeBlock{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) + + if N₁ == N₂ == 0 + return FusionTreeBlock(trees) + elseif N₁ == 0 + cs = sort!(collect(filter(isone, ⊗(uncoupled[2]...)))) + elseif N₂ == 0 + cs = sort!(collect(filter(isone, ⊗(uncoupled[1]...)))) + else + cs = sort!(collect(intersect(⊗(uncoupled[1]...), ⊗(uncoupled[2]...)))) + end + + for c in cs + for f₁ in fusiontrees(uncoupled[1], c, isdual[1]), + f₂ in fusiontrees(uncoupled[2], c, isdual[2]) + + push!(trees, (f₁, f₂)) + end + end + return FusionTreeBlock(trees) +end + # Properties +# ---------- sectortype(::Type{<:FusionTree{I}}) where {I <: Sector} = I sectortype(::Type{<:FusionTreePair{I}}) where {I <: Sector} = I -FusionStyle(::Type{<:FusionTree{I}}) where {I <: Sector} = FusionStyle(I) -FusionStyle(::Type{<:FusionTreePair{I}}) where {I <: Sector} = FusionStyle(I) -BraidingStyle(::Type{<:FusionTree{I}}) where {I <: Sector} = BraidingStyle(I) -BraidingStyle(::Type{<:FusionTreePair{I}}) where {I <: Sector} = BraidingStyle(I) -Base.length(::Type{<:FusionTree{<:Sector, N}}) where {N} = N +sectortype(::Type{<:FusionTreeBlock{I}}) where {I} = I -FusionStyle(f::FusionTree) = FusionStyle(typeof(f)) -FusionStyle(f::FusionTreePair) = FusionStyle(typeof(f)) -BraidingStyle(f::FusionTree) = BraidingStyle(typeof(f)) -BraidingStyle(f::FusionTreePair) = BraidingStyle(typeof(f)) -Base.length(f::FusionTree) = length(typeof(f)) +FusionStyle(f::Union{FusionTree, FusionTreePair, FusionTreeBlock}) = + FusionStyle(typeof(f)) +FusionStyle(::Type{F}) where {F <: Union{FusionTree, FusionTreePair, FusionTreeBlock}} = + FusionStyle(sectortype(F)) +BraidingStyle(f::Union{FusionTree, FusionTreePair, FusionTreeBlock}) = + BraidingStyle(typeof(f)) +BraidingStyle(::Type{F}) where {F <: Union{FusionTree, FusionTreePair, FusionTreeBlock}} = + BraidingStyle(sectortype(F)) + +Base.length(f::FusionTree) = length(typeof(f)) +Base.length(::Type{<:FusionTree{<:Sector, N}}) where {N} = N # Note: cannot define the following since FusionTreePair is a const for a Tuple # Base.length(::Type{<:FusionTreePair{<:Sector, N₁, N₂}}) where {N₁, N₂} = N₁ + N₂ # Base.length(f::FusionTreePair) = length(typeof(f)) +Base.length(block::FusionTreeBlock) = length(fusiontrees(block)) + +numout(fs::Union{FusionTreePair, FusionTreeBlock}) = numout(typeof(fs)) +numout(::Type{<:FusionTreePair{I, N₁}}) where {I, N₁} = N₁ +numout(::Type{<:FusionTreeBlock{I, N₁}}) where {I, N₁} = N₁ +numin(fs::Union{FusionTreePair, FusionTreeBlock}) = numin(typeof(fs)) +numin(::Type{<:FusionTreePair{I, N₁, N₂}}) where {I, N₁, N₂} = N₂ +numin(::Type{<:FusionTreeBlock{I, N₁, N₂}}) where {I, N₁, N₂} = N₂ +numind(fs::Union{FusionTreePair, FusionTreeBlock}) = numind(typeof(fs)) +numind(::Type{T}) where {T <: Union{FusionTreePair, FusionTreeBlock}} = numin(T) + numout(T) + +Base.propertynames(::FusionTreeBlock, private::Bool = false) = (:trees, :uncoupled, :isdual) +Base.@constprop :aggressive function Base.getproperty(block::FusionTreeBlock, prop::Symbol) + if prop === :uncoupled + f₁, f₂ = first(block.trees) + return f₁.uncoupled, f₂.uncoupled + elseif prop === :isdual + f₁, f₂ = first(block.trees) + return f₁.isdual, f₂.isdual + else + return getfield(block, prop) + end +end +fusiontrees(block::FusionTreeBlock) = block.trees # Hashing, important for using fusion trees as key in a dictionary function Base.hash(f::FusionTree{I}, h::UInt) where {I} @@ -163,6 +229,24 @@ function Base.:(==)(f₁::FusionTree{I, N}, f₂::FusionTree{I, N}) where {I <: end Base.:(==)(f₁::FusionTree, f₂::FusionTree) = false +# Within one block, all values of uncoupled and isdual are equal, so avoid hashing these +function treeindex_data((f₁, f₂)) + I = sectortype(f₁) + if FusionStyle(I) isa GenericFusion + return (f₁.coupled, f₁.innerlines..., f₂.innerlines...), + (f₁.vertices..., f₂.vertices...) + elseif FusionStyle(I) isa MultipleFusion + return (f₁.coupled, f₁.innerlines..., f₂.innerlines...) + else # there should be only a single element anyways + return false + end +end +function treeindex_map(fs::FusionTreeBlock) + I = sectortype(fs) + return fusiontreedict(I)(treeindex_data(f) => ind for (ind, f) in enumerate(fusiontrees(fs))) +end + + # Facilitate getting correct fusion tree types Base.@assume_effects :foldable function fusiontreetype(::Type{I}, N::Int) where {I <: Sector} return if N === 0 @@ -177,6 +261,8 @@ Base.@assume_effects :foldable function fusiontreetype(::Type{I}, N₁::Int, N return Tuple{fusiontreetype(I, N₁), fusiontreetype(I, N₂)} end +fusiontreedict(I) = FusionStyle(I) isa UniqueFusion ? SingletonDict : FusionTreeDict + # converting to actual array function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, 0}) where {I} X = convert(A, fusiontensor(unit(I), unit(I), unit(I)))[1, 1, :] @@ -267,10 +353,11 @@ end # Fusion tree iterators include("iterator.jl") -include("fusiontreeblocks.jl") # Manipulate fusion trees -include("manipulations.jl") +include("basic_manipulations.jl") +include("duality_manipulations.jl") +include("braiding_manipulations.jl") # auxiliary routines # _abelianinner: generate the inner indices for given outer indices in the abelian case diff --git a/src/fusiontrees/manipulations.jl b/src/fusiontrees/manipulations.jl deleted file mode 100644 index ae87acaab..000000000 --- a/src/fusiontrees/manipulations.jl +++ /dev/null @@ -1,1081 +0,0 @@ -fusiontreedict(I) = FusionStyle(I) isa UniqueFusion ? SingletonDict : FusionTreeDict - -# BASIC MANIPULATIONS: -#---------------------------------------------- -# -> rewrite generic fusion tree in basis of fusion trees in standard form -# -> only depend on Fsymbol - -""" - insertat(f::FusionTree{I, N₁}, i::Int, f₂::FusionTree{I, N₂}) - -> <:AbstractDict{<:FusionTree{I, N₁+N₂-1}, <:Number} - -Attach a fusion tree `f₂` to the uncoupled leg `i` of the fusion tree `f₁` and bring it -into a linear combination of fusion trees in standard form. This requires that -`f₂.coupled == f₁.uncoupled[i]` and `f₁.isdual[i] == false`. -""" -function insertat(f₁::FusionTree{I}, i::Int, f₂::FusionTree{I, 0}) where {I} - # this actually removes uncoupled line i, which should be trivial - (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || - throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) - coeff = one(sectorscalartype(I)) - - uncoupled = TupleTools.deleteat(f₁.uncoupled, i) - coupled = f₁.coupled - isdual = TupleTools.deleteat(f₁.isdual, i) - if length(uncoupled) <= 2 - inner = () - else - inner = TupleTools.deleteat(f₁.innerlines, max(1, i - 2)) - end - if length(uncoupled) <= 1 - vertices = () - else - vertices = TupleTools.deleteat(f₁.vertices, max(1, i - 1)) - end - f = FusionTree(uncoupled, coupled, isdual, inner, vertices) - return fusiontreedict(I)(f => coeff) -end -function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 1}) where {I} - # identity operation - (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || - throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) - coeff = one(sectorscalartype(I)) - isdual′ = TupleTools.setindex(f₁.isdual, f₂.isdual[1], i) - f = FusionTree{I}(f₁.uncoupled, f₁.coupled, isdual′, f₁.innerlines, f₁.vertices) - return fusiontreedict(I)(f => coeff) -end -function insertat(f₁::FusionTree{I}, i, f₂::FusionTree{I, 2}) where {I} - # elementary building block, - (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || - throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) - uncoupled = f₁.uncoupled - coupled = f₁.coupled - inner = f₁.innerlines - b, c = f₂.uncoupled - isdual = f₁.isdual - isdualb, isdualc = f₂.isdual - if i == 1 - uncoupled′ = (b, c, tail(uncoupled)...) - isdual′ = (isdualb, isdualc, tail(isdual)...) - inner′ = (uncoupled[1], inner...) - vertices′ = (f₂.vertices..., f₁.vertices...) - coeff = one(sectorscalartype(I)) - f′ = FusionTree(uncoupled′, coupled, isdual′, inner′, vertices′) - return fusiontreedict(I)(f′ => coeff) - end - uncoupled′ = TupleTools.insertafter(TupleTools.setindex(uncoupled, b, i), i, (c,)) - isdual′ = TupleTools.insertafter(TupleTools.setindex(isdual, isdualb, i), i, (isdualc,)) - inner_extended = (uncoupled[1], inner..., coupled) - a = inner_extended[i - 1] - d = inner_extended[i] - e′ = uncoupled[i] - if FusionStyle(I) isa MultiplicityFreeFusion - local newtrees - for e in a ⊗ b - coeff = conj(Fsymbol(a, b, c, d, e, e′)) - iszero(coeff) && continue - inner′ = TupleTools.insertafter(inner, i - 2, (e,)) - f′ = FusionTree(uncoupled′, coupled, isdual′, inner′) - if @isdefined newtrees - push!(newtrees, f′ => coeff) - else - newtrees = fusiontreedict(I)(f′ => coeff) - end - end - return newtrees - else - local newtrees - κ = f₂.vertices[1] - λ = f₁.vertices[i - 1] - for e in a ⊗ b - inner′ = TupleTools.insertafter(inner, i - 2, (e,)) - Fmat = Fsymbol(a, b, c, d, e, e′) - for μ in axes(Fmat, 1), ν in axes(Fmat, 2) - coeff = conj(Fmat[μ, ν, κ, λ]) - iszero(coeff) && continue - vertices′ = TupleTools.setindex(f₁.vertices, ν, i - 1) - vertices′ = TupleTools.insertafter(vertices′, i - 2, (μ,)) - f′ = FusionTree(uncoupled′, coupled, isdual′, inner′, vertices′) - if @isdefined newtrees - push!(newtrees, f′ => coeff) - else - newtrees = fusiontreedict(I)(f′ => coeff) - end - end - end - return newtrees - end -end -function insertat(f₁::FusionTree{I, N₁}, i, f₂::FusionTree{I, N₂}) where {I, N₁, N₂} - F = fusiontreetype(I, N₁ + N₂ - 1) - (f₁.uncoupled[i] == f₂.coupled && !f₁.isdual[i]) || - throw(SectorMismatch("cannot connect $(f₂.uncoupled) to $(f₁.uncoupled[i])")) - T = sectorscalartype(I) - coeff = one(T) - if length(f₁) == 1 - return fusiontreedict(I){F, T}(f₂ => coeff) - end - if i == 1 - uncoupled = (f₂.uncoupled..., tail(f₁.uncoupled)...) - isdual = (f₂.isdual..., tail(f₁.isdual)...) - inner = (f₂.innerlines..., f₂.coupled, f₁.innerlines...) - vertices = (f₂.vertices..., f₁.vertices...) - coupled = f₁.coupled - f′ = FusionTree(uncoupled, coupled, isdual, inner, vertices) - return fusiontreedict(I){F, T}(f′ => coeff) - else # recursive definition - N2 = length(f₂) - f₂′, f₂′′ = split(f₂, N2 - 1) - local newtrees::fusiontreedict(I){F, T} - for (f, coeff) in insertat(f₁, i, f₂′′) - for (f′, coeff′) in insertat(f, i, f₂′) - if @isdefined newtrees - coeff′′ = coeff * coeff′ - newtrees[f′] = get(newtrees, f′, zero(coeff′′)) + coeff′′ - else - newtrees = fusiontreedict(I){F, T}(f′ => coeff * coeff′) - end - end - end - return newtrees - end -end - -""" - split(f::FusionTree{I, N}, M::Int) - -> (::FusionTree{I, M}, ::FusionTree{I, N-M+1}) - -Split a fusion tree into two. The first tree has as uncoupled sectors the first `M` -uncoupled sectors of the input tree `f`, whereas its coupled sector corresponds to the -internal sector between uncoupled sectors `M` and `M+1` of the original tree `f`. The -second tree has as first uncoupled sector that same internal sector of `f`, followed by -remaining `N-M` uncoupled sectors of `f`. It couples to the same sector as `f`. This -operation is the inverse of `insertat` in the sense that if -`f₁, f₂ = split(t, M) ⇒ f == insertat(f₂, 1, f₁)`. -""" -@inline function split(f::FusionTree{I, N}, M::Int) where {I, N} - if M > N || M < 0 - throw(ArgumentError("M should be between 0 and N = $N")) - elseif M === N - (f, FusionTree{I}((f.coupled,), f.coupled, (false,), (), ())) - elseif M === 1 - isdual1 = (f.isdual[1],) - isdual2 = TupleTools.setindex(f.isdual, false, 1) - f₁ = FusionTree{I}((f.uncoupled[1],), f.uncoupled[1], isdual1, (), ()) - f₂ = FusionTree{I}(f.uncoupled, f.coupled, isdual2, f.innerlines, f.vertices) - return f₁, f₂ - elseif M === 0 - u = leftunit(f.uncoupled[1]) - f₁ = FusionTree{I}((), u, (), ()) - uncoupled2 = (u, f.uncoupled...) - coupled2 = f.coupled - isdual2 = (false, f.isdual...) - innerlines2 = N >= 2 ? (f.uncoupled[1], f.innerlines...) : () - if FusionStyle(I) isa GenericFusion - vertices2 = (1, f.vertices...) - return f₁, FusionTree{I}(uncoupled2, coupled2, isdual2, innerlines2, vertices2) - else - return f₁, FusionTree{I}(uncoupled2, coupled2, isdual2, innerlines2) - end - else - uncoupled1 = ntuple(n -> f.uncoupled[n], M) - isdual1 = ntuple(n -> f.isdual[n], M) - innerlines1 = ntuple(n -> f.innerlines[n], max(0, M - 2)) - coupled1 = f.innerlines[M - 1] - vertices1 = ntuple(n -> f.vertices[n], M - 1) - - uncoupled2 = ntuple(N - M + 1) do n - return n == 1 ? f.innerlines[M - 1] : f.uncoupled[M + n - 1] - end - isdual2 = ntuple(N - M + 1) do n - return n == 1 ? false : f.isdual[M + n - 1] - end - innerlines2 = ntuple(n -> f.innerlines[M - 1 + n], N - M - 1) - coupled2 = f.coupled - vertices2 = ntuple(n -> f.vertices[M - 1 + n], N - M) - - f₁ = FusionTree{I}(uncoupled1, coupled1, isdual1, innerlines1, vertices1) - f₂ = FusionTree{I}(uncoupled2, coupled2, isdual2, innerlines2, vertices2) - return f₁, f₂ - end -end - -""" - merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I, μ = 1) - -> <:AbstractDict{<:FusionTree{I, N₁+N₂}, <:Number} - -Merge two fusion trees together to a linear combination of fusion trees whose uncoupled -sectors are those of `f₁` followed by those of `f₂`, and where the two coupled sectors of -`f₁` and `f₂` are further fused to `c`. In case of -`FusionStyle(I) == GenericFusion()`, also a degeneracy label `μ` for the fusion of -the coupled sectors of `f₁` and `f₂` to `c` needs to be specified. -""" -function merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I) where {I, N₁, N₂} - if FusionStyle(I) isa GenericFusion - throw(ArgumentError("vertex label for merging required")) - end - return merge(f₁, f₂, c, 1) -end -function merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I, μ) where {I, N₁, N₂} - if !(c in f₁.coupled ⊗ f₂.coupled) - throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) - end - if μ > Nsymbol(f₁.coupled, f₂.coupled, c) - throw(ArgumentError("invalid fusion vertex label $μ")) - end - f₀ = FusionTree{I}((f₁.coupled, f₂.coupled), c, (false, false), (), (μ,)) - f, coeff = first(insertat(f₀, 1, f₁)) # takes fast path, single output - @assert coeff == one(coeff) - return insertat(f, N₁ + 1, f₂) -end -function merge(f₁::FusionTree{I, 0}, f₂::FusionTree{I, 0}, c::I, μ) where {I} - Nsymbol(f₁.coupled, f₂.coupled, c) == μ == 1 || - throw(SectorMismatch("cannot fuse sectors $(f₁.coupled) and $(f₂.coupled) to $c")) - return fusiontreedict(I)(f₁ => Fsymbol(c, c, c, c, c, c)[1, 1, 1, 1]) -end - -# ELEMENTARY DUALITY MANIPULATIONS: A- and B-moves -#--------------------------------------------------------- -# -> elementary manipulations that depend on the duality (rigidity) and pivotal structure -# -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) -# -> B-move (bendleft, bendright) is simple in standard basis -# -> A-move (foldleft, foldright) is complicated, needs to be reexpressed in standard form - -# flip a duality flag of a fusion tree -function flip((f₁, f₂)::FusionTreePair{I, N₁, N₂}, i::Int; inv::Bool = false) where {I, N₁, N₂} - @assert 0 < i ≤ N₁ + N₂ - if i ≤ N₁ - a = f₁.uncoupled[i] - χₐ = frobenius_schur_phase(a) - θₐ = twist(a) - if !inv - factor = f₁.isdual[i] ? χₐ * θₐ : one(θₐ) - else - factor = f₁.isdual[i] ? one(θₐ) : χₐ * conj(θₐ) - end - isdual′ = TupleTools.setindex(f₁.isdual, !f₁.isdual[i], i) - f₁′ = FusionTree{I}(f₁.uncoupled, f₁.coupled, isdual′, f₁.innerlines, f₁.vertices) - return SingletonDict((f₁′, f₂) => factor) - else - i -= N₁ - a = f₂.uncoupled[i] - χₐ = frobenius_schur_phase(a) - θₐ = twist(a) - if !inv - factor = f₂.isdual[i] ? χₐ * one(θₐ) : θₐ - else - factor = f₂.isdual[i] ? conj(θₐ) : χₐ * one(θₐ) - end - isdual′ = TupleTools.setindex(f₂.isdual, !f₂.isdual[i], i) - f₂′ = FusionTree{I}(f₂.uncoupled, f₂.coupled, isdual′, f₂.innerlines, f₂.vertices) - return SingletonDict((f₁, f₂′) => factor) - end -end -function flip((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ind; inv::Bool = false) where {I, N₁, N₂} - f₁′, f₂′ = f₁, f₂ - factor = one(sectorscalartype(I)) - for i in ind - (f₁′, f₂′), s = only(flip((f₁′, f₂′), i; inv)) - factor *= s - end - return SingletonDict((f₁′, f₂′) => factor) -end - -# change to N₁ - 1, N₂ + 1 -function bendright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} - # map final splitting vertex (a, b)<-c to fusion vertex a<-(c, dual(b)) - @assert N₁ > 0 - c = f₁.coupled - a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : - (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) - b = f₁.uncoupled[N₁] - - uncoupled1 = TupleTools.front(f₁.uncoupled) - isdual1 = TupleTools.front(f₁.isdual) - inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () - vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () - f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) - - uncoupled2 = (f₂.uncoupled..., dual(b)) - isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) - inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () - - coeff₀ = sqrtdim(c) * invsqrtdim(a) - if f₁.isdual[N₁] - coeff₀ *= conj(frobenius_schur_phase(dual(b))) - end - if FusionStyle(I) isa MultiplicityFreeFusion - coeff = coeff₀ * Bsymbol(a, b, c) - vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - return SingletonDict((f₁′, f₂′) => coeff) - else - local newtrees - Bmat = Bsymbol(a, b, c) - μ = N₁ > 1 ? f₁.vertices[end] : 1 - for ν in axes(Bmat, 2) - coeff = coeff₀ * Bmat[μ, ν] - iszero(coeff) && continue - vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - if @isdefined newtrees - push!(newtrees, (f₁′, f₂′) => coeff) - else - newtrees = FusionTreeDict((f₁′, f₂′) => coeff) - end - end - return newtrees - end -end -# change to N₁ + 1, N₂ - 1 -function bendleft((f₁, f₂)::FusionTreePair{I}) where {I} - # map final fusion vertex c<-(a, b) to splitting vertex (c, dual(b))<-a - return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) - for ((f₂′, f₁′), coeff) in bendright((f₂, f₁)) - ) -end - -# change to N₁ - 1, N₂ + 1 -function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} - # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) - @assert N₁ > 0 - a = f₁.uncoupled[1] - isduala = f₁.isdual[1] - factor = sqrtdim(a) - if !isduala - factor *= conj(frobenius_schur_phase(a)) - end - c1 = dual(a) - c2 = f₁.coupled - uncoupled = Base.tail(f₁.uncoupled) - isdual = Base.tail(f₁.isdual) - if FusionStyle(I) isa UniqueFusion - c = first(c1 ⊗ c2) - fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) - fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) - return fusiontreedict(I)((fl, fr) => factor) - else - local newtrees - if N₁ == 1 - cset = (leftunit(c1),) # or rightunit(a) - elseif N₁ == 2 - cset = (f₁.uncoupled[2],) - else - cset = ⊗(Base.tail(f₁.uncoupled)...) - end - for c in c1 ⊗ c2 - c ∈ cset || continue - for μ in 1:Nsymbol(c1, c2, c) - fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) - fr_coeffs = insertat(fc, 2, f₂) - for (fl′, coeff1) in insertat(fc, 2, f₁) - N₁ > 1 && !isunit(fl′.innerlines[1]) && continue - coupled = fl′.coupled - uncoupled = Base.tail(Base.tail(fl′.uncoupled)) - isdual = Base.tail(Base.tail(fl′.isdual)) - inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) - vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) - fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in fr_coeffs - coeff = factor * coeff1 * conj(coeff2) - if (@isdefined newtrees) - newtrees[(fl, fr)] = get(newtrees, (fl, fr), zero(coeff)) + - coeff - else - newtrees = fusiontreedict(I)((fl, fr) => coeff) - end - end - end - end - end - return newtrees - end -end -# change to N₁ + 1, N₂ - 1 -function foldleft((f₁, f₂)::FusionTreePair{I}) where {I} - # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b - return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) - for ((f₂′, f₁′), coeff) in foldright((f₂, f₁)) - ) -end - -# COMPOSITE DUALITY MANIPULATIONS PART 1: Repartition and transpose -#------------------------------------------------------------------- -# -> composite manipulations that depend on the duality (rigidity) and pivotal structure -# -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) -# -> transpose expressed as cyclic permutation -# one-argument version: check whether `p` is a cyclic permutation (of `1:length(p)`) -function iscyclicpermutation(p) - N = length(p) - @inbounds for i in 1:N - p[mod1(i + 1, N)] == mod1(p[i] + 1, N) || return false - end - return true -end -# two-argument version: check whether `v1` is a cyclic permutation of `v2` -function iscyclicpermutation(v1, v2) - length(v1) == length(v2) || return false - return iscyclicpermutation(indexin(v1, v2)) -end - -# clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft -function cycleclockwise((f₁, f₂)::FusionTreePair{I}) where {I} - local newtrees - if length(f₁) > 0 - for ((f1a, f2a), coeffa) in foldright((f₁, f₂)) - for ((f1b, f2b), coeffb) in bendleft((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - else - for ((f1a, f2a), coeffa) in bendleft((f₁, f₂)) - for ((f1b, f2b), coeffb) in foldright((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - end - return newtrees -end - -# anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright -function cycleanticlockwise((f₁, f₂)::FusionTreePair{I}) where {I} - local newtrees - if length(f₂) > 0 - for ((f1a, f2a), coeffa) in foldleft((f₁, f₂)) - for ((f1b, f2b), coeffb) in bendright((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - else - for ((f1a, f2a), coeffa) in bendright((f₁, f₂)) - for ((f1b, f2b), coeffb) in foldleft((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - end - return newtrees -end - -# repartition double fusion tree -""" - repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, N::Int) where {I, N₁, N₂} - -> <:AbstractDict{<:FusionTreePair{I, N, N₁+N₂-N}}, <:Number} - -Input is a double fusion tree that describes the fusion of a set of incoming uncoupled -sectors to a set of outgoing uncoupled sectors, represented using the individual trees of -outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical coupled sector -`f₁.coupled == f₂.coupled`). Computes new trees and corresponding coefficients obtained from -repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to -have `N` outgoing sectors. -""" -@inline function repartition((f₁, f₂)::FusionTreePair, N::Int) - f₁.coupled == f₂.coupled || throw(SectorMismatch()) - @assert 0 <= N <= length(f₁) + length(f₂) - return _recursive_repartition((f₁, f₂), Val(N)) -end - -function _recursive_repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ::Val{N}) where {I, N₁, N₂, N} - # recursive definition is only way to get correct number of loops for - # GenericFusion, but is too complex for type inference to handle, so we - # precompute the parameters of the return type - F₁ = fusiontreetype(I, N) - F₂ = fusiontreetype(I, N₁ + N₂ - N) - FF = Tuple{F₁, F₂} - T = sectorscalartype(I) - coeff = one(T) - if N == N₁ - return fusiontreedict(I){Tuple{F₁, F₂}, T}((f₁, f₂) => coeff) - else - local newtrees::fusiontreedict(I){Tuple{F₁, F₂}, T} - for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright((f₁, f₂)) : bendleft((f₁, f₂))) - for ((f₁′′, f₂′′), coeff2) in _recursive_repartition((f₁′, f₂′), Val(N)) - if (@isdefined newtrees) - push!(newtrees, (f₁′′, f₂′′) => coeff1 * coeff2) - else - newtrees = fusiontreedict(I){FF, T}((f₁′′, f₂′′) => coeff1 * coeff2) - end - end - end - return newtrees - end -end - -""" - transpose((f₁, f₂)::FusionTreePair{I}, p::(Index2Tuple{N₁, N₂}) where {I, N₁, N₂} - -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} - -Input is a double fusion tree that describes the fusion of a set of incoming uncoupled -sectors to a set of outgoing uncoupled sectors, represented using the individual trees of -outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled sector -`t1.coupled == t2.coupled`). Computes new trees and corresponding coefficients obtained from -repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors -`p2` become incoming. -""" -function Base.transpose((f₁, f₂)::FusionTreePair{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂} - N = N₁ + N₂ - @assert length(f₁) + length(f₂) == N - p′ = linearizepermutation(p..., length(f₁), length(f₂)) - @assert iscyclicpermutation(p′) - return fstranspose(((f₁, f₂), p)) -end - -const FSTransposeKey{I, N₁, N₂} = Tuple{<:FusionTreePair{I}, Index2Tuple{N₁, N₂}} - -function _fsdicttype(I, N₁, N₂) - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) - T = sectorscalartype(I) - return fusiontreedict(I){Tuple{F₁, F₂}, T} -end - -@cached function fstranspose(key::FSTransposeKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I, N₁, N₂} - (f₁, f₂), (p1, p2) = key - N = N₁ + N₂ - p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - newtrees = repartition((f₁, f₂), N₁) - length(p) == 0 && return newtrees - i1 = findfirst(==(1), p) - @assert i1 !== nothing - i1 == 1 && return newtrees - Nhalf = N >> 1 - while 1 < i1 <= Nhalf - local newtrees′ - for ((f1a, f2a), coeffa) in newtrees - for ((f1b, f2b), coeffb) in cycleanticlockwise((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees′) - newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff - else - newtrees′ = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - newtrees = newtrees′ - i1 -= 1 - end - while Nhalf < i1 - local newtrees′ - for ((f1a, f2a), coeffa) in newtrees - for ((f1b, f2b), coeffb) in cycleclockwise((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees′) - newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff - else - newtrees′ = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - newtrees = newtrees′ - i1 = mod1(i1 + 1, N) - end - return newtrees -end - -function CacheStyle(::typeof(fstranspose), k::FSTransposeKey{I}) where {I} - if FusionStyle(I) isa UniqueFusion - return NoCache() - else - return GlobalLRUCache() - end -end - -# COMPOSITE DUALITY MANIPULATIONS PART 2: Planar traces -#------------------------------------------------------------------- -# -> composite manipulations that depend on the duality (rigidity) and pivotal structure -# -> planar manipulations that do not require braiding, everything is in Fsymbol (A/Bsymbol) - -function planar_trace( - (f₁, f₂)::FusionTreePair{I}, (p1, p2)::Index2Tuple{N₁, N₂}, (q1, q2)::Index2Tuple{N₃, N₃} - ) where {I, N₁, N₂, N₃} - N = N₁ + N₂ + 2N₃ - @assert length(f₁) + length(f₂) == N - if N₃ == 0 - return transpose((f₁, f₂), (p1, p2)) - end - - linearindex = ( - ntuple(identity, Val(length(f₁)))..., - reverse(length(f₁) .+ ntuple(identity, Val(length(f₂))))..., - ) - - q1′ = TupleTools.getindices(linearindex, q1) - q2′ = TupleTools.getindices(linearindex, q2) - p1′, p2′ = let q′ = (q1′..., q2′...) - ( - map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p1)), - map(l -> l - count(l .> q′), TupleTools.getindices(linearindex, p2)), - ) - end - - T = sectorscalartype(I) - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) - newtrees = FusionTreeDict{Tuple{F₁, F₂}, T}() - for ((f₁′, f₂′), coeff′) in repartition((f₁, f₂), N) - for (f₁′′, coeff′′) in planar_trace(f₁′, (q1′, q2′)) - for (f12′′′, coeff′′′) in transpose((f₁′′, f₂′), (p1′, p2′)) - coeff = coeff′ * coeff′′ * coeff′′′ - if !iszero(coeff) - newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff - end - end - end - end - return newtrees -end - -""" - planar_trace(f::FusionTree{I,N}, (q1, q2)::Index2Tuple{N₃,N₃}) where {I,N,N₃} - -> <:AbstractDict{FusionTree{I,N-2*N₃}, <:Number} - -Perform a planar trace of the uncoupled indices of the fusion tree `f` at `q1` with those at -`q2`, where `q1[i]` is connected to `q2[i]` for all `i`. The result is returned as a dictionary -of output trees and corresponding coefficients. -""" -function planar_trace(f::FusionTree{I, N}, (q1, q2)::Index2Tuple{N₃, N₃}) where {I, N, N₃} - T = sectorscalartype(I) - F = fusiontreetype(I, N - 2 * N₃) - newtrees = FusionTreeDict{F, T}() - N₃ === 0 && return push!(newtrees, f => one(T)) - - for (i, j) in zip(q1, q2) - (f.uncoupled[i] == dual(f.uncoupled[j]) && f.isdual[i] != f.isdual[j]) || - return newtrees - end - k = 1 - local i, j - while k <= N₃ - if mod1(q1[k] + 1, N) == q2[k] - i = q1[k] - j = q2[k] - break - elseif mod1(q2[k] + 1, N) == q1[k] - i = q2[k] - j = q1[k] - break - else - k += 1 - end - end - k > N₃ && throw(ArgumentError("Not a planar trace")) - - q1′ = let i = i, j = j - map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q1, k)) - end - q2′ = let i = i, j = j - map(l -> (l - (l > i) - (l > j)), TupleTools.deleteat(q2, k)) - end - for (f′, coeff′) in elementary_trace(f, i) - for (f′′, coeff′′) in planar_trace(f′, (q1′, q2′)) - coeff = coeff′ * coeff′′ - if !iszero(coeff) - newtrees[f′′] = get(newtrees, f′′, zero(coeff)) + coeff - end - end - end - return newtrees -end - -# trace two neighbouring indices of a single fusion tree -""" - elementary_trace(f::FusionTree{I,N}, i) where {I,N} -> <:AbstractDict{FusionTree{I,N-2}, <:Number} - -Perform an elementary trace of neighbouring uncoupled indices `i` and -`i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and -corresponding coefficients. -""" -function elementary_trace(f::FusionTree{I, N}, i) where {I, N} - (N > 1 && 1 <= i <= N) || - throw(ArgumentError("Cannot trace outputs i=$i and i+1 out of only $N outputs")) - i < N || isunit(f.coupled) || - throw(ArgumentError("Cannot trace outputs i=$N and 1 of fusion tree that couples to non-trivial sector")) - - T = sectorscalartype(I) - F = fusiontreetype(I, N - 2) - newtrees = FusionTreeDict{F, T}() - - j = mod1(i + 1, N) - b = f.uncoupled[i] - b′ = f.uncoupled[j] - # if trace is zero, return empty dict - (b == dual(b′) && f.isdual[i] != f.isdual[j]) || return newtrees - if i < N - inner_extended = (leftunit(f.uncoupled[1]), f.uncoupled[1], f.innerlines..., f.coupled) - a = inner_extended[i] - d = inner_extended[i + 2] - a == d || return newtrees - uncoupled′ = TupleTools.deleteat(TupleTools.deleteat(f.uncoupled, i + 1), i) - isdual′ = TupleTools.deleteat(TupleTools.deleteat(f.isdual, i + 1), i) - coupled′ = f.coupled - if N <= 4 - inner′ = () - else - inner′ = i <= 2 ? Base.tail(Base.tail(f.innerlines)) : - TupleTools.deleteat(TupleTools.deleteat(f.innerlines, i - 1), i - 2) - end - if N <= 3 - vertices′ = () - else - vertices′ = i <= 2 ? Base.tail(Base.tail(f.vertices)) : - TupleTools.deleteat(TupleTools.deleteat(f.vertices, i), i - 1) - end - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - coeff = sqrtdim(b) - if i > 1 - c = f.innerlines[i - 1] - if FusionStyle(I) isa MultiplicityFreeFusion - coeff *= Fsymbol(a, b, dual(b), a, c, rightunit(a)) - else - μ = f.vertices[i - 1] - ν = f.vertices[i] - coeff *= Fsymbol(a, b, dual(b), a, c, rightunit(a))[μ, ν, 1, 1] - end - end - if f.isdual[i] - coeff *= frobenius_schur_phase(b) - end - push!(newtrees, f′ => coeff) - return newtrees - else # i == N - unit = leftunit(b) - if N == 2 - f′ = FusionTree{I}((), unit, (), (), ()) - coeff = sqrtdim(b) - if !(f.isdual[N]) - coeff *= conj(frobenius_schur_phase(b)) - end - push!(newtrees, f′ => coeff) - return newtrees - end - uncoupled_ = TupleTools.front(f.uncoupled) - inner_ = TupleTools.front(f.innerlines) - coupled_ = f.innerlines[end] - isdual_ = TupleTools.front(f.isdual) - vertices_ = TupleTools.front(f.vertices) - f_ = FusionTree(uncoupled_, coupled_, isdual_, inner_, vertices_) - fs = FusionTree((b,), b, (!f.isdual[1],), (), ()) - for (f_′, coeff) in merge(fs, f_, unit, 1) - f_′.innerlines[1] == unit || continue - uncoupled′ = Base.tail(Base.tail(f_′.uncoupled)) - isdual′ = Base.tail(Base.tail(f_′.isdual)) - inner′ = N <= 4 ? () : Base.tail(Base.tail(f_′.innerlines)) - vertices′ = N <= 3 ? () : Base.tail(Base.tail(f_′.vertices)) - f′ = FusionTree(uncoupled′, unit, isdual′, inner′, vertices′) - coeff *= sqrtdim(b) - if !(f.isdual[N]) - coeff *= conj(frobenius_schur_phase(b)) - end - newtrees[f′] = get(newtrees, f′, zero(coeff)) + coeff - end - return newtrees - end -end - -# BRAIDING MANIPULATIONS: -#----------------------------------------------- -# -> manipulations that depend on a braiding -# -> requires both Fsymbol and Rsymbol -""" - artin_braid(f::FusionTree, i; inv::Bool = false) -> <:AbstractDict{typeof(f), <:Number} - -Perform an elementary braid (Artin generator) of neighbouring uncoupled indices `i` and -`i+1` on a fusion tree `f`, and returns the result as a dictionary of output trees and -corresponding coefficients. - -The keyword `inv` determines whether index `i` will braid above or below index `i+1`, i.e. -applying `artin_braid(f′, i; inv = true)` to all the outputs `f′` of -`artin_braid(f, i; inv = false)` and collecting the results should yield a single fusion -tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword has no effect -if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. -""" -function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} - 1 <= i < N || - throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) - uncoupled = f.uncoupled - a, b = uncoupled[i], uncoupled[i + 1] - uncoupled′ = TupleTools.setindex(uncoupled, b, i) - uncoupled′ = TupleTools.setindex(uncoupled′, a, i + 1) - coupled′ = f.coupled - isdual′ = TupleTools.setindex(f.isdual, f.isdual[i], i + 1) - isdual′ = TupleTools.setindex(isdual′, f.isdual[i + 1], i) - inner = f.innerlines - inner_extended = (uncoupled[1], inner..., coupled′) - vertices = f.vertices - oneT = one(sectorscalartype(I)) - - if isunit(a) || isunit(b) - # braiding with trivial sector: simple and always possible - inner′ = inner - vertices′ = vertices - if i > 1 # we also need to alter innerlines and vertices - inner′ = TupleTools.setindex( - inner, - inner_extended[isunit(a) ? (i + 1) : (i - 1)], - i - 1 - ) - vertices′ = TupleTools.setindex(vertices′, vertices[i], i - 1) - vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) - end - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - return fusiontreedict(I)(f′ => oneT) - end - - BraidingStyle(I) isa NoBraiding && - throw(SectorMismatch("Cannot braid sectors $(uncoupled[i]) and $(uncoupled[i + 1])")) - - if i == 1 - c = N > 2 ? inner[1] : coupled′ - if FusionStyle(I) isa MultiplicityFreeFusion - R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) - return fusiontreedict(I)(f′ => R) - else # GenericFusion - μ = vertices[1] - Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) - local newtrees - for ν in axes(Rmat, 2) - R = oftype(oneT, Rmat[μ, ν]) - iszero(R) && continue - vertices′ = TupleTools.setindex(vertices, ν, 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) - if (@isdefined newtrees) - push!(newtrees, f′ => R) - else - newtrees = fusiontreedict(I)(f′ => R) - end - end - return newtrees - end - end - # case i > 1: other naming convention - b = uncoupled[i] - d = uncoupled[i + 1] - a = inner_extended[i - 1] - c = inner_extended[i] - e = inner_extended[i + 1] - if FusionStyle(I) isa UniqueFusion - c′ = first(a ⊗ d) - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - return fusiontreedict(I)(f′ => coeff) - elseif FusionStyle(I) isa SimpleFusion - local newtrees - cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) - for c′ in cs - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) - iszero(coeff) && continue - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - if (@isdefined newtrees) - push!(newtrees, f′ => coeff) - else - newtrees = fusiontreedict(I)(f′ => coeff) - end - end - return newtrees - else # GenericFusion - local newtrees - cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) - for c′ in cs - Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) - Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) - Fmat = Fsymbol(d, a, b, e, c′, c) - μ = vertices[i - 1] - ν = vertices[i] - for σ in 1:Nsymbol(a, d, c′) - for λ in 1:Nsymbol(c′, b, e) - coeff = zero(oneT) - for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) - coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * conj(Rmat2[σ, κ]) - end - iszero(coeff) && continue - vertices′ = TupleTools.setindex(vertices, σ, i - 1) - vertices′ = TupleTools.setindex(vertices′, λ, i) - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - if (@isdefined newtrees) - push!(newtrees, f′ => coeff) - else - newtrees = fusiontreedict(I)(f′ => coeff) - end - end - end - end - return newtrees - end -end - -# braid fusion tree -""" - braid(f::FusionTree{<:Sector, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) - -> <:AbstractDict{typeof(t), <:Number} - -Perform a braiding of the uncoupled indices of the fusion tree `f` and return the result as -a `<:AbstractDict` of output trees and corresponding coefficients. The braiding is -determined by specifying that the new sector at position `k` corresponds to the sector that -was originally at the position `i = p[k]`, and assigning to every index `i` of the original -fusion tree a distinct level or depth `levels[i]`. This permutation is then decomposed into -elementary swaps between neighbouring indices, where the swaps are applied as braids such -that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` and -``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general -braid, but a general braid can be obtained by combining such operations. -""" -function braid(f::FusionTree{I, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) where {I, N} - TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) - if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding - coeff = one(sectorscalartype(I)) - for i in 1:N - for j in 1:(i - 1) - if p[j] > p[i] - a, b = f.uncoupled[p[j]], f.uncoupled[p[i]] - coeff *= Rsymbol(a, b, first(a ⊗ b)) - end - end - end - uncoupled′ = TupleTools._permute(f.uncoupled, p) - coupled′ = f.coupled - isdual′ = TupleTools._permute(f.isdual, p) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′) - return fusiontreedict(I)(f′ => coeff) - else - T = sectorscalartype(I) - coeff = one(T) - trees = FusionTreeDict(f => coeff) - newtrees = empty(trees) - for s in permutation2swaps(p) - inv = levels[s] > levels[s + 1] - for (f, c) in trees - for (f′, c′) in artin_braid(f, s; inv) - newtrees[f′] = get(newtrees, f′, zero(coeff)) + c * c′ - end - end - l = levels[s] - levels = TupleTools.setindex(levels, levels[s + 1], s) - levels = TupleTools.setindex(levels, l, s + 1) - trees, newtrees = newtrees, trees - empty!(newtrees) - end - return trees - end -end - -# permute fusion tree -""" - permute(f::FusionTree, p::NTuple{N, Int}) -> <:AbstractDict{typeof(t), <:Number} - -Perform a permutation of the uncoupled indices of the fusion tree `f` and returns the result -as a `<:AbstractDict` of output trees and corresponding coefficients; this requires that -`BraidingStyle(sectortype(f)) isa SymmetricBraiding`. -""" -function permute(f::FusionTree{I, N}, p::NTuple{N, Int}) where {I, N} - @assert BraidingStyle(I) isa SymmetricBraiding - return braid(f, p, ntuple(identity, Val(N))) -end - -# braid double fusion tree -""" - braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) - -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} - -Input is a fusion-splitting tree pair that describes the fusion of a set of incoming -uncoupled sectors to a set of outgoing uncoupled sectors, represented using the splitting -tree `f₁` and fusion tree `f₂`, such that the incoming sectors `f₂.uncoupled` are fused to -`f₁.coupled == f₂.coupled` and then to the outgoing sectors `f₁.uncoupled`. Compute new -trees and corresponding coefficients obtained from repartitioning and braiding the tree such -that sectors `p1` become outgoing and sectors `p2` become incoming. The uncoupled indices in -splitting tree `f₁` and fusion tree `f₂` have levels (or depths) `levels1` and `levels2` -respectively, which determines how indices braid. In particular, if `i` and `j` cross, -``τ_{i,j}`` is applied if `levels[i] < levels[j]` and ``τ_{j,i}^{-1}`` if `levels[i] > -levels[j]`. This does not allow to encode the most general braid, but a general braid can -be obtained by combining such operations. -""" -function braid((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple, (levels1, levels2)::Index2Tuple) - @assert length(f₁) + length(f₂) == length(p1) + length(p2) - @assert length(f₁) == length(levels1) && length(f₂) == length(levels2) - @assert TupleTools.isperm((p1..., p2...)) - return fsbraid(((f₁, f₂), (p1, p2), (levels1, levels2))) -end -const FSBraidKey{I, N₁, N₂} = Tuple{<:FusionTreePair{I}, Index2Tuple{N₁, N₂}, Index2Tuple} - -@cached function fsbraid(key::FSBraidKey{I, N₁, N₂})::_fsdicttype(I, N₁, N₂) where {I, N₁, N₂} - ((f₁, f₂), (p1, p2), (l1, l2)) = key - p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - levels = (l1..., reverse(l2)...) - local newtrees - for ((f, f0), coeff1) in repartition((f₁, f₂), N₁ + N₂) - for (f′, coeff2) in braid(f, p, levels) - for ((f₁′, f₂′), coeff3) in repartition((f′, f0), N₁) - if @isdefined newtrees - newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + - coeff1 * coeff2 * coeff3 - else - newtrees = fusiontreedict(I)((f₁′, f₂′) => coeff1 * coeff2 * coeff3) - end - end - end - end - return newtrees -end - -function CacheStyle(::typeof(fsbraid), k::FSBraidKey{I}) where {I <: Sector} - if FusionStyle(I) isa UniqueFusion - return NoCache() - else - return GlobalLRUCache() - end -end - -""" - permute((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple) - -> <:AbstractDict{<:FusionTreePair{I, N₁, N₂}}, <:Number} - -Input is a double fusion tree that describes the fusion of a set of incoming uncoupled -sectors to a set of outgoing uncoupled sectors, represented using the individual trees of -outgoing (`t1`) and incoming sectors (`t2`) respectively (with identical coupled sector -`t1.coupled == t2.coupled`). Computes new trees and corresponding coefficients obtained from -repartitioning and permuting the tree such that sectors `p1` become outgoing and sectors -`p2` become incoming. -""" -function permute((f₁, f₂)::FusionTreePair, (p1, p2)::Index2Tuple) - @assert BraidingStyle(sectortype(f₁)) isa SymmetricBraiding - levels1 = ntuple(identity, length(f₁)) - levels2 = length(f₁) .+ ntuple(identity, length(f₂)) - return braid((f₁, f₂), (p1, p2), (levels1, levels2)) -end From 364a1d70d99c37d4eca1a500f8824b3ed8e1318a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 18 Nov 2025 17:46:48 -0500 Subject: [PATCH 34/39] rework indexmanipulations to use fusionblocks --- src/fusiontrees/braiding_manipulations.jl | 12 +- src/fusiontrees/duality_manipulations.jl | 36 ++-- src/spaces/homspace.jl | 19 ++ src/tensors/abstracttensor.jl | 2 + src/tensors/diagonal.jl | 28 ++- src/tensors/indexmanipulations.jl | 249 +++++++++++++--------- src/tensors/tensoroperations.jl | 67 ++++-- src/tensors/treetransformers.jl | 42 ++-- 8 files changed, 272 insertions(+), 183 deletions(-) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index b4a93f8d8..28505d762 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -380,16 +380,14 @@ const FSPBraidKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁, N const FSBBraidKey{I, N₁, N₂} = Tuple{FusionTreeBlock{I}, Index2Tuple{N₁, N₂}, Index2Tuple} Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSPBraidKey{I, N₁, N₂}} - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) E = sectorscalartype(I) - return fusiontreedict(I){Tuple{F₁, F₂}, E} + return Pair{fusiontreetype(I, N₁, N₂), E} end Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSBBraidKey{I, N₁, N₂}} F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) E = sectorscalartype(I) - return Tuple{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} + return Pair{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} end @cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSPBraidKey{I, N₁, N₂}} @@ -409,7 +407,7 @@ end end end end - return newtrees + return only(newtrees) end function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} @@ -450,11 +448,11 @@ end end if N₂ == 0 - return dst, U + return dst => U else dst, U_tmp = repartition(dst, N₁) U = U_tmp * U - return dst, U + return dst => U end end diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index a679be085..67e0a9b74 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -6,10 +6,14 @@ # -> A-move (foldleft, foldright) is complicated, needs to be reexpressed in standard form @doc """ - bendright(src) -> dst, coeffs + bendright((f₁, f₂)::FusionTreePair) -> (f₃, f₄) => coeff + bendright(src::FusionTreeBlock) -> dst => coeffs -Map the final splitting vertex `a ⊗ b ← c` of each tree in `src` to a (linear combination of) -fusion vertices `a ← c ⊗ dual(b)` in `dst`. +Map the final splitting vertex `a ⊗ b ← c` of `src` to a fusion vertex `a ← c ⊗ dual(b)` in `dst`. +For `FusionStyle(src) === UniqueFusion()`, both `src` and `dst` are simple `FusionTreePair`s, and the +transformation consists of a single coefficient `coeff`. +For generic `FusionStyle`s, the input and output consist of `FusionTreeBlock`s that bundle together +all trees with the same uncoupled charges, and `coeffs` now forms a transformation matrix ``` ╰─┬─╯ | | | ╰─┬─╯ | | | @@ -25,6 +29,7 @@ See also [`bendleft`](@ref). """ bendright function bendright(src::FusionTreePair) + @assert FusionStyle(src) === UniqueFusion() I, N₁, N₂ = sectortype(src), numout(src), numin(src) f₁, f₂ = src @assert N₁ > 0 @@ -151,6 +156,7 @@ See also [`bendright`](@ref). """ bendleft function bendleft((f₁, f₂)::FusionTreePair{I}) where {I} + @assert FusionStyle(I) === UniqueFusion() return fusiontreedict(I)( (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in bendright((f₂, f₁)) ) @@ -222,6 +228,7 @@ end # change to N₁ - 1, N₂ + 1 function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} + @assert FusionStyle(I) === UniqueFusion() # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) @assert N₁ > 0 a = f₁.uncoupled[1] @@ -347,6 +354,7 @@ end # change to N₁ + 1, N₂ - 1 function foldleft((f₁, f₂)::FusionTreePair{I}) where {I} + @assert FusionStyle(I) === UniqueFusion() # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b return fusiontreedict(I)( (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in foldright((f₂, f₁)) @@ -427,6 +435,7 @@ end # clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft function cycleclockwise((f₁, f₂)::FusionTreePair{I}) where {I} + @assert FusionStyle(I) === UniqueFusion() local newtrees if length(f₁) > 0 for ((f1a, f2a), coeffa) in foldright((f₁, f₂)) @@ -466,6 +475,7 @@ end # anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright function cycleanticlockwise((f₁, f₂)::FusionTreePair{I}) where {I} + @assert FusionStyle(I) === UniqueFusion() local newtrees if length(f₂) > 0 for ((f1a, f2a), coeffa) in foldleft((f₁, f₂)) @@ -522,6 +532,7 @@ repartitioning the tree by bending incoming to outgoing sectors (or vice versa) have `N` outgoing sectors. """ @inline function repartition((f₁, f₂)::FusionTreePair, N::Int) + @assert FusionStyle((f₁, f₂)) === UniqueFusion() f₁.coupled == f₂.coupled || throw(SectorMismatch()) @assert 0 <= N <= length(f₁) + length(f₂) return _recursive_repartition((f₁, f₂), Val(N)) @@ -592,7 +603,6 @@ function _repartition_body(N) return ex end @generated function repartition(src::FusionTreeBlock, ::Val{N}) where {N} - 1 + 1 return _repartition_body(numout(src) - N) end @@ -618,16 +628,14 @@ const FSPTransposeKey{I, N₁, N₂} = Tuple{FusionTreePair{I}, Index2Tuple{N₁ const FSBTransposeKey{I, N₁, N₂} = Tuple{FusionTreeBlock{I}, Index2Tuple{N₁, N₂}} Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSPTransposeKey{I, N₁, N₂}} - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) E = sectorscalartype(I) - return fusiontreedict(I){Tuple{F₁, F₂}, E} + return Pair{fusiontreetype(I, N₁, N₂), E} end Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N₂, T <: FSBTransposeKey{I, N₁, N₂}} F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) E = sectorscalartype(I) - return Tuple{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} + return Pair{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} end @cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSPTransposeKey{I, N₁, N₂}} @@ -635,10 +643,10 @@ end N = N₁ + N₂ p = linearizepermutation(p1, p2, length(f₁), length(f₂)) newtrees = repartition((f₁, f₂), N₁) - length(p) == 0 && return newtrees + length(p) == 0 && return only(newtrees) i1 = findfirst(==(1), p) @assert i1 !== nothing - i1 == 1 && return newtrees + i1 == 1 && return only(newtrees) Nhalf = N >> 1 while 1 < i1 <= Nhalf local newtrees′ @@ -670,7 +678,7 @@ end newtrees = newtrees′ i1 = mod1(i1 + 1, N) end - return newtrees + return only(newtrees) end @cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSBTransposeKey{I, N₁, N₂}} src, (p1, p2) = key @@ -679,9 +687,9 @@ end p = linearizepermutation(p1, p2, numout(src), numin(src)) dst, U = repartition(src, N₁) - length(p) == 0 && return dst, U + length(p) == 0 && return dst => U i1 = findfirst(==(1), p)::Int - i1 == 1 && return dst, U + i1 == 1 && return dst => U Nhalf = N >> 1 while 1 < i1 ≤ Nhalf @@ -695,7 +703,7 @@ end i1 = mod1(i1 + 1, N) end - return dst, U + return dst => U end CacheStyle(::typeof(fstranspose), k::FSPTransposeKey{I}) where {I} = diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 27df834c4..7a70ee3f1 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -134,6 +134,25 @@ Return the fusiontrees corresponding to all valid fusion channels of a given `Ho """ fusiontrees(W::HomSpace) = fusionblockstructure(W).fusiontreelist +""" + fusionblocks(W::HomSpace) + +Return the fusionblocks corresponding to all valid fusion channels of a given `HomSpace`, +grouped by their uncoupled charges. +""" +function fusionblocks(W::HomSpace) + I = sectortype(W) + N₁, N₂ = numout(W), numin(W) + isdual_src = (map(isdual, codomain(W).spaces), map(isdual, domain(W).spaces)) + fblocks = Vector{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}}() + for cod_uncoupled_src in sectors(codomain(W)), dom_uncoupled_src in sectors(domain(W)) + fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) + trees_src = fusiontrees(fs_src) + isempty(trees_src) || push!(fblocks, fs_src) + end + return fblocks +end + # Operations on HomSpaces # ----------------------- """ diff --git a/src/tensors/abstracttensor.jl b/src/tensors/abstracttensor.jl index 393bd905e..b5b613177 100644 --- a/src/tensors/abstracttensor.jl +++ b/src/tensors/abstracttensor.jl @@ -269,6 +269,8 @@ end return (f₁, f₂) end +fusionblocks(t::AbstractTensorMap) = fusionblocks(space(t)) + # tensor data: block access #--------------------------- @doc """ diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 0062c44e5..9f71272d5 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -194,23 +194,31 @@ end function permute( d::DiagonalTensorMap, (p₁, p₂)::Index2Tuple{1, 1}; copy::Bool = false ) - if p₁ === (1,) && p₂ === (2,) - return copy ? Base.copy(d) : d - elseif p₁ === (2,) && p₂ === (1,) # transpose - if has_shared_permute(d, (p₁, p₂)) # tranpose for bosonic sectors - return DiagonalTensorMap(copy ? Base.copy(d.data) : d.data, dual(d.domain)) - end - d′ = typeof(d)(undef, dual(d.domain)) + # special cases handled first + p₁ === (1,) && p₂ === (2,) && return copy ? Base.copy(d) : d + (p₁, p₂) === ((2,), (1,)) || # has to be transpose + throw(ArgumentError("invalid permutation $((p₁, p₂)) for tensor in space $(space(d))")) + has_shared_permute(d, (p₁, p₂)) && # tranpose for bosonic sectors + return DiagonalTensorMap(copy ? Base.copy(d.data) : d.data, dual(d.domain)) + + d′ = typeof(d)(undef, dual(d.domain)) + if FusionStyle(sectortype(d)) === UniqueFusion() for (c, b) in blocks(d) f = only(fusiontrees(codomain(d), c)) - ((f′, _), coeff) = only(permute((f, f), (p₁, p₂))) + (f′, _), coeff = permute((f, f), (p₁, p₂)) c′ = f′.coupled scale!(block(d′, c′), b, coeff) end - return d′ else - throw(ArgumentError("invalid permutation $((p₁, p₂)) for tensor in space $(space(d))")) + for src in fusionblocks(d) + dst, U = permute(src, (p₁, p₂)) + c = only(fusiontrees(src))[1].coupled + c′ = only(fusiontrees(dst))[1].coupled + scale!(block(d′, c′), block(d, c), only(U)) + end end + + return d′ end # VectorInterface diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index ef3d89be3..92f77af14 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -482,22 +482,13 @@ function add_transform!( else I = sectortype(tdst) if I === Trivial - _add_trivial_kernel!(tdst, tsrc, p, transformer, α, β, backend...) - elseif FusionStyle(I) === UniqueFusion() - if use_threaded_transform(tdst, transformer) - _add_abelian_kernel_threaded!(tdst, tsrc, p, transformer, α, β, backend...) - else - _add_abelian_kernel_nonthreaded!( - tdst, tsrc, p, transformer, α, β, backend... - ) - end + add_trivial_kernel!(tdst, tsrc, p, transformer, α, β, backend...) else + style = FusionStyle(I) if use_threaded_transform(tdst, transformer) - _add_general_kernel_threaded!(tdst, tsrc, p, transformer, α, β, backend...) + add_kernel_threaded!(style, tdst, tsrc, p, transformer, α, β, backend...) else - _add_general_kernel_nonthreaded!( - tdst, tsrc, p, transformer, α, β, backend... - ) + add_kernel_nonthreaded!(style, tdst, tsrc, p, transformer, α, β, backend...) end end end @@ -514,95 +505,131 @@ end # Trivial implementations # ----------------------- -function _add_trivial_kernel!(tdst, tsrc, p, transformer, α, β, backend...) +function add_trivial_kernel!(tdst, tsrc, p, transformer, α, β, backend...) TO.tensoradd!(tdst[], tsrc[], p, false, α, β, backend...) return nothing end -# Abelian implementations -# ----------------------- -function _add_abelian_kernel_nonthreaded!( - tdst, tsrc, p, transformer::AbelianTreeTransformer, α, β, backend... +# Non-threaded implementations +# ---------------------------- +function add_kernel_nonthreaded!( + ::UniqueFusion, tdst, tsrc, p, transformer, α, β, backend... + ) + for (f₁, f₂) in fusiontrees(tsrc) + _add_transform_single!(tdst, tsrc, p, (f₁, f₂), transformer, α, β, backend...) + end + return nothing +end +function add_kernel_nonthreaded!( + ::UniqueFusion, tdst, tsrc, p, transformer::AbelianTreeTransformer, α, β, backend... ) for subtransformer in transformer.data _add_transform_single!(tdst, tsrc, p, subtransformer, α, β, backend...) end return nothing end +function add_kernel_nonthreaded!(::FusionStyle, tdst, tsrc, p, transformer, α, β, backend...) + # preallocate buffers + buffers = allocate_buffers(tdst, tsrc, transformer) + + for src in fusionblocks(tsrc) + if length(src) == 1 + _add_transform_single!(tdst, tsrc, p, src, transformer, α, β, backend...) + else + _add_transform_multi!(tdst, tsrc, p, src, transformer, α, β, backend...) + end + end + return nothing +end +# specialization in the case of TensorMap +function add_kernel_nonthreaded!( + ::FusionStyle, tdst, tsrc, p, transformer::GenericTreeTransformer, α, β, backend... + ) + # preallocate buffers + buffers = allocate_buffers(tdst, tsrc, transformer) -function _add_abelian_kernel_threaded!( - tdst, tsrc, p, transformer::AbelianTreeTransformer, - α, β, backend...; + for subtransformer in transformer.data + # Special case without intermediate buffers whenever there is only a single block + if length(subtransformer[1]) == 1 + _add_transform_single!(tdst, tsrc, p, subtransformer, α, β, backend...) + else + _add_transform_multi!(tdst, tsrc, p, subtransformer, buffers, α, β, backend...) + end + end + return nothing +end + +# Threaded implementations +# ------------------------ +function add_kernel_threaded!( + ::UniqueFusion, tdst, tsrc, p, transformer, α, β, backend...; ntasks::Int = get_num_transformer_threads() ) - nblocks = length(transformer.data) + trees = fusiontrees(tsrc) + nblocks = length(trees) counter = Threads.Atomic{Int}(1) Threads.@sync for _ in 1:min(ntasks, nblocks) Threads.@spawn begin while true local_counter = Threads.atomic_add!(counter, 1) local_counter > nblocks && break - @inbounds subtransformer = transformer.data[local_counter] - _add_transform_single!(tdst, tsrc, p, subtransformer, α, β, backend...) + @inbounds (f₁, f₂) = trees[local_counter] + _add_transform_single!(tdst, tsrc, p, (f₁, f₂), transformer, α, β, backend...) end end end return nothing end - -function _add_transform_single!( - tdst, tsrc, p, (coeff, struct_dst, struct_src)::_AbelianTransformerData, - α, β, backend... +function add_kernel_threaded!( + ::UniqueFusion, tdst, tsrc, p, transformer::AbelianTreeTransformer, α, β, backend...; + ntasks::Int = get_num_transformer_threads() ) - subblock_dst = StridedView(tdst.data, struct_dst...) - subblock_src = StridedView(tsrc.data, struct_src...) - TO.tensoradd!(subblock_dst, subblock_src, p, false, α * coeff, β, backend...) - return nothing -end - -function _add_abelian_kernel_nonthreaded!(tdst, tsrc, p, transformer, α, β, backend...) - for (f₁, f₂) in fusiontrees(tsrc) - _add_abelian_block!(tdst, tsrc, p, transformer, f₁, f₂, α, β, backend...) - end - return nothing -end - -function _add_abelian_kernel_threaded!(tdst, tsrc, p, transformer, α, β, backend...) - Threads.@sync for (f₁, f₂) in fusiontrees(tsrc) - Threads.@spawn _add_abelian_block!(tdst, tsrc, p, transformer, f₁, f₂, α, β, backend...) + nblocks = length(transformer.data) + counter = Threads.Atomic{Int}(1) + Threads.@sync for _ in 1:min(ntasks, nblocks) + Threads.@spawn begin + while true + local_counter = Threads.atomic_add!(counter, 1) + local_counter > nblocks && break + @inbounds subtransformer = transformer.data[local_counter] + _add_transform_single!(tdst, tsrc, p, subtransformer, α, β, backend...) + end + end end return nothing end -function _add_abelian_block!(tdst, tsrc, p, transformer, f₁, f₂, α, β, backend...) - (f₁′, f₂′), coeff = first(transformer((f₁, f₂))) - @inbounds TO.tensoradd!( - tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, β, backend... +function add_kernel_threaded!( + ::FusionStyle, tdst, tsrc, p, transformer, α, β, backend...; + ntasks::Int = get_num_transformer_threads() ) - return nothing -end + allblocks = fusionblocks(tsrc) + nblocks = length(allblocks) -# Non-abelian implementations -# --------------------------- -function _add_general_kernel_nonthreaded!( - tdst, tsrc, p, transformer::GenericTreeTransformer, α, β, backend... - ) - # preallocate buffers - buffers = allocate_buffers(tdst, tsrc, transformer) + counter = Threads.Atomic{Int}(1) + Threads.@sync for _ in 1:min(ntasks, nblocks) + Threads.@spawn begin + # preallocate buffers for each task + buffers = allocate_buffers(tdst, tsrc, transformer) - for subtransformer in transformer.data - # Special case without intermediate buffers whenever there is only a single block - if length(subtransformer[1]) == 1 - _add_transform_single!(tdst, tsrc, p, subtransformer, α, β, backend...) - else - _add_transform_multi!(tdst, tsrc, p, subtransformer, buffers, α, β, backend...) + while true + local_counter = Threads.atomic_add!(counter, 1) + local_counter > nblocks && break + @inbounds src = allblocks[local_counter] + if length(src) == 1 + _add_transform_single!(tdst, tsrc, p, src, transformer, α, β, backend...) + else + _add_transform_multi!(tdst, tsrc, p, src, transformer, buffers, α, β, backend...) + end + end end end + return nothing end - -function _add_general_kernel_threaded!( - tdst, tsrc, p, transformer::GenericTreeTransformer, α, β, backend...; +# specialization in the case of TensorMap +function add_kernel_threaded!( + ::FusionStyle, tdst, tsrc, p, transformer::GenericTreeTransformer, α, β, backend...; ntasks::Int = get_num_transformer_threads() ) nblocks = length(transformer.data) @@ -629,22 +656,30 @@ function _add_general_kernel_threaded!( return nothing end -function _add_general_kernel_nonthreaded!(tdst, tsrc, p, transformer, α, β, backend...) - if iszero(β) - tdst = zerovector!(tdst) - elseif !isone(β) - tdst = scale!(tdst, β) - end - for (f₁, f₂) in fusiontrees(tsrc) - for ((f₁′, f₂′), coeff) in transformer((f₁, f₂)) - @inbounds TO.tensoradd!( - tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, One(), backend... - ) - end - end +# Auxiliary methods +# ----------------- +function _add_transform_single!(tdst, tsrc, p, (f₁, f₂)::FusionTreePair, transformer, α, β, backend...) + (f₁′, f₂′), coeff = transformer((f₁, f₂)) + @inbounds TO.tensoradd!(tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, β, backend...) + return nothing +end +function _add_transform_single!(tdst, tsrc, p, src::FusionTreeBlock, transformer, α, β, backend...) + dst, U = transformer(src) + f₁, f₂ = only(fusiontrees(src)) + f₁′, f₂′ = only(fusiontrees(dst)) + coeff = only(U) + @inbounds TO.tensoradd!(tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, β, backend...) + return nothing +end +function _add_transform_single!( + tdst, tsrc, p, (coeff, struct_dst, struct_src)::_AbelianTransformerData, + α, β, backend... + ) + subblock_dst = StridedView(tdst.data, struct_dst...) + subblock_src = StridedView(tsrc.data, struct_src...) + TO.tensoradd!(subblock_dst, subblock_src, p, false, α * coeff, β, backend...) return nothing end - function _add_transform_single!( tdst, tsrc, p, (basistransform, structs_dst, structs_src)::_GenericTransformerData, α, β, backend... @@ -656,6 +691,36 @@ function _add_transform_single!( return nothing end +function _add_transform_multi!(tdst, tsrc, p, src::FusionTreeBlock, transformer, (buffer1, buffer2), α, β, backend...) + dst, U = transformer(src) + rows, cols = size(U) + sz_src = size(tsrc[first(fusiontrees(src))...]) + blocksize = prod(sz_src) + matsize = ( + prod(TupleTools.getindices(sz_src, codomainind(tsrc))), + prod(TupleTools.getindices(sz_src, domainind(tsrc))), + ) + + # Filling up a buffer with contiguous data + buffer_src = StridedView(buffers2, (blocksize, cols), (1, blocksize), 0) + for (i, (f₁, f₂)) in enumerate(fusiontrees(src)) + subblock_src = sreshape(tsrc[f₁, f₂], matsize) + _copyto!(buffer_src[:, i], subblock_src) + end + + # Resummation into a second buffer using BLAS + buffer_dst = StridedView(buffer1, (blocksize, rows), (1, blocksize), 0) + mul!(buffer_dst, buffer_src, U, α, Zero()) + + # Filling up the output + for (i, (f₃, f₄)) in enumerate(fusiontrees(dst)) + subblock_dst = tdst[f₃, f₄] + bufblock_dst = sreshape(buffer_dst[:, i], sz_src) + TO.tensoradd!(subblock_dst, bufblock_dst, p, false, One(), β, backend...) + end + + return nothing +end function _add_transform_multi!( tdst, tsrc, p, (basistransform, (sz_dst, structs_dst), (sz_src, structs_src)), (buffer1, buffer2), α, β, backend... @@ -687,27 +752,3 @@ function _add_transform_multi!( return nothing end - -function _add_general_kernel_threaded!(tdst, tsrc, p, transformer, α, β, backend...) - if iszero(β) - tdst = zerovector!(tdst) - elseif !isone(β) - tdst = scale!(tdst, β) - end - Threads.@sync for s₁ in sectors(codomain(tsrc)), s₂ in sectors(domain(tsrc)) - Threads.@spawn _add_nonabelian_sector!(tdst, tsrc, p, transformer, s₁, s₂, α, backend...) - end - return nothing -end - -function _add_nonabelian_sector!(tdst, tsrc, p, fusiontreetransform, s₁, s₂, α, backend...) - for (f₁, f₂) in fusiontrees(tsrc) - (f₁.uncoupled == s₁ && f₂.uncoupled == s₂) || continue - for ((f₁′, f₂′), coeff) in fusiontreetransform((f₁, f₂)) - @inbounds TO.tensoradd!( - tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, One(), backend... - ) - end - end - return nothing -end diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 9cf3d1b08..091b59cd4 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -200,38 +200,63 @@ function trace_permute!( end I = sectortype(S) - # TODO: is it worth treating UniqueFusion separately? Is it worth to add multithreading support? if I === Trivial cod = codomain(tsrc) dom = domain(tsrc) n = length(cod) TO.tensortrace!(tdst[], tsrc[], (p₁, p₂), (q₁, q₂), false, α, β, backend) - # elseif FusionStyle(I) isa UniqueFusion - else - cod = codomain(tsrc) - dom = domain(tsrc) - n = length(cod) - scale!(tdst, β) - r₁ = (p₁..., q₁...) - r₂ = (p₂..., q₂...) + return tdst + end + + cod = codomain(tsrc) + dom = domain(tsrc) + n = length(cod) + scale!(tdst, β) + r₁ = (p₁..., q₁...) + r₂ = (p₂..., q₂...) + + # TODO: Is it worth to add multithreading support? + if FusionStyle(I) isa UniqueFusion for (f₁, f₂) in fusiontrees(tsrc) - for ((f₁′, f₂′), coeff) in permute((f₁, f₂), (r₁, r₂)) - f₁′′, g₁ = split(f₁′, N₁) - f₂′′, g₂ = split(f₂′, N₂) - g₁ == g₂ || continue - coeff *= dim(g₁.coupled) / dim(g₁.uncoupled[1]) - for i in 2:length(g₁.uncoupled) - if !(g₁.isdual[i]) - coeff *= twist(g₁.uncoupled[i]) + (f₁′, f₂′), coeff = permute((f₁, f₂), (r₁, r₂)) + f₁′′, g₁ = split(f₁′, N₁) + f₂′′, g₂ = split(f₂′, N₂) + g₁ == g₂ || continue + coeff *= dim(g₁.coupled) / dim(g₁.uncoupled[1]) + for i in 2:length(g₁.uncoupled) + if !(g₁.isdual[i]) + coeff *= twist(g₁.uncoupled[i]) + end + end + C = tdst[f₁′′, f₂′′] + A = tsrc[f₁, f₂] + α′ = α * coeff + TO.tensortrace!(C, A, (p₁, p₂), (q₁, q₂), false, α′, One(), backend) + end + else + for src in fusionblocks(tsrc) + dst, U = permute(src, (r₁, r₂)) + for (i, (f₁, f₂)) in enumerate(fusiontrees(src)) + for (j, (f₁′, f₂′)) in enumerate(fusiontrees(dst)) + coeff = U[j, i] + f₁′′, g₁ = split(f₁′, N₁) + f₂′′, g₂ = split(f₂′, N₂) + g₁ == g₂ || continue + coeff *= dim(g₁.coupled) / dim(g₁.uncoupled[1]) + for i in 2:length(g₁.uncoupled) + if !(g₁.isdual[i]) + coeff *= twist(g₁.uncoupled[i]) + end end + C = tdst[f₁′′, f₂′′] + A = tsrc[f₁, f₂] + α′ = α * coeff + TO.tensortrace!(C, A, (p₁, p₂), (q₁, q₂), false, α′, One(), backend) end - C = tdst[f₁′′, f₂′′] - A = tsrc[f₁, f₂] - α′ = α * coeff - TO.tensortrace!(C, A, (p₁, p₂), (q₁, q₂), false, α′, One(), backend) end end end + return tdst end diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 78ce43d18..b7a636de0 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -26,7 +26,7 @@ function AbelianTreeTransformer(transform, p, Vdst, Vsrc) for i in 1:L f₁, f₂ = structure_src.fusiontreelist[i] - (f₃, f₄), coeff = only(transform((f₁, f₂))) + (f₃, f₄), coeff = transform((f₁, f₂)) j = structure_dst.fusiontreeindices[(f₃, f₄)] stridestructure_dst = structure_dst.fusiontreestructure[j] stridestructure_src = structure_src.fusiontreestructure[i] @@ -69,32 +69,19 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) N₁ = numout(Vsrc) N₂ = numin(Vsrc) - isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) - - data = Vector{_GenericTransformerData{T, N}}() + fblocks = fusionblocks(Vsrc) + nblocks = length(fblocks) + data = Vector{_GenericTransformerData{T, N}}(undef, nblocks) nthreads = get_num_manipulation_threads() if nthreads > 1 - fusiontreeblocks = Vector{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}}() - for cod_uncoupled_src in sectors(codomain(Vsrc)), - dom_uncoupled_src in sectors(domain(Vsrc)) - - fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) - trees_src = fusiontrees(fs_src) - if !isempty(trees_src) - push!(fusiontreeblocks, fs_src) - end - end - nblocks = length(fusiontreeblocks) - - resize!(data, nblocks) counter = Threads.Atomic{Int}(1) Threads.@sync for _ in 1:min(nthreads, nblocks) Threads.@spawn begin while true local_counter = Threads.atomic_add!(counter, 1) local_counter > nblocks && break - fs_src = fusiontreeblocks[local_counter] + fs_src = fblocks[local_counter] fs_dst, U = transform(fs_src) matrix = copy(transpose(U)) # TODO: should we avoid this @@ -118,17 +105,11 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) end transformer = GenericTreeTransformer{T, N}(data) else - isdual_src = (map(isdual, codomain(Vsrc).spaces), map(isdual, domain(Vsrc).spaces)) - for cod_uncoupled_src in sectors(codomain(Vsrc)), - dom_uncoupled_src in sectors(domain(Vsrc)) - - fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src) - trees_src = fusiontrees(fs_src) - isempty(trees_src) && continue - + for (i, fs_src) in enumerate(fblocks) fs_dst, U = transform(fs_src) matrix = copy(transpose(U)) # TODO: should we avoid this + trees_src = fusiontrees(fs_src) inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) trees_dst = fusiontrees(fs_dst) inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) @@ -142,7 +123,7 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) fusionstructure_dst, inds_dst ) - push!(data, (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src))) + data[i] = matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src) end transformer = GenericTreeTransformer{T, N}(data) end @@ -181,6 +162,13 @@ function allocate_buffers( sz = buffersize(transformer) return similar(tdst.data, sz), similar(tsrc.data, sz) end +function allocate_buffers( + tdst::AbstractTensorMap, tsrc::AbstractTensorMap, transformer + ) + # be pessimistic and assume the worst for now + sz = dim(space(tsrc)) + return similar(storagetype(tdst), sz), similar(storagetype(tsrc), sz) +end function treetransformertype(Vdst, Vsrc) I = sectortype(Vdst) From 37622f7dcb6f3116aa0de0bb5cf423f2ba00d232 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 18 Nov 2025 19:07:25 -0500 Subject: [PATCH 35/39] update implementation to remove code duplication --- src/fusiontrees/braiding_manipulations.jl | 190 +++--------- src/fusiontrees/duality_manipulations.jl | 358 +++++++--------------- 2 files changed, 155 insertions(+), 393 deletions(-) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 28505d762..a75b4e268 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -16,8 +16,9 @@ tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword ha if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`. """ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} - 1 <= i < N || - throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) + 1 <= i < N || throw(ArgumentError(lazy"Cannot swap outputs i=$i and i+1 out of only $N outputs")) + @assert FusionStyle(I) === UniqueFusion() + uncoupled = f.uncoupled a, b = uncoupled[i], uncoupled[i + 1] uncoupled′ = TupleTools.setindex(uncoupled, b, i) @@ -44,7 +45,7 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i) end f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - return fusiontreedict(I)(f′ => oneT) + return f′ => oneT end BraidingStyle(I) isa NoBraiding && @@ -52,104 +53,33 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N} if i == 1 c = N > 2 ? inner[1] : coupled′ - if FusionStyle(I) isa MultiplicityFreeFusion - R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) - return fusiontreedict(I)(f′ => R) - else # GenericFusion - μ = vertices[1] - Rmat = inv ? Rsymbol(b, a, c)' : Rsymbol(a, b, c) - local newtrees - for ν in axes(Rmat, 2) - R = oftype(oneT, Rmat[μ, ν]) - iszero(R) && continue - vertices′ = TupleTools.setindex(vertices, ν, 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′) - if (@isdefined newtrees) - push!(newtrees, f′ => R) - else - newtrees = fusiontreedict(I)(f′ => R) - end - end - return newtrees - end + R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c))) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices) + return f′ => R end + # case i > 1: other naming convention b = uncoupled[i] d = uncoupled[i + 1] a = inner_extended[i - 1] c = inner_extended[i] e = inner_extended[i + 1] - if FusionStyle(I) isa UniqueFusion - c′ = first(a ⊗ d) - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - return fusiontreedict(I)(f′ => coeff) - elseif FusionStyle(I) isa SimpleFusion - local newtrees - cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) - for c′ in cs - coeff = oftype( - oneT, - if inv - conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) - else - Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) - end - ) - iszero(coeff) && continue - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) - if (@isdefined newtrees) - push!(newtrees, f′ => coeff) - else - newtrees = fusiontreedict(I)(f′ => coeff) - end + c′ = first(a ⊗ d) + coeff = oftype( + oneT, + if inv + conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′) + else + Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′)) end - return newtrees - else # GenericFusion - local newtrees - cs = collect(I, intersect(a ⊗ d, e ⊗ conj(b))) - for c′ in cs - Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e) - Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′) - Fmat = Fsymbol(d, a, b, e, c′, c) - μ = vertices[i - 1] - ν = vertices[i] - for σ in 1:Nsymbol(a, d, c′) - for λ in 1:Nsymbol(c′, b, e) - coeff = zero(oneT) - for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′) - coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) * conj(Rmat2[σ, κ]) - end - iszero(coeff) && continue - vertices′ = TupleTools.setindex(vertices, σ, i - 1) - vertices′ = TupleTools.setindex(vertices′, λ, i) - inner′ = TupleTools.setindex(inner, c′, i - 1) - f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′) - if (@isdefined newtrees) - push!(newtrees, f′ => coeff) - else - newtrees = fusiontreedict(I)(f′ => coeff) - end - end - end - end - return newtrees - end + ) + inner′ = TupleTools.setindex(inner, c′, i - 1) + f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′) + return f′ => coeff end function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where {I, N} - 1 <= i < N || - throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) + 1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs")) uncoupled = src.uncoupled[1] a, b = uncoupled[i], uncoupled[i + 1] @@ -185,7 +115,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where row = indexmap[treeindex_data((f′, f₂))] @inbounds U[row, col] = oneT end - return dst, U + return dst => U end BraidingStyle(I) isa NoBraiding && @@ -282,7 +212,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where end end - return dst, U + return dst => U end # braid fusion tree @@ -300,9 +230,12 @@ that if `i` and `j` cross, ``τ_{i,j}`` is applied if `levels[i] < levels[j]` an ``τ_{j,i}^{-1}`` if `levels[i] > levels[j]`. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations. """ -function braid(f::FusionTree{I, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) where {I, N} +braid(f::FusionTree{I, N}, p::IndexTuple{N}, levels::IndexTuple{N}) where {I, N} = + braid(f, (p, ()), (levels, ())) +function braid(f::FusionTree{I, N}, (p, _)::Index2Tuple{N, 0}, (levels, _)::Index2Tuple{N, 0}) where {I, N} TupleTools.isperm(p) || throw(ArgumentError("not a valid permutation: $p")) - if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding + @assert FusionStyle(I) isa UniqueFusion + if BraidingStyle(I) isa SymmetricBraiding # this assumes Fsymbols are 1! coeff = one(sectorscalartype(I)) for i in 1:N for j in 1:(i - 1) @@ -316,26 +249,20 @@ function braid(f::FusionTree{I, N}, p::NTuple{N, Int}, levels::NTuple{N, Int}) w coupled′ = f.coupled isdual′ = TupleTools._permute(f.isdual, p) f′ = FusionTree{I}(uncoupled′, coupled′, isdual′) - return fusiontreedict(I)(f′ => coeff) + return f′ => coeff else T = sectorscalartype(I) - coeff = one(T) - trees = FusionTreeDict(f => coeff) - newtrees = empty(trees) + c = one(T) for s in permutation2swaps(p) inv = levels[s] > levels[s + 1] - for (f, c) in trees - for (f′, c′) in artin_braid(f, s; inv) - newtrees[f′] = get(newtrees, f′, zero(coeff)) + c * c′ - end - end + f, c′ = artin_braid(f, s; inv) + c *= c′ l = levels[s] levels = TupleTools.setindex(levels, levels[s + 1], s) levels = TupleTools.setindex(levels, l, s + 1) - trees, newtrees = newtrees, trees - empty!(newtrees) end - return trees + + return f => c end end @@ -394,32 +321,10 @@ end ((f₁, f₂), (p1, p2), (l1, l2)) = key p = linearizepermutation(p1, p2, length(f₁), length(f₂)) levels = (l1..., reverse(l2)...) - local newtrees - for ((f, f0), coeff1) in repartition((f₁, f₂), N₁ + N₂) - for (f′, coeff2) in braid(f, p, levels) - for ((f₁′, f₂′), coeff3) in repartition((f′, f0), N₁) - if @isdefined newtrees - newtrees[(f₁′, f₂′)] = get(newtrees, (f₁′, f₂′), zero(coeff3)) + - coeff1 * coeff2 * coeff3 - else - newtrees = fusiontreedict(I)((f₁′, f₂′) => coeff1 * coeff2 * coeff3) - end - end - end - end - return only(newtrees) -end - -function transformation_matrix(transform, dst::FusionTreeBlock{I}, src::FusionTreeBlock{I}) where {I} - U = zeros(sectorscalartype(I), length(dst), length(src)) - indexmap = treeindex_map(dst) - for (col, f) in enumerate(fusiontrees(src)) - for (f′, c) in transform(f) - row = indexmap[f′] - U[row, col] = c - end - end - return U + (f, f0), coeff1 = repartition((f₁, f₂), N₁ + N₂) + f′, coeff2 = braid(f, p, levels) + (f₁′, f₂′), coeff3 = repartition((f′, f0), N₁) + return (f₁′, f₂′) => coeff1 * coeff2 * coeff3 end @cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSBBraidKey{I, N₁, N₂}} src, (p1, p2), (l1, l2) = key @@ -429,22 +334,13 @@ end dst, U = repartition(src, numind(src)) - if FusionStyle(I) isa UniqueFusion && BraidingStyle(I) isa SymmetricBraiding - uncoupled′ = TupleTools._permute(dst.uncoupled[1], p) - isdual′ = TupleTools._permute(dst.isdual[1], p) - - dst′ = FusionTreeBlock{I}(uncoupled′, isdual′) - U_tmp = transformation_matrix(dst′, dst) do (f₁, f₂) - return ((f₁′, f₂) => c for (f₁, c) in braid(f₁, p, levels)) - end - dst = dst′ + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + dst, U_tmp = artin_braid(dst, s; inv) U = U_tmp * U - else - for s in permutation2swaps(p) - inv = levels[s] > levels[s + 1] - dst, U_tmp = artin_braid(dst, s; inv) - U = U_tmp * U - end + l = levels[s] + levels = TupleTools.setindex(levels, levels[s + 1], s) + levels = TupleTools.setindex(levels, l, s + 1) end if N₂ == 0 diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index 67e0a9b74..e1a054dd2 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -13,7 +13,7 @@ Map the final splitting vertex `a ⊗ b ← c` of `src` to a fusion vertex `a For `FusionStyle(src) === UniqueFusion()`, both `src` and `dst` are simple `FusionTreePair`s, and the transformation consists of a single coefficient `coeff`. For generic `FusionStyle`s, the input and output consist of `FusionTreeBlock`s that bundle together -all trees with the same uncoupled charges, and `coeffs` now forms a transformation matrix +all trees with the same uncoupled charges, and `coeffs` now forms a transformation matrix. ``` ╰─┬─╯ | | | ╰─┬─╯ | | | @@ -28,52 +28,34 @@ all trees with the same uncoupled charges, and `coeffs` now forms a transformati See also [`bendleft`](@ref). """ bendright -function bendright(src::FusionTreePair) - @assert FusionStyle(src) === UniqueFusion() - I, N₁, N₂ = sectortype(src), numout(src), numin(src) - f₁, f₂ = src +function bendright((f₁, f₂)::FusionTreePair) + I = sectortype((f₁, f₂)) + @assert FusionStyle(I) === UniqueFusion() + N₁, N₂ = numout((f₁, f₂)), numin((f₁, f₂)) @assert N₁ > 0 c = f₁.coupled - a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : - (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) + a = N₁ == 1 ? leftunit(f₁.uncoupled[1]) : (N₁ == 2 ? f₁.uncoupled[1] : f₁.innerlines[end]) b = f₁.uncoupled[N₁] + # construct the new fusiontree pair uncoupled1 = TupleTools.front(f₁.uncoupled) isdual1 = TupleTools.front(f₁.isdual) inner1 = N₁ > 2 ? TupleTools.front(f₁.innerlines) : () vertices1 = N₁ > 1 ? TupleTools.front(f₁.vertices) : () - f₁′ = FusionTree(uncoupled1, a, isdual1, inner1, vertices1) + f₁′ = FusionTree{I}(uncoupled1, a, isdual1, inner1, vertices1) uncoupled2 = (f₂.uncoupled..., dual(b)) isdual2 = (f₂.isdual..., !(f₁.isdual[N₁])) inner2 = N₂ > 1 ? (f₂.innerlines..., c) : () + vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () + f₂′ = FusionTree{I}(uncoupled2, a, isdual2, inner2, vertices2) + # compute the coefficient coeff₀ = sqrtdim(c) * invsqrtdim(a) - if f₁.isdual[N₁] - coeff₀ *= conj(frobenius_schur_phase(dual(b))) - end - if FusionStyle(I) isa MultiplicityFreeFusion - coeff = coeff₀ * Bsymbol(a, b, c) - vertices2 = N₂ > 0 ? (f₂.vertices..., 1) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - return SingletonDict((f₁′, f₂′) => coeff) - else - local newtrees - Bmat = Bsymbol(a, b, c) - μ = N₁ > 1 ? f₁.vertices[end] : 1 - for ν in axes(Bmat, 2) - coeff = coeff₀ * Bmat[μ, ν] - iszero(coeff) && continue - vertices2 = N₂ > 0 ? (f₂.vertices..., ν) : () - f₂′ = FusionTree(uncoupled2, a, isdual2, inner2, vertices2) - if @isdefined newtrees - push!(newtrees, (f₁′, f₂′) => coeff) - else - newtrees = FusionTreeDict((f₁′, f₂′) => coeff) - end - end - return newtrees - end + f₁.isdual[N₁] && (coeff₀ *= conj(frobenius_schur_phase(dual(b)))) + coeff = coeff₀ * Bsymbol(a, b, c) + + return (f₁′, f₂′) => coeff end function bendright(src::FusionTreeBlock) uncoupled_dst = ( @@ -133,14 +115,18 @@ function bendright(src::FusionTreeBlock) end end - return dst, U + return dst => U end @doc """ - bendleft(src) -> dst, coeffs + bendleft((f₁, f₂)::FusionTreePair) -> (f₃, f₄) => coeff + bendleft(src::FusionTreeBlock) -> dst => coeffs -Map the final fusion vertex `a ← c ⊗ dual(b)` of each tree in `src` to a (linear combination of) -splitting vertices `a ⊗ b ← c` in `dst`. +Map the final fusion vertex `a ← c ⊗ dual(b)` of `src` to a splitting vertex `a ⊗ b ← c` in `dst`. +For `FusionStyle(src) === UniqueFusion()`, both `src` and `dst` are simple `FusionTreePair`s, and the +transformation consists of a single coefficient `coeff`. +For generic `FusionStyle`s, the input and output consist of `FusionTreeBlock`s that bundle together +all trees with the same uncoupled charges, and `coeffs` now forms a transformation matrix. ``` ╰─┬─╯ | ╭─╮ ╰─┬─╯ | @@ -155,11 +141,10 @@ splitting vertices `a ⊗ b ← c` in `dst`. See also [`bendright`](@ref). """ bendleft -function bendleft((f₁, f₂)::FusionTreePair{I}) where {I} - @assert FusionStyle(I) === UniqueFusion() - return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in bendright((f₂, f₁)) - ) +function bendleft((f₁, f₂)::FusionTreePair) + @assert FusionStyle((f₁, f₂)) === UniqueFusion() + (f₂′, f₁′), coeff = bendright((f₂, f₁)) + return (f₁′, f₂′) => conj(coeff) end # !! note that this is more or less a copy of bendright through @@ -222,66 +207,52 @@ function bendleft(src::FusionTreeBlock) end end - return dst, U + return dst => U end -# change to N₁ - 1, N₂ + 1 -function foldright((f₁, f₂)::FusionTreePair{I, N₁, N₂}) where {I, N₁, N₂} +@doc """ + foldright((f₁, f₂)::FusionTreePair) -> (f₃, f₄) => coeff + foldright(src::FusionTreeBlock) -> dst => coeffs + +Map the first splitting vertex `a ⊗ b ← c` of `src` to a fusion vertex `a ← c ⊗ dual(b)` in `dst`. +For `FusionStyle(src) === UniqueFusion()`, both `src` and `dst` are simple `FusionTreePair`s, and the +transformation consists of a single coefficient `coeff`. +For generic `FusionStyle`s, the input and output consist of `FusionTreeBlock`s that bundle together +all trees with the same uncoupled charges, and `coeffs` now forms a transformation matrix. + +``` + | ╰─┬─╯ | | ╰─┬─╯ | | | + | ╰─┬─╯ | ╰─┬─╯ | | + | ╰ ⋯ ┬╯ ╰─┬─╯ | + | | → ╰ ⋯ ┬╯ + | ╭ ⋯ ┴╮ | + | ╭─┴─╮ | ╭─ ⋯ ┴╮ + ╰───┴─╮ | | ╭─┴─╮ | +``` + +See also [`foldleft`](@ref). +""" foldright + +function foldright((f₁, f₂)::FusionTreePair) + I = sectortype((f₁, f₂)) @assert FusionStyle(I) === UniqueFusion() - # map first splitting vertex (a, b)<-c to fusion vertex b<-(dual(a), c) - @assert N₁ > 0 + @assert length(f₁) > 0 + + # compute new trees a = f₁.uncoupled[1] isduala = f₁.isdual[1] - factor = sqrtdim(a) - if !isduala - factor *= conj(frobenius_schur_phase(a)) - end c1 = dual(a) c2 = f₁.coupled - uncoupled = Base.tail(f₁.uncoupled) - isdual = Base.tail(f₁.isdual) - if FusionStyle(I) isa UniqueFusion - c = first(c1 ⊗ c2) - fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) - fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) - return fusiontreedict(I)((fl, fr) => factor) - else - local newtrees - if N₁ == 1 - cset = (leftunit(c1),) # or rightunit(a) - elseif N₁ == 2 - cset = (f₁.uncoupled[2],) - else - cset = ⊗(Base.tail(f₁.uncoupled)...) - end - for c in c1 ⊗ c2 - c ∈ cset || continue - for μ in 1:Nsymbol(c1, c2, c) - fc = FusionTree((c1, c2), c, (!isduala, false), (), (μ,)) - fr_coeffs = insertat(fc, 2, f₂) - for (fl′, coeff1) in insertat(fc, 2, f₁) - N₁ > 1 && !isunit(fl′.innerlines[1]) && continue - coupled = fl′.coupled - uncoupled = Base.tail(Base.tail(fl′.uncoupled)) - isdual = Base.tail(Base.tail(fl′.isdual)) - inner = N₁ <= 3 ? () : Base.tail(Base.tail(fl′.innerlines)) - vertices = N₁ <= 2 ? () : Base.tail(Base.tail(fl′.vertices)) - fl = FusionTree{I}(uncoupled, coupled, isdual, inner, vertices) - for (fr, coeff2) in fr_coeffs - coeff = factor * coeff1 * conj(coeff2) - if (@isdefined newtrees) - newtrees[(fl, fr)] = get(newtrees, (fl, fr), zero(coeff)) + - coeff - else - newtrees = fusiontreedict(I)((fl, fr) => coeff) - end - end - end - end - end - return newtrees - end + c = first(c1 ⊗ c2) + fl = FusionTree{I}(Base.tail(f₁.uncoupled), c, Base.tail(f₁.isdual)) + fr = FusionTree{I}((c1, f₂.uncoupled...), c, (!isduala, f₂.isdual...)) + + # compute new coefficients + factor = sqrtdim(a) + isduala || (factor *= conj(frobenius_schur_phase(a))) + + return (fl, fr) => factor end function foldright(src::FusionTreeBlock) @@ -292,7 +263,6 @@ function foldright(src::FusionTreeBlock) isdual_dst = (Base.tail(src.isdual[1]), (!first(src.isdual[1]), src.isdual[2]...)) I = sectortype(src) N₁ = numout(src) - N₂ = numin(src) @assert N₁ > 0 dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src)) @@ -349,16 +319,36 @@ function foldright(src::FusionTreeBlock) end end - return dst, U + return dst => U end -# change to N₁ + 1, N₂ - 1 -function foldleft((f₁, f₂)::FusionTreePair{I}) where {I} - @assert FusionStyle(I) === UniqueFusion() - # map first fusion vertex c<-(a, b) to splitting vertex (dual(a), c)<-b - return fusiontreedict(I)( - (f₁′, f₂′) => conj(coeff) for ((f₂′, f₁′), coeff) in foldright((f₂, f₁)) - ) +@doc """ + foldleft((f₁, f₂)::FusionTreePair) -> (f₃, f₄) => coeff + foldleft(src::FusionTreeBlock) -> dst => coeffs + +Map the first fusion vertex `a ← c ⊗ dual(b)` of `src` to a splitting vertex `a ⊗ b ← c` in `dst`. +For `FusionStyle(src) === UniqueFusion()`, both `src` and `dst` are simple `FusionTreePair`s, and the +transformation consists of a single coefficient `coeff`. +For generic `FusionStyle`s, the input and output consist of `FusionTreeBlock`s that bundle together +all trees with the same uncoupled charges, and `coeffs` now forms a transformation matrix. + +``` + ╭───┬─╯ | | ╰─┬─╯ | + | ╰─┬─╯ | ╰ ⋯ ┬╯ + | ╰ ⋯ ┬╯ | + | | → ╭ ⋯ ┴╮ + | ╭ ⋯ ┴╮ ╭─┴─╮ | + | ╭─┴─╮ | ╭─┴─╮ | | + | ╭─┴─╮ | | ╭─┴─╮ | | | +``` + +See also [`foldright`](@ref). +""" foldleft + +function foldleft((f₁, f₂)::FusionTreePair) + @assert FusionStyle((f₁, f₂)) === UniqueFusion() + (f₂′, f₁′), coeff = foldright((f₂, f₁)) + return (f₁′, f₂′) => conj(coeff) end # !! note that this is more or less a copy of foldright through @@ -430,39 +420,15 @@ function foldleft(src::FusionTreeBlock) end end end - return dst, U + return dst => U end # clockwise cyclic permutation while preserving (N₁, N₂): foldright & bendleft -function cycleclockwise((f₁, f₂)::FusionTreePair{I}) where {I} - @assert FusionStyle(I) === UniqueFusion() - local newtrees - if length(f₁) > 0 - for ((f1a, f2a), coeffa) in foldright((f₁, f₂)) - for ((f1b, f2b), coeffb) in bendleft((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - else - for ((f1a, f2a), coeffa) in bendleft((f₁, f₂)) - for ((f1b, f2b), coeffb) in foldright((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - end - return newtrees -end -function cycleclockwise(src::FusionTreeBlock) +# anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright +# These are utility functions that preserve the type of the input/output trees, +# and are therefore used to craft type-stable transpose implementations. + +function cycleclockwise(src::Union{FusionTreePair, FusionTreeBlock}) if numout(src) > 0 tmp, U₁ = foldright(src) dst, U₂ = bendleft(tmp) @@ -470,39 +436,9 @@ function cycleclockwise(src::FusionTreeBlock) tmp, U₁ = bendleft(src) dst, U₂ = foldright(tmp) end - return dst, U₂ * U₁ + return dst => U₂ * U₁ end - -# anticlockwise cyclic permutation while preserving (N₁, N₂): foldleft & bendright -function cycleanticlockwise((f₁, f₂)::FusionTreePair{I}) where {I} - @assert FusionStyle(I) === UniqueFusion() - local newtrees - if length(f₂) > 0 - for ((f1a, f2a), coeffa) in foldleft((f₁, f₂)) - for ((f1b, f2b), coeffb) in bendright((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - else - for ((f1a, f2a), coeffa) in bendright((f₁, f₂)) - for ((f1b, f2b), coeffb) in foldleft((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees) - newtrees[(f1b, f2b)] = get(newtrees, (f1b, f2b), zero(coeff)) + coeff - else - newtrees = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - end - return newtrees -end -function cycleanticlockwise(src::FusionTreeBlock) +function cycleanticlockwise(src::Union{FusionTreePair, FusionTreeBlock}) if numin(src) > 0 tmp, U₁ = foldleft(src) dst, U₂ = bendright(tmp) @@ -510,7 +446,7 @@ function cycleanticlockwise(src::FusionTreeBlock) tmp, U₁ = bendright(src) dst, U₂ = foldleft(tmp) end - return dst, U₂ * U₁ + return dst => U₂ * U₁ end # COMPOSITE DUALITY MANIPULATIONS PART 1: Repartition and transpose @@ -531,39 +467,7 @@ outgoing (`f₁`) and incoming sectors (`f₂`) respectively (with identical cou repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have `N` outgoing sectors. """ -@inline function repartition((f₁, f₂)::FusionTreePair, N::Int) - @assert FusionStyle((f₁, f₂)) === UniqueFusion() - f₁.coupled == f₂.coupled || throw(SectorMismatch()) - @assert 0 <= N <= length(f₁) + length(f₂) - return _recursive_repartition((f₁, f₂), Val(N)) -end - -function _recursive_repartition((f₁, f₂)::FusionTreePair{I, N₁, N₂}, ::Val{N}) where {I, N₁, N₂, N} - # recursive definition is only way to get correct number of loops for - # GenericFusion, but is too complex for type inference to handle, so we - # precompute the parameters of the return type - F₁ = fusiontreetype(I, N) - F₂ = fusiontreetype(I, N₁ + N₂ - N) - FF = Tuple{F₁, F₂} - T = sectorscalartype(I) - coeff = one(T) - if N == N₁ - return fusiontreedict(I){Tuple{F₁, F₂}, T}((f₁, f₂) => coeff) - else - local newtrees::fusiontreedict(I){Tuple{F₁, F₂}, T} - for ((f₁′, f₂′), coeff1) in (N < N₁ ? bendright((f₁, f₂)) : bendleft((f₁, f₂))) - for ((f₁′′, f₂′′), coeff2) in _recursive_repartition((f₁′, f₂′), Val(N)) - if (@isdefined newtrees) - push!(newtrees, (f₁′′, f₂′′) => coeff1 * coeff2) - else - newtrees = fusiontreedict(I){FF, T}((f₁′′, f₂′′) => coeff1 * coeff2) - end - end - end - return newtrees - end -end -@inline function repartition(src::FusionTreeBlock, N::Int) +@inline function repartition(src::Union{FusionTreePair, FusionTreeBlock}, N::Int) @assert 0 <= N <= numind(src) return repartition(src, Val(N)) end @@ -584,8 +488,12 @@ function _repartition_body(N) if N == 0 ex = quote T = sectorscalartype(sectortype(src)) - U = copyto!(zeros(T, length(src), length(src)), LinearAlgebra.I) - return src, U + if FusionStyle(src) === UniqueFusion() + return src => one(T) + else + U = copyto!(zeros(T, length(src), length(src)), LinearAlgebra.I) + return src, U + end end else f = N < 0 ? bendleft : bendright @@ -597,12 +505,12 @@ function _repartition_body(N) ex = quote dst, U = $f(src) $ex_rep - return dst, U + return dst => U end end return ex end -@generated function repartition(src::FusionTreeBlock, ::Val{N}) where {N} +@generated function repartition(src::Union{FusionTreePair, FusionTreeBlock}, ::Val{N}) where {N} return _repartition_body(numout(src) - N) end @@ -638,49 +546,7 @@ Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N return Pair{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} end -@cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSPTransposeKey{I, N₁, N₂}} - (f₁, f₂), (p1, p2) = key - N = N₁ + N₂ - p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - newtrees = repartition((f₁, f₂), N₁) - length(p) == 0 && return only(newtrees) - i1 = findfirst(==(1), p) - @assert i1 !== nothing - i1 == 1 && return only(newtrees) - Nhalf = N >> 1 - while 1 < i1 <= Nhalf - local newtrees′ - for ((f1a, f2a), coeffa) in newtrees - for ((f1b, f2b), coeffb) in cycleanticlockwise((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees′) - newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff - else - newtrees′ = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - newtrees = newtrees′ - i1 -= 1 - end - while Nhalf < i1 - local newtrees′ - for ((f1a, f2a), coeffa) in newtrees - for ((f1b, f2b), coeffb) in cycleclockwise((f1a, f2a)) - coeff = coeffa * coeffb - if (@isdefined newtrees′) - newtrees′[(f1b, f2b)] = get(newtrees′, (f1b, f2b), zero(coeff)) + coeff - else - newtrees′ = fusiontreedict(I)((f1b, f2b) => coeff) - end - end - end - newtrees = newtrees′ - i1 = mod1(i1 + 1, N) - end - return only(newtrees) -end -@cached function fstranspose(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSBTransposeKey{I, N₁, N₂}} +@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 N = N₁ + N₂ From d5301ee7d4e69e57d1beab80f7d2aa3927084c72 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 18 Nov 2025 21:28:16 -0500 Subject: [PATCH 36/39] update fusiontensor overload --- src/TensorKit.jl | 2 +- src/fusiontrees/fusiontrees.jl | 41 +++++++++++++++++----------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 06c6b8998..1b62a95ab 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -119,7 +119,7 @@ using ScopedValues using TensorKitSectors import TensorKitSectors: dim, BraidingStyle, FusionStyle, ⊠, ⊗ -import TensorKitSectors: dual, type_repr +import TensorKitSectors: dual, type_repr, fusiontensor import TensorKitSectors: twist using Base: @boundscheck, @propagate_inbounds, @constprop, diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 36b8e5ec5..895a5d619 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -264,50 +264,50 @@ end fusiontreedict(I) = FusionStyle(I) isa UniqueFusion ? SingletonDict : FusionTreeDict # converting to actual array -function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, 0}) where {I} - X = convert(A, fusiontensor(unit(I), unit(I), unit(I)))[1, 1, :] - return X -end -function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, 1}) where {I} +Base.convert(A::Type{<:AbstractArray}, f::FusionTree) = convert(A, fusiontensor(f)) +# TODO: is this piracy? +Base.convert(A::Type{<:AbstractArray}, (f₁, f₂)::FusionTreePair) = + convert(A, fusiontensor((f₁, f₂))) + +fusiontensor(::FusionTree{I, 0}) where {I} = fusiontensor(unit(I), unit(I), unit(I))[1, 1, :] +function fusiontensor(f::FusionTree{I, 1}) where {I} c = f.coupled if f.isdual[1] sqrtdc = sqrtdim(c) - Zcbartranspose = sqrtdc * convert(A, fusiontensor(dual(c), c, unit(c)))[:, :, 1, 1] + Zcbartranspose = sqrtdc * fusiontensor(dual(c), c, unit(c))[:, :, 1, 1] X = conj!(Zcbartranspose) # we want Zcbar^† else - X = convert(A, fusiontensor(c, unit(c), c))[:, 1, :, 1, 1] + X = fusiontensor(c, unit(c), c)[:, 1, :, 1, 1] end return X end - -function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, 2}) where {I} +function fusiontensor(f::FusionTree{I, 2}) where {I} a, b = f.uncoupled isduala, isdualb = f.isdual c = f.coupled μ = (FusionStyle(I) isa GenericFusion) ? f.vertices[1] : 1 - C = convert(A, fusiontensor(a, b, c))[:, :, :, μ] + C = fusiontensor(a, b, c)[:, :, :, μ] X = C if isduala - Za = convert(A, FusionTree((a,), a, (isduala,), ())) + Za = fusiontensor(FusionTree((a,), a, (isduala,), ())) @tensor X[a′, b, c] := Za[a′, a] * X[a, b, c] end if isdualb - Zb = convert(A, FusionTree((b,), b, (isdualb,), ())) + Zb = fusiontensor(FusionTree((b,), b, (isdualb,), ())) @tensor X[a, b′, c] := Zb[b′, b] * X[a, b, c] end return X end - -function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, N}) where {I, N} +function fusiontensor(f::FusionTree{I, N}) where {I, N} tailout = (f.innerlines[1], TupleTools.tail2(f.uncoupled)...) isdualout = (false, TupleTools.tail2(f.isdual)...) ftail = FusionTree(tailout, f.coupled, isdualout, Base.tail(f.innerlines), Base.tail(f.vertices)) - Ctail = convert(A, ftail) + Ctail = fusiontensor(ftail) f₁ = FusionTree( (f.uncoupled[1], f.uncoupled[2]), f.innerlines[1], (f.isdual[1], f.isdual[2]), (), (f.vertices[1],) ) - C1 = convert(A, f₁) + C1 = fusiontensor(f₁) dtail = size(Ctail) d1 = size(C1) X = similar(C1, (d1[1], d1[2], Base.tail(dtail)...)) @@ -320,20 +320,19 @@ function Base.convert(A::Type{<:AbstractArray}, f::FusionTree{I, N}) where {I, N ) end -# TODO: is this piracy? -function Base.convert(A::Type{<:AbstractArray}, (f₁, f₂)::FusionTreePair{I}) where {I} - F₁ = convert(A, f₁) - F₂ = convert(A, f₂) +function fusiontensor((f₁, f₂)::FusionTreePair) + F₁ = fusiontensor(f₁) + F₂ = fusiontensor(f₂) sz1 = size(F₁) sz2 = size(F₂) d1 = TupleTools.front(sz1) d2 = TupleTools.front(sz2) - return reshape( reshape(F₁, TupleTools.prod(d1), sz1[end]) * reshape(F₂, TupleTools.prod(d2), sz2[end])', (d1..., d2...) ) end +fusiontensor(src::FusionTreeBlock) = sum(fusiontensor, fusiontrees(src)) # Show methods function Base.show(io::IO, t::FusionTree{I}) where {I <: Sector} From c31bc93a02e347c897ad9308d2a14421b0ac3329 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 18 Nov 2025 21:28:23 -0500 Subject: [PATCH 37/39] temporary trace fix --- src/fusiontrees/duality_manipulations.jl | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index e1a054dd2..7d06a2b54 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -609,12 +609,24 @@ function planar_trace( F₁ = fusiontreetype(I, N₁) F₂ = fusiontreetype(I, N₂) newtrees = FusionTreeDict{Tuple{F₁, F₂}, T}() - for ((f₁′, f₂′), coeff′) in repartition((f₁, f₂), N) + if FusionStyle(I) isa UniqueFusion + (f₁′, f₂′), coeff′ = repartition((f₁, f₂), N) for (f₁′′, coeff′′) in planar_trace(f₁′, (q1′, q2′)) - for (f12′′′, coeff′′′) in transpose((f₁′′, f₂′), (p1′, p2′)) - coeff = coeff′ * coeff′′ * coeff′′′ - if !iszero(coeff) - newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff + (f12′′′, coeff′′′) = transpose((f₁′′, f₂′), (p1′, p2′)) + coeff = coeff′ * coeff′′ * coeff′′′ + iszero(coeff) || (newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff) + end + else + # TODO: this is a bit of a hack to fix the traces for now + src = FusionTreeBlock([(f₁, f₂)]) + dst, U = repartition(src, N) + for ((f₁′, f₂′), coeff′) in zip(fusiontrees(dst), U) + for (f₁′′, coeff′′) in planar_trace(f₁′, (q1′, q2′)) + src′ = FusionTreeBlock([(f₁′′, f₂′)]) + dst′, U′ = transpose(src′, (p1′, p2′)) + for (f12′′′, coeff′′′) in zip(fusiontrees(dst′), U′) + coeff = coeff′ * coeff′′ * coeff′′′ + iszero(coeff) || (newtrees[f12′′′] = get(newtrees, f12′′′, zero(coeff)) + coeff) end end end From 8f35e7391b80be885e466402fe8852857a975c62 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 18 Nov 2025 21:28:27 -0500 Subject: [PATCH 38/39] start tackling tests --- test/symmetries/fusiontrees.jl | 456 +++++++++++++++++---------------- 1 file changed, 232 insertions(+), 224 deletions(-) diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl index f5113f2f0..e577839f9 100644 --- a/test/symmetries/fusiontrees.jl +++ b/test/symmetries/fusiontrees.jl @@ -1,12 +1,18 @@ using Test, TestExtras using TensorKit +using TensorKit: FusionTreeBlock import TensorKit as TK using Random: randperm using TensorOperations +using MatrixAlgebraKit: isunitary # TODO: remove this once type_repr works for all included types using TensorKitSectors +_isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) +_isunitary(x; kwargs...) = isunitary(x; kwargs...) +_isone(x; kwargs...) = isapprox(x, one(x); kwargs...) + @isdefined(TestSetup) || include("../setup.jl") using .TestSetup @@ -18,7 +24,7 @@ using .TestSetup in = rand(collect(⊗(out...))) numtrees = length(fusiontrees(out, in, isdual)) @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - while !(0 < numtrees < 30) + while !(0 < numtrees < 30) && !(one(in) in ⊗(out...)) out = ntuple(n -> randsector(I), N) in = rand(collect(⊗(out...))) numtrees = length(fusiontrees(out, in, isdual)) @@ -99,20 +105,21 @@ using .TestSetup @test c′ == one(c′) return t′ end - braid_i_to_1 = braid(f1, (i, (1:(i - 1))..., ((i + 1):N)...), levels) - trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) - trees3 = empty(trees2) - p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) - levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) - for (t, coeff) in trees2 - for (t′, coeff′) in braid(t, p, levels) - trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ - end - end - for (t, coeff) in trees3 - coeff′ = get(trees, t, zero(coeff)) - @test isapprox(coeff′, coeff; atol = 1.0e-12, rtol = 1.0e-12) - end + # TODO: restore this? + # braid_i_to_1 = braid(f1, (i, (1:(i - 1))..., ((i + 1):N)...), levels) + # trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) + # trees3 = empty(trees2) + # p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) + # levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) + # for (t, coeff) in trees2 + # for (t′, coeff′) in braid(t, p, levels) + # trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ + # end + # end + # for (t, coeff) in trees3 + # coeff′ = get(trees, t, zero(coeff)) + # @test isapprox(coeff′, coeff; atol = 1.0e-12, rtol = 1.0e-12) + # end if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) Af1 = convert(Array, f1) @@ -226,89 +233,86 @@ using .TestSetup @testset "Fusion tree $Istr: elementary artin braid" begin N = length(out) isdual = ntuple(n -> rand(Bool), N) - for in in ⊗(out...) - for i in 1:(N - 1) - for f in fusiontrees(out, in, isdual) - d1 = @constinferred TK.artin_braid(f, i) - @test norm(values(d1)) ≈ 1 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, i; inv = true) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - for (f2, coeff2) in d2 - if f2 == f - @test coeff2 ≈ 1 - else - @test isapprox(coeff2, 0; atol = 1.0e-12, rtol = 1.0e-12) - end - end + if FusionStyle(I) isa UniqueFusion + for in in ⊗(out...) + src = only(fusiontrees(out, in, isdual)) + + for i in 1:(N - 1) + dst, U = @constinferred TK.artin_braid(src, i) + @test _isunitary(U) + dst′, U′ = @constinferred TK.artin_braid(dst, i; inv = true) + @test U' ≈ U′ end end - end - - f = rand(collect(it)) - d1 = TK.artin_braid(f, 2) - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 3) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - d1 = d2 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 3; inv = true) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 - end - end - d1 = d2 - d2 = empty(d1) - for (f1, coeff1) in d1 - for (f2, coeff2) in TK.artin_braid(f1, 2; inv = true) - d2[f2] = get(d2, f2, zero(coeff1)) + coeff2 * coeff1 + else + src = FusionTreeBlock{I}((out, ()), (isdual, ())) + length(src) > 0 && for i in 1:(N - 1) + dst, U = @constinferred TK.artin_braid(src, i) + @test _isunitary(U) + dst′, U′ = @constinferred TK.artin_braid(dst, i; inv = true) + @test U' ≈ U′ end end - d1 = d2 - for (f1, coeff1) in d1 - if f1 == f - @test coeff1 ≈ 1 - else - @test isapprox(coeff1, 0; atol = 1.0e-12, rtol = 1.0e-12) + + # Test double braid-unbraid + if FusionStyle(I) isa UniqueFusion + in = rand(collect(⊗(out...))) + src = only(fusiontrees(out, in, isdual)) + dst, U = TK.artin_braid(src, 2) + dst′, U′ = TK.artin_braid(dst, 3) + dst″, U″ = TK.artin_braid(dst′, 3; inv = true) + dst‴, U‴ = TK.artin_braid(dst″, 2; inv = true) + @test U * U′ * U″ * U‴ ≈ 1 + else + src = FusionTreeBlock{I}((out, ()), (isdual, ())) + if length(src) > 0 + dst, U = TK.artin_braid(src, 2) + dst′, U′ = TK.artin_braid(dst, 3) + dst″, U″ = TK.artin_braid(dst′, 3; inv = true) + dst‴, U‴ = TK.artin_braid(dst″, 2; inv = true) + @test U * U′ * U″ * U‴ ≈ LinearAlgebra.I end end end + @testset "Fusion tree $Istr: braiding and permuting" begin - f = rand(collect(it)) p = tuple(randperm(N)...) ip = invperm(p) - levels = ntuple(identity, N) - d = @constinferred braid(f, p, levels) - d2 = Dict{typeof(f), valtype(d)}() levels2 = p - for (f2, coeff) in d - for (f1, coeff2) in braid(f2, ip, levels2) - d2[f1] = get(d2, f1, zero(coeff)) + coeff2 * coeff - end - end - for (f1, coeff2) in d2 - if f1 == f - @test coeff2 ≈ 1 - else - @test isapprox(coeff2, 0; atol = 1.0e-12, rtol = 1.0e-12) - end + + if FusionStyle(I) isa UniqueFusion + f = only(fusiontrees(out, in, isdual)) + else + f = FusionTreeBlock{I}((out, ()), (isdual, ())) end + dst, U = @constinferred braid(f, (p, ()), (levels, ())) + @test _isunitary(U) + dst′, U′ = braid(dst, (ip, ()), (levels2, ())) + @test U' ≈ U′ + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - Af = convert(Array, f) - Afp = permutedims(Af, (p..., N + 1)) - Afp2 = zero(Afp) - for (f1, coeff) in d - Afp2 .+= coeff .* convert(Array, f1) + if FusionStyle(I) isa UniqueFusion + A = permutedims(fusiontensor(f), (p..., N + 1)) + A′ = U * fusiontensor(dst) + @test A ≈ A′ + else + A = map(x -> permutedims(fusiontensor(x[1]), (p..., N + 1)), fusiontrees(f)) + A′ = map(fusiontensor ∘ first, fusiontrees(dst)) + for (i, Ai) in enumerate(A) + Aj = sum(U[i, :] .* A′) + @test Ai ≈ Aj + end end - @test Afp ≈ Afp2 + # Af = convert(Array, f) + # Afp = permutedims(Af, (p..., N + 1)) + # Afp2 = zero(Afp) + # for (f1, coeff) in d + # Afp2 .+= coeff .* convert(Array, f1) + # end + # @test Afp ≈ Afp2 end end @@ -347,11 +351,21 @@ using .TestSetup end perm = ((N .+ (1:N))..., (1:N)...) levels = ntuple(identity, 2 * N) + for (t, coeff) in trees1 - for (t′, coeff′) in braid(t, perm, levels) + if FusionStyle(I) isa UniqueFusion + t′, coeff′ = braid(t, perm, levels) trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ + else + src = FusionTreeBlock([(t, FusionTree{I}((), t.coupled, (), ()))]) + dst, U = braid(src, (perm, ()), (levels, ())) + for ((t′, _), coeff′) in zip(fusiontrees(dst), U) + trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ + + end end end + for (t, coeff) in trees3 coeff′ = get(trees2, t, zero(coeff)) @test isapprox(coeff, coeff′; atol = 1.0e-12, rtol = 1.0e-12) @@ -393,58 +407,47 @@ using .TestSetup numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) end incoming = rand(collect(⊗(out...))) - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + + if FusionStyle(I) isa UniqueFusion + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + src = (f1, f2) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A = fusiontensor(src) + end + else + src = FusionTreeBlock{I}((out, out), (ntuple(n -> rand(Bool), N), ntuple(n -> rand(Bool), N))) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A = map(fusiontensor, fusiontrees(src)) + end + end @testset "Double fusion tree $Istr: repartitioning" begin for n in 0:(2 * N) - d = @constinferred TK.repartition((f1, f2), $n) - @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)), valtype(d)}() - for ((f1′, f2′), coeff) in d - for ((f1′′, f2′′), coeff2) in TK.repartition((f1′, f2′), N) - d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff - end - end - for ((f1′, f2′), coeff2) in d2 - if f1 == f1′ && f2 == f2′ - @test coeff2 ≈ 1 - else - @test isapprox(coeff2, 0; atol = 1.0e-12, rtol = 1.0e-12) - end - end + dst, U = @constinferred TK.repartition(src, $n) + # @test _isunitary(U) + + dst′, U′ = repartition(dst, N) + @test _isone(U * U′) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - Af1 = convert(Array, f1) - Af2 = permutedims(convert(Array, f2), [N:-1:1; N + 1]) - sz1 = size(Af1) - sz2 = size(Af2) - d1 = prod(sz1[1:(end - 1)]) - d2 = prod(sz2[1:(end - 1)]) - dc = sz1[end] - A = reshape( - reshape(Af1, (d1, dc)) * reshape(Af2, (d2, dc))', - (sz1[1:(end - 1)]..., sz2[1:(end - 1)]...) - ) - A2 = zero(A) - for ((f1′, f2′), coeff) in d - Af1′ = convert(Array, f1′) - Af2′ = permutedims(convert(Array, f2′), [(2N - n):-1:1; 2N - n + 1]) - sz1′ = size(Af1′) - sz2′ = size(Af2′) - d1′ = prod(sz1′[1:(end - 1)]) - d2′ = prod(sz2′[1:(end - 1)]) - dc′ = sz1′[end] - A2 += coeff * - reshape( - reshape(Af1′, (d1′, dc′)) * reshape(Af2′, (d2′, dc′))', - (sz1′[1:(end - 1)]..., sz2′[1:(end - 1)]...) - ) + all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) + p₁ = ntuple(i -> all_inds[i], numout(dst)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst)], numin(dst))) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + Aj = sum(U[:, i] .* A″) + @test Ai ≈ Aj + end end - @test A ≈ A2 end end end + @testset "Double fusion tree $Istr: permutation" begin if BraidingStyle(I) isa SymmetricBraiding for n in 0:(2N) @@ -453,54 +456,52 @@ using .TestSetup ip = invperm(p) ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] - d = @constinferred TensorKit.permute((f1, f2), (p1, p2)) - @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)), valtype(d)}() - for ((f1′, f2′), coeff) in d - d′ = TensorKit.permute((f1′, f2′), (ip1, ip2)) - for ((f1′′, f2′′), coeff2) in d′ - d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + - coeff2 * coeff - end - end - for ((f1′, f2′), coeff2) in d2 - if f1 == f1′ && f2 == f2′ - @test coeff2 ≈ 1 - else - @test abs(coeff2) < 1.0e-12 - end - end + dst, U = @constinferred TensorKit.permute(src, (p1, p2)) + # @test _isunitary(U) + dst′, U′ = @constinferred TensorKit.permute(dst, (ip1, ip2)) + # @test U' ≈ U′ + @test _isone(U * U′) if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - Af1 = convert(Array, f1) - Af2 = convert(Array, f2) - sz1 = size(Af1) - sz2 = size(Af2) - d1 = prod(sz1[1:(end - 1)]) - d2 = prod(sz2[1:(end - 1)]) - dc = sz1[end] - A = reshape( - reshape(Af1, (d1, dc)) * reshape(Af2, (d2, dc))', - (sz1[1:(end - 1)]..., sz2[1:(end - 1)]...) - ) - Ap = permutedims(A, (p1..., p2...)) - A2 = zero(Ap) - for ((f1′, f2′), coeff) in d - Af1′ = convert(Array, f1′) - Af2′ = convert(Array, f2′) - sz1′ = size(Af1′) - sz2′ = size(Af2′) - d1′ = prod(sz1′[1:(end - 1)]) - d2′ = prod(sz2′[1:(end - 1)]) - dc′ = sz1′[end] - A2 += coeff * reshape( - reshape(Af1′, (d1′, dc′)) * - reshape(Af2′, (d2′, dc′))', - (sz1′[1:(end - 1)]..., sz2′[1:(end - 1)]...) - ) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + Aj = sum(U[:, i] .* A″) + @test Ai ≈ Aj + end end - @test Ap ≈ A2 + # + # Af1 = convert(Array, f1) + # Af2 = convert(Array, f2) + # sz1 = size(Af1) + # sz2 = size(Af2) + # d1 = prod(sz1[1:(end - 1)]) + # d2 = prod(sz2[1:(end - 1)]) + # dc = sz1[end] + # A = reshape( + # reshape(Af1, (d1, dc)) * reshape(Af2, (d2, dc))', + # (sz1[1:(end - 1)]..., sz2[1:(end - 1)]...) + # ) + # Ap = permutedims(A, (p1..., p2...)) + # A2 = zero(Ap) + # for ((f1′, f2′), coeff) in d + # Af1′ = convert(Array, f1′) + # Af2′ = convert(Array, f2′) + # sz1′ = size(Af1′) + # sz2′ = size(Af2′) + # d1′ = prod(sz1′[1:(end - 1)]) + # d2′ = prod(sz2′[1:(end - 1)]) + # dc′ = sz1′[end] + # A2 += coeff * reshape( + # reshape(Af1′, (d1′, dc′)) * + # reshape(Af2′, (d2′, dc′))', + # (sz1′[1:(end - 1)]..., sz2′[1:(end - 1)]...) + # ) + # end + # @test Ap ≈ A2 end end end @@ -515,67 +516,74 @@ using .TestSetup ip′ = tuple(getindex.(Ref(vcat(1:n, (2N):-1:(n + 1))), ip)...) ip1, ip2 = ip′[1:N], ip′[(2N):-1:(N + 1)] - d = @constinferred transpose((f1, f2), (p1, p2)) - @test dim(incoming) ≈ - sum(abs2(coef) * dim(f1.coupled) for ((f1, f2), coef) in d) - d2 = Dict{typeof((f1, f2)), valtype(d)}() - for ((f1′, f2′), coeff) in d - d′ = transpose((f1′, f2′), (ip1, ip2)) - for ((f1′′, f2′′), coeff2) in d′ - d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff2 * coeff - end - end - for ((f1′, f2′), coeff2) in d2 - if f1 == f1′ && f2 == f2′ - @test coeff2 ≈ 1 - else - @test abs(coeff2) < 1.0e-12 - end - end + dst, U = @constinferred transpose(src, (p1, p2)) + # @test _isunitary(U) + dst′, U′ = @constinferred transpose(dst, (ip1, ip2)) + # @test U' ≈ U′ + @test _isone(U * U′) if BraidingStyle(I) isa Bosonic - d3 = permute((f1, f2), (p1, p2)) - for (f1′, f2′) in union(keys(d), keys(d3)) - coeff1 = get(d, (f1′, f2′), zero(valtype(d))) - coeff3 = get(d3, (f1′, f2′), zero(valtype(d3))) - @test isapprox(coeff1, coeff3; atol = 1.0e-12) - end + dst″, U″ = permute(src, (p1, p2)) + @test U″ ≈ U end if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - Af1 = convert(Array, f1) - Af2 = convert(Array, f2) - sz1 = size(Af1) - sz2 = size(Af2) - d1 = prod(sz1[1:(end - 1)]) - d2 = prod(sz2[1:(end - 1)]) - dc = sz1[end] - A = reshape( - reshape(Af1, (d1, dc)) * reshape(Af2, (d2, dc))', - (sz1[1:(end - 1)]..., sz2[1:(end - 1)]...) - ) - Ap = permutedims(A, (p1..., p2...)) - A2 = zero(Ap) - for ((f1′, f2′), coeff) in d - Af1′ = convert(Array, f1′) - Af2′ = convert(Array, f2′) - sz1′ = size(Af1′) - sz2′ = size(Af2′) - d1′ = prod(sz1′[1:(end - 1)]) - d2′ = prod(sz2′[1:(end - 1)]) - dc′ = sz1′[end] - A2 += coeff * reshape( - reshape(Af1′, (d1′, dc′)) * - reshape(Af2′, (d2′, dc′))', - (sz1′[1:(end - 1)]..., sz2′[1:(end - 1)]...) - ) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + Aj = sum(U[:, i] .* A″) + @test Ai ≈ Aj + end end - @test Ap ≈ A2 + + # Af1 = convert(Array, f1) + # Af2 = convert(Array, f2) + # sz1 = size(Af1) + # sz2 = size(Af2) + # d1 = prod(sz1[1:(end - 1)]) + # d2 = prod(sz2[1:(end - 1)]) + # dc = sz1[end] + # A = reshape( + # reshape(Af1, (d1, dc)) * reshape(Af2, (d2, dc))', + # (sz1[1:(end - 1)]..., sz2[1:(end - 1)]...) + # ) + # Ap = permutedims(A, (p1..., p2...)) + # A2 = zero(Ap) + # for ((f1′, f2′), coeff) in d + # Af1′ = convert(Array, f1′) + # Af2′ = convert(Array, f2′) + # sz1′ = size(Af1′) + # sz2′ = size(Af2′) + # d1′ = prod(sz1′[1:(end - 1)]) + # d2′ = prod(sz2′[1:(end - 1)]) + # dc′ = sz1′[end] + # A2 += coeff * reshape( + # reshape(Af1′, (d1′, dc′)) * + # reshape(Af2′, (d2′, dc′))', + # (sz1′[1:(end - 1)]..., sz2′[1:(end - 1)]...) + # ) + # end + # @test Ap ≈ A2 end end end - @testset "Double fusion tree $Istr: planar trace" begin - d1 = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) + false && @testset "Double fusion tree $Istr: planar trace" begin + + + if FusionStyle(I) isa UniqueFusion + f1, f1 = src + dst, U = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) + d1 = zip((dst,), (U,)) + else + f1, f1 = first(fusiontrees(src)) + dst, U = transpose(src, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) + @show size(U) + d1 = zip(fusiontrees(dst), U[:, 1]) + end + f1front, = TK.split(f1, N - 1) T = TensorKitSectors._Fscalartype(I) d2 = Dict{typeof((f1front, f1front)), T}() From 23c399459636942a4232bab34e3206d1a8a4bcd6 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 21 Dec 2025 15:46:25 -0500 Subject: [PATCH 39/39] attempts at cleanup --- test/symmetries/fusiontrees.jl | 149 +++++++++++++-------------------- 1 file changed, 59 insertions(+), 90 deletions(-) diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl index 819a57e80..7a0d887e5 100644 --- a/test/symmetries/fusiontrees.jl +++ b/test/symmetries/fusiontrees.jl @@ -5,6 +5,7 @@ import TensorKit as TK using Random: randperm using TensorOperations using MatrixAlgebraKit: isunitary +using LinearAlgebra # TODO: remove this once type_repr works for all included types using TensorKitSectors @@ -86,6 +87,11 @@ using .TestSetup end end end + function _reinsert_partial_tree(t, f) + (t′, c′) = first(TK.insertat(t, 1, f)) + @test c′ == one(c′) + return t′ + end @testset "Fusion tree $Istr: insertat" begin N = 4 out2 = random_fusion(I, Val(N)) @@ -112,50 +118,24 @@ using .TestSetup @test first(TK.insertat(f1b, 1, f1a)) == (f1 => 1) levels = ntuple(identity, N) - function _reinsert_partial_tree(t, f) - (t′, c′) = first(TK.insertat(t, 1, f)) - @test c′ == one(c′) - return t′ - end - - # TODO: restore this? - # braid_i_to_1 = braid(f1, (i, (1:(i - 1))..., ((i + 1):N)...), levels) - # trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) - # trees3 = empty(trees2) - # p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) - # levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) - # for (t, coeff) in trees2 - # for (t′, coeff′) in braid(t, p, levels) - # trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ + + # TODO: restore this? + # if UnitStyle(I) isa SimpleUnit + # braid_i_to_1 = braid(f1, (i, (1:(i - 1))..., ((i + 1):N)...), levels) + # trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) + # trees3 = empty(trees2) + # p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) + # levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) + # for (t, coeff) in trees2 + # for (t′, coeff′) in braid(t, p, levels) + # trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ + # end + # end + # for (t, coeff) in trees3 + # coeff′ = get(trees, t, zero(coeff)) + # @test isapprox(coeff′, coeff; atol = 1.0e-12, rtol = 1.0e-12) # end # end - # for (t, coeff) in trees3 - # coeff′ = get(trees, t, zero(coeff)) - # @test isapprox(coeff′, coeff; atol = 1.0e-12, rtol = 1.0e-12) - # end - # - if UnitStyle(I) isa SimpleUnit - levels = ntuple(identity, N) - function _reinsert_partial_tree(t, f) - (t′, c′) = first(TK.insertat(t, 1, f)) - @test c′ == one(c′) - return t′ - end - braid_i_to_1 = braid(f1, levels, (i, (1:(i - 1))..., ((i + 1):N)...)) - trees2 = Dict(_reinsert_partial_tree(t, f2) => c for (t, c) in braid_i_to_1) - trees3 = empty(trees2) - p = (((N + 1):(N + i - 1))..., (1:N)..., ((N + i):(2N - 1))...) - levels = ((i:(N + i - 1))..., (1:(i - 1))..., ((i + N):(2N - 1))...) - for (t, coeff) in trees2 - for (t′, coeff′) in braid(t, levels, p) - trees3[t′] = get(trees3, t′, zero(coeff′)) + coeff * coeff′ - end - end - for (t, coeff) in trees3 - coeff′ = get(trees, t, zero(coeff)) - @test isapprox(coeff′, coeff; atol = 1.0e-12, rtol = 1.0e-12) - end - end if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) Af1 = convert(Array, f1) @@ -173,6 +153,7 @@ using .TestSetup end end end + @testset "Fusion tree $Istr: planar trace" begin if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) s = randsector(I) @@ -266,6 +247,7 @@ using .TestSetup end end end + (BraidingStyle(I) isa HasBraiding) && @testset "Fusion tree $Istr: elementary artin braid" begin N = length(out) isdual = ntuple(n -> rand(Bool), N) @@ -305,7 +287,7 @@ using .TestSetup dst′, U′ = TK.artin_braid(dst, 3) dst″, U″ = TK.artin_braid(dst′, 3; inv = true) dst‴, U‴ = TK.artin_braid(dst″, 2; inv = true) - @test U * U′ * U″ * U‴ ≈ LinearAlgebra.I + @test _isone(U * U′ * U″ * U‴) end end end @@ -337,21 +319,14 @@ using .TestSetup A = map(x -> permutedims(fusiontensor(x[1]), (p..., N + 1)), fusiontrees(f)) A′ = map(fusiontensor ∘ first, fusiontrees(dst)) for (i, Ai) in enumerate(A) - Aj = sum(U[i, :] .* A′) + Aj = sum(A′ .* U[:, i]) @test Ai ≈ Aj end end - # Af = convert(Array, f) - # Afp = permutedims(Af, (p..., N + 1)) - # Afp2 = zero(Afp) - # for (f1, coeff) in d - # Afp2 .+= coeff .* convert(Array, f1) - # end - # @test Afp ≈ Afp2 end end - @testset "Fusion tree $Istr: merging" begin + FusionStyle(I) isa UniqueFusion && @testset "Fusion tree $Istr: merging" begin N = 3 out1 = random_fusion(I, Val(N)) out2 = random_fusion(I, Val(N)) @@ -397,9 +372,8 @@ using .TestSetup perm = ((N .+ (1:N))..., (1:N)...) levels = ntuple(identity, 2 * N) for (t, coeff) in trees1 - for (t′, coeff′) in braid(t, levels, perm) - trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ - end + t′, coeff′ = braid(t, levels, perm) + trees3[t′] = get(trees3, t′, zero(valtype(trees3))) + coeff * coeff′ end for (t, coeff) in trees3 coeff′ = get(trees2, t, zero(coeff)) @@ -408,12 +382,9 @@ using .TestSetup # test via conversion if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - Af1 = convert(Array, f1) - Af2 = convert(Array, f2) - Af0 = convert( - Array, - FusionTree((f1.coupled, f2.coupled), c, (false, false), (), (μ,)) - ) + Af1 = fusiontensor(f1) + Af2 = fusiontensor(f2) + Af0 = fusiontensor(FusionTree((f1.coupled, f2.coupled), c, (false, false), (), (μ,))) _Af = TensorOperations.tensorcontract( 1:(N + 2), Af1, [1:N; -1], Af0, [-1; N + 1; N + 2] ) @@ -494,7 +465,7 @@ using .TestSetup A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) A″ = map(fusiontensor, fusiontrees(dst)) for (i, Ai) in enumerate(A′) - Aj = sum(U[:, i] .* A″) + Aj = sum(A″ .* U[:, i]) @test Ai ≈ Aj end end @@ -502,30 +473,29 @@ using .TestSetup end end - @testset "Double fusion tree $Istr: permutation" begin - if BraidingStyle(I) isa SymmetricBraiding - for n in 0:(2N) - p = (randperm(2 * N)...,) - p1, p2 = p[1:n], p[(n + 1):(2N)] - ip = invperm(p) - ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] - - dst, U = @constinferred TensorKit.permute(src, (p1, p2)) - # @test _isunitary(U) - dst′, U′ = @constinferred TensorKit.permute(dst, (ip1, ip2)) - # @test U' ≈ U′ - @test _isone(U * U′) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) - A″ = map(fusiontensor, fusiontrees(dst)) - for (i, Ai) in enumerate(A′) - Aj = sum(U[:, i] .* A″) - @test Ai ≈ Aj - end + BraidingStyle(I) isa SymmetricBraiding && @testset "Double fusion tree $Istr: permutation" begin + for n in 0:(2N) + p = (randperm(2 * N)...,) + p1, p2 = p[1:n], p[(n + 1):(2N)] + ip = invperm(p) + ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] + + dst, U = @constinferred TensorKit.permute(src, (p1, p2)) + # @test _isunitary(U) + dst′, U′ = @constinferred TensorKit.permute(dst, (ip1, ip2)) + # @test U' ≈ U′ + @test _isone(U * U′) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + Aj = sum(A″ .* U[:, i]) + @test Ai ≈ Aj + end end end end @@ -572,8 +542,8 @@ using .TestSetup d1 = zip((dst,), (U,)) else f1, f1 = first(fusiontrees(src)) - dst, U = transpose(src, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - @show size(U) + src′ = FusionTreeBlock{I}((f1.uncoupled, f1.uncoupled), (f1.isdual, f1.isdual)) + dst, U = transpose(src′, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) d1 = zip(fusiontrees(dst), U[:, 1]) end @@ -581,8 +551,7 @@ using .TestSetup T = sectorscalartype(I) d2 = Dict{typeof((f1front, f1front)), T}() for ((f1′, f2′), coeff′) in d1 - for ((f1′′, f2′′), coeff′′) in - TK.planar_trace( + for ((f1′′, f2′′), coeff′′) in TK.planar_trace( (f1′, f2′), ((2:N...,), (1, ((2N):-1:(N + 3))...)), ((N + 1,), (N + 2,)) ) coeff = coeff′ * coeff′′