Skip to content
Open
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
65ea338
FusionTreePair
lkdvos Jul 30, 2025
ba97ca1
implement "vectorized" fusiontree manipulations
lkdvos Jul 31, 2025
74b323a
Refactor treetransformer to make use of vectorized implementation
lkdvos Jul 31, 2025
610e195
fix arg order `braid`
lkdvos Jul 31, 2025
42fa59b
refactor in terms of FusionTreeBlock
lkdvos Jul 31, 2025
7cd9fbc
Fix unbound type parameter
lkdvos Jul 31, 2025
59f8188
refactor repartition to unroll loop
lkdvos Jul 31, 2025
260688b
dont depend on intricate scoping rules
lkdvos Aug 13, 2025
cb8ab0d
Refactor bendright to avoid extra dictionary
lkdvos Aug 13, 2025
1ae2608
Refactor bendleft to avoid extra dictionaries
lkdvos Aug 13, 2025
6edf83c
Refactor foldright to avoid extra dictionaries
lkdvos Aug 13, 2025
10c04ae
Refactor foldleft to avoid extra dictionaries
lkdvos Aug 13, 2025
2cfe587
remove unused variable
lkdvos Aug 13, 2025
cf52331
some docs fixes
lkdvos Aug 14, 2025
47cbe11
Avoid using `one(I)`
lkdvos Aug 14, 2025
83d9a2a
format
lkdvos Aug 14, 2025
ecdaa94
Move independent computations out of loop
lkdvos Aug 15, 2025
fdcaddc
add utility fusiontreetype
lkdvos Aug 15, 2025
fb8d815
add multithreaded treetransformer implementation
lkdvos Aug 15, 2025
f303050
refactor treeindex_map
lkdvos Aug 15, 2025
77e484c
Refactor artin_braid to avoid extra dicts
lkdvos Aug 16, 2025
cea4e1c
type stability improvements
lkdvos Aug 16, 2025
a77dde5
fix multithreaded implementation
lkdvos Aug 17, 2025
f70405c
speed up hashing by hashing less things
lkdvos Aug 17, 2025
6210d04
Slight refactor of artin_braid
lkdvos Aug 17, 2025
bf9e6d9
reduce allocations with sizehints
lkdvos Aug 17, 2025
f5292d1
separate treemanipulation threads
lkdvos Aug 17, 2025
bc48a05
update `leftunit` and `rightunit`
lkdvos Nov 12, 2025
68cef16
add missing `treeindex_data`
lkdvos Nov 12, 2025
c3ded84
update `frobeniusschur` to `frobenius_schur_phase`
lkdvos Nov 12, 2025
02c0a05
remove stray file
lkdvos Nov 14, 2025
9dc27c4
some QOL additions
lkdvos Nov 16, 2025
ed00524
some reorganization
lkdvos Nov 16, 2025
364a1d7
rework indexmanipulations to use fusionblocks
lkdvos Nov 18, 2025
37622f7
update implementation to remove code duplication
lkdvos Nov 19, 2025
d5301ee
update fusiontensor overload
lkdvos Nov 19, 2025
c31bc93
temporary trace fix
lkdvos Nov 19, 2025
8f35e73
start tackling tests
lkdvos Nov 19, 2025
dd19c4d
Merge branch 'main' into ld-outer
lkdvos Dec 19, 2025
23c3994
attempts at cleanup
lkdvos Dec 21, 2025
c2ea490
fix argument order
lkdvos Jan 3, 2026
60348dd
handle empty case
lkdvos Jan 3, 2026
7866948
correctly import times
lkdvos Jan 3, 2026
5ac0d42
reorganize tests
lkdvos Jan 3, 2026
00c4a81
account for complex frobenius schur factors
lkdvos Jan 3, 2026
691f8e7
Merge branch 'main' into ld-outer
lkdvos Jan 3, 2026
b7dc600
resolve ambiguities
lkdvos Jan 3, 2026
10b2777
fix braidingtensor subblock
lkdvos Jan 5, 2026
411a423
fix braidingtensor block
lkdvos Jan 5, 2026
72d0df1
refactor braidingtensor access
lkdvos Jan 5, 2026
da8e939
fix planarcontract for braidingtensor
lkdvos Jan 5, 2026
58bedb6
code suggestions
lkdvos Jan 5, 2026
5f71cf1
docs fixes
lkdvos Jan 5, 2026
b44a9c5
Merge branch 'ld-outer' of https://github.com/QuantumKitHub/TensorKit…
lkdvos Jan 5, 2026
c518988
more docs fixes
lkdvos Jan 5, 2026
db772b0
(0, 0) FusionTreeBlock
lkdvos Jan 5, 2026
dff1193
correct for multifusion
lkdvos Jan 5, 2026
720580a
Merge branch 'main' into ld-outer
lkdvos Jan 5, 2026
cf504cf
fix missing innerlines
lkdvos Jan 6, 2026
f07586f
fix foldright for UniqueFusion with non-trivial Fsymbols
lkdvos Jan 6, 2026
ea5d346
Merge branch 'main' into ld-outer
lkdvos Jan 6, 2026
90751dd
actually use BLAS
lkdvos Jan 6, 2026
22e6fdc
isunit
lkdvos Jan 6, 2026
3126378
Apply suggestions from code review
lkdvos Jan 8, 2026
aa1f859
special case braiding with unit
lkdvos Jan 8, 2026
b48d2f8
Merge branch 'main' into ld-outer
lkdvos Jan 8, 2026
b5f0dd3
correctly handle stridedviews with `conj` flag
lkdvos Jan 8, 2026
67010a7
careful with reshapes and adjoints
lkdvos Jan 9, 2026
bd0dfe8
reorganise and reimplement basic manipulations
Jutho Jan 19, 2026
cea0848
small fix and comments
Jutho Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Documenter
using Random
using TensorKit
using TensorKit: FusionTreePair, Index2Tuple
using TensorKit.TensorKitSectors
using TensorKit.MatrixAlgebraKit
using DocumenterInterLinks
Expand Down
6 changes: 3 additions & 3 deletions docs/src/man/sectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
17 changes: 15 additions & 2 deletions src/TensorKit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ using OhMyThreads
using ScopedValues

using TensorKitSectors
import TensorKitSectors: dim, BraidingStyle, FusionStyle, ⊠, ⊗
import TensorKitSectors: dual, type_repr
import TensorKitSectors: dim, BraidingStyle, FusionStyle, ⊠, ⊗, ×
import TensorKitSectors: dual, type_repr, fusiontensor
import TensorKitSectors: twist

using Base: @boundscheck, @propagate_inbounds, @constprop,
Expand Down Expand Up @@ -216,6 +216,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
Expand Down
14 changes: 14 additions & 0 deletions src/auxiliary/auxiliary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
273 changes: 273 additions & 0 deletions src/fusiontrees/basic_manipulations.jl
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f, coeff = first(insertat(f₀, 1, f₁)) # takes fast path, single output
f, coeff = only(insertat(f₀, 1, f₁)) # takes fast path, single output

@assert coeff == one(coeff)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we need to keep this.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flip really needs some comments/doc string to remember the logic. I 'll try to come up with something after rereading the PR that introduced it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I don't think this is a very basic manipulation, as it involves both duality and braiding (twists), so not sure it is really in its place here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I am going to leave this as a TODO and think about it later. I forgot what the original constraints and design considerations were for flip.

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] ? conj(χₐ) * 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
Loading
Loading