From 3dd5ff2d74a24ffc84576f4080f8f7eee4d72e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 4 Dec 2024 12:48:09 +0100 Subject: [PATCH 01/52] split project --- Project.toml | 30 ++ src/FusionTensors.jl | 58 +- src/fusion_trees/clebsch_gordan_tensors.jl | 109 ++++ src/fusion_trees/fusion_tree_tensors.jl | 358 +++++++++++++ src/fusion_trees/fusiontree.jl | 192 +++++++ src/fusiontensor/array_cast.jl | 530 +++++++++++++++++++ src/fusiontensor/base_interface.jl | 119 +++++ src/fusiontensor/fusedaxes.jl | 101 ++++ src/fusiontensor/fusiontensor.jl | 168 ++++++ src/fusiontensor/linear_algebra_interface.jl | 72 +++ src/fusiontensor/tensor_algebra_interface.jl | 42 ++ src/permutedims/permutedims.jl | 262 +++++++++ src/permutedims/unitaries.jl | 290 ++++++++++ test/runtests.jl | 10 +- test/test_array_cast.jl | 399 ++++++++++++++ test/test_basics.jl | 233 +++++++- test/test_contraction.jl | 67 +++ test/test_fusion_trees.jl | 100 ++++ test/test_linear_algebra.jl | 37 ++ test/test_permutedims.jl | 158 ++++++ test/test_unitaries.jl | 54 ++ 21 files changed, 3380 insertions(+), 9 deletions(-) create mode 100644 src/fusion_trees/clebsch_gordan_tensors.jl create mode 100644 src/fusion_trees/fusion_tree_tensors.jl create mode 100644 src/fusion_trees/fusiontree.jl create mode 100644 src/fusiontensor/array_cast.jl create mode 100644 src/fusiontensor/base_interface.jl create mode 100644 src/fusiontensor/fusedaxes.jl create mode 100644 src/fusiontensor/fusiontensor.jl create mode 100644 src/fusiontensor/linear_algebra_interface.jl create mode 100644 src/fusiontensor/tensor_algebra_interface.jl create mode 100644 src/permutedims/permutedims.jl create mode 100644 src/permutedims/unitaries.jl create mode 100644 test/test_array_cast.jl create mode 100644 test/test_contraction.jl create mode 100644 test/test_fusion_trees.jl create mode 100644 test/test_linear_algebra.jl create mode 100644 test/test_permutedims.jl create mode 100644 test/test_unitaries.jl diff --git a/Project.toml b/Project.toml index 3de3ed6..cb9f147 100644 --- a/Project.toml +++ b/Project.toml @@ -3,9 +3,39 @@ uuid = "e16ca583-1f51-4df0-8e12-57d32947d33e" authors = ["ITensor developers and contributors"] version = "0.1.0" +[deps] +BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" +BlockSparseArrays = "2c9a651f-6452-4ace-a6ac-809f4280fbb4" +BroadcastMapConversion = "4a4adec5-520f-4750-bb37-d5e66b4ddeb2" +GradedUnitRanges = "e2de450a-8a67-46c7-b59c-01d5a3d041c5" +HalfIntegers = "f0d1745a-41c9-11e9-1dd9-e5d34d218721" +LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" +LabelledNumbers = "f856a3a6-4152-4ec4-b2a7-02c1a55d7993" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +NestedPermutedDimsArrays = "2c2a8ec4-3cfc-4276-aa3e-1307b4294e58" +SparseArraysBase = "0d5efcca-f356-4864-8770-e1ed8d78f208" +SymmetrySectors = "f8a8ad64-adbc-4fce-92f7-ffe2bb36a86e" +TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" +TypeParameterAccessors = "7e5a90cf-f82e-492e-a09b-e3e26432c138" +WignerSymbols = "9f57e263-0b3d-5e2e-b1be-24f2bb48858b" + [compat] Aqua = "0.8.9" +BlockArrays = "1.2.0" +BlockSparseArrays = "0.1.0" +BroadcastMapConversion = "0.1.0" +GradedUnitRanges = "0.1.0" +HalfIntegers = "1.6.0" +LRUCache = "1.6.1" +LabelledNumbers = "0.1.0" +LinearAlgebra = "1.11.0" +NestedPermutedDimsArrays = "0.1.0" +SparseArraysBase = "0.1.0" +SymmetrySectors = "0.1.0" +TensorAlgebra = "0.1.0" Test = "1.10" +TypeParameterAccessors = "0.1.0" +WignerSymbols = "2.0.0" julia = "1.10" [extras] diff --git a/src/FusionTensors.jl b/src/FusionTensors.jl index 336ea97..7ce6085 100644 --- a/src/FusionTensors.jl +++ b/src/FusionTensors.jl @@ -1,5 +1,61 @@ module FusionTensors -# Write your package code here. +using LinearAlgebra: LinearAlgebra, Adjoint, norm, tr +using BlockArrays: + AbstractBlockArray, + AbstractBlockMatrix, + Block, + BlockArray, + BlockedArray, + BlockMatrix, + blockedrange, + blocklength, + blocklengths, + blocks +using LRUCache: LRU + +using BlockSparseArrays: + AbstractBlockSparseMatrix, + BlockSparseArrays, + BlockSparseArray, + BlockSparseMatrix, + stored_indices, + view! +using GradedUnitRanges: + GradedUnitRanges, + AbstractGradedUnitRange, + blocklabels, + blockmergesort, + dual, + fusion_product, + gradedrange, + isdual, + labelled_blocks, + sector_type, + space_isequal, + unlabel_blocks +using SymmetrySectors: + AbstractSector, TrivialSector, block_dimensions, istrivial, quantum_dimension, trivial +using TensorAlgebra: + TensorAlgebra, + Algorithm, + BlockedPermutation, + blockedperm, + blockpermute, + contract, + contract! + +include("fusion_trees/fusiontree.jl") +include("fusion_trees/clebsch_gordan_tensors.jl") +include("fusion_trees/fusion_tree_tensors.jl") + +include("fusiontensor/fusedaxes.jl") +include("fusiontensor/fusiontensor.jl") +include("fusiontensor/base_interface.jl") +include("fusiontensor/array_cast.jl") +include("fusiontensor/linear_algebra_interface.jl") +include("fusiontensor/tensor_algebra_interface.jl") +include("permutedims/unitaries.jl") +include("permutedims/permutedims.jl") end diff --git a/src/fusion_trees/clebsch_gordan_tensors.jl b/src/fusion_trees/clebsch_gordan_tensors.jl new file mode 100644 index 0000000..3c757b2 --- /dev/null +++ b/src/fusion_trees/clebsch_gordan_tensors.jl @@ -0,0 +1,109 @@ +# This file defines Clebsch-Gordan tensors +# one tensor is defined from 3 simple objects s1, s2 and s3 +# and contains the coefficients fusing s1 ⊗ s2 -> s3 + +using HalfIntegers: half +using WignerSymbols: clebschgordan + +using SymmetrySectors: ⊗, AbstractSector, O2, SU, sector_label, zero_odd + +function symbol_1j(s::AbstractSector) + cgt = clebsch_gordan_tensor(s, dual(s), trivial(s), 1) + return sqrt(quantum_dimension(s)) * cgt[:, :, 1] +end + +function clebsch_gordan_tensor( + s1::AbstractSector, + s2::AbstractSector, + s3::AbstractSector, + arrow1::Bool, + arrow2::Bool, + inner_mult_index::Int, +) + cgt = clebsch_gordan_tensor(s1, s2, s3, inner_mult_index) + if arrow1 + flip1 = symbol_1j(s1) + cgt = TensorAlgebra.contract((1, 2, 3), flip1, (4, 1), cgt, (4, 2, 3)) + end + if arrow2 + flip2 = symbol_1j(s2) + cgt = TensorAlgebra.contract((1, 2, 3), flip2, (4, 2), cgt, (1, 4, 3)) + end + return cgt +end + +function clebsch_gordan_tensor(s1::O2, s2::O2, s3::O2, ::Int) + return clebsch_gordan_tensor(s1, s2, s3) # no inner multiplicity +end + +function clebsch_gordan_tensor(s1::O2, s2::O2, s3::O2) + d1 = quantum_dimension(s1) + d2 = quantum_dimension(s2) + d3 = quantum_dimension(s3) + cgt = zeros((d1, d2, d3)) + s3 ∉ blocklabels(fusion_product(s1, s2)) && return cgt + + # adapted from TensorKit + l1 = sector_label(s1) + l2 = sector_label(s2) + l3 = sector_label(s3) + if l3 <= 0 # 0even or 0odd + if l1 <= 0 && l2 <= 0 + cgt[1, 1, 1, 1] = 1.0 + else + if istrivial(s3) + cgt[1, 2, 1, 1] = 1.0 / sqrt(2) + cgt[2, 1, 1, 1] = 1.0 / sqrt(2) + else + cgt[1, 2, 1, 1] = 1.0 / sqrt(2) + cgt[2, 1, 1, 1] = -1.0 / sqrt(2) + end + end + elseif l1 <= 0 # 0even or 0odd + cgt[1, 1, 1, 1] = 1.0 + cgt[1, 2, 2, 1] = s1 == zero_odd(O2) ? -1.0 : 1.0 + elseif l2 == 0 + cgt[1, 1, 1, 1] = 1.0 + cgt[2, 1, 2, 1] = s2 == zero_odd(O2) ? -1.0 : 1.0 + elseif l3 == l1 + l2 + cgt[1, 1, 1, 1] = 1.0 + cgt[2, 2, 2, 1] = 1.0 + elseif l3 == l1 - l2 + cgt[1, 2, 1, 1] = 1.0 + cgt[2, 1, 2, 1] = 1.0 + elseif l3 == l2 - l1 + cgt[2, 1, 1, 1] = 1.0 + cgt[1, 2, 2, 1] = 1.0 + end + return cgt +end + +function clebsch_gordan_tensor(s1::SU{2}, s2::SU{2}, s3::SU{2}, ::Int) + return clebsch_gordan_tensor(s1, s2, s3) # no inner multiplicity +end + +function clebsch_gordan_tensor(s1::SU{2}, s2::SU{2}, s3::SU{2}) + d1 = quantum_dimension(s1) + d2 = quantum_dimension(s2) + d3 = quantum_dimension(s3) + j1 = half(d1 - 1) + j2 = half(d2 - 1) + j3 = half(d3 - 1) + cgtensor = Array{Float64,3}(undef, (d1, d2, d3)) + for (i, j, k) in Iterators.product(1:d1, 1:d2, 1:d3) + m1 = j1 - i + 1 + m2 = j2 - j + 1 + m3 = j3 - k + 1 + cgtensor[i, j, k] = clebschgordan(j1, m1, j2, m2, j3, m3) + end + return cgtensor +end + +function clebsch_gordan_tensor(s1::SU{3}, s2::SU{3}, s3::SU{3}, inner_mult_index::Int) + d1 = quantum_dimension(s1) + d2 = quantum_dimension(s2) + d3 = quantum_dimension(s3) + cgtensor = zeros(d1, d2, d3) + # dummy + return cgtensor +end diff --git a/src/fusion_trees/fusion_tree_tensors.jl b/src/fusion_trees/fusion_tree_tensors.jl new file mode 100644 index 0000000..ed4e800 --- /dev/null +++ b/src/fusion_trees/fusion_tree_tensors.jl @@ -0,0 +1,358 @@ +# This file defines fusion trees for any abelian or non-abelian group + +# TBD +# compatibility with TensorKit conventions? + +# +# A fusion tree tensor is a N+1 legs Array that corresponds to the projector defined by the +# associated fusion tree. +# +# dim_sec struct_mult_sec +# \ / +# \/ +# / +# / +# /\ +# / \ +# /\ \ +# / \ \ +# dim1 dim2 dim3 +# +# +# It is convenient to "merge the tree leaves" by merging together the dimension legs to +# yield a 3-dim tensor with size (dim1*dim2*...*dimN, dim_sec, struct_mult_sec) +# +# --------------------------- +# | | | +# dim1*dim2*dim3 dim_sec struct_mult_sec +# +# +# convention: the trees are not normalized, i.e they do not define a projector on a given +# sector but carry a scaling factor sqrt(dim_sec) +# +# convention: irreps are already dualed if needed, arrows do not affect them. They only +# affect the basis on which the tree projects for self-dual irreps. +# +# +# The interface uses AbstractGradedUnitRanges as input for interface simplicity +# however only blocklabels are used and blocklengths are never read. + +using SymmetrySectors: + ⊗, AbelianStyle, AbstractSector, NotAbelianStyle, SectorProduct, SymmetryStyle, arguments + +# =================================== Utility tools ====================================== +function braid_tuples(t1::Tuple{Vararg{<:Any,N}}, t2::Tuple{Vararg{<:Any,N}}) where {N} + t12 = (t1, t2) + nested = ntuple(i -> getindex.(t12, i), N) + return TensorAlgebra.flatten_tuples(nested) +end + +# compute Kronecker product of fusion trees +# more efficient with recursive construction +trees_kron(a, b, c...) = trees_kron(trees_kron(a, b), c...) + +function trees_kron(a, b) + return map( + ((t1, t2),) -> _tensor_kron(t1, t2), Iterators.flatten((Iterators.product(a, b),),) + ) +end + +# LinearAlgebra.kron does not allow input for ndims>2 +function _tensor_kron(a::AbstractArray{<:Any,N}, b::AbstractArray{<:Any,N}) where {N} + t1 = ntuple(_ -> 1, N) + sha = braid_tuples(size(a), t1) + shb = braid_tuples(t1, size(b)) + c = reshape(a, sha) .* reshape(b, shb) + return reshape(c, size(a) .* size(b)) +end + +# ================================ High level interface ================================== +function merge_tree_leaves(a::AbstractArray) + shape_3leg = (prod(size(a)[begin:(end - 2)]), size(a, ndims(a) - 1), size(a, ndims(a))) + return reshape(a, shape_3leg) +end + +function unmerge_tree_leaves( + tree::AbstractArray{<:Real,3}, irreps::NTuple{<:Any,<:AbstractSector} +) + irreps_shape = quantum_dimension.(irreps) + return unmerge_tree_leaves(tree, irreps_shape) +end + +function unmerge_tree_leaves(tree::AbstractArray{<:Real,3}, irreps_shape::NTuple{<:Any,Int}) + new_shape = (irreps_shape..., size(tree, 2), size(tree, 3)) + return reshape(tree, new_shape) +end + +function get_fusion_tree_tensors!( + cache::Dict{NTuple{N,Int},<:Vector{A}}, + it::NTuple{N,Int}, + legs::NTuple{N,AbstractGradedUnitRange}, + allowed_sectors::Vector{<:AbstractSector}, +) where {N,A<:Array{<:Real}} + get!(cache, it) do + tree_arrows = isdual.(legs) + irreps = getindex.(blocklabels.(legs), it) + return compute_pruned_fusion_tree_tensors(A, irreps, tree_arrows, allowed_sectors) + end +end + +# explicitly cast trees to 3 leg format +function compute_pruned_fusion_tree_tensors( + ::Type{<:Array{<:Real,3}}, + irreps::NTuple{N,<:AbstractSector}, + tree_arrows::NTuple{N,Bool}, + target_sectors::Vector{<:AbstractSector}, +) where {N} + return merge_tree_leaves.( + compute_pruned_fusion_tree_tensors(Any, irreps, tree_arrows, target_sectors) + ) +end + +function compute_pruned_fusion_tree_tensors( + ::Type, + irreps::NTuple{N,<:AbstractSector}, + tree_arrows::NTuple{N,Bool}, + target_sectors::Vector{<:AbstractSector}, +) where {N} + + # it is possible to prune trees during the construction process and to avoid constructing + # trees that will never fuse to target_sectors + # currently this is not implemented and no pruning is done inside fusion_trees + tree_irreps_pairs = fusion_tree_tensors(irreps, tree_arrows) + tree_irreps = first.(tree_irreps_pairs) + trees = last.(tree_irreps_pairs) + + # pruning is only done here by discarding irreps that are not in target_sectors + # also insert dummy trees in sectors that did not appear in the fusion product of irreps + irreps_dims = quantum_dimension.(irreps) + trees_sector = [ # fill with dummy + zeros((irreps_dims..., quantum_dimension(sec), 0)) for sec in target_sectors + ] + + # set trees at their correct position + for (i, s) in enumerate(target_sectors) + j = findfirst(==(s), tree_irreps) + if !isnothing(j) + trees_sector[i] = trees[j] + end + end + return trees_sector +end + +# ================================ Low level interface =================================== +function fusion_tree_tensors(::Tuple{}, ::Tuple{}) + return [TrivialSector() => ones((1, 1))] +end + +function fusion_tree_tensors( + irreps::NTuple{N,<:SectorProduct}, tree_arrows::NTuple{N,Bool} +) where {N} + # construct fusion_tree(SectorProduct) as kron(fusion_trees(inner_sectors)) + + argument_irreps = arguments.(irreps) + n_args = length(first(argument_irreps)) + + # construct fusion tree for each sector + transposed_args = ntuple(s -> getindex.(argument_irreps, s), n_args) + sector_trees_irreps = map(a -> fusion_tree_tensors(a, tree_arrows), transposed_args) + + # reconstruct sector for each product tree + T = eltype(argument_irreps) + fused_arguments = broadcast.(first, sector_trees_irreps) + tree_irreps = map( + SectorProduct ∘ T, Iterators.flatten((Iterators.product(fused_arguments...),)) + ) + + # compute Kronecker product of fusion trees + trees = trees_kron(broadcast.(last, sector_trees_irreps)...) + + # sort irreps. Each sector is sorted, permutation is obtained by reversing loop order + perm = sortperm(tree_irreps) + permute!(tree_irreps, perm) + permute!(trees, perm) + return tree_irreps .=> trees +end + +function fusion_tree_tensors( + irreps::NTuple{N,<:AbstractSector}, tree_arrows::NTuple{N,Bool} +) where {N} + return fusion_tree_tensors(SymmetryStyle(first(irreps)), irreps, tree_arrows) +end + +# ===================================== Internals ======================================== + +# fusion tree for an Abelian group is trivial +# it does not depend on arrow directions +function fusion_tree_tensors(::AbelianStyle, irreps::Tuple, ::Tuple) + irrep_prod = reduce(⊗, irreps) + return [irrep_prod => ones(ntuple(_ -> 1, length(irreps) + 2))] +end + +function build_children_trees( + parent_tree::Matrix, + parent_irrep::AbstractSector, + level_irrep::AbstractSector, + level_arrow::Bool, + inner_multiplicity::Integer, + sec::AbstractSector, +) + sector_trees = typeof(parent_tree)[] + for inner_mult_index in 1:inner_multiplicity + cgt_inner_mult = clebsch_gordan_tensor( + parent_irrep, level_irrep, sec, false, level_arrow, inner_mult_index + ) + dim_parent_irrep, dim_level_irrep, dim_sec = size(cgt_inner_mult) + tree = + parent_tree * reshape(cgt_inner_mult, (dim_parent_irrep, dim_level_irrep * dim_sec)) + child_tree = reshape(tree, (size(parent_tree, 1) * dim_level_irrep, dim_sec)) + push!(sector_trees, child_tree) + end + return sector_trees +end + +function build_children_trees( + parent_tree::Matrix, + parent_irrep::AbstractSector, + level_irrep::AbstractSector, + level_arrow::Bool, +) + children_trees = typeof(parent_tree)[] + children_irreps = typeof(parent_irrep)[] + rep = fusion_product(parent_irrep, level_irrep) + for (inner_multiplicity, sec) in zip(blocklengths(rep), blocklabels(rep)) + sector_trees = build_children_trees( + parent_tree, parent_irrep, level_irrep, level_arrow, inner_multiplicity, sec + ) + append!(children_trees, sector_trees) + append!(children_irreps, repeat([sec], inner_multiplicity)) + end + return children_trees, children_irreps +end + +function build_next_level_trees( + parent_trees::Vector, + parent_trees_irreps::Vector, + level_irrep::AbstractSector, + level_arrow::Bool, +) + next_level_trees = empty(parent_trees) + next_level_irreps = empty(parent_trees_irreps) + for (parent_tree, parent_irrep) in zip(parent_trees, parent_trees_irreps) + children_trees, children_irreps = build_children_trees( + parent_tree, parent_irrep, level_irrep, level_arrow + ) + append!(next_level_trees, children_trees) + append!(next_level_irreps, children_irreps) + end + return next_level_trees, next_level_irreps +end + +function build_trees(trees::Vector, tree_irreps::Vector, irreps::Tuple, tree_arrows::Tuple) + next_level_trees, next_level_irreps = build_next_level_trees( + trees, tree_irreps, first(irreps), first(tree_arrows) + ) + return build_trees(next_level_trees, next_level_irreps, irreps[2:end], tree_arrows[2:end]) +end + +function build_trees(trees::Vector, tree_irreps::Vector, ::Tuple{}, ::Tuple{}) + return trees, tree_irreps +end + +function compute_thin_trees(irreps::Tuple, tree_arrows::Tuple) + # init from trivial, NOT from first(irreps) to get first arrow correct + thin_trees = [ones((1, 1))] + tree_irreps = [trivial(first(irreps))] + return build_trees(thin_trees, tree_irreps, irreps, tree_arrows) +end + +function cat_thin_trees( + thin_trees::Vector, uncat_tree_irreps::Vector, sector_irrep::AbstractSector +) + indices_irrep = findall(==(sector_irrep), uncat_tree_irreps) + thin_trees_irrep = getindex.(Ref(thin_trees), indices_irrep) + thick_shape = (size(first(thin_trees_irrep))..., length(indices_irrep)) + return reshape(reduce(hcat, thin_trees_irrep), thick_shape) +end + +function cat_thin_trees(thin_trees::Vector, uncat_tree_irreps::Vector) + # cat trees fusing on the same irrep + tree_irreps = sort(unique(uncat_tree_irreps)) + thick_trees = map( + irrep -> cat_thin_trees(thin_trees, uncat_tree_irreps, irrep), tree_irreps + ) + return thick_trees, tree_irreps +end + +# arrow direction is needed to define non-trivial CG tensor +function fusion_tree_tensors(::NotAbelianStyle, irreps::Tuple, tree_arrows::Tuple) + # compute "thin" trees: 1 tree = fuses on ONE irrep + thin_trees, uncat_tree_irreps = compute_thin_trees(irreps, tree_arrows) + + # cat thin trees into "thick" trees + thick_mergedleaves_trees, tree_irreps = cat_thin_trees(thin_trees, uncat_tree_irreps) + + irrep_dims = quantum_dimension.(irreps) + thick_trees = map(tree -> unmerge_tree_leaves(tree, irrep_dims), thick_mergedleaves_trees) + return tree_irreps .=> thick_trees +end + +Base.convert(T::Type{<:Array}, f::FusionTree) = convert(T, to_tensor(f)) + +to_tensor(::FusionTree{<:Any,0}) = ones(1) + +function to_tensor(f::FusionTree) + # init with dummy trivial leg to get arrow correct and deal with size-1 case + cgt1 = clebsch_gordan_tensor( + trivial(eltype(f)), + first(leaves(f)), + first(branch_sectors(f)), + false, + first(arrows(f)), + 1, + ) + return grow_tensor_tree(cgt1[1, :, :], f) +end + +function contract_clebsch_gordan( + tree_tensor::AbstractArray{<:Real,N}, cgt::AbstractArray{<:Real,3} +) where {N} + return contract( + (ntuple(identity, N - 1)..., N + 1, N + 2), + tree_tensor, + ntuple(identity, N), + cgt, + (N, N + 1, N + 2), + ) +end + +function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,1}, ::FusionTree{<:Any,1}) + return tree_tensor +end + +function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,N}, f::FusionTree) where {N} + cgt = clebsch_gordan_tensor( + branch_sectors(f)[N - 1], + leaves(f)[N], + branch_sectors(f)[N], + false, + arrows(f)[N], + outer_multiplicty_indices(f)[N - 1], + ) + next_level_tree = contract_clebsch_gordan(tree_tensor, cgt) + return grow_tensor_tree(next_level_tree, f) +end + +function grow_tensor_tree( + tree_tensor::AbstractArray{<:Real,N}, f::FusionTree{<:Any,N} +) where {N} + cgt = clebsch_gordan_tensor( + last(branch_sectors(f)), + last(leaves(f)), + root_sector(f), + false, + last(arrows(f)), + last(outer_multiplicty_indices(f)), + ) + return contract_clebsch_gordan(tree_tensor, cgt) +end diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl new file mode 100644 index 0000000..a734169 --- /dev/null +++ b/src/fusion_trees/fusiontree.jl @@ -0,0 +1,192 @@ +# This file defines fusion trees for any abelian or non-abelian group + +# TBD +# compatibility with TensorKit conventions? + +# +# A fusion tree fuses N sectors sec1, secN onto one sector fused_sec. A given set of +# sectors and arrow directions (as defined by a given outer block) contains several fusion +# trees that typically fuse to several sectors (in the abelian group case, there is only one) +# irrep in the fusion ring and each of them corresponds to a single "thin" fusion tree with +# +# +# +# / +# sec123 +# /\ +# / \ +# sec12 \ +# /\ \ +# / \ \ +# sec1 sec2 sec3 +# +# +# +# +# convention: irreps are already dualed if needed, arrows do not affect them. They only +# affect the basis on which the tree projects for self-dual irreps. +# +# +# The interface uses AbstractGradedUnitRanges as input for interface simplicity +# however only blocklabels are used and blocklengths are never read. + +using SymmetrySectors: SymmetrySectors + +struct FusionTree{S,N,M} + leaves::NTuple{N,S} # TBD rename outer_sectors or leave_sectors? + arrows::NTuple{N,Bool} + root_sector::S + branch_sectors::NTuple{M,S} # M = N-1 + outer_multiplicty_indices::NTuple{M,Int} # M = N-1 + + # TBD could have branch_sectors with length N-2 + # currently first(branch_sectors) == first(leaves) + # redundant but allows for simpler, generic grow_tree code + + function FusionTree( + leaves, arrows, root_sector, branch_sectors, outer_multiplicty_indices + ) + N = length(leaves) + @assert length(branch_sectors) == max(0, N - 1) + @assert length(outer_multiplicty_indices) == max(0, N - 1) + return new{typeof(root_sector),length(leaves),length(branch_sectors)}( + leaves, arrows, root_sector, branch_sectors, outer_multiplicty_indices + ) + end +end + +# getters +arrows(f::FusionTree) = f.arrows +leaves(f::FusionTree) = f.leaves +root_sector(f::FusionTree) = f.root_sector +branch_sectors(f::FusionTree) = f.branch_sectors +outer_multiplicty_indices(f::FusionTree) = f.outer_multiplicty_indices + +# interface +Base.length(::FusionTree{<:Any,N}) where {N} = N +Base.isless(f1::FusionTree, f2::FusionTree) = isless(to_tuple(f1), to_tuple(f2)) + +function to_tuple(f::FusionTree) # TBD defined as Base.Tuple(::FusionTree)? + return ( + leaves(f)..., + arrows(f)..., + root_sector(f), + branch_sectors(f)..., + outer_multiplicty_indices(f)..., + ) +end + +GradedUnitRanges.sector_type(::FusionTree{S}) where {S} = S # TBD use different function name? +Base.eltype(::FusionTree{S}) where {S} = S + +function build_trees(legs::Vararg{AbstractGradedUnitRange{LA}}) where {LA} + # TBD when to impose LA to be the same for every leg? + tree_arrows = isdual.(legs) + sectors = blocklabels.(legs) + return mapreduce(vcat, CartesianIndices(blocklength.(legs))) do it + block_sectors = getindex.(sectors, Tuple(it)) # why not type stable? + return build_trees(block_sectors, tree_arrows) + end +end + +function SymmetrySectors.:×(f1::FusionTree, f2::FusionTree) + @assert arrows(f1) == arrows(f2) + product_leaves = .×(leaves(f1), leaves(f2)) + product_root_sector = root_sector(f1) × root_sector(f2) + product_branch_sectors = .×(branch_sectors(f1), branch_sectors(f2)) + product_outer_multiplicty_indices = + outer_multiplicity_kron.( + Base.tail(leaves(f1)), + branch_sectors(f1), + (Base.tail(branch_sectors(f1))..., root_sector(f1)), + outer_multiplicty_indices(f1), + outer_multiplicty_indices(f2), + ) + return FusionTree( + product_leaves, + arrows(f1), + product_root_sector, + product_branch_sectors, + product_outer_multiplicty_indices, + ) +end + +function outer_multiplicity_kron( + sec1, sec2, fused, outer_multiplicity1, outer_multiplicity2 +) + full_space = fusion_product(sec1, sec2) + nsymbol = blocklengths(full_space)[findfirst(==(fused), blocklabels(full_space))] + linear_inds = LinearIndices((nsymbol, outer_multiplicity2)) + return linear_inds[outer_multiplicity1, outer_multiplicity2] +end + +function outer_multiplicity_split(sec1, sec2, fused, outer_multiplicity) + args1 = SymmetrySectors.arguments(sec1) + args2 = SymmetrySectors.arguments(sec2) + args12 = SymmetrySectors.arguments(fused) + nsymbols = map(zip(args1, args2, args12)) do (sec1, sec2, sec12) + full_space = fusion_product(sec1, sec2) + return blocklengths(full_space)[findfirst(==(sec12), blocklabels(full_space))] + end + return CartesianIndices(nsymbols)[outer_multiplicity] +end + +# zero leg: need S to get sector type information +function FusionTree{S}() where {S<:AbstractSector} + return FusionTree((), (), trivial(S), (), ()) +end +function FusionTree{S}(::Tuple{}, ::Tuple{}) where {S<:AbstractSector} + return FusionTree((), (), trivial(S), (), ()) +end + +# one leg +FusionTree(sect::AbstractSector, arrow::Bool) = FusionTree((sect,), (arrow,), sect, (), ()) + +# ===================================== Internals ======================================== +function grow_tree( + parent_tree::FusionTree, + branch_sector::AbstractSector, + level_arrow::Bool, + child_root_sector, + outer_mult, +) + child_leaves = (leaves(parent_tree)..., branch_sector) + child_arrows = (arrows(parent_tree)..., level_arrow) + child_branch_sectors = (branch_sectors(parent_tree)..., root_sector(parent_tree)) + child_outer_mul = (outer_multiplicty_indices(parent_tree)..., outer_mult) + return FusionTree( + child_leaves, child_arrows, child_root_sector, child_branch_sectors, child_outer_mul + ) +end + +function grow_tree( + parent_tree::FusionTree, branch_sector::AbstractSector, level_arrow::Bool +) + new_space = fusion_product(root_sector(parent_tree), branch_sector) + return mapreduce(vcat, zip(blocklabels(new_space), blocklengths(new_space))) do (la, n) + return [ + grow_tree(parent_tree, branch_sector, level_arrow, la, outer_mult) for + outer_mult in 1:n + ] + end +end + +function build_trees(old_trees::Vector, sectors_to_fuse::Tuple, arrows_to_fuse::Tuple) + next_level_trees = mapreduce(vcat, old_trees) do tree + return grow_tree(tree, first(sectors_to_fuse), first(arrows_to_fuse)) + end + return build_trees( + next_level_trees, Base.tail(sectors_to_fuse), Base.tail(arrows_to_fuse) + ) +end + +function build_trees(trees::Vector, ::Tuple{}, ::Tuple{}) + return trees +end + +function build_trees( + sectors_to_fuse::NTuple{N,S}, arrows_to_fuse::NTuple{N,Bool} +) where {N,S<:AbstractSector} + trees = [FusionTree(first(sectors_to_fuse), first(arrows_to_fuse))] + return build_trees(trees, Base.tail(sectors_to_fuse), Base.tail(arrows_to_fuse)) +end diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl new file mode 100644 index 0000000..04f8957 --- /dev/null +++ b/src/fusiontensor/array_cast.jl @@ -0,0 +1,530 @@ +# This file defines interface to cast from and to generic array + +# ================================= High level interface ================================= + +#### cast from array to symmetric +function FusionTensor( + array::AbstractArray, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, +) + return cast_from_array(array, codomain_legs, domain_legs) +end + +#### cast from symmetric to array +function BlockSparseArrays.BlockSparseArray(ft::FusionTensor) + return cast_to_array(ft) +end + +# ================================= Low level interface ================================== +function cast_from_array( + array::AbstractArray, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, +) + bounds = block_dimensions.((codomain_legs..., domain_legs...)) + blockarray = BlockedArray(array, bounds...) + return cast_from_array(blockarray, codomain_legs, domain_legs) +end + +function cast_from_array( + blockarray::AbstractBlockArray, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, +) + # input validation + if length(codomain_legs) + length(domain_legs) != ndims(blockarray) # compile time + throw(codomainError("legs are incompatible with array ndims")) + end + if quantum_dimension.((codomain_legs..., domain_legs...)) != size(blockarray) + throw(codomainError("legs dimensions are incompatible with array")) + end + + ft = FusionTensor(eltype(blockarray), codomain_legs, domain_legs) + + # TODO cache FusedAxes into FusionTensor + codomain_fused_axes = FusedAxes{sector_type(ft)}(codomain_legs) + domain_fused_axes = FusedAxes{sector_type(ft)}(dual.(domain_legs)) + fill_matrix_blocks!(data_matrix(ft), blockarray, codomain_fused_axes, domain_fused_axes) + return ft +end + +function cast_to_array(ft::FusionTensor) + return cast_to_array(data_matrix(ft), codomain_axes(ft), domain_axes(ft)) +end + +function cast_to_array( + data_mat::AbstractMatrix, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, +) + bounds = block_dimensions.((codomain_legs..., domain_legs...)) + blockarray = BlockSparseArray{eltype(data_mat)}(blockedrange.(bounds)) + codomain_fused_axes = FusedAxes(codomain_legs) + domain_fused_axes = FusedAxes(dual.(domain_legs)) + fill_blockarray!(blockarray, data_mat, codomain_fused_axes, domain_fused_axes) + return blockarray +end + +# ===================================== Internals ======================================== + +#------------------------------------ utility tools --------------------------------------- +function split_axes(fa::FusedAxes) + legs = axes(fa) + degens = blocklengths.(legs) + dimensions = broadcast.(quantum_dimension, blocklabels.(legs)) + return legs, degens, dimensions +end + +function split_degen_dims( + array_block::AbstractArray, + codomain_block_degens::Tuple, + codomain_block_dims::Tuple, + domain_block_degens::Tuple, + domain_block_dims::Tuple, +) + array_block_split_shape = ( + braid_tuples(codomain_block_degens, codomain_block_dims)..., + braid_tuples(domain_block_degens, domain_block_dims)..., + ) + split_array_block = reshape(array_block, array_block_split_shape) + return split_array_block +end + +function merge_degen_dims(split_array_block::AbstractArray) + s0 = size(split_array_block) + array_shape = + ntuple(i -> s0[2 * i - 1], length(s0) ÷ 2) .* ntuple(i -> s0[2 * i], length(s0) ÷ 2) + array_block = reshape(split_array_block, array_shape) + return array_block +end + +function permute_split_array_block(split_array_block::AbstractArray) + N = ndims(split_array_block) ÷ 2 + array_data_perm = (ntuple(i -> 2 * i - 1, N)..., ntuple(i -> 2 * i, N)...) + permuted_split_array_block = permutedims(split_array_block, array_data_perm) + return permuted_split_array_block +end + +function unpermute_split_array_block(permuted_split_array_block::AbstractArray) + twoN = ndims(permuted_split_array_block) + N = twoN ÷ 2 + inverse_array_data_perm = ntuple(i -> fld1(i, 2) + (1 - i % 2) * N, twoN) + split_array_block = permutedims(permuted_split_array_block, inverse_array_data_perm) + return split_array_block +end + +function reshape_permuted_to_fused( + permuted_split_array_block::AbstractArray, ::Val{N_CO} +) where {N_CO} + N = ndims(permuted_split_array_block) ÷ 2 + permuted_array_shape = size(permuted_split_array_block) + fused_array_block_shape = ( + prod(permuted_array_shape[begin:N_CO]), + prod(permuted_array_shape[(N_CO + 1):N]), + prod(permuted_array_shape[(N + 1):(N + N_CO)]), + prod(permuted_array_shape[(N + N_CO + 1):end]), + ) + fused_array_block = reshape(permuted_split_array_block, fused_array_block_shape) + return fused_array_block +end + +function reshape_fused_to_permuted( + fused_array_block::AbstractArray, + codomain_block_degens::Tuple, + codomain_block_dims::Tuple, + domain_block_degens::Tuple, + domain_block_dims::Tuple, +) + degen_dim_shape = ( + codomain_block_degens..., + domain_block_degens..., + codomain_block_dims..., + domain_block_dims..., + ) + permuted_split_array_block = reshape(fused_array_block, degen_dim_shape) + return permuted_split_array_block +end + +function fuse_array_block( + array_block::AbstractArray, + codomain_block_degens::Tuple, + codomain_block_dims::Tuple, + domain_block_degens::Tuple, + domain_block_dims::Tuple, +) + # start from an array outer block with e.g. N=6 axes divided into N_DO=3 ndims_codomain + # and N_CO=3 ndims_domain. Each leg k can be decomposed as a product of external an + # multiplicity extk and a quantum dimension dimk + # + # ------------------------------array_block------------------------------- + # | | | | | | + # ext1*dim1 ext2*dim2 ext3*dim3 ext4*dim4 ext5*dim5 ext6*dim6 + # + + # each leg of this this array outer block can now be opened to form a 2N-dims tensor. + # note that this 2N-dims form is only defined at the level of the outer block, + # not for a larger block. + # + # ------------------------------split_array_block------------------------- + # | | | | | | + # / \ / \ / \ / \ / \ / \ + # / \ / \ / \ / \ / \ / \ + # ext1 dim1 ext2 dim2 ext3 dim3 ext4 dim4 ext5 dim5 ext6 dim6 + # + split_array_block = split_degen_dims( + array_block, + codomain_block_degens, + codomain_block_dims, + domain_block_degens, + domain_block_dims, + ) + + # Now we permute the axes to group together degenearacies on one side and irrep + # dimensions on the other side. This is the bottleneck. + # + # -------------------permuted_split_array_block----------------------------------- + # | | | | | | | | | | | | + # ext1 ext2 ext3 ext4 ext5 ext6 dim1 dim2 dim3 dim4 dim5 dim6 + # + permuted_split_array_block = permute_split_array_block(split_array_block) + + # Finally, it is convenient to merge together legs corresponding to codomain or + # to domain and produce a 4-dims tensor + # + # ---------------------fused_array_block-------------------- + # | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 + # + fused_array_block = reshape_permuted_to_fused( + permuted_split_array_block, Val(length(codomain_block_degens)) + ) + return fused_array_block +end + +function contract_fusion_trees( + fused_array_block::AbstractArray{<:Number,4}, + tree_codomain::AbstractArray{<:Real,3}, + tree_domain::AbstractArray{<:Real,3}, +) + # Input: + # + # ---------------------fused_array_block-------------------- + # | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 + # + # + # + # ---------------tree_codomain------------ + # | | | + # dim1*dim2*dim3 dim_sec struct_sec_codomain + # + # + # ----------------tree_domain----------- + # | | | + # dim4*dim5*dim6 dim_sec struct_sec_domain + # + # in this form, we can apply fusion trees on both the domain and the codomain. + # + + # contract domain tree + # -------------------------data_1tree--------------------------- + # | | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim_sec struct_sec_domain + # + data_1tree = contract( + (1, 2, 3, 5, 6), fused_array_block, (1, 2, 3, 4), tree_domain, (4, 5, 6) + ) + + # contract codomain tree + # -----------------------sym_data---------------------------- + # | | | | + # ext1*ext2*ext3 struct_sec_domain ext4*ext5*ext6 struct_sec_codomain + # + T = promote_type(eltype(fused_array_block), Float64) + dim_sec = size(tree_codomain, 2) + sym_data::Array{T,4} = contract( + (1, 7, 2, 6), # HERE WE SET INNER STRUCTURE FOR MATRIX BLOCKS + data_1tree, + (1, 2, 3, 5, 6), + tree_codomain, + (3, 5, 7), + 1 / dim_sec, # normalization factor + ) + + # ----------------------sym_block_sec--------------- + # | | + # ext1*ext2*ext3*struct_sec_domain ext4*ext5*ext6*struct_sec_codomain + # + sym_shape = (size(sym_data, 1) * size(sym_data, 2), size(sym_data, 3) * size(sym_data, 4)) + sym_block_sec = reshape(sym_data, sym_shape) + return sym_block_sec +end + +#---------------------------------- cast from array --------------------------------------- + +function fill_matrix_blocks!( + data_mat::AbstractBlockSparseMatrix, + blockarray::AbstractBlockArray, + codomain_fused_axes::FusedAxes, + domain_fused_axes::FusedAxes, +) + codomain_legs, codomain_degens, codomain_dims = split_axes(codomain_fused_axes) + domain_legs, domain_degens, domain_dims = split_axes(domain_fused_axes) + + matrix_block_indices = intersect(codomain_fused_axes, domain_fused_axes) + allowed_matrix_blocks = [view!(data_mat, Block(bi)) for bi in matrix_block_indices] + allowed_sectors = blocklabels(codomain_fused_axes)[first.(matrix_block_indices)] + allowed_outer_blocks = allowed_outer_blocks_sectors( + codomain_fused_axes, domain_fused_axes, matrix_block_indices + ) + + # cache computed trees + codomain_tree_tensors_cache = Dict{ + NTuple{ndims(codomain_fused_axes),Int},Vector{Array{Float64,3}} + }() + domain_tree_tensors_cache = Dict{ + NTuple{ndims(domain_fused_axes),Int},Vector{Array{Float64,3}} + }() + + # Below, we loop over every allowed outer block, contract codomain and domain fusion trees + # for each allowed sector and write the result inside a symmetric matrix block + # + # ----------------dim_sec--------- + # | | + # | struct_mult_codomain_sec | struct_mult_domain_sec + # \ / \ / + # \/ \/ + # / / + # / / + # /\ /\ + # / \ / \ + # /\ \ /\ \ + # / \ \ / \ \ + # dim1 dim2 dim3 dim4 dim5 dim6 + # | | | | | | + # ------------------array_block------------- + # | | | | | | + # ext1 ext2 ext3 ext4 ext5 ext6 + + # loop for each allowed outer block + for (outer_block, outer_block_sectors) in allowed_outer_blocks + iter_do = outer_block[begin:ndims(codomain_fused_axes)] + codomain_block_trees = get_fusion_tree_tensors!( + codomain_tree_tensors_cache, iter_do, codomain_legs, allowed_sectors + ) + + iter_co = outer_block[(ndims(codomain_fused_axes) + 1):end] + domain_block_trees = get_fusion_tree_tensors!( + domain_tree_tensors_cache, iter_co, domain_legs, allowed_sectors + ) + + fused_array_block = fuse_array_block( + view(blockarray, Block(iter_do..., iter_co...)), + getindex.(codomain_degens, iter_do), + getindex.(codomain_dims, iter_do), + getindex.(domain_degens, iter_co), + getindex.(domain_dims, iter_co), + ) + + # loop for each symmetry sector allowed in this outer block + for sect in outer_block_sectors + + # actual implementation: legs are conveniently merged + # + # ----------------dim_sec--------- + # | | + # | struct_mult_codomain_sec | struct_mult_domain_sec + # \ / \ / + # \/ \/ + # / / + # | | + # dim1*dim2*dim3 dim4*dim5*dim6 + # | | + # ------------fused_array_block---- + # | | + # ext1*ext2*ext3 ext4*ext5*ext6 + + # contract fusion trees and reshape symmetric block as a matrix + # Note: a final permutedims is needed after the last contract + # therefore cannot efficiently use contract!(allowed_matrix_blocks[...], ...) + i_sec = findfirst(==(sect), allowed_sectors) + sym_block_sec = contract_fusion_trees( + fused_array_block, codomain_block_trees[i_sec], domain_block_trees[i_sec] + ) + + # find outer block location inside this matrix block && write it + row_range = find_block_range(codomain_fused_axes, iter_do, sect) + col_range = find_block_range(domain_fused_axes, iter_co, sect) + @views allowed_matrix_blocks[i_sec][row_range, col_range] = sym_block_sec + end + end +end + +#----------------------------------- cast to array ---------------------------------------- +function add_sector_block!( + fused_array_block::AbstractArray{<:Number,4}, + sym_block_sec::AbstractMatrix, + tree_codomain::AbstractArray{<:Real,3}, + tree_domain::AbstractArray{<:Real,3}, +) + codomain_block_struct_sector = size(tree_codomain, 3) + domain_block_struct_sector = size(tree_domain, 3) + # ----------------------sym_block_sec--------------- + # | | + # ext1*ext2*ext3*struct_sec_domain ext4*ext5*ext6*struct_sec_codomain + # + sym_data_shape = ( + size(sym_block_sec, 1) ÷ codomain_block_struct_sector, + codomain_block_struct_sector, + size(sym_block_sec, 2) ÷ domain_block_struct_sector, + domain_block_struct_sector, + ) + + # -----------------------sym_data---------------------------- + # | | | | + # ext1*ext2*ext3 struct_sec_domain ext4*ext5*ext6 struct_sec_codomain + # + sym_data = reshape(sym_block_sec, sym_data_shape) + + # contract codomain tree + # -----------------------------data_1tree------------------------------ + # | | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 struct_sec_codomain dim1*dim2*dim3 dim_sec + # + data_1tree = contract((1, 2, 6, 3, 5), sym_data, (1, 7, 2, 6), tree_codomain, (3, 5, 7)) + + # contract domain tree + # ---------------------fused_array_block-------------------- + # | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 + # + return contract!( + fused_array_block, + (1, 2, 3, 4), + data_1tree, + (1, 2, 6, 3, 5), + tree_domain, + (4, 5, 6), + 1.0, + 1.0, + ) +end + +function unfuse_array_block( + fused_array_block::AbstractArray{<:Number,4}, + codomain_block_degens::Tuple, + codomain_block_dims::Tuple, + domain_block_degens::Tuple, + domain_block_dims::Tuple, +) + # ---------------------fused_array_block-------------------- + # | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 + # + + # -------------------permuted_split_array_block----------------------------------- + # | | | | | | | | | | | | + # ext1 ext2 ext3 ext4 ext5 ext6 dim1 dim2 dim3 dim4 dim5 dim6 + # + permuted_split_array_block = reshape_fused_to_permuted( + fused_array_block, + codomain_block_degens, + codomain_block_dims, + domain_block_degens, + domain_block_dims, + ) + + # ------------------------------split_array_block------------------------- + # | | | | | | + # / \ / \ / \ / \ / \ / \ + # / \ / \ / \ / \ / \ / \ + # ext1 dim1 ext2 dim2 ext3 dim3 ext4 dim4 ext5 dim5 ext6 dim6 + # + split_array_block = unpermute_split_array_block(permuted_split_array_block) + + # + # ------------------------------array_block------------------------------- + # | | | | | | + # ext1*dim1 ext2*dim2 ext3*dim3 ext4*dim4 ext5*dim5 ext6*dim6 + # + array_block = merge_degen_dims(split_array_block) + return array_block +end + +function fill_blockarray!( + blockarray::AbstractBlockArray, + data_mat::AbstractMatrix, + codomain_fused_axes::FusedAxes, + domain_fused_axes::FusedAxes, +) + codomain_legs, codomain_degens, codomain_dims = split_axes(codomain_fused_axes) + domain_legs, domain_degens, domain_dims = split_axes(domain_fused_axes) + + matrix_block_blocks = sort(collect(block_stored_indices(data_mat))) + existing_matrix_blocks = [view(data_mat, b) for b in matrix_block_blocks] + matrix_block_indices = reinterpret(Tuple{Int,Int}, matrix_block_blocks) + existing_sectors = blocklabels(codomain_fused_axes)[first.(matrix_block_indices)] + existing_outer_blocks = allowed_outer_blocks_sectors( + codomain_fused_axes, domain_fused_axes, matrix_block_indices + ) + + # cache computed trees + codomain_tree_tensors_cache = Dict{ + NTuple{ndims(codomain_fused_axes),Int},Vector{Array{Float64,3}} + }() + domain_tree_tensors_cache = Dict{ + NTuple{ndims(domain_fused_axes),Int},Vector{Array{Float64,3}} + }() + + # loop for each existing outer block + for (outer_block, outer_block_sectors) in existing_outer_blocks + iter_do = outer_block[begin:ndims(codomain_fused_axes)] + codomain_block_degens = getindex.(codomain_degens, iter_do) + codomain_block_dims = getindex.(codomain_dims, iter_do) + codomain_block_trees = get_fusion_tree_tensors!( + codomain_tree_tensors_cache, iter_do, codomain_legs, existing_sectors + ) + + iter_co = outer_block[(ndims(codomain_fused_axes) + 1):end] + domain_block_degens = getindex.(domain_degens, iter_co) + domain_block_dims = getindex.(domain_dims, iter_co) + domain_block_trees = get_fusion_tree_tensors!( + domain_tree_tensors_cache, iter_co, domain_legs, existing_sectors + ) + + # ---------------------fused_array_block-------------------- + # | | | | + # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 + # + fused_array_block_shape = ( + prod(codomain_block_degens), + prod(domain_block_degens), + prod(codomain_block_dims), + prod(domain_block_dims), + ) + fused_array_block = zeros(eltype(blockarray), fused_array_block_shape) + + # loop for each symmetry sector inside this configuration + for sect in outer_block_sectors + i_sec = findfirst(==(sect), existing_sectors) + row_range = find_block_range(codomain_fused_axes, iter_do, sect) + col_range = find_block_range(domain_fused_axes, iter_co, sect) + sym_block_sec = view(existing_matrix_blocks[i_sec], row_range, col_range) + add_sector_block!( + fused_array_block, + sym_block_sec, + codomain_block_trees[i_sec], + domain_block_trees[i_sec], + ) + end + + blockarray[Block(iter_do..., iter_co...)] = unfuse_array_block( + fused_array_block, + codomain_block_degens, + codomain_block_dims, + domain_block_degens, + domain_block_dims, + ) + end +end diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl new file mode 100644 index 0000000..f5ab872 --- /dev/null +++ b/src/fusiontensor/base_interface.jl @@ -0,0 +1,119 @@ +# This files defines Base functions for FusionTensor + +function Base.:*(x::Number, ft::FusionTensor) + return FusionTensor(x * data_matrix(ft), codomain_axes(ft), domain_axes(ft)) +end + +function Base.:*(ft::FusionTensor, x::Number) + return FusionTensor(x * data_matrix(ft), codomain_axes(ft), domain_axes(ft)) +end + +# tensor contraction is a block data_matrix product. +# allow to contract with different eltype and let BlockSparseArray ensure compatibility +# impose matching type and number of axes at compile time +# impose matching axes at run time +function Base.:*(left::FusionTensor, right::FusionTensor) + if !matching_dual(domain_axes(left), codomain_axes(right)) + throw(codomainError("Incompatible tensor axes")) + end + new_data_matrix = data_matrix(left) * data_matrix(right) + return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(right)) +end + +Base.:+(ft::FusionTensor) = ft + +# tensor addition is a block data_matrix add. +# impose matching axes, allow different eltypes +function Base.:+(left::FusionTensor, right::FusionTensor) + if !matching_axes(axes(left), axes(right)) + throw(codomainError("Incompatible tensor axes")) + end + new_data_matrix = data_matrix(left) + data_matrix(right) + return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(left)) +end + +function Base.:-(ft::FusionTensor) + new_data_matrix = -data_matrix(ft) + return FusionTensor(new_data_matrix, codomain_axes(ft), domain_axes(ft)) +end + +function Base.:-(left::FusionTensor, right::FusionTensor) + if !matching_axes(axes(left), axes(right)) + throw(codomainError("Incompatible tensor axes")) + end + new_data_matrix = data_matrix(left) - data_matrix(right) + return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(left)) +end + +function Base.:/(ft::FusionTensor, x::Number) + return FusionTensor(data_matrix(ft) / x, codomain_axes(ft), domain_axes(ft)) +end + +Base.Array(ft::FusionTensor) = Array(cast_to_array(ft)) + +# adjoint is costless: dual axes, swap codomain and domain, take data_matrix adjoint. +# data_matrix coeff are not modified (beyond complex conjugation) +function Base.adjoint(ft::FusionTensor) + return FusionTensor( + adjoint(data_matrix(ft)), dual.(domain_axes(ft)), dual.(codomain_axes(ft)) + ) +end + +Base.axes(ft::FusionTensor) = (codomain_axes(ft)..., domain_axes(ft)...) + +# conj is defined as coefficient wise complex conjugation, without axis dual +# same object for real element type +Base.conj(ft::FusionTensor{<:Real}) = ft + +function Base.conj(ft::FusionTensor) + return FusionTensor(conj(data_matrix(ft)), codomain_axes(ft), domain_axes(ft)) +end + +function Base.copy(ft::FusionTensor) + new_data_matrix = copy(data_matrix(ft)) + new_codomain_axes = copy.(codomain_axes(ft)) + new_domain_axes = copy.(domain_axes(ft)) + return FusionTensor(new_data_matrix, new_codomain_axes, new_domain_axes) +end + +function Base.deepcopy(ft::FusionTensor) + new_data_matrix = deepcopy(data_matrix(ft)) + new_codomain_axes = deepcopy.(codomain_axes(ft)) + new_domain_axes = deepcopy.(domain_axes(ft)) + return FusionTensor(new_data_matrix, new_codomain_axes, new_domain_axes) +end + +# eachindex is automatically defined for AbstractArray. We do not want it. +function Base.eachindex(::FusionTensor) + throw(codomainError("eachindex", "eachindex not defined for FusionTensor")) +end + +Base.ndims(::FusionTensor{T,N}) where {T,N} = N + +Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args...) + +function Base.similar(ft::FusionTensor) + mat = similar(data_matrix(ft)) + return FusionTensor(mat, codomain_axes(ft), domain_axes(ft)) +end + +function Base.similar(ft::FusionTensor, elt::Type) + return FusionTensor(elt, codomain_axes(ft), domain_axes(ft)) +end + +function Base.similar(::FusionTensor, elt::Type, new_axes::Tuple{<:Tuple,<:Tuple}) + return FusionTensor(elt, new_axes[1], new_axes[2]) +end + +Base.show(io::IO, ft::FusionTensor) = print(io, "$(ndims(ft))-dim FusionTensor") + +function Base.show(io::IO, ::MIME"text/plain", ft::FusionTensor) + println(io, "$(ndims(ft))-dim FusionTensor with axes:") + for ax in axes(ft) + display(ax) + println(io) + end + return nothing +end + +Base.size(ft::FusionTensor) = quantum_dimension.(axes(ft)) diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl new file mode 100644 index 0000000..be2248d --- /dev/null +++ b/src/fusiontensor/fusedaxes.jl @@ -0,0 +1,101 @@ +# This file defines helper functions to access FusionTensor internal structures + +struct FusedAxes{A,B,C} + outer_axes::A + fused_axis::B + trees_to_ranges_mapping::C + + function FusedAxes( + outer_legs::NTuple{N,AbstractGradedUnitRange{LA}}, + fused_axis::AbstractGradedUnitRange{LA}, + trees_to_ranges_mapping::Dict{<:FusionTree{<:AbstractSector,N}}, + ) where {N,LA} + return new{typeof(outer_legs),typeof(fused_axis),typeof(trees_to_ranges_mapping)}( + outer_legs, fused_axis, trees_to_ranges_mapping + ) + end +end + +# getters +fused_axis(fa::FusedAxes) = fa.fused_axis +fusion_trees(fa::FusedAxes) = keys(trees_to_ranges_mapping(fa)) +trees_to_ranges_mapping(fa::FusedAxes) = fa.trees_to_ranges_mapping + +# Base interface +Base.axes(fa::FusedAxes) = fa.outer_axes +Base.ndims(fa::FusedAxes) = length(axes(fa)) + +# GradedUnitRanges interface +GradedUnitRanges.blocklabels(fa::FusedAxes) = blocklabels(fused_axis(fa)) + +# constructors +function FusedAxes{S}(::Tuple{}) where {S<:AbstractSector} + fused_axis = gradedrange([trivial(S) => 1]) + trees_to_ranges_mapping = Dict([FusionTree{S}() => Block(1)[1:1]]) + return FusedAxes((), fused_axis, trees_to_ranges_mapping) +end + +function fusion_trees_external_multiplicites( + outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} +) + N = length(outer_legs) + tree_arrows = isdual.(outer_legs) + return mapreduce(vcat, CartesianIndices(blocklength.(outer_legs))) do it + block_sectors = ntuple(i -> blocklabels(outer_legs[i])[it[i]], N) + block_mult = prod(ntuple(i -> blocklengths(outer_legs[i])[it[i]], N)) + return build_trees(block_sectors, tree_arrows) .=> block_mult + end +end + +function FusedAxes{S}( + outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} +) where {S<:AbstractSector} + fusion_trees_mult = fusion_trees_external_multiplicites(outer_legs) + + fused_leg, range_mapping = compute_inner_ranges(fusion_trees_mult) + return FusedAxes(outer_legs, fused_leg, range_mapping) +end + +function compute_inner_ranges( + fusion_trees_mult::AbstractVector{<:Pair{<:FusionTree,<:Integer}} +) + fused_leg = blockmergesort( + gradedrange(root_sector.(first.(fusion_trees_mult)) .=> last.(fusion_trees_mult)) + ) + range_mapping = Dict{typeof(first(first(fusion_trees_mult))),typeof(Block(1)[1:1])}() + fused_sectors = blocklabels(fused_leg) + shifts = ones(Int, blocklength(fused_leg)) + for (f, m) in fusion_trees_mult + s = root_sector(f) + i = findfirst(==(s), fused_sectors) + range_mapping[f] = Block(i)[shifts[i]:(shifts[i] + m - 1)] + shifts[i] += m + end + return fused_leg, range_mapping +end + +function Base.intersect(left::FusedAxes, right::FusedAxes) + left_labels = blocklabels(left) + right_labels = blocklabels(right) + return find_shared_indices(left_labels, right_labels) +end + +function allowed_outer_blocks_sectors( + left::FusedAxes, right::FusedAxes, shared_indices::AbstractVector +) + left_labels = blocklabels(left) + left_indices = first.(shared_indices) + right_indices = last.(shared_indices) + @assert left_labels[left_indices] == blocklabels(right)[right_indices] + reduced_left = .!isempty.(index_matrix(left)[:, left_indices]) + reduced_right = .!isempty.(index_matrix(right)[:, right_indices]) + + block_sectors = Dict{NTuple{ndims(left) + ndims(right),Int},Vector{eltype(left_labels)}}() + for i in axes(reduced_left, 1), j in axes(reduced_right, 1) + intersection = findall(>(0), reduced_left[i, :] .* reduced_right[j, :]) + isempty(intersection) && continue + full_block = (unravel_index(i, left)..., unravel_index(j, right)...) + block_sectors[full_block] = left_labels[left_indices[intersection]] + end + return block_sectors +end diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl new file mode 100644 index 0000000..28340fd --- /dev/null +++ b/src/fusiontensor/fusiontensor.jl @@ -0,0 +1,168 @@ +# This file defines struct FusionTensor and constructors + +using BlockSparseArrays: block_stored_indices + +struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat} <: AbstractArray{T,N} + data_matrix::Mat + codomain_axes::CoDomainAxes + domain_axes::DomainAxes + + # inner constructor to impose constraints on types + # TBD replace codomain_legs with FusedAxes(codomain_legs)? + function FusionTensor( + mat::Union{BlockSparseMatrix,Adjoint{<:Number,<:BlockSparseMatrix}}, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}}, + ) where {LA} + return new{ + eltype(mat), + length(codomain_legs) + length(domain_legs), + typeof(codomain_legs), + typeof(domain_legs), + typeof(mat), + }( + mat, codomain_legs, domain_legs + ) + end +end + +# getters +data_matrix(ft::FusionTensor) = ft.data_matrix +codomain_axes(ft::FusionTensor) = ft.codomain_axes +domain_axes(ft::FusionTensor) = ft.domain_axes + +# misc access +ndims_codomain(ft::FusionTensor) = length(codomain_axes(ft)) +ndims_domain(ft::FusionTensor) = length(domain_axes(ft)) + +matrix_size(ft::FusionTensor) = quantum_dimension.(axes(data_matrix(ft))) +matrix_row_axis(ft::FusionTensor) = first(axes(data_matrix(ft))) +matrix_column_axis(ft::FusionTensor) = last(axes(data_matrix(ft))) + +function sanitize_axes(raw_legs) + legs = unify_sector_type(raw_legs) + @assert all(check_unique_blocklabels.(legs)) + return legs +end + +function unify_sector_type(legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}}) where {LA} # nothing to do + return legs +end + +check_unique_blocklabels(g) = length(unique(blocklabels(g))) == blocklength(g) + +# TODO move this to SymmetrySectors or GradedUnitRanges +# merge with SymmetrySectors.map_blocklabels +function find_common_sector_type(sector_or_axes_enum) + # fuse trivial sectors to produce unified type + # avoid depending on SymmetrySectors internals + return label_type(fusion_product(trivial.(sector_or_axes_enum)...)) +end + +function unify_sector_type(legs::Tuple{Vararg{AbstractGradedUnitRange}}) + T = find_common_sector_type(legs) + unified_legs = map(g -> unify_sector_type(T, g), legs) + return unified_legs +end + +function unify_sector_type(T::Type{<:SectorProduct}, g::AbstractGradedUnitRange) + # fuse with trivial to insert all missing arguments inside each GradedAxis + # avoid depending on SymmetrySectors internals + glabels = map(s -> only(blocklabels(fusion_product(trivial(T), s))), blocklabels(g)) + # use labelled_blocks to preserve GradedUnitRange + unified_g = labelled_blocks(unlabel_blocks(g), glabels) + return isdual(g) ? flip(unified_g) : unified_g +end + +# empty matrix +function FusionTensor( + data_type::Type, + codomain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, +) + legs = sanitize_axes((codomain_legs_raw..., domain_legs_raw...)) + S = sector_type(first(legs)) + codomain_legs = legs[begin:length(codomain_legs_raw)] + domain_legs = legs[(length(codomain_legs_raw) + 1):end] + codomain_fused_axes = FusedAxes{S}(codomain_legs) + domain_fused_axes = FusedAxes{S}(dual.(domain_legs)) + mat = initialize_data_matrix(data_type, codomain_fused_axes, domain_fused_axes) + return FusionTensor(mat, codomain_legs, domain_legs) +end + +function FusionTensor(data_type::Type, ::Tuple{}, ::Tuple{}) + codomain_fused_axes = FusedAxes{TrivialSector}(()) + domain_fused_axes = FusedAxes{TrivialSector}(()) + mat = initialize_data_matrix(data_type, codomain_fused_axes, domain_fused_axes) + return FusionTensor(mat, (), ()) +end + +# init data_matrix +function initialize_data_matrix( + data_type::Type{<:Number}, codomain_fused_axes::FusedAxes, domain_fused_axes::FusedAxes +) + # fusion trees have Float64 eltype: need compatible type + promoted = promote_type(data_type, Float64) + mat_row_axis = fused_axis(codomain_fused_axes) + mat_col_axis = dual(fused_axis(domain_fused_axes)) + mat = BlockSparseArray{promoted}(mat_row_axis, mat_col_axis) + initialize_allowed_sectors!(mat) + return mat +end + +function initialize_allowed_sectors!(mat::AbstractBlockMatrix) + row_sectors = blocklabels(axes(mat, 1)) + col_sectors = blocklabels(dual(axes(mat, 2))) + row_blocks = findall(in(col_sectors), row_sectors) + col_blocks = findall(in(row_sectors), col_sectors) + for (r, c) in zip(row_blocks, col_blocks) + mat[Block(r, c)] = zeros(size(mat[Block(r, c)])) + end +end + +function check_data_matrix_axes( + mat::BlockSparseMatrix, codomain_legs::Tuple, domain_legs::Tuple +) + ft0 = FusionTensor(Float64, codomain_legs, domain_legs) + @assert space_isequal(matrix_row_axis(ft0), axes(mat, 1)) + @assert space_isequal(matrix_column_axis(ft0), axes(mat, 2)) +end + +function check_data_matrix_axes(mat::Adjoint, codomain_legs::Tuple, domain_legs::Tuple) + return check_data_matrix_axes(adjoint(mat), dual.(domain_legs), dual.(codomain_legs)) +end + +function check_sanity(ft::FusionTensor) + nca = ndims_domain(ft) + @assert nca == length(domain_axes(ft)) "ndims_domain does not match domain_axes" + @assert nca <= ndims(ft) "invalid ndims_domain" + + nda = ndims_codomain(ft) + @assert nda == length(codomain_axes(ft)) "ndims_codomain does not match codomain_axes" + @assert nda <= ndims(ft) "invalid ndims_codomain" + @assert nda + nca == ndims(ft) "invalid ndims" + + @assert length(axes(ft)) == ndims(ft) "ndims does not match axes" + @assert matching_axes(axes(ft)[begin:nda], codomain_axes(ft)) "axes do not match codomain_axes" + @assert matching_axes(axes(ft)[(nda + 1):end], domain_axes(ft)) "axes do not match domain_axes" + + m = data_matrix(ft) + @assert ndims(m) == 2 "invalid data_matrix ndims" + row_axis = matrix_row_axis(ft) + column_axis = matrix_column_axis(ft) + @assert row_axis === axes(m, 1) "invalid row_axis" + @assert column_axis === axes(m, 2) "invalid column_axis" + check_data_matrix_axes(data_matrix(ft), codomain_axes(ft), domain_axes(ft)) + + for b in block_stored_indices(m) + it = Int.(Tuple(b)) + @assert blocklabels(row_axis)[it[1]] == blocklabels(dual(column_axis))[it[2]] "forbidden block" + end + return nothing +end + +matching_dual(axes1::Tuple, axes2::Tuple) = matching_axes(dual.(axes1), axes2) +matching_axes(axes1::Tuple, axes2::Tuple) = false +function matching_axes(axes1::T, axes2::T) where {T<:Tuple} + return all(space_isequal.(axes1, axes2)) +end diff --git a/src/fusiontensor/linear_algebra_interface.jl b/src/fusiontensor/linear_algebra_interface.jl new file mode 100644 index 0000000..b950ecd --- /dev/null +++ b/src/fusiontensor/linear_algebra_interface.jl @@ -0,0 +1,72 @@ +# This file defines linalg for FusionTensor + +# allow to contract with different eltype and let BlockSparseArray ensure compatibility +# impose matching type and number of axes at compile time +# impose matching axes at run time +# TODO remove this once TensorAlgebra.contract can be used? +function LinearAlgebra.mul!( + C::FusionTensor, A::FusionTensor, B::FusionTensor, α::Number, β::Number +) + + # compile time checks + if ndims_domain(A) != ndims_codomain(B) + throw(codomainError("Incompatible tensor structures for A and B")) + end + if ndims_codomain(A) != ndims_codomain(C) + throw(codomainError("Incompatible tensor structures for A and C")) + end + if ndims_domain(B) != ndims_domain(C) + throw(codomainError("Incompatible tensor structures for B and C")) + end + + # input validation + if !matching_dual(domain_axes(A), codomain_axes(B)) + throw(codomainError("Incompatible tensor axes for A and B")) + end + if !matching_axes(codomain_axes(C), codomain_axes(A)) + throw(codomainError("Incompatible tensor axes for C and A")) + end + if !matching_axes(domain_axes(C), domain_axes(B)) + throw(codomainError("Incompatible tensor axes for C and B")) + end + LinearAlgebra.mul!(data_matrix(C), data_matrix(A), data_matrix(B), α, β) + return C +end + +function LinearAlgebra.norm(ft::FusionTensor) + m = data_matrix(ft) + row_sectors = blocklabels(matrix_row_axis(ft)) + n2 = mapreduce( + idx -> quantum_dimension(row_sectors[idx[1]]) * norm(m[Block(Tuple(idx))])^2, + +, + stored_indices(blocks(m)); + init=0.0, + ) + return sqrt(n2) +end + +function LinearAlgebra.tr(ft::FusionTensor) + m = data_matrix(ft) + row_sectors = blocklabels(matrix_row_axis(ft)) + return mapreduce( + idx -> quantum_dimension(row_sectors[idx[1]]) * tr(m[Block(Tuple(idx))]), + +, + stored_indices(blocks(m)); + init=eltype(ft)(0), + ) +end + +function LinearAlgebra.qr(ft::FusionTensor) + qmat, rmat = BlockSparseArrays.block_qr(data_matrix(ft)) + qtens = FusionTensor(qmat, codomain_axes(ft), (axes(qmat)[1],)) + rtens = FusionTensor(rmat, (axes(rmat)[0],), domain_axes(ft)) + return qtens, rtens +end + +function LinearAlgebra.svd(ft::FusionTensor) + umat, s, vmat = BlockSparseArrays.block_svd(data_matrix(ft)) + utens = FusionTensor(umat, codomain_axes(ft), (axes(umat)[1],)) + stens = FusionTensor(s, (axes(umat)[1],), (axes(vmat)[0],)) + vtens = FusionTensor(vmat, (axes(vmat)[0],), domain_axes(ft)) + return utens, stens, vtens +end diff --git a/src/fusiontensor/tensor_algebra_interface.jl b/src/fusiontensor/tensor_algebra_interface.jl new file mode 100644 index 0000000..ff19bd9 --- /dev/null +++ b/src/fusiontensor/tensor_algebra_interface.jl @@ -0,0 +1,42 @@ +# This file defines TensorAlgebra interface for a FusionTensor + +# TBD how to deal with inner contraction = no ouput axis? +function TensorAlgebra.allocate_output( + ::typeof(contract), + biperm_dest::BlockedPermutation{2}, + a1::FusionTensor{T1,N}, + biperm1::BlockedPermutation{2,N}, + a2::FusionTensor{T2,M}, + biperm2::BlockedPermutation{2,M}, + α::Number=true, +) where {T1,T2,N,M} + axes_dest = ( + (i -> axes(a1)[i]).(biperm1[Block(1)]), (i -> axes(a2)[i]).(biperm2[Block(2)]) + ) + return similar(a1, promote_type(eltype(a1), eltype(a2), typeof(α)), axes_dest) +end + +# TBD do really I need to defined these as I cannot use them in contract! and has to redefine it? +# TensorAlgebra.fusedims(ft::FusionTensor, perm::BlockedPermutation) = permutedims(ft, perm) +# function TensorAlgebra.splitdims(ft1::FusionTensor, ft2::FusionTensor, blockedperm::BlockedPermutation) +# function TensorAlgebra.splitdims!(ft1::FusionTensor, ft2::FusionTensor, blockedperm::BlockedPermutation) + +# I cannot use contract! from TensorAlgebra/src/contract/contract_matricize/contract.jl +# as it calls _mul!, which I should not overload. +# TBD I can also overload higher up and do not allow use of different algorithms +function TensorAlgebra.contract!( + alg::Algorithm, + a_dest::FusionTensor, + biperm_dest::BlockedPermutation, + a1::FusionTensor, + biperm1::BlockedPermutation, + a2::FusionTensor, + biperm2::BlockedPermutation, + α::Number, + β::Number, +) + a1_perm = permutedims(a1, biperm1) + a2_perm = permutedims(a2, biperm2) + LinearAlgebra.mul!(a_dest, a1_perm, a2_perm, α, β) + return a_dest +end diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl new file mode 100644 index 0000000..72ce211 --- /dev/null +++ b/src/permutedims/permutedims.jl @@ -0,0 +1,262 @@ +# This file defines permutedims for a FusionTensor + +# ================================= High level interface ================================= +# permutedims with 1 tuple of 2 separate tuples +function fusiontensor_permutedims(ft::FusionTensor, new_leg_indices::Tuple{NTuple,NTuple}) + return fusiontensor_permutedims(ft, new_leg_indices...) +end + +# permutedims with 2 separate tuples +function fusiontensor_permutedims( + ft::FusionTensor, new_codomain_indices::Tuple, new_domain_indices::Tuple +) + biperm = blockedperm(new_codomain_indices, new_domain_indices) + return permutedims(ft, biperm) +end + +function fusiontensor_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2}) + @assert ndims(ft) == length(biperm) + + # early return for identity operation. Do not copy. Also handle tricky 0-dim case. + if ndims_codomain(ft) == first(blocklengths(biperm)) # compile time + if Tuple(biperm) == ntuple(identity, ndims(ft)) + return ft + end + end + + permuted_data_matrix = permute_data_matrix( + data_matrix(ft), codomain_axes(ft), domain_axes(ft), biperm + ) + new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) + permuted = FusionTensor(permuted_data_matrix, new_codomain_legs, new_domain_legs) + return permuted +end + +function naive_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2}) + @assert ndims(ft) == length(biperm) + new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) + + # stupid permute: cast to dense, permutedims, cast to FusionTensor + arr = Array(ft) + permuted_arr = permutedims(arr, Tuple(biperm)) + permuted = FusionTensor(permuted_arr, new_codomain_legs, new_domain_legs) + + return permuted +end + +# ================================= Low level interface ================================== +function permute_data_matrix( + old_data_matrix::AbstractMatrix, + old_codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + old_domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + biperm::BlockedPermutation{2}, +) + # TBD use FusedAxes as input? + + unitaries = compute_unitaries(old_codomain_legs, old_domain_legs, biperm) + + # TODO cache FusedAxes + old_codomain_fused_axes = FusedAxes(old_codomain_legs) + old_domain_fused_axes = FusedAxes(dual.(old_domain_legs)) + new_codomain_legs, new_domain_legs = blockpermute( + (old_codomain_legs..., old_domain_legs...), biperm + ) + new_codomain_fused_axes = FusedAxes(new_codomain_legs) + new_domain_fused_axes = FusedAxes(dual.(new_domain_legs)) + new_data_matrix = initialize_data_matrix( + eltype(old_data_matrix), new_codomain_fused_axes, new_domain_fused_axes + ) + + fill_data_matrix!( + new_data_matrix, + old_data_matrix, + old_codomain_fused_axes, + old_domain_fused_axes, + new_codomain_fused_axes, + new_domain_fused_axes, + biperm, + unitaries, + ) + return new_data_matrix +end + +# ===================================== Internals ======================================== +function add_structural_axes( + biperm::BlockedPermutation{2}, ::Val{OldNDoAxes} +) where {OldNDoAxes} + flat = Tuple(biperm) + extended_perm = ( + OldNDoAxes + 1, length(biperm) + 2, (flat .+ (flat .>= OldNDoAxes + 1))... + ) + return extended_perm +end + +function fill_data_matrix!( + new_data_matrix::AbstractMatrix, + old_data_matrix::AbstractMatrix, + old_codomain_fused_axes::FusedAxes, + old_domain_fused_axes::FusedAxes, + new_codomain_fused_axes::FusedAxes, + new_domain_fused_axes::FusedAxes, + biperm::BlockedPermutation{2}, + unitaries::Dict, +) + @assert ndims(old_codomain_fused_axes) + ndims(old_domain_fused_axes) == length(biperm) + @assert ndims(new_codomain_fused_axes) + ndims(new_domain_fused_axes) == length(biperm) + + old_matrix_block_indices = reinterpret( + Tuple{Int,Int}, sort(collect(block_stored_indices(old_data_matrix))) + ) + old_matrix_blocks = Dict( + blocklabels(old_codomain_fused_axes)[first(b)] => view(old_data_matrix, Block(b)) for + b in old_matrix_block_indices + ) + + new_matrix_block_indices = intersect(new_codomain_fused_axes, new_domain_fused_axes) + new_matrix_blocks = Dict( + blocklabels(new_codomain_fused_axes)[first(b)] => view!(new_data_matrix, Block(b)) for + b in new_matrix_block_indices + ) + + old_existing_outer_blocks = allowed_outer_blocks_sectors( + old_codomain_fused_axes, old_domain_fused_axes, old_matrix_block_indices + ) + + new_existing_outer_blocks = allowed_outer_blocks_sectors( + new_codomain_fused_axes, new_domain_fused_axes, new_matrix_block_indices + ) + + extended_perm = add_structural_axes(biperm, Val(ndims(old_codomain_fused_axes))) + + # loop for each existing outer block TODO parallelize + for old_outer_block_sectors in old_existing_outer_blocks + new_outer_block = map(i -> first(old_outer_block_sectors)[i], Tuple(biperm)) + new_outer_block_sectors = new_outer_block => new_existing_outer_blocks[new_outer_block] + permute_outer_block!( + new_matrix_blocks, + old_matrix_blocks, + old_outer_block_sectors, + new_outer_block_sectors, + unitaries[first(old_outer_block_sectors)], + extended_perm, + old_codomain_fused_axes, + old_domain_fused_axes, + new_codomain_fused_axes, + new_domain_fused_axes, + ) + end +end + +function permute_outer_block!( + new_matrix_blocks::Dict{S,<:AbstractMatrix}, + old_matrix_blocks::Dict{S,<:AbstractMatrix}, + old_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, + new_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, + unitary::AbstractBlockMatrix, + extended_perm::Tuple{Vararg{Int}}, + old_codomain_fused_axes::FusedAxes, + old_domain_fused_axes::FusedAxes, + new_codomain_fused_axes::FusedAxes, + new_domain_fused_axes::FusedAxes, +) where {S<:AbstractSector} + new_outer_array = permute_old_outer_block( + old_matrix_blocks, + old_outer_block_sectors, + old_codomain_fused_axes, + old_domain_fused_axes, + extended_perm, + unitary, + ) + return write_new_outer_block!( + new_matrix_blocks, + new_outer_array, + new_outer_block_sectors, + new_codomain_fused_axes, + new_domain_fused_axes, + ) +end + +function permute_old_outer_block( + old_matrix_blocks::Dict{S,<:AbstractMatrix}, + old_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, + old_codomain_fused_axes::FusedAxes, + old_domain_fused_axes::FusedAxes, + extended_perm::Tuple{Vararg{Int}}, + unitary::AbstractBlockMatrix, +) where {S<:AbstractSector} + old_codomain_block = first(old_outer_block_sectors)[begin:ndims(old_codomain_fused_axes)] + old_domain_block = first(old_outer_block_sectors)[(ndims(old_codomain_fused_axes) + 1):end] + old_codomain_ext_mult = block_external_multiplicities( + old_codomain_fused_axes, old_codomain_block + ) + old_domain_ext_mult = block_external_multiplicities( + old_domain_fused_axes, old_domain_block + ) + return mapreduce(+, enumerate(last(old_outer_block_sectors))) do (i_sec, old_sector) + old_row_range = find_block_range( + old_codomain_fused_axes, old_codomain_block, old_sector + ) + old_col_range = find_block_range(old_domain_fused_axes, old_domain_block, old_sector) + old_sym_block_matrix = view(old_matrix_blocks[old_sector], old_row_range, old_col_range) + old_tensor_shape = ( + old_codomain_ext_mult..., + block_structural_multiplicity( + old_codomain_fused_axes, old_codomain_block, old_sector + ), + old_domain_ext_mult..., + block_structural_multiplicity(old_domain_fused_axes, old_domain_block, old_sector), + ) + old_sym_block_tensor = reshape(old_sym_block_matrix, old_tensor_shape) + unitary_column = unitary[:, Block(i_sec)] # take all new blocks at once + return change_basis_block_sector(old_sym_block_tensor, unitary_column, extended_perm) + end +end + +function change_basis_block_sector( + old_sym_block_tensor::AbstractArray, + unitary_column::AbstractBlockMatrix, + extended_perm::Tuple{Vararg{Int}}, +) + old_permuted = permutedims(old_sym_block_tensor, extended_perm) + new_shape = (size(unitary_column, 2), prod(size(old_permuted)[3:end])) + reshaped = reshape(old_permuted, new_shape) + new_outer_array = unitary_column * reshaped + return new_outer_array +end + +function write_new_outer_block!( + new_matrix_blocks::Dict{S,<:AbstractMatrix}, + new_outer_array::AbstractMatrix, + new_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, + new_codomain_fused_axes::FusedAxes, + new_domain_fused_axes::FusedAxes, +) where {S<:AbstractSector} + new_codomain_block = first(new_outer_block_sectors)[begin:ndims(new_codomain_fused_axes)] + new_domain_block = first(new_outer_block_sectors)[(ndims(new_codomain_fused_axes) + 1):end] + new_codomain_ext_mult = block_external_multiplicities( + new_codomain_fused_axes, new_codomain_block + ) + new_domain_ext_mult = block_external_multiplicities( + new_domain_fused_axes, new_domain_block + ) + for (i_sec, new_sector) in enumerate(last(new_outer_block_sectors)) # not worth parallelize + new_shape_4d = ( + block_structural_multiplicity( + new_codomain_fused_axes, new_codomain_block, new_sector + ), + block_structural_multiplicity(new_domain_fused_axes, new_domain_block, new_sector), + prod(new_codomain_ext_mult), + prod(new_domain_ext_mult), + ) + new_block_4d = reshape(view(new_outer_array, Block(i_sec), :), new_shape_4d) + permuted_new_block = permutedims(new_block_4d, (3, 1, 2, 4)) + mat_shape = (new_shape_4d[3] * new_shape_4d[1], new_shape_4d[2] * new_shape_4d[4]) + new_mat = reshape(permuted_new_block, mat_shape) + + new_row_range = find_block_range( + new_codomain_fused_axes, new_codomain_block, new_sector + ) + new_col_range = find_block_range(new_domain_fused_axes, new_domain_block, new_sector) + new_matrix_blocks[new_sector][new_row_range, new_col_range] = new_mat + end +end diff --git a/src/permutedims/unitaries.jl b/src/permutedims/unitaries.jl new file mode 100644 index 0000000..f9afb4a --- /dev/null +++ b/src/permutedims/unitaries.jl @@ -0,0 +1,290 @@ +# This file defines unitaries to be used in permutedims + +# Current implementation: +# * Unitary = BlockMatrix +# * no unitary cache + +# Notes: +# - The interface uses AbstractGradedUnitRanges as input for interface simplicity +# however only blocklabels are used and blocklengths are never used. + +using BlockArrays: BlockedOneTo + +const unitary_cache = LRU{Any,AbstractMatrix}(; maxsize=10000) + +# ====================================== Interface ======================================= +function compute_unitaries( + old_codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + old_domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + biperm::BlockedPermutation{2}, +) + @assert length(old_codomain_legs) + length(old_domain_legs) == length(biperm) + return compute_unitaries_clebsch_gordan(old_codomain_legs, old_domain_legs, biperm) +end + +function unitary_key(codomain_legs, domain_legs, old_outer_block, biperm) + legs = (codomain_legs..., domain_legs...) + old_arrows = isdual.(legs) + old_sectors = ntuple(i -> blocklabels(legs[i])[old_outer_block[i]], length(legs)) + return (old_arrows, old_sectors, length(codomain_legs), biperm) +end + +# =========================== Constructor from Clebsch-Gordan ============================ +function contract_singlet_space_projector( + codomain_tree_tensor::AbstractArray{<:Real}, + domain_tree_tensor::AbstractArray{<:Real}, + irreps_perm::NTuple{N,Int}, +) where {N} + # compile time values + NCoAxes = ndims(codomain_tree_tensor) - 2 + NDoAxes = ndims(domain_tree_tensor) - 2 + @assert NDoAxes + NCoAxes == N + + irrep_dims_prod = + prod(size(codomain_tree_tensor)[begin:NCoAxes]) * + prod(size(domain_tree_tensor)[begin:NDoAxes]) + + # some trees are empty: return projector on null space + if length(codomain_tree_tensor) == 0 || length(domain_tree_tensor) == 0 + return zeros((irrep_dims_prod, 0)) + end + + labels_codomain = (ntuple(identity, NCoAxes)..., N + 3, N + 1) + labels_domain = (ntuple(i -> i + NCoAxes, NDoAxes)..., N + 3, N + 2) + labels_dest = (irreps_perm..., N + 1, N + 2) + + # ----------------dim_sec--------- + # | | + # | struct_mult_codomain_sec | struct_mult_domain_sec + # \ / \ / + # \/ \/ + # / / + # / / + # /\ /\ + # / \ / \ + # /\ \ /\ \ + # / \ \ / \ \ + # dim1 dim2 dim3 dim4 dim5 dim6 + # + projector = TensorAlgebra.contract( + labels_dest, codomain_tree_tensor, labels_codomain, domain_tree_tensor, labels_domain + ) + + # reshape as a matrix + # ---------------projector_sector---------------- + # | | + # dim1*dim2*...*dim6 struct_mult_codomain_sec*struct_mult_domain_sec + # + return reshape(projector, (irrep_dims_prod, :)) +end + +function intersect_trees( + codomain_tree_tensors::Vector{<:AbstractArray{<:Real}}, + domain_tree_tensors::Vector{<:AbstractArray{<:Real}}, +) + kept_indices = findall( + .!isempty.(codomain_tree_tensors) .* .!isempty.(domain_tree_tensors) + ) + return codomain_tree_tensors[kept_indices] .=> domain_tree_tensors[kept_indices] +end + +function overlap_fusion_trees( + old_codomain_block_trees::Vector{<:AbstractArray{<:Real}}, + old_domain_block_trees::Vector{<:AbstractArray{<:Real}}, + new_codomain_block_trees::Vector{<:AbstractArray{<:Real}}, + new_domain_block_trees::Vector{<:AbstractArray{<:Real}}, + irreps_perm::NTuple{N,Int}, +) where {N} + # filter trees that do not share the same sectors + old_trees = intersect_trees(old_codomain_block_trees, old_domain_block_trees) + new_trees = intersect_trees(new_codomain_block_trees, new_domain_block_trees) + return overlap_filtered_fusion_trees(old_trees, new_trees, irreps_perm) +end + +function overlap_filtered_fusion_trees( + old_trees::Vector{<:Pair{<:AbstractArray{<:Real},<:AbstractArray{<:Real}}}, + new_trees::Vector{<:Pair{<:AbstractArray{<:Real},<:AbstractArray{<:Real}}}, + irreps_perm::NTuple{N,Int}, +) where {N} + old_codomain_block_trees = first.(old_trees) + old_domain_block_trees = last.(old_trees) + new_codomain_block_trees = first.(new_trees) + new_domain_block_trees = last.(new_trees) + # compile time + + OldNCoAxes = ndims(eltype(old_codomain_block_trees)) - 2 + OldNDoAxes = ndims(eltype(old_domain_block_trees)) - 2 + NCoAxesNew = ndims(eltype(new_codomain_block_trees)) - 2 + NDoAxesNew = ndims(eltype(new_domain_block_trees)) - 2 + @assert OldNCoAxes + OldNDoAxes == N + @assert NCoAxesNew + NDoAxesNew == N + @assert N > 0 + + # initialize output as a BlockArray with + # blocksize: (n_new_sectors, n_old_sectors) + # blocksizes: (struct_mult_new_block_sectors, struct_mult_old_block_sectors) + block_rows = + size.(new_codomain_block_trees, NCoAxesNew + 2) .* + size.(new_domain_block_trees, NDoAxesNew + 2) + block_cols = + size.(old_codomain_block_trees, OldNCoAxes + 2) .* + size.(old_domain_block_trees, OldNDoAxes + 2) + unitary = BlockArray{Float64}(undef, block_rows, block_cols) + + # contract codomain and domain fusion trees to construct projector on each allowed sector + new_projectors = + contract_singlet_space_projector.( + new_codomain_block_trees, new_domain_block_trees, Ref(ntuple(identity, N)) + ) + + for j in eachindex(block_cols) + old_proj = contract_singlet_space_projector( + old_codomain_block_trees[j], old_domain_block_trees[j], irreps_perm + ) + for (i, new_proj) in enumerate(new_projectors) + + # Contract new and old projectors to construct singlet space basis change matrix. + # Construction is blockwise. One block (i,j) corresponds to fusing old axes + # over sector sec_j and new axes over sector sec_i. It has a 4-dim tensor + # internal structure which is reshaped into a matrix. + # + # --------------dim_sec_j--------- + # | | + # | struct_mult | struct_mult_old_domain_sec_j + # \ / _old_codomain_sec_j \ / + # \/ \/ + # / / + # /\ /\ + # / \ / \ + # /\ \ /\ \ + # / \ \ / \ \ + # dim1 dim2 dim3 dim4 dim5 dim6 + # | | | | | | + # ----------------- irreps_perm ------------ + # | | | | | | + # dim4 dim1 dim2 dim6 dim3 dim5 + # \ / / / \ / + # \ / / / \ / + # \ / / \ + # \ / / \ + # \ / \ + # \ / \ + # \/ \ + # \ \ + # /\ /\ + # / \ / \ + # | struct_mult | struct_mult_new_domain_sec_i + # | _old_codomain_sec_j | + # | | + # -------------dim_sec_i--------- + # + dim_sec_i = size(new_codomain_block_trees[i], NCoAxesNew + 1) + unitary[Block(i, j)] = (new_proj'old_proj) / dim_sec_i + end + end + + return unitary +end + +function compute_unitaries_clebsch_gordan( + old_codomain_legs::NTuple{OldNDoAxes,AbstractGradedUnitRange}, + old_domain_legs::NTuple{OldNCoAxes,AbstractGradedUnitRange}, + biperm::BlockedPermutation{2,N}, +) where {OldNDoAxes,OldNCoAxes,N} + @assert OldNDoAxes + OldNCoAxes == N + + new_codomain_legs, nondual_new_domain_legs = TensorAlgebra.blockpermute( + (old_codomain_legs..., old_domain_legs...), biperm + ) + new_domain_legs = dual.(nondual_new_domain_legs) + new_row_labels = blocklabels(fusion_product(new_codomain_legs...)) + new_column_labels = blocklabels(fusion_product(new_domain_legs...)) + new_allowed_sectors = new_row_labels[first.( + find_shared_indices(new_row_labels, new_column_labels) + )] + + # TBD use FusedAxes as input? + old_codomain_fused_axes = FusedAxes(old_codomain_legs) + old_domain_fused_axes = FusedAxes(dual.(old_domain_legs)) + old_matrix_block_indices = intersect(old_codomain_fused_axes, old_domain_fused_axes) + old_allowed_sectors = blocklabels(old_codomain_fused_axes)[first.( + old_matrix_block_indices + )] + old_allowed_outer_blocks = allowed_outer_blocks_sectors( + old_codomain_fused_axes, old_domain_fused_axes, old_matrix_block_indices + ) + + # initialize output + unitaries = Dict{ + NTuple{N,Int64}, + BlockMatrix{ # TBD use BlockSparseArray 4-dim? + Float64, + Matrix{Matrix{Float64}}, + Tuple{BlockedOneTo{Int64,Vector{Int64}},BlockedOneTo{Int64,Vector{Int64}}}, + }, + }() + + # cache computed Clebsch-Gordan trees. + old_codomain_tree_tensors_cache = Dict{ + NTuple{OldNDoAxes,Int},Vector{Array{Float64,OldNDoAxes + 2}} + }() + old_domain_tree_tensors_cache = Dict{ + NTuple{OldNCoAxes,Int},Vector{Array{Float64,OldNCoAxes + 2}} + }() + new_codomain_tree_tensors_cache = Dict{ + NTuple{length(new_codomain_legs),Int}, + Vector{Array{Float64,length(new_codomain_legs) + 2}}, + }() + new_domain_tree_tensors_cache = Dict{ + NTuple{length(new_domain_legs),Int},Vector{Array{Float64,length(new_domain_legs) + 2}} + }() + + flat_permutation = Tuple(biperm) + + # loop over all allowed outer blocks. + for (old_outer_block, _) in old_allowed_outer_blocks + ukey = unitary_key(old_codomain_legs, old_domain_legs, old_outer_block, biperm) + u = get!(unitary_cache, ukey) do + new_codomain_outer_block, new_domain_outer_block = blockpermute(old_outer_block, biperm) + old_codomain_block_trees = get_fusion_tree_tensors!( + old_codomain_tree_tensors_cache, + old_outer_block[begin:OldNDoAxes], + old_codomain_legs, + old_allowed_sectors, + ) + old_domain_block_trees = get_fusion_tree_tensors!( + old_domain_tree_tensors_cache, + old_outer_block[(OldNDoAxes + 1):end], + dual.(old_domain_legs), + old_allowed_sectors, + ) + new_codomain_block_trees = get_fusion_tree_tensors!( + new_codomain_tree_tensors_cache, + new_codomain_outer_block, + new_codomain_legs, + new_allowed_sectors, + ) + new_domain_block_trees = get_fusion_tree_tensors!( + new_domain_tree_tensors_cache, + new_domain_outer_block, + new_domain_legs, + new_allowed_sectors, + ) + + u = overlap_fusion_trees( + old_codomain_block_trees, + old_domain_block_trees, + new_codomain_block_trees, + new_domain_block_trees, + flat_permutation, + ) + return u + end + unitaries[old_outer_block] = u + end + + return unitaries +end + +# ================================= Constructor from 6j ================================== +# dummy diff --git a/test/runtests.jl b/test/runtests.jl index 3c16373..5bf73eb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,12 +1,12 @@ @eval module $(gensym()) -using Test: @testset - -@testset "FusionTensors.jl" begin +using Test: @test, @testset +@testset "$(@__DIR__)" begin filenames = filter(readdir(@__DIR__)) do f startswith("test_")(f) && endswith(".jl")(f) end - @testset "Test $filename" for filename in filenames - include(filename) + @testset "Test $(@__DIR__)/$filename" for filename in filenames + println("Running $(@__DIR__)/$filename") + @time include(filename) end end end diff --git a/test/test_array_cast.jl b/test/test_array_cast.jl new file mode 100644 index 0000000..333bc81 --- /dev/null +++ b/test/test_array_cast.jl @@ -0,0 +1,399 @@ +@eval module $(gensym()) +using LinearAlgebra: LinearAlgebra, norm +using Test: @test, @testset, @test_broken + +using BlockArrays: Block, blocksize + +using FusionTensors: FusionTensor, check_sanity, data_matrix +using GradedUnitRanges: dual, fusion_product, gradedrange +using SymmetrySectors: O2, SectorProduct, SU2, TrivialSector, U1 + +@testset "Trivial FusionTensor" begin + @testset "trivial matrix" begin + g = gradedrange([TrivialSector() => 1]) + gb = dual(g) + m = ones((1, 1)) + ft = FusionTensor(m, (g,), (gb,)) + @test size(data_matrix(ft)) == (1, 1) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[1, 1] ≈ 1.0 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ m + @test Array(adjoint(ft)) ≈ m + + for elt in (Int, UInt32, Float32) + m = ones(elt, (1, 1)) + ft = FusionTensor(m, (g,), (gb,)) + @test eltype(ft) === Float64 + @test Array(ft) ≈ m + end + + for elt in (ComplexF32, ComplexF64) + m = ones(elt, (1, 1)) + ft = FusionTensor(m, (g,), (gb,)) + @test eltype(ft) === ComplexF64 + @test Array(ft) ≈ m + end + end + + @testset "several axes, one block" begin + g1 = gradedrange([TrivialSector() => 2]) + g2 = gradedrange([TrivialSector() => 3]) + g3 = gradedrange([TrivialSector() => 4]) + g4 = gradedrange([TrivialSector() => 2]) + domain_legs = (g1, g2) + codomain_legs = dual.((g3, g4)) + t = convert.(Float64, reshape(collect(1:48), (2, 3, 4, 2))) + ft = FusionTensor(t, domain_legs, codomain_legs) + @test size(data_matrix(ft)) == (6, 8) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[Block(1, 1)] ≈ reshape(t, (6, 8)) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ t + @test Array(adjoint(ft)) ≈ permutedims(t, (3, 4, 1, 2)) + end +end + +@testset "Abelian FusionTensor" begin + @testset "trivial matrix" begin + g = gradedrange([U1(0) => 1]) + gb = dual(g) + m = ones((1, 1)) + ft = FusionTensor(m, (g,), (gb,)) + @test size(data_matrix(ft)) == (1, 1) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[1, 1] ≈ 1.0 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ m + @test Array(adjoint(ft)) ≈ m + end + + @testset "non self-conjugate matrix" begin + g = gradedrange([U1(1) => 2]) + gb = dual(g) + m = ones((2, 2)) + ft = FusionTensor(m, (g,), (gb,)) + @test size(data_matrix(ft)) == (2, 2) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[Block(1, 1)] ≈ m + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ m + @test Array(adjoint(ft)) ≈ m + end + + @testset "2-block identity" begin + g = gradedrange([U1(1) => 1, U1(2) => 2]) + domain_legs = (g,) + codomain_legs = (dual(g),) + dense = Array{Float64}(LinearAlgebra.I(3)) + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test size(data_matrix(ft)) == (3, 3) + @test blocksize(data_matrix(ft)) == (2, 2) + @test data_matrix(ft)[Block(1, 1)] ≈ ones((1, 1)) + @test data_matrix(ft)[Block(2, 2)] ≈ LinearAlgebra.I(2) + @test data_matrix(ft) ≈ dense + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ dense + @test Array(adjoint(ft)) ≈ adjoint(dense) + end + + @testset "several axes, one block" begin + g1 = gradedrange([U1(1) => 2]) + g2 = gradedrange([U1(2) => 3]) + g3 = gradedrange([U1(3) => 4]) + g4 = gradedrange([U1(0) => 2]) + domain_legs = dual.((g1, g2)) + codomain_legs = (g3, g4) + t = convert.(Float64, reshape(collect(1:48), (2, 3, 4, 2))) + ft = FusionTensor(t, domain_legs, codomain_legs) + @test size(data_matrix(ft)) == (6, 8) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[Block(1, 1)] ≈ reshape(t, (6, 8)) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ t + @test Array(adjoint(ft)) ≈ permutedims(t, (3, 4, 1, 2)) + end + + @testset "several axes, several blocks" begin + g1 = gradedrange([U1(1) => 2, U1(2) => 2]) + g2 = gradedrange([U1(2) => 3, U1(3) => 2]) + g3 = gradedrange([U1(3) => 4, U1(4) => 1]) + g4 = gradedrange([U1(0) => 2, U1(2) => 1]) + domain_legs = (g1, g2) + codomain_legs = dual.((g3, g4)) + dense = zeros((4, 5, 5, 3)) + dense[1:2, 1:3, 1:4, 1:2] .= 1.0 + dense[3:4, 1:3, 5:5, 1:2] .= 2.0 + dense[1:2, 4:5, 5:5, 1:2] .= 3.0 + dense[3:4, 4:5, 1:4, 3:3] .= 4.0 + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test size(data_matrix(ft)) == (20, 15) + @test blocksize(data_matrix(ft)) == (3, 4) + @test norm(ft) ≈ norm(dense) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ dense + @test Array(adjoint(ft)) ≈ permutedims(dense, (3, 4, 1, 2)) + end + + @testset "mixing dual and nondual" begin + g1 = gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1]) + g2 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g3 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) + g4 = gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1]) + domain_legs = (g1,) + codomain_legs = (dual(g2), dual(g3), g4) + dense = zeros(ComplexF64, (3, 6, 5, 4)) + dense[2:2, 1:1, 1:2, 2:3] .= 1.0im + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test size(data_matrix(ft)) == (3, 120) + @test blocksize(data_matrix(ft)) == (3, 8) + @test norm(ft) ≈ norm(dense) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ dense + @test Array(adjoint(ft)) ≈ conj(permutedims(dense, (2, 3, 4, 1))) + end + + @testset "Less than 2 axes" begin + g = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + v = zeros((6,)) + v[1] = 1.0 + + ft1 = FusionTensor(v, (g,), ()) + @test isnothing(check_sanity(ft1)) + @test ndims(ft1) == 1 + @test vec(Array(data_matrix(ft1))) ≈ v + @test Array(ft1) ≈ v + @test Array(adjoint(ft1)) ≈ v + + ft2 = FusionTensor(v, (), (dual(g),)) + @test isnothing(check_sanity(ft2)) + @test ndims(ft2) == 1 + @test vec(Array(data_matrix(ft2))) ≈ v + @test Array(ft2) ≈ v + @test Array(adjoint(ft2)) ≈ v + + ft3 = FusionTensor(v, (dual(g),), ()) + @test isnothing(check_sanity(ft3)) + @test Array(ft3) ≈ v + @test Array(adjoint(ft3)) ≈ v + + ft4 = FusionTensor(v, (), (g,)) + @test isnothing(check_sanity(ft4)) + @test Array(ft4) ≈ v + @test Array(adjoint(ft4)) ≈ v + + zerodim = ones(()) + if VERSION < v"1.11" + @test_broken FusionTensor(zerodim, (), ()) isa FusionTensor # https://github.com/JuliaLang/julia/issues/52615 + else + ft = FusionTensor(zerodim, (), ()) + @test ft isa FusionTensor + @test ndims(ft) == 0 + @test isnothing(check_sanity(ft)) + @test size(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[1, 1] ≈ 1.0 + @test_broken Array(ft) ≈ zerodim # cannot create zerodim BlockSparseArray + end + end +end + +@testset "O(2) FusionTensor" begin + @testset "trivial matrix" begin + g = gradedrange([O2(0) => 1]) + gb = dual(g) + m = ones((1, 1)) + ft = FusionTensor(m, (gb,), (g,)) + @test size(data_matrix(ft)) == (1, 1) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[1, 1] ≈ 1.0 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ m + @test Array(adjoint(ft)) ≈ m + end + + @testset "spin 1/2 S.S" begin + g2 = gradedrange([O2(1//2) => 1]) + g2b = dual(g2) + + # identity + id2 = LinearAlgebra.I((2)) + ft = FusionTensor(id2, (g2b,), (g2,)) + @test norm(ft) ≈ √2 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ id2 + @test Array(adjoint(ft)) ≈ id2 + + # S⋅S + sds22 = reshape( + [ + [0.25, 0.0, 0.0, 0.0] + [0.0, -0.25, 0.5, 0.0] + [0.0, 0.5, -0.25, 0.0] + [0.0, 0.0, 0.0, 0.25] + ], + (2, 2, 2, 2), + ) + dense, domain_legs, codomain_legs = sds22, (g2b, g2b), (g2, g2) + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test norm(ft) ≈ √3 / 2 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ sds22 + @test Array(adjoint(ft)) ≈ sds22 + + # dual over one spin. This changes the dense coefficients but not the FusionTensor ones + sds22b = reshape( + [ + [-0.25, 0.0, 0.0, -0.5] + [0.0, 0.25, 0.0, 0.0] + [0.0, 0.0, 0.25, 0.0] + [-0.5, 0.0, 0.0, -0.25] + ], + (2, 2, 2, 2), + ) + sds22b_codomain_legs = (g2, g2b) + dense, domain_legs, codomain_legs = sds22b, (g2, g2b), (g2b, g2) + ftb = FusionTensor(dense, domain_legs, codomain_legs) + @test norm(ftb) ≈ √3 / 2 + @test isnothing(check_sanity(ft)) + @test Array(ftb) ≈ sds22b + @test Array(adjoint(ftb)) ≈ sds22b + + # no codomain axis + dense, domain_legs, codomain_legs = sds22, (g2b, g2b, g2, g2), () + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ sds22 + @test Array(adjoint(ft)) ≈ sds22 + + # no domain axis + dense, domain_legs, codomain_legs = sds22, (), (g2b, g2b, g2, g2) + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ sds22 + @test Array(adjoint(ft)) ≈ sds22 + end +end + +@testset "SU(2) FusionTensor" begin + @testset "trivial matrix" begin + g = gradedrange([SU2(0) => 1]) + gb = dual(g) + m = ones((1, 1)) + ft = FusionTensor(m, (gb,), (g,)) + @test size(data_matrix(ft)) == (1, 1) + @test blocksize(data_matrix(ft)) == (1, 1) + @test data_matrix(ft)[1, 1] ≈ 1.0 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ m + @test Array(adjoint(ft)) ≈ m + end + + @testset "spin 1/2 S.S" begin + g2 = gradedrange([SU2(1 / 2) => 1]) + g2b = dual(g2) + + # identity + id2 = LinearAlgebra.I((2)) + ft = FusionTensor(id2, (g2b,), (g2,)) + @test norm(ft) ≈ √2 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ id2 + @test Array(adjoint(ft)) ≈ id2 + + # S⋅S + sds22 = reshape( + [ + [0.25, 0.0, 0.0, 0.0] + [0.0, -0.25, 0.5, 0.0] + [0.0, 0.5, -0.25, 0.0] + [0.0, 0.0, 0.0, 0.25] + ], + (2, 2, 2, 2), + ) + dense, domain_legs, codomain_legs = sds22, (g2b, g2b), (g2, g2) + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test norm(ft) ≈ √3 / 2 + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ sds22 + @test Array(adjoint(ft)) ≈ sds22 + + # dual over one spin. This changes the dense coefficients but not the FusionTensor ones + sds22b = reshape( + [ + [-0.25, 0.0, 0.0, -0.5] + [0.0, 0.25, 0.0, 0.0] + [0.0, 0.0, 0.25, 0.0] + [-0.5, 0.0, 0.0, -0.25] + ], + (2, 2, 2, 2), + ) + sds22b_codomain_legs = (g2, g2b) + dense, domain_legs, codomain_legs = sds22b, (g2, g2b), (g2b, g2) + ftb = FusionTensor(dense, domain_legs, codomain_legs) + @test norm(ftb) ≈ √3 / 2 + @test isnothing(check_sanity(ft)) + @test Array(ftb) ≈ sds22b + @test Array(adjoint(ftb)) ≈ sds22b + + # no codomain axis + dense, domain_legs, codomain_legs = sds22, (g2b, g2b, g2, g2), () + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ sds22 + @test Array(adjoint(ft)) ≈ sds22 + + # no domain axis + dense, domain_legs, codomain_legs = sds22, (), (g2b, g2b, g2, g2) + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ sds22 + @test Array(adjoint(ft)) ≈ sds22 + end + + @testset "large identity" begin + g = reduce(fusion_product, (SU2(1 / 2), SU2(1 / 2), SU2(1 / 2))) + N = 3 + codomain_legs = ntuple(_ -> g, N) + domain_legs = dual.(codomain_legs) + d = 8 + dense = reshape(LinearAlgebra.I(d^N), ntuple(_ -> d, 2 * N)) + ft = FusionTensor(dense, domain_legs, codomain_legs) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ dense + @test Array(adjoint(ft)) ≈ dense + end +end + +@testset "U(1)×SU(2) FusionTensor" begin + for d in 1:6 # any spin dimension + s = SU2((d - 1)//2) # d = 2s+1 + D = d + 1 + tRVB = zeros((d, D, D, D, D)) # tensor RVB SU(2) for spin s + for i in 1:d + tRVB[i, i + 1, 1, 1, 1] = 1.0 + tRVB[i, 1, i + 1, 1, 1] = 1.0 + tRVB[i, 1, 1, i + 1, 1] = 1.0 + tRVB[i, 1, 1, 1, i + 1] = 1.0 + end + + gd = gradedrange([SectorProduct(s, U1(3)) => 1]) + domain_legs = (dual(gd),) + gD = gradedrange([SectorProduct(SU2(0), U1(1)) => 1, SectorProduct(s, U1(0)) => 1]) + codomain_legs = (gD, gD, gD, gD) + ft = FusionTensor(tRVB, domain_legs, codomain_legs) + @test isnothing(check_sanity(ft)) + @test Array(ft) ≈ tRVB + + # same with NamedTuples + gd_nt = gradedrange([SectorProduct(; S=s, N=U1(3)) => 1]) + domain_legs_nt = (dual(gd_nt),) + gD_nt = gradedrange([ + SectorProduct(; S=SU2(0), N=U1(1)) => 1, SectorProduct(; S=s, N=U1(0)) => 1 + ]) + codomain_legs_nt = (gD_nt, gD_nt, gD_nt, gD_nt) + ft_nt = FusionTensor(tRVB, domain_legs_nt, codomain_legs_nt) + @test isnothing(check_sanity(ft_nt)) + @test Array(ft_nt) ≈ tRVB + end +end +end diff --git a/test/test_basics.jl b/test/test_basics.jl index 4eeedd4..4225fa5 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,8 +1,235 @@ @eval module $(gensym()) -using FusionTensors: FusionTensors using Test: @test, @testset -@testset "FusionTensors" begin - # Tests go here. +using BlockSparseArrays: BlockSparseArray +using FusionTensors: + FusionTensor, + domain_axes, + codomain_axes, + data_matrix, + matching_axes, + matching_dual, + matrix_column_axis, + matrix_row_axis, + matrix_size, + ndims_domain, + ndims_codomain, + check_sanity +using GradedUnitRanges: + blockmergesort, dual, flip, fusion_product, gradedrange, space_isequal +using SymmetrySectors: U1 + +@testset "Fusion matrix" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g2 = dual(gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1])) + + # check dual convention when initializing data_matrix + ft0 = FusionTensor(Float64, (g1,), (g2,)) + @test space_isequal(matrix_row_axis(ft0), g1) + @test space_isequal(matrix_column_axis(ft0), g2) + + m = BlockSparseArray{Float64}(g1, g2) + ft1 = FusionTensor(m, (g1,), (g2,)) + + # getters + @test data_matrix(ft1) === m + @test matching_axes(codomain_axes(ft1), (g1,)) + @test matching_axes(domain_axes(ft1), (g2,)) + + # misc + @test matching_axes(axes(ft1), (g1, g2)) + @test ndims_codomain(ft1) == 1 + @test ndims_domain(ft1) == 1 + @test matrix_size(ft1) == (6, 5) + @test space_isequal(matrix_row_axis(ft1), g1) + @test space_isequal(matrix_column_axis(ft1), g2) + @test isnothing(check_sanity(ft0)) + @test isnothing(check_sanity(ft1)) + + # Base methods + @test eltype(ft1) === Float64 + @test length(ft1) == 30 + @test ndims(ft1) == 2 + @test size(ft1) == (6, 5) + + # copy + ft2 = copy(ft1) + @test isnothing(check_sanity(ft2)) + @test ft2 !== ft1 + @test data_matrix(ft2) == data_matrix(ft1) + @test data_matrix(ft2) !== data_matrix(ft1) + @test matching_axes(codomain_axes(ft2), codomain_axes(ft1)) + @test matching_axes(domain_axes(ft2), domain_axes(ft1)) + + ft2 = deepcopy(ft1) + @test ft2 !== ft1 + @test data_matrix(ft2) == data_matrix(ft1) + @test data_matrix(ft2) !== data_matrix(ft1) + @test matching_axes(codomain_axes(ft2), codomain_axes(ft1)) + @test matching_axes(domain_axes(ft2), domain_axes(ft1)) + + # similar + ft2 = similar(ft1) + @test isnothing(check_sanity(ft2)) + @test eltype(ft2) == Float64 + @test matching_axes(codomain_axes(ft2), codomain_axes(ft1)) + @test matching_axes(domain_axes(ft2), domain_axes(ft1)) + + ft3 = similar(ft1, ComplexF64) + @test isnothing(check_sanity(ft3)) + @test eltype(ft3) == ComplexF64 + @test matching_axes(codomain_axes(ft3), codomain_axes(ft1)) + @test matching_axes(domain_axes(ft3), domain_axes(ft1)) + + ft4 = similar(ft1, Float32) + @test eltype(ft4) == Float64 # promoted + + ft5 = similar(ft1, ComplexF32, ((g1, g1), (g2,))) + @test isnothing(check_sanity(ft5)) + @test eltype(ft5) == ComplexF64 + @test matching_axes(codomain_axes(ft5), (g1, g1)) + @test matching_axes(domain_axes(ft5), (g2,)) +end + +@testset "More than 2 axes" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g2 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) + g3 = dual(gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1])) + g4 = dual(gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1])) + gr = fusion_product(g1, g2) + gc = dual(fusion_product(dual(g3), dual(g4))) + m2 = BlockSparseArray{Float64}(gr, gc) + ft = FusionTensor(m2, (g1, g2), (g3, g4)) + + @test data_matrix(ft) === m2 + @test matching_axes(codomain_axes(ft), (g1, g2)) + @test matching_axes(domain_axes(ft), (g3, g4)) + + @test axes(ft) == (g1, g2, g3, g4) + @test ndims_codomain(ft) == 2 + @test ndims_domain(ft) == 2 + @test matrix_size(ft) == (30, 12) + @test space_isequal(matrix_row_axis(ft), gr) + @test space_isequal(matrix_column_axis(ft), gc) + @test isnothing(check_sanity(ft)) + + @test ndims(ft) == 4 + @test size(ft) == (6, 5, 4, 3) +end + +@testset "Less than 2 axes" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + + # one row axis + ft1 = FusionTensor(Float64, (g1,), ()) + @test ndims_codomain(ft1) == 1 + @test ndims_domain(ft1) == 0 + @test ndims(ft1) == 1 + @test size(ft1) == (6,) + @test size(data_matrix(ft1)) == (6, 1) + @test isnothing(check_sanity(ft1)) + + # one column axis + ft2 = FusionTensor(Float64, (), (g1,)) + @test ndims_codomain(ft2) == 0 + @test ndims_domain(ft2) == 1 + @test ndims(ft2) == 1 + @test size(ft2) == (6,) + @test size(data_matrix(ft2)) == (1, 6) + @test isnothing(check_sanity(ft2)) + + # zero axis + ft3 = FusionTensor(Float64, (), ()) + @test ndims_codomain(ft3) == 0 + @test ndims_domain(ft3) == 0 + @test ndims(ft3) == 0 + @test size(ft3) == () + @test size(data_matrix(ft3)) == (1, 1) + @test isnothing(check_sanity(ft3)) +end + +@testset "Base operations" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g2 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) + g3 = gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1]) + g4 = gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1]) + ft3 = FusionTensor(Float64, (g1, g2), (g3, g4)) + @test isnothing(check_sanity(ft3)) + + ft4 = +ft3 + @test ft4 === ft3 # same object + + ft4 = -ft3 + @test isnothing(check_sanity(ft4)) + @test codomain_axes(ft4) === codomain_axes(ft3) + @test domain_axes(ft4) === domain_axes(ft3) + + ft4 = ft3 + ft3 + @test codomain_axes(ft4) === codomain_axes(ft3) + @test domain_axes(ft4) === domain_axes(ft3) + @test space_isequal(matrix_row_axis(ft4), matrix_row_axis(ft3)) + @test space_isequal(matrix_column_axis(ft4), matrix_column_axis(ft3)) + @test isnothing(check_sanity(ft4)) + + ft4 = ft3 - ft3 + @test codomain_axes(ft4) === codomain_axes(ft3) + @test domain_axes(ft4) === domain_axes(ft3) + @test space_isequal(matrix_row_axis(ft4), matrix_row_axis(ft3)) + @test space_isequal(matrix_column_axis(ft4), matrix_column_axis(ft3)) + @test isnothing(check_sanity(ft4)) + + ft4 = 2 * ft3 + @test codomain_axes(ft4) === codomain_axes(ft3) + @test domain_axes(ft4) === domain_axes(ft3) + @test space_isequal(matrix_row_axis(ft4), matrix_row_axis(ft3)) + @test space_isequal(matrix_column_axis(ft4), matrix_column_axis(ft3)) + @test isnothing(check_sanity(ft4)) + @test eltype(ft4) == Float64 + + ft4 = 2.0 * ft3 + @test codomain_axes(ft4) === codomain_axes(ft3) + @test domain_axes(ft4) === domain_axes(ft3) + @test space_isequal(matrix_row_axis(ft4), matrix_row_axis(ft3)) + @test space_isequal(matrix_column_axis(ft4), matrix_column_axis(ft3)) + @test isnothing(check_sanity(ft4)) + @test eltype(ft4) == Float64 + + ft4 = ft3 / 2.0 + @test codomain_axes(ft4) === codomain_axes(ft3) + @test domain_axes(ft4) === domain_axes(ft3) + @test space_isequal(matrix_row_axis(ft4), matrix_row_axis(ft3)) + @test space_isequal(matrix_column_axis(ft4), matrix_column_axis(ft3)) + @test isnothing(check_sanity(ft4)) + @test eltype(ft4) == Float64 + + ft5 = 2.0im * ft3 + @test codomain_axes(ft5) === codomain_axes(ft3) + @test domain_axes(ft5) === domain_axes(ft3) + @test space_isequal(matrix_row_axis(ft5), matrix_row_axis(ft3)) + @test space_isequal(matrix_column_axis(ft5), matrix_column_axis(ft3)) + @test isnothing(check_sanity(ft4)) + @test eltype(ft5) == ComplexF64 + + ft4 = conj(ft3) + @test ft4 === ft3 # same object + + ft6 = conj(ft5) + @test ft6 !== ft5 # different object + @test isnothing(check_sanity(ft6)) + @test codomain_axes(ft6) === codomain_axes(ft5) + @test domain_axes(ft6) === domain_axes(ft5) + @test space_isequal(matrix_row_axis(ft6), matrix_row_axis(ft5)) + @test space_isequal(matrix_column_axis(ft6), matrix_column_axis(ft5)) + @test eltype(ft6) == ComplexF64 + + ad = adjoint(ft3) + @test ad isa FusionTensor + @test ndims_codomain(ad) == 2 + @test ndims_domain(ad) == 2 + @test space_isequal(dual(g1), domain_axes(ad)[1]) + @test space_isequal(dual(g2), domain_axes(ad)[2]) + @test space_isequal(dual(g3), codomain_axes(ad)[1]) + @test space_isequal(dual(g4), codomain_axes(ad)[2]) + @test isnothing(check_sanity(ad)) end end diff --git a/test/test_contraction.jl b/test/test_contraction.jl new file mode 100644 index 0000000..7e799b6 --- /dev/null +++ b/test/test_contraction.jl @@ -0,0 +1,67 @@ +@eval module $(gensym()) +using LinearAlgebra: LinearAlgebra +using Test: @test, @testset, @test_broken + +using BlockSparseArrays: BlockSparseArray +using FusionTensors: FusionTensor, domain_axes, codomain_axes, check_sanity +using GradedUnitRanges: dual, gradedrange +using SymmetrySectors: U1 +using TensorAlgebra: contract + +@testset "contraction" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g2 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) + g3 = gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1]) + g4 = gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1]) + + ft1 = FusionTensor(Float64, (g1, g2), (g3, g4)) + @test isnothing(check_sanity(ft1)) + + ft2 = FusionTensor(Float64, dual.((g3, g4)), (g1,)) + @test isnothing(check_sanity(ft2)) + + ft3 = ft1 * ft2 # tensor contraction + @test isnothing(check_sanity(ft3)) + @test domain_axes(ft3) === domain_axes(ft1) + @test codomain_axes(ft3) === codomain_axes(ft2) + + # test LinearAlgebra.mul! with in-place matrix product + LinearAlgebra.mul!(ft3, ft1, ft2) + @test isnothing(check_sanity(ft3)) + @test domain_axes(ft3) === domain_axes(ft1) + @test codomain_axes(ft3) === codomain_axes(ft2) + + LinearAlgebra.mul!(ft3, ft1, ft2, 1.0, 1.0) + @test isnothing(check_sanity(ft2)) + @test domain_axes(ft3) === domain_axes(ft1) + @test codomain_axes(ft3) === codomain_axes(ft2) +end + +@testset "TensorAlgebra interface" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g2 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) + g3 = gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1]) + g4 = gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1]) + + ft1 = FusionTensor(Float64, (g1, g2), (g3, g4)) + ft2 = FusionTensor(Float64, dual.((g3, g4)), (dual(g1),)) + ft3 = FusionTensor(Float64, dual.((g3, g4)), dual.((g1, g2))) + + ft4, legs = contract(ft1, (1, 2, 3, 4), ft2, (3, 4, 5)) + @test legs == (1, 2, 5) + @test isnothing(check_sanity(ft4)) + @test domain_axes(ft4) === domain_axes(ft1) + @test codomain_axes(ft4) === codomain_axes(ft2) + + ft5 = contract((1, 2, 5), ft1, (1, 2, 3, 4), ft2, (3, 4, 5)) + @test isnothing(check_sanity(ft5)) + + # biperm is not allowed + @test_broken contract(((1, 2), (5,)), ft1, (1, 2, 3, 4), ft2, (3, 4, 5)) + + # issue with 0 axis + @test_broken permutedims(ft1, (), (1, 2, 3, 4)) * permutedims(ft3, (3, 4, 1, 2), ()) isa + FusionTensor{Float64,0} + @test_broken contract(ft1, (1, 2, 3, 4), ft3, (3, 4, 1, 2)) +end +end diff --git a/test/test_fusion_trees.jl b/test/test_fusion_trees.jl new file mode 100644 index 0000000..fe83376 --- /dev/null +++ b/test/test_fusion_trees.jl @@ -0,0 +1,100 @@ +@eval module $(gensym()) +using Test: @test, @testset +using LinearAlgebra: LinearAlgebra + +using BlockArrays: BlockArrays + +using FusionTensors: fusion_tree_tensors +using GradedUnitRanges: blocklabels, fusion_product +using LabelledNumbers: unlabel +using SymmetrySectors: SectorProduct, SU, SU2, TrivialSector, U1, Z, quantum_dimension + +@testset "Trivial fusion trees" begin + tree_irreps_pairs1 = fusion_tree_tensors((), ()) + @test first.(tree_irreps_pairs1) == [TrivialSector()] + @test last.(tree_irreps_pairs1) == [ones((1, 1))] + + tree_irreps_pairs2 = fusion_tree_tensors( + (TrivialSector(), TrivialSector()), (false, false) + ) + @test first.(tree_irreps_pairs2) == [TrivialSector()] + @test last.(tree_irreps_pairs2) == [ones((1, 1, 1, 1))] + + tree_irreps_pairs3 = fusion_tree_tensors( + (TrivialSector(), TrivialSector(), TrivialSector()), (false, true, true) + ) + @test first.(tree_irreps_pairs3) == [TrivialSector()] + @test last.(tree_irreps_pairs3) == [ones((1, 1, 1, 1, 1))] +end + +@testset "Abelian fusion trees" begin + tree_irreps_pairs1 = fusion_tree_tensors((U1(0), U1(0)), (false, false)) + @test first.(tree_irreps_pairs1) == [U1(0)] + @test last.(tree_irreps_pairs1) == [ones((1, 1, 1, 1))] + + tree_irreps_pairs2 = fusion_tree_tensors((U1(1), U1(1), U1(-2)), (false, false, false)) + @test first.(tree_irreps_pairs2) == [U1(0)] + @test last.(tree_irreps_pairs2) == [ones((1, 1, 1, 1, 1))] + + s1 = SectorProduct(Z{2}(1), U1(0)) + s2 = SectorProduct(Z{2}(1), U1(1)) + tree_irreps_z2u1 = fusion_tree_tensors((s1, s2), (false, false)) + @test first.(tree_irreps_z2u1) == [SectorProduct(Z{2}(0), U1(1))] + @test last.(tree_irreps_z2u1) == [ones((1, 1, 1, 1))] + + s3 = SectorProduct(; A=Z{2}(1), B=U1(0)) + s4 = SectorProduct(; A=Z{2}(1), B=U1(1)) + tree_irreps_nt = fusion_tree_tensors((s3, s4), (false, false)) + @test first.(tree_irreps_nt) == [SectorProduct(; A=Z{2}(0), B=U1(1))] + @test last.(tree_irreps_nt) == [ones((1, 1, 1, 1))] +end + +@testset "SU(2) fusion trees" begin + tree_irreps_pairs1 = fusion_tree_tensors((SU2(0), SU2(0), SU2(0)), (false, false, false)) + @test first.(tree_irreps_pairs1) == [SU2(0)] + @test last.(tree_irreps_pairs1) == [ones((1, 1, 1, 1, 1))] + + tree_irreps_pairs2 = fusion_tree_tensors((SU2(1 / 2),), (false,)) + @test first.(tree_irreps_pairs2) == [SU2(1 / 2)] + @test last.(tree_irreps_pairs2) == [reshape(LinearAlgebra.I(2), (2, 2, 1))] + + tree_irreps_pairs3 = fusion_tree_tensors((SU2(1 / 2),), (true,)) + @test first.(tree_irreps_pairs3) == [SU2(1 / 2)] + @test last.(tree_irreps_pairs3) ≈ [reshape([0, 1, -1, 0], (2, 2, 1))] + + tree_irreps_pairs4 = fusion_tree_tensors( + (SU2(1 / 2), SU2(1 / 2), SU2(1 / 2)), (false, false, false) + ) + @test first.(tree_irreps_pairs4) == [SU2(1 / 2), SU2(3 / 2)] + @test size.(last.(tree_irreps_pairs4)) == [(2, 2, 2, 2, 2), (2, 2, 2, 4, 1)] +end + +@testset "SectorProduct fusion trees" begin + hole = SectorProduct(; N=U1(1), S=SU2(0)) + s12 = SectorProduct(; N=U1(0), S=SU2(1 / 2)) + tree_irreps_pairs1 = fusion_tree_tensors( + (hole, hole, hole, s12), (false, false, false, false) + ) + @test first.(tree_irreps_pairs1) == [SectorProduct(; N=U1(3), S=SU2(1 / 2))] + @test last.(tree_irreps_pairs1) == [reshape(LinearAlgebra.I(2), (1, 1, 1, 2, 2, 1))] + + s3 = SectorProduct(SU((1, 0)), SU((1,)), U1(1)) + irreps = (s3, s3, s3) + arrows = (false, false, false) + tree_irreps_pairs2 = fusion_tree_tensors(irreps, arrows) + tree_irreps, trees = first.(tree_irreps_pairs2), last.(tree_irreps_pairs2) + rep = fusion_product(irreps...) + @test blocklabels(rep) == tree_irreps + @test quantum_dimension.(tree_irreps) == size.(trees, 4) + @test unlabel.(BlockArrays.blocklengths(rep)) == size.(trees, 5) + + s_nt = SectorProduct(; A=SU((1, 0)), B=SU((1,)), C=U1(1)) + irreps_nt = (s_nt, s_nt, s_nt) + tree_irreps_nt_pairs = fusion_tree_tensors(irreps_nt, arrows) + tree_irreps_nt, trees_nt = first.(tree_irreps_nt_pairs), last.(tree_irreps_nt_pairs) + rep_nt = reduce(fusion_product, irreps_nt) + @test blocklabels(rep_nt) == tree_irreps_nt + @test quantum_dimension.(tree_irreps_nt) == size.(trees_nt, 4) + @test unlabel.(BlockArrays.blocklengths(rep_nt)) == size.(trees_nt, 5) +end +end diff --git a/test/test_linear_algebra.jl b/test/test_linear_algebra.jl new file mode 100644 index 0000000..93d2ff3 --- /dev/null +++ b/test/test_linear_algebra.jl @@ -0,0 +1,37 @@ +@eval module $(gensym()) +using LinearAlgebra: norm, tr +using Test: @test, @testset + +using BlockArrays: BlockArrays + +using BlockSparseArrays: BlockSparseArrays +using FusionTensors: FusionTensor, check_sanity +using GradedUnitRanges: dual, gradedrange +using SymmetrySectors: U1, SU2, TrivialSector + +@testset "LinearAlgebra interface" begin + sds22 = [ + 0.25 0.0 0.0 0.0 + 0.0 -0.25 0.5 0.0 + 0.0 0.5 -0.25 0.0 + 0.0 0.0 0.0 0.25 + ] + sdst = reshape(sds22, (2, 2, 2, 2)) + + g0 = gradedrange([TrivialSector() => 2]) + gu1 = gradedrange([U1(1) => 1, U1(-1) => 1]) + gsu2 = gradedrange([SU2(1 / 2) => 1]) + + for g in [g0, gu1, gsu2] + ft0 = FusionTensor(Float64, (g, g), (dual(g), dual(g))) + @test isnothing(check_sanity(ft0)) + @test norm(ft0) == 0 + @test tr(ft0) == 0 + + ft = FusionTensor(sdst, (g, g), (dual(g), dual(g))) + @test isnothing(check_sanity(ft)) + @test norm(ft) ≈ √3 / 2 + @test isapprox(tr(ft), 0; atol=eps(Float64)) + end +end +end diff --git a/test/test_permutedims.jl b/test/test_permutedims.jl new file mode 100644 index 0000000..e4042ae --- /dev/null +++ b/test/test_permutedims.jl @@ -0,0 +1,158 @@ +@eval module $(gensym()) +using Test: @test, @testset, @test_broken + +using FusionTensors: + FusionTensor, + check_sanity, + data_matrix, + matching_axes, + matrix_column_axis, + matrix_row_axis, + naive_permutedims, + ndims_domain, + ndims_codomain +using GradedUnitRanges: dual, gradedrange, space_isequal +using SymmetrySectors: O2, U1, SectorProduct, SU2 +using TensorAlgebra: blockedperm + +@testset "Abelian permutedims" begin + @testset "dummy" begin + g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + g2 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) + g3 = gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1]) + g4 = gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1]) + + for elt in (Float64, ComplexF64) + ft1 = FusionTensor(elt, dual.((g1, g2)), (g3, g4)) + @test isnothing(check_sanity(ft1)) + + # test permutedims interface + ft2 = permutedims(ft1, (1, 2), (3, 4)) # trivial with 2 tuples + @test ft2 === ft1 # same object + + ft2 = permutedims(ft1, ((1, 2), (3, 4))) # trivial with tuple of 2 tuples + @test ft2 === ft1 # same object + + biperm = blockedperm((1, 2), (3, 4)) + ft2 = permutedims(ft1, biperm) # trivial with biperm + @test ft2 === ft1 # same object + + ft3 = permutedims(ft1, (4,), (1, 2, 3)) + @test ft3 !== ft1 + @test ft3 isa FusionTensor{elt,4} + @test matching_axes(axes(ft3), (g4, dual(g1), dual(g2), g3)) + @test ndims_domain(ft3) == 1 + @test ndims_codomain(ft3) == 3 + @test ndims(ft3) == 4 + @test isnothing(check_sanity(ft3)) + + ft4 = permutedims(ft3, (2, 3), (4, 1)) + @test matching_axes(axes(ft1), axes(ft4)) + @test space_isequal(matrix_column_axis(ft1), matrix_column_axis(ft4)) + @test space_isequal(matrix_row_axis(ft1), matrix_row_axis(ft4)) + @test ft4 ≈ ft1 + end + end + + @testset "Many axes" begin + g1 = gradedrange([U1(1) => 2, U1(2) => 2]) + g2 = gradedrange([U1(2) => 3, U1(3) => 2]) + g3 = gradedrange([U1(3) => 4, U1(4) => 1]) + g4 = gradedrange([U1(0) => 2, U1(2) => 1]) + domain_legs = (g1, g2) + codomain_legs = dual.((g3, g4)) + arr = zeros(ComplexF64, (4, 5, 5, 3)) + arr[1:2, 1:3, 1:4, 1:2] .= 1.0im + arr[3:4, 1:3, 5:5, 1:2] .= 2.0 + arr[1:2, 4:5, 5:5, 1:2] .= 3.0 + arr[3:4, 4:5, 1:4, 3:3] .= 4.0 + ft = FusionTensor(arr, domain_legs, codomain_legs) + biperm = blockedperm((3,), (2, 4, 1)) + + ftp = permutedims(ft, biperm) + @test ftp ≈ naive_permutedims(ft, biperm) + ftpp = permutedims(ftp, (4, 2), (1, 3)) + @test ftpp ≈ ft + + ft2 = adjoint(ft) + ftp2 = permutedims(ft2, biperm) + @test ftp2 ≈ naive_permutedims(ft2, biperm) + ftpp2 = permutedims(ftp2, (4, 2), (1, 3)) + @test ftpp2 ≈ ft2 + @test adjoint(ftpp2) ≈ ft + end + + @testset "Less than two axes" begin + if VERSION >= v"1.11" + ft0 = FusionTensor(ones(()), (), ()) + ft0p = permutedims(ft0, (), ()) + @test ft0p isa FusionTensor{Float64,0} + @test data_matrix(ft0p) ≈ data_matrix(ft0) + @test ft0p ≈ ft0 + + @test permutedims(ft0, ((), ())) isa FusionTensor{Float64,0} + @test permutedims(ft0, blockedperm((), ())) isa FusionTensor{Float64,0} + end + + g = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + v = zeros((6,)) + v[1] = 1.0 + biperm = blockedperm((), (1,)) + ft1 = FusionTensor(v, (g,), ()) + @test_broken permutedims(ft1, biperm) isa FusionTensor + #ft2 = permutedims(ft1, biperm) isa FusionTensor + #@test isnothing(check_sanity(ft2)) + #@test ft2 ≈ naive_permutedims(ft1, biperm) + #ft3 = permutedims(ft2, (1,), ()) + #@test ft1 ≈ ft3 + end +end + +@testset "Non-abelian permutedims" begin + sds22 = reshape( + [ + [0.25, 0.0, 0.0, 0.0] + [0.0, -0.25, 0.5, 0.0] + [0.0, 0.5, -0.25, 0.0] + [0.0, 0.0, 0.0, 0.25] + ], + (2, 2, 2, 2), + ) + + sds22b = reshape( + [ + [-0.25, 0.0, 0.0, -0.5] + [0.0, 0.25, 0.0, 0.0] + [0.0, 0.0, 0.25, 0.0] + [-0.5, 0.0, 0.0, -0.25] + ], + (2, 2, 2, 2), + ) + + for g2 in ( + gradedrange([O2(1//2) => 1]), + dual(gradedrange([O2(1//2) => 1])), + gradedrange([SU2(1//2) => 1]), + dual(gradedrange([SU2(1//2) => 1])), + gradedrange([SectorProduct(SU2(1//2), U1(0)) => 1]), + gradedrange([SectorProduct(SU2(1//2), SU2(0)) => 1]), + ) + g2b = dual(g2) + for biperm in [ + blockedperm((2, 1), (3, 4)), blockedperm((3, 1), (2, 4)), blockedperm((3, 1, 4), (2,)) + ] + ft = FusionTensor(sds22, (g2, g2), (g2b, g2b)) + @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) + @test permutedims(adjoint(ft), biperm) ≈ naive_permutedims(adjoint(ft), biperm) + + ft = FusionTensor(sds22b, (g2, g2b), (g2, g2b)) + @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) + @test permutedims(adjoint(ft), biperm) ≈ naive_permutedims(adjoint(ft), biperm) + end + for biperm in [blockedperm((1, 2, 3, 4), ()), blockedperm((), (3, 1, 2, 4))] + ft = FusionTensor(sds22, (g2, g2), (g2b, g2b)) + @test_broken permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) + end + end +end +end diff --git a/test/test_unitaries.jl b/test/test_unitaries.jl new file mode 100644 index 0000000..c47c261 --- /dev/null +++ b/test/test_unitaries.jl @@ -0,0 +1,54 @@ +@eval module $(gensym()) +using Test: @test, @testset, @test_broken +using LinearAlgebra: LinearAlgebra + +using BlockArrays: AbstractBlockMatrix, blocksize + +using FusionTensors: compute_unitaries_clebsch_gordan +using GradedUnitRanges: gradedrange, dual, GradedUnitRanges +using SymmetrySectors: SymmetrySectors, SU, SU2, TrivialSector, U1, Z +using TensorAlgebra: blockedperm, TensorAlgebra + +@testset "Trivial unitaries" begin + function check_trivial_unitary(u) + @test u isa AbstractBlockMatrix + @test blocksize(u) == (1, 1) + @test size(u) == (1, 1) + @test u[1, 1] == 1.0 + end + + g = gradedrange([TrivialSector() => 1]) + old_domain_legs = (g, g) + old_codomain_legs = (g,) + biperm = blockedperm((2,), (3, 1)) + u_d = compute_unitaries_clebsch_gordan(old_domain_legs, old_codomain_legs, biperm) + @test collect(keys(u_d)) == [(1, 1, 1)] + check_trivial_unitary(u_d[1, 1, 1]) + + g = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) + for biperm in [ + blockedperm((1, 2), (3, 4, 5)), + blockedperm((2, 1), (5, 3, 4)), + blockedperm((3, 1, 4, 5), (2,)), + ] + u_d = compute_unitaries_clebsch_gordan((g, g), (dual(g), dual(g), g), biperm) + @test length(u_d) == 45 + check_trivial_unitary.(values(u_d)) + end + for biperm in [blockedperm((), (5, 2, 1, 3, 4)), blockedperm((5, 2, 1, 3, 4), ())] + @test_broken compute_unitaries_clebsch_gordan((g, g), (dual(g), dual(g), g), biperm) isa + Dict + end +end + +@testset "SU(2) unitaries" begin + g12 = gradedrange([SU2(1 / 2) => 1]) + g1 = gradedrange([SU2(1) => 1]) + biperm = blockedperm((2,), (3, 1)) + u_d = compute_unitaries_clebsch_gordan((g12, g12), (g1,), biperm) + @test collect(keys(u_d)) == [(1, 1, 1)] + @test u_d[1, 1, 1] ≈ -√(3 / 2) * ones((1, 1)) +end + +@testset "SectorProduct unitaries" begin end +end From 11a5d88f00d6b207e628f2646bbcdaefc6f3d1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 5 Dec 2024 18:52:59 +0100 Subject: [PATCH 02/52] write intersect_sectors --- src/FusionTensors.jl | 1 + src/fusiontensor/fusedaxes.jl | 68 +++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/FusionTensors.jl b/src/FusionTensors.jl index 7ce6085..d5a3a44 100644 --- a/src/FusionTensors.jl +++ b/src/FusionTensors.jl @@ -8,6 +8,7 @@ using BlockArrays: Block, BlockArray, BlockedArray, + BlockIndexRange, BlockMatrix, blockedrange, blocklength, diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index be2248d..1ac5669 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -35,6 +35,15 @@ function FusedAxes{S}(::Tuple{}) where {S<:AbstractSector} return FusedAxes((), fused_axis, trees_to_ranges_mapping) end +function FusedAxes{S}( + outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} +) where {S<:AbstractSector} + fusion_trees_mult = fusion_trees_external_multiplicites(outer_legs) + + fused_leg, range_mapping = compute_inner_ranges(fusion_trees_mult) + return FusedAxes(outer_legs, fused_leg, range_mapping) +end + function fusion_trees_external_multiplicites( outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} ) @@ -47,15 +56,6 @@ function fusion_trees_external_multiplicites( end end -function FusedAxes{S}( - outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} -) where {S<:AbstractSector} - fusion_trees_mult = fusion_trees_external_multiplicites(outer_legs) - - fused_leg, range_mapping = compute_inner_ranges(fusion_trees_mult) - return FusedAxes(outer_legs, fused_leg, range_mapping) -end - function compute_inner_ranges( fusion_trees_mult::AbstractVector{<:Pair{<:FusionTree,<:Integer}} ) @@ -74,28 +74,36 @@ function compute_inner_ranges( return fused_leg, range_mapping end -function Base.intersect(left::FusedAxes, right::FusedAxes) - left_labels = blocklabels(left) - right_labels = blocklabels(right) - return find_shared_indices(left_labels, right_labels) +function to_blockindexrange(b1::BlockIndexRange{1}, b2::BlockIndexRange{1}) + t = (b1, b2) + return Block(Block.(t))[BlockSparseArrays.to_block_indices.(t)...] end -function allowed_outer_blocks_sectors( - left::FusedAxes, right::FusedAxes, shared_indices::AbstractVector -) - left_labels = blocklabels(left) - left_indices = first.(shared_indices) - right_indices = last.(shared_indices) - @assert left_labels[left_indices] == blocklabels(right)[right_indices] - reduced_left = .!isempty.(index_matrix(left)[:, left_indices]) - reduced_right = .!isempty.(index_matrix(right)[:, right_indices]) - - block_sectors = Dict{NTuple{ndims(left) + ndims(right),Int},Vector{eltype(left_labels)}}() - for i in axes(reduced_left, 1), j in axes(reduced_right, 1) - intersection = findall(>(0), reduced_left[i, :] .* reduced_right[j, :]) - isempty(intersection) && continue - full_block = (unravel_index(i, left)..., unravel_index(j, right)...) - block_sectors[full_block] = left_labels[left_indices[intersection]] +# TBD choose one +function intersect_sectors(left::FusedAxes, right::FusedAxes) + shared_sectors = intersect(blocklabels(left), blocklabels(right)) + blockindexrange_vec = mapreduce(vcat, shared_sectors) do s + codomain_trees = filter(f -> root_sector(f) == s, keys(trees_to_ranges_mapping(left))) + domain_trees = filter(f -> root_sector(f) == s, keys(trees_to_ranges_mapping(right))) + return vec( + collect( + (f1, f2) => to_blockindexrange( + trees_to_ranges_mapping(left)[f1], trees_to_ranges_mapping(left)[f2] + ) for f1 in codomain_trees, f2 in domain_trees + ), + ) end - return block_sectors + return Dict(blockindexrange_vec) +end + +function intersect_sectors2(left::FusedAxes, right::FusedAxes) + return Dict( + map( + t -> first.(t) => to_blockindexrange(last.(t)...), + Iterators.filter( + t -> root_sector(first(t[1])) == root_sector(first(t[2])), + Iterators.product(trees_to_ranges_mapping(left), trees_to_ranges_mapping(right)), + ), + ), + ) end From afed27af2b3c22582f58580aec867ab19e140fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 5 Dec 2024 20:00:56 +0100 Subject: [PATCH 03/52] define tree_to_block_mapping --- src/fusiontensor/fusedaxes.jl | 17 ----------- src/fusiontensor/fusiontensor.jl | 49 ++++++++++++++++++++++++-------- test/test_basics.jl | 4 +-- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index 1ac5669..e356f1d 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -79,24 +79,7 @@ function to_blockindexrange(b1::BlockIndexRange{1}, b2::BlockIndexRange{1}) return Block(Block.(t))[BlockSparseArrays.to_block_indices.(t)...] end -# TBD choose one function intersect_sectors(left::FusedAxes, right::FusedAxes) - shared_sectors = intersect(blocklabels(left), blocklabels(right)) - blockindexrange_vec = mapreduce(vcat, shared_sectors) do s - codomain_trees = filter(f -> root_sector(f) == s, keys(trees_to_ranges_mapping(left))) - domain_trees = filter(f -> root_sector(f) == s, keys(trees_to_ranges_mapping(right))) - return vec( - collect( - (f1, f2) => to_blockindexrange( - trees_to_ranges_mapping(left)[f1], trees_to_ranges_mapping(left)[f2] - ) for f1 in codomain_trees, f2 in domain_trees - ), - ) - end - return Dict(blockindexrange_vec) -end - -function intersect_sectors2(left::FusedAxes, right::FusedAxes) return Dict( map( t -> first.(t) => to_blockindexrange(last.(t)...), diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 28340fd..ac86bba 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -2,26 +2,34 @@ using BlockSparseArrays: block_stored_indices -struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat} <: AbstractArray{T,N} +struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T,N} data_matrix::Mat codomain_axes::CoDomainAxes domain_axes::DomainAxes + tree_to_block_mapping::Mapping # inner constructor to impose constraints on types # TBD replace codomain_legs with FusedAxes(codomain_legs)? function FusionTensor( - mat::Union{BlockSparseMatrix,Adjoint{<:Number,<:BlockSparseMatrix}}, - codomain_legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}}, - domain_legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}}, - ) where {LA} + mat::AbstractMatrix, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + tree_to_block_mapping::Dict, + ) + S = sector_type(axes(mat, 1)) + @assert sector_type(axes(mat, 2)) === S + @assert keytype(tree_to_block_mapping) <: Tuple{<:FusionTree{S},<:FusionTree{S}} + @assert all(sector_type.(codomain_legs) .=== S) + @assert all(sector_type.(domain_legs) .=== S) return new{ eltype(mat), length(codomain_legs) + length(domain_legs), typeof(codomain_legs), typeof(domain_legs), typeof(mat), + typeof(tree_to_block_mapping), }( - mat, codomain_legs, domain_legs + mat, codomain_legs, domain_legs, tree_to_block_mapping ) end end @@ -30,6 +38,7 @@ end data_matrix(ft::FusionTensor) = ft.data_matrix codomain_axes(ft::FusionTensor) = ft.codomain_axes domain_axes(ft::FusionTensor) = ft.domain_axes +tree_to_block_mapping(ft::FusionTensor) = ft.tree_to_block_mapping # misc access ndims_codomain(ft::FusionTensor) = length(codomain_axes(ft)) @@ -38,6 +47,7 @@ ndims_domain(ft::FusionTensor) = length(domain_axes(ft)) matrix_size(ft::FusionTensor) = quantum_dimension.(axes(data_matrix(ft))) matrix_row_axis(ft::FusionTensor) = first(axes(data_matrix(ft))) matrix_column_axis(ft::FusionTensor) = last(axes(data_matrix(ft))) +GradedUnitRanges.sector_type(ft::FusionTensor) = sector_type(matrix_row_axis(ft)) function sanitize_axes(raw_legs) legs = unify_sector_type(raw_legs) @@ -74,6 +84,19 @@ function unify_sector_type(T::Type{<:SectorProduct}, g::AbstractGradedUnitRange) return isdual(g) ? flip(unified_g) : unified_g end +function FusionTensor( + mat::AbstractBlockSparseMatrix, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, +) + ft = FusionTensor(eltype(mat), codomain_legs, domain_legs) + for b in block_stored_indices(mat) + @assert last(b) in block_stored_indices(data_matrix(ft)) + data_matrix(ft)[last(b)] = mat[last(b)] + end + return ft +end + # empty matrix function FusionTensor( data_type::Type, @@ -87,14 +110,16 @@ function FusionTensor( codomain_fused_axes = FusedAxes{S}(codomain_legs) domain_fused_axes = FusedAxes{S}(dual.(domain_legs)) mat = initialize_data_matrix(data_type, codomain_fused_axes, domain_fused_axes) - return FusionTensor(mat, codomain_legs, domain_legs) + tree_to_block_mapping = intersect_sectors(codomain_fused_axes, domain_fused_axes) + return FusionTensor(mat, codomain_legs, domain_legs, tree_to_block_mapping) end function FusionTensor(data_type::Type, ::Tuple{}, ::Tuple{}) codomain_fused_axes = FusedAxes{TrivialSector}(()) domain_fused_axes = FusedAxes{TrivialSector}(()) mat = initialize_data_matrix(data_type, codomain_fused_axes, domain_fused_axes) - return FusionTensor(mat, (), ()) + tree_to_block_mapping = intersect_sectors(codomain_fused_axes, domain_fused_axes) + return FusionTensor(mat, (), (), tree_to_block_mapping) end # init data_matrix @@ -113,10 +138,10 @@ end function initialize_allowed_sectors!(mat::AbstractBlockMatrix) row_sectors = blocklabels(axes(mat, 1)) col_sectors = blocklabels(dual(axes(mat, 2))) - row_blocks = findall(in(col_sectors), row_sectors) - col_blocks = findall(in(row_sectors), col_sectors) - for (r, c) in zip(row_blocks, col_blocks) - mat[Block(r, c)] = zeros(size(mat[Block(r, c)])) + row_block_indices = findall(in(col_sectors), row_sectors) + col_block_indices = findall(in(row_sectors), col_sectors) + for rc in zip(row_block_indices, col_block_indices) + mat[Block(rc)] = mat[Block(rc)] end end diff --git a/test/test_basics.jl b/test/test_basics.jl index 4225fa5..bbbf92a 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -32,7 +32,7 @@ using SymmetrySectors: U1 ft1 = FusionTensor(m, (g1,), (g2,)) # getters - @test data_matrix(ft1) === m + @test data_matrix(ft1) == m @test matching_axes(codomain_axes(ft1), (g1,)) @test matching_axes(domain_axes(ft1), (g2,)) @@ -101,7 +101,7 @@ end m2 = BlockSparseArray{Float64}(gr, gc) ft = FusionTensor(m2, (g1, g2), (g3, g4)) - @test data_matrix(ft) === m2 + @test data_matrix(ft) == m2 @test matching_axes(codomain_axes(ft), (g1, g2)) @test matching_axes(domain_axes(ft), (g3, g4)) From 5ed5b8ed93a8be2ae35d2998b1510479159c0bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 5 Dec 2024 20:09:28 +0100 Subject: [PATCH 04/52] define getindex --- src/fusiontensor/base_interface.jl | 7 +++++++ src/fusiontensor/fusiontensor.jl | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index f5ab872..3c169ba 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -88,6 +88,13 @@ function Base.eachindex(::FusionTensor) throw(codomainError("eachindex", "eachindex not defined for FusionTensor")) end +function Base.getindex(ft::FusionTensor, trees::Tuple{<:FusionTree,<:FusionTree}) + return data_matrix(ft)[trees_block_mapping(ft)[trees]] +end +function Base.getindex(ft::FusionTensor, f1::FusionTree, f2::FusionTree) + return ft[(f1, f2)] +end + Base.ndims(::FusionTensor{T,N}) where {T,N} = N Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args...) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index ac86bba..0232e19 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -6,7 +6,7 @@ struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T, data_matrix::Mat codomain_axes::CoDomainAxes domain_axes::DomainAxes - tree_to_block_mapping::Mapping + trees_block_mapping::Mapping # inner constructor to impose constraints on types # TBD replace codomain_legs with FusedAxes(codomain_legs)? @@ -14,11 +14,11 @@ struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T, mat::AbstractMatrix, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - tree_to_block_mapping::Dict, + trees_block_mapping::Dict, ) S = sector_type(axes(mat, 1)) @assert sector_type(axes(mat, 2)) === S - @assert keytype(tree_to_block_mapping) <: Tuple{<:FusionTree{S},<:FusionTree{S}} + @assert keytype(trees_block_mapping) <: Tuple{<:FusionTree{S},<:FusionTree{S}} @assert all(sector_type.(codomain_legs) .=== S) @assert all(sector_type.(domain_legs) .=== S) return new{ @@ -27,9 +27,9 @@ struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T, typeof(codomain_legs), typeof(domain_legs), typeof(mat), - typeof(tree_to_block_mapping), + typeof(trees_block_mapping), }( - mat, codomain_legs, domain_legs, tree_to_block_mapping + mat, codomain_legs, domain_legs, trees_block_mapping ) end end @@ -38,7 +38,7 @@ end data_matrix(ft::FusionTensor) = ft.data_matrix codomain_axes(ft::FusionTensor) = ft.codomain_axes domain_axes(ft::FusionTensor) = ft.domain_axes -tree_to_block_mapping(ft::FusionTensor) = ft.tree_to_block_mapping +trees_block_mapping(ft::FusionTensor) = ft.trees_block_mapping # misc access ndims_codomain(ft::FusionTensor) = length(codomain_axes(ft)) From d66540c34af1e5990fa063d372a7aa1e961307ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 9 Dec 2024 09:53:30 +0100 Subject: [PATCH 05/52] array cast with FusionTree --- src/FusionTensors.jl | 16 +- src/fusion_trees/clebsch_gordan_tensors.jl | 23 +- src/fusion_trees/fusion_tree_tensors.jl | 358 --------------- src/fusion_trees/fusiontree.jl | 208 +++++++-- src/fusiontensor/array_cast.jl | 502 +++------------------ src/fusiontensor/base_interface.jl | 33 +- src/fusiontensor/fusedaxes.jl | 8 +- src/fusiontensor/fusiontensor.jl | 19 + test/test_array_cast.jl | 83 ++-- 9 files changed, 362 insertions(+), 888 deletions(-) delete mode 100644 src/fusion_trees/fusion_tree_tensors.jl diff --git a/src/FusionTensors.jl b/src/FusionTensors.jl index d5a3a44..c7eb64f 100644 --- a/src/FusionTensors.jl +++ b/src/FusionTensors.jl @@ -29,6 +29,7 @@ using GradedUnitRanges: blocklabels, blockmergesort, dual, + findblock, fusion_product, gradedrange, isdual, @@ -37,7 +38,19 @@ using GradedUnitRanges: space_isequal, unlabel_blocks using SymmetrySectors: - AbstractSector, TrivialSector, block_dimensions, istrivial, quantum_dimension, trivial + ⊗, + AbelianStyle, + AbstractSector, + NotAbelianStyle, + SectorProduct, + SymmetrySectors, + SymmetryStyle, + TrivialSector, + arguments, + block_dimensions, + istrivial, + quantum_dimension, + trivial using TensorAlgebra: TensorAlgebra, Algorithm, @@ -49,7 +62,6 @@ using TensorAlgebra: include("fusion_trees/fusiontree.jl") include("fusion_trees/clebsch_gordan_tensors.jl") -include("fusion_trees/fusion_tree_tensors.jl") include("fusiontensor/fusedaxes.jl") include("fusiontensor/fusiontensor.jl") diff --git a/src/fusion_trees/clebsch_gordan_tensors.jl b/src/fusion_trees/clebsch_gordan_tensors.jl index 3c757b2..10b4781 100644 --- a/src/fusion_trees/clebsch_gordan_tensors.jl +++ b/src/fusion_trees/clebsch_gordan_tensors.jl @@ -32,8 +32,19 @@ function clebsch_gordan_tensor( return cgt end -function clebsch_gordan_tensor(s1::O2, s2::O2, s3::O2, ::Int) - return clebsch_gordan_tensor(s1, s2, s3) # no inner multiplicity +function clebsch_gordan_tensor(s1::S, s2::S, s3::S, outer_mult_index::Int) where {S} + return clebsch_gordan_tensor(SymmetryStyle(S), s1, s2, s3, outer_mult_index) +end + +function clebsch_gordan_tensor( + ::AbelianStyle, s1::S, s2::S, s3::S, outer_mult_index::Int +) where {S} + @assert outer_mult_index == 1 + return s1 ⊗ s2 == s3 ? ones((1, 1, 1)) : zeros((1, 1, 1)) +end + +function clebsch_gordan_tensor(::NotAbelianStyle, s1::O2, s2::O2, s3::O2, ::Int) + return clebsch_gordan_tensor(s1, s2, s3) # no outer multiplicity end function clebsch_gordan_tensor(s1::O2, s2::O2, s3::O2) @@ -78,8 +89,8 @@ function clebsch_gordan_tensor(s1::O2, s2::O2, s3::O2) return cgt end -function clebsch_gordan_tensor(s1::SU{2}, s2::SU{2}, s3::SU{2}, ::Int) - return clebsch_gordan_tensor(s1, s2, s3) # no inner multiplicity +function clebsch_gordan_tensor(::NotAbelianStyle, s1::SU{2}, s2::SU{2}, s3::SU{2}, ::Int) + return clebsch_gordan_tensor(s1, s2, s3) # no outer multiplicity end function clebsch_gordan_tensor(s1::SU{2}, s2::SU{2}, s3::SU{2}) @@ -99,7 +110,9 @@ function clebsch_gordan_tensor(s1::SU{2}, s2::SU{2}, s3::SU{2}) return cgtensor end -function clebsch_gordan_tensor(s1::SU{3}, s2::SU{3}, s3::SU{3}, inner_mult_index::Int) +function clebsch_gordan_tensor( + ::NotAbelianStyle, s1::SU{3}, s2::SU{3}, s3::SU{3}, outer_mult_index::Int +) d1 = quantum_dimension(s1) d2 = quantum_dimension(s2) d3 = quantum_dimension(s3) diff --git a/src/fusion_trees/fusion_tree_tensors.jl b/src/fusion_trees/fusion_tree_tensors.jl deleted file mode 100644 index ed4e800..0000000 --- a/src/fusion_trees/fusion_tree_tensors.jl +++ /dev/null @@ -1,358 +0,0 @@ -# This file defines fusion trees for any abelian or non-abelian group - -# TBD -# compatibility with TensorKit conventions? - -# -# A fusion tree tensor is a N+1 legs Array that corresponds to the projector defined by the -# associated fusion tree. -# -# dim_sec struct_mult_sec -# \ / -# \/ -# / -# / -# /\ -# / \ -# /\ \ -# / \ \ -# dim1 dim2 dim3 -# -# -# It is convenient to "merge the tree leaves" by merging together the dimension legs to -# yield a 3-dim tensor with size (dim1*dim2*...*dimN, dim_sec, struct_mult_sec) -# -# --------------------------- -# | | | -# dim1*dim2*dim3 dim_sec struct_mult_sec -# -# -# convention: the trees are not normalized, i.e they do not define a projector on a given -# sector but carry a scaling factor sqrt(dim_sec) -# -# convention: irreps are already dualed if needed, arrows do not affect them. They only -# affect the basis on which the tree projects for self-dual irreps. -# -# -# The interface uses AbstractGradedUnitRanges as input for interface simplicity -# however only blocklabels are used and blocklengths are never read. - -using SymmetrySectors: - ⊗, AbelianStyle, AbstractSector, NotAbelianStyle, SectorProduct, SymmetryStyle, arguments - -# =================================== Utility tools ====================================== -function braid_tuples(t1::Tuple{Vararg{<:Any,N}}, t2::Tuple{Vararg{<:Any,N}}) where {N} - t12 = (t1, t2) - nested = ntuple(i -> getindex.(t12, i), N) - return TensorAlgebra.flatten_tuples(nested) -end - -# compute Kronecker product of fusion trees -# more efficient with recursive construction -trees_kron(a, b, c...) = trees_kron(trees_kron(a, b), c...) - -function trees_kron(a, b) - return map( - ((t1, t2),) -> _tensor_kron(t1, t2), Iterators.flatten((Iterators.product(a, b),),) - ) -end - -# LinearAlgebra.kron does not allow input for ndims>2 -function _tensor_kron(a::AbstractArray{<:Any,N}, b::AbstractArray{<:Any,N}) where {N} - t1 = ntuple(_ -> 1, N) - sha = braid_tuples(size(a), t1) - shb = braid_tuples(t1, size(b)) - c = reshape(a, sha) .* reshape(b, shb) - return reshape(c, size(a) .* size(b)) -end - -# ================================ High level interface ================================== -function merge_tree_leaves(a::AbstractArray) - shape_3leg = (prod(size(a)[begin:(end - 2)]), size(a, ndims(a) - 1), size(a, ndims(a))) - return reshape(a, shape_3leg) -end - -function unmerge_tree_leaves( - tree::AbstractArray{<:Real,3}, irreps::NTuple{<:Any,<:AbstractSector} -) - irreps_shape = quantum_dimension.(irreps) - return unmerge_tree_leaves(tree, irreps_shape) -end - -function unmerge_tree_leaves(tree::AbstractArray{<:Real,3}, irreps_shape::NTuple{<:Any,Int}) - new_shape = (irreps_shape..., size(tree, 2), size(tree, 3)) - return reshape(tree, new_shape) -end - -function get_fusion_tree_tensors!( - cache::Dict{NTuple{N,Int},<:Vector{A}}, - it::NTuple{N,Int}, - legs::NTuple{N,AbstractGradedUnitRange}, - allowed_sectors::Vector{<:AbstractSector}, -) where {N,A<:Array{<:Real}} - get!(cache, it) do - tree_arrows = isdual.(legs) - irreps = getindex.(blocklabels.(legs), it) - return compute_pruned_fusion_tree_tensors(A, irreps, tree_arrows, allowed_sectors) - end -end - -# explicitly cast trees to 3 leg format -function compute_pruned_fusion_tree_tensors( - ::Type{<:Array{<:Real,3}}, - irreps::NTuple{N,<:AbstractSector}, - tree_arrows::NTuple{N,Bool}, - target_sectors::Vector{<:AbstractSector}, -) where {N} - return merge_tree_leaves.( - compute_pruned_fusion_tree_tensors(Any, irreps, tree_arrows, target_sectors) - ) -end - -function compute_pruned_fusion_tree_tensors( - ::Type, - irreps::NTuple{N,<:AbstractSector}, - tree_arrows::NTuple{N,Bool}, - target_sectors::Vector{<:AbstractSector}, -) where {N} - - # it is possible to prune trees during the construction process and to avoid constructing - # trees that will never fuse to target_sectors - # currently this is not implemented and no pruning is done inside fusion_trees - tree_irreps_pairs = fusion_tree_tensors(irreps, tree_arrows) - tree_irreps = first.(tree_irreps_pairs) - trees = last.(tree_irreps_pairs) - - # pruning is only done here by discarding irreps that are not in target_sectors - # also insert dummy trees in sectors that did not appear in the fusion product of irreps - irreps_dims = quantum_dimension.(irreps) - trees_sector = [ # fill with dummy - zeros((irreps_dims..., quantum_dimension(sec), 0)) for sec in target_sectors - ] - - # set trees at their correct position - for (i, s) in enumerate(target_sectors) - j = findfirst(==(s), tree_irreps) - if !isnothing(j) - trees_sector[i] = trees[j] - end - end - return trees_sector -end - -# ================================ Low level interface =================================== -function fusion_tree_tensors(::Tuple{}, ::Tuple{}) - return [TrivialSector() => ones((1, 1))] -end - -function fusion_tree_tensors( - irreps::NTuple{N,<:SectorProduct}, tree_arrows::NTuple{N,Bool} -) where {N} - # construct fusion_tree(SectorProduct) as kron(fusion_trees(inner_sectors)) - - argument_irreps = arguments.(irreps) - n_args = length(first(argument_irreps)) - - # construct fusion tree for each sector - transposed_args = ntuple(s -> getindex.(argument_irreps, s), n_args) - sector_trees_irreps = map(a -> fusion_tree_tensors(a, tree_arrows), transposed_args) - - # reconstruct sector for each product tree - T = eltype(argument_irreps) - fused_arguments = broadcast.(first, sector_trees_irreps) - tree_irreps = map( - SectorProduct ∘ T, Iterators.flatten((Iterators.product(fused_arguments...),)) - ) - - # compute Kronecker product of fusion trees - trees = trees_kron(broadcast.(last, sector_trees_irreps)...) - - # sort irreps. Each sector is sorted, permutation is obtained by reversing loop order - perm = sortperm(tree_irreps) - permute!(tree_irreps, perm) - permute!(trees, perm) - return tree_irreps .=> trees -end - -function fusion_tree_tensors( - irreps::NTuple{N,<:AbstractSector}, tree_arrows::NTuple{N,Bool} -) where {N} - return fusion_tree_tensors(SymmetryStyle(first(irreps)), irreps, tree_arrows) -end - -# ===================================== Internals ======================================== - -# fusion tree for an Abelian group is trivial -# it does not depend on arrow directions -function fusion_tree_tensors(::AbelianStyle, irreps::Tuple, ::Tuple) - irrep_prod = reduce(⊗, irreps) - return [irrep_prod => ones(ntuple(_ -> 1, length(irreps) + 2))] -end - -function build_children_trees( - parent_tree::Matrix, - parent_irrep::AbstractSector, - level_irrep::AbstractSector, - level_arrow::Bool, - inner_multiplicity::Integer, - sec::AbstractSector, -) - sector_trees = typeof(parent_tree)[] - for inner_mult_index in 1:inner_multiplicity - cgt_inner_mult = clebsch_gordan_tensor( - parent_irrep, level_irrep, sec, false, level_arrow, inner_mult_index - ) - dim_parent_irrep, dim_level_irrep, dim_sec = size(cgt_inner_mult) - tree = - parent_tree * reshape(cgt_inner_mult, (dim_parent_irrep, dim_level_irrep * dim_sec)) - child_tree = reshape(tree, (size(parent_tree, 1) * dim_level_irrep, dim_sec)) - push!(sector_trees, child_tree) - end - return sector_trees -end - -function build_children_trees( - parent_tree::Matrix, - parent_irrep::AbstractSector, - level_irrep::AbstractSector, - level_arrow::Bool, -) - children_trees = typeof(parent_tree)[] - children_irreps = typeof(parent_irrep)[] - rep = fusion_product(parent_irrep, level_irrep) - for (inner_multiplicity, sec) in zip(blocklengths(rep), blocklabels(rep)) - sector_trees = build_children_trees( - parent_tree, parent_irrep, level_irrep, level_arrow, inner_multiplicity, sec - ) - append!(children_trees, sector_trees) - append!(children_irreps, repeat([sec], inner_multiplicity)) - end - return children_trees, children_irreps -end - -function build_next_level_trees( - parent_trees::Vector, - parent_trees_irreps::Vector, - level_irrep::AbstractSector, - level_arrow::Bool, -) - next_level_trees = empty(parent_trees) - next_level_irreps = empty(parent_trees_irreps) - for (parent_tree, parent_irrep) in zip(parent_trees, parent_trees_irreps) - children_trees, children_irreps = build_children_trees( - parent_tree, parent_irrep, level_irrep, level_arrow - ) - append!(next_level_trees, children_trees) - append!(next_level_irreps, children_irreps) - end - return next_level_trees, next_level_irreps -end - -function build_trees(trees::Vector, tree_irreps::Vector, irreps::Tuple, tree_arrows::Tuple) - next_level_trees, next_level_irreps = build_next_level_trees( - trees, tree_irreps, first(irreps), first(tree_arrows) - ) - return build_trees(next_level_trees, next_level_irreps, irreps[2:end], tree_arrows[2:end]) -end - -function build_trees(trees::Vector, tree_irreps::Vector, ::Tuple{}, ::Tuple{}) - return trees, tree_irreps -end - -function compute_thin_trees(irreps::Tuple, tree_arrows::Tuple) - # init from trivial, NOT from first(irreps) to get first arrow correct - thin_trees = [ones((1, 1))] - tree_irreps = [trivial(first(irreps))] - return build_trees(thin_trees, tree_irreps, irreps, tree_arrows) -end - -function cat_thin_trees( - thin_trees::Vector, uncat_tree_irreps::Vector, sector_irrep::AbstractSector -) - indices_irrep = findall(==(sector_irrep), uncat_tree_irreps) - thin_trees_irrep = getindex.(Ref(thin_trees), indices_irrep) - thick_shape = (size(first(thin_trees_irrep))..., length(indices_irrep)) - return reshape(reduce(hcat, thin_trees_irrep), thick_shape) -end - -function cat_thin_trees(thin_trees::Vector, uncat_tree_irreps::Vector) - # cat trees fusing on the same irrep - tree_irreps = sort(unique(uncat_tree_irreps)) - thick_trees = map( - irrep -> cat_thin_trees(thin_trees, uncat_tree_irreps, irrep), tree_irreps - ) - return thick_trees, tree_irreps -end - -# arrow direction is needed to define non-trivial CG tensor -function fusion_tree_tensors(::NotAbelianStyle, irreps::Tuple, tree_arrows::Tuple) - # compute "thin" trees: 1 tree = fuses on ONE irrep - thin_trees, uncat_tree_irreps = compute_thin_trees(irreps, tree_arrows) - - # cat thin trees into "thick" trees - thick_mergedleaves_trees, tree_irreps = cat_thin_trees(thin_trees, uncat_tree_irreps) - - irrep_dims = quantum_dimension.(irreps) - thick_trees = map(tree -> unmerge_tree_leaves(tree, irrep_dims), thick_mergedleaves_trees) - return tree_irreps .=> thick_trees -end - -Base.convert(T::Type{<:Array}, f::FusionTree) = convert(T, to_tensor(f)) - -to_tensor(::FusionTree{<:Any,0}) = ones(1) - -function to_tensor(f::FusionTree) - # init with dummy trivial leg to get arrow correct and deal with size-1 case - cgt1 = clebsch_gordan_tensor( - trivial(eltype(f)), - first(leaves(f)), - first(branch_sectors(f)), - false, - first(arrows(f)), - 1, - ) - return grow_tensor_tree(cgt1[1, :, :], f) -end - -function contract_clebsch_gordan( - tree_tensor::AbstractArray{<:Real,N}, cgt::AbstractArray{<:Real,3} -) where {N} - return contract( - (ntuple(identity, N - 1)..., N + 1, N + 2), - tree_tensor, - ntuple(identity, N), - cgt, - (N, N + 1, N + 2), - ) -end - -function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,1}, ::FusionTree{<:Any,1}) - return tree_tensor -end - -function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,N}, f::FusionTree) where {N} - cgt = clebsch_gordan_tensor( - branch_sectors(f)[N - 1], - leaves(f)[N], - branch_sectors(f)[N], - false, - arrows(f)[N], - outer_multiplicty_indices(f)[N - 1], - ) - next_level_tree = contract_clebsch_gordan(tree_tensor, cgt) - return grow_tensor_tree(next_level_tree, f) -end - -function grow_tensor_tree( - tree_tensor::AbstractArray{<:Real,N}, f::FusionTree{<:Any,N} -) where {N} - cgt = clebsch_gordan_tensor( - last(branch_sectors(f)), - last(leaves(f)), - root_sector(f), - false, - last(arrows(f)), - last(outer_multiplicty_indices(f)), - ) - return contract_clebsch_gordan(tree_tensor, cgt) -end diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index a734169..a05c633 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -30,27 +30,25 @@ # The interface uses AbstractGradedUnitRanges as input for interface simplicity # however only blocklabels are used and blocklengths are never read. -using SymmetrySectors: SymmetrySectors - struct FusionTree{S,N,M} leaves::NTuple{N,S} # TBD rename outer_sectors or leave_sectors? arrows::NTuple{N,Bool} root_sector::S branch_sectors::NTuple{M,S} # M = N-1 - outer_multiplicty_indices::NTuple{M,Int} # M = N-1 + outer_multiplicity_indices::NTuple{M,Int} # M = N-1 # TBD could have branch_sectors with length N-2 # currently first(branch_sectors) == first(leaves) # redundant but allows for simpler, generic grow_tree code function FusionTree( - leaves, arrows, root_sector, branch_sectors, outer_multiplicty_indices + leaves, arrows, root_sector, branch_sectors, outer_multiplicity_indices ) N = length(leaves) @assert length(branch_sectors) == max(0, N - 1) - @assert length(outer_multiplicty_indices) == max(0, N - 1) + @assert length(outer_multiplicity_indices) == max(0, N - 1) return new{typeof(root_sector),length(leaves),length(branch_sectors)}( - leaves, arrows, root_sector, branch_sectors, outer_multiplicty_indices + leaves, arrows, root_sector, branch_sectors, outer_multiplicity_indices ) end end @@ -60,24 +58,15 @@ arrows(f::FusionTree) = f.arrows leaves(f::FusionTree) = f.leaves root_sector(f::FusionTree) = f.root_sector branch_sectors(f::FusionTree) = f.branch_sectors -outer_multiplicty_indices(f::FusionTree) = f.outer_multiplicty_indices +outer_multiplicity_indices(f::FusionTree) = f.outer_multiplicity_indices -# interface -Base.length(::FusionTree{<:Any,N}) where {N} = N +# Base interface +Base.convert(T::Type{<:Array}, f::FusionTree) = convert(T, to_tensor(f)) Base.isless(f1::FusionTree, f2::FusionTree) = isless(to_tuple(f1), to_tuple(f2)) +Base.length(::FusionTree{<:Any,N}) where {N} = N -function to_tuple(f::FusionTree) # TBD defined as Base.Tuple(::FusionTree)? - return ( - leaves(f)..., - arrows(f)..., - root_sector(f), - branch_sectors(f)..., - outer_multiplicty_indices(f)..., - ) -end - -GradedUnitRanges.sector_type(::FusionTree{S}) where {S} = S # TBD use different function name? -Base.eltype(::FusionTree{S}) where {S} = S +# GradedUnitRanges interface +GradedUnitRanges.sector_type(::FusionTree{S}) where {S} = S function build_trees(legs::Vararg{AbstractGradedUnitRange{LA}}) where {LA} # TBD when to impose LA to be the same for every leg? @@ -89,48 +78,110 @@ function build_trees(legs::Vararg{AbstractGradedUnitRange{LA}}) where {LA} end end +# SymmetrySectors interface function SymmetrySectors.:×(f1::FusionTree, f2::FusionTree) @assert arrows(f1) == arrows(f2) product_leaves = .×(leaves(f1), leaves(f2)) product_root_sector = root_sector(f1) × root_sector(f2) product_branch_sectors = .×(branch_sectors(f1), branch_sectors(f2)) - product_outer_multiplicty_indices = + product_outer_multiplicity_indices = outer_multiplicity_kron.( Base.tail(leaves(f1)), branch_sectors(f1), (Base.tail(branch_sectors(f1))..., root_sector(f1)), - outer_multiplicty_indices(f1), - outer_multiplicty_indices(f2), + outer_multiplicity_indices(f1), + outer_multiplicity_indices(f2), ) return FusionTree( product_leaves, arrows(f1), product_root_sector, product_branch_sectors, - product_outer_multiplicty_indices, + product_outer_multiplicity_indices, + ) +end + +function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct}) + transposed_indices = + outer_multiplicity_split.( + Base.tail(leaves(f)), + branch_sectors(f), + (Base.tail(branch_sectors(f))..., root_sector(f)), + outer_multiplicity_indices(f), + ) + arguments_root = arguments(root_sector(f)) + arguments_leaves = arguments.(leaves(f)) + arguments_branch_sectors = arguments.(branch_sectors(f)) + # TODO way to avoid explicit ntuple? + # works fine for Tuple and NamedTuple SectorProduct + return ntuple( + i -> FusionTree( + getindex.(arguments_leaves, i), + arrows(f), + arguments_root[i], + getindex.(arguments_branch_sectors, i), + getindex.(transposed_indices, i), + ), + length(arguments_root), ) end +function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct,0}) + return map(arg -> FusionTree((), (), arg, (), ()), arguments(root_sector(f))) +end + +function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct,1}) + arguments_root = arguments(root_sector(f)) + arguments_leave = arguments(only(leaves(f))) + # use map(keys) to stay agnostic with respect to SectorProduct implementation + return map( + k -> FusionTree((arguments_leave[k],), arrows(f), arguments_root[k], (), ()), + keys(arguments_root), + ) +end + +# +# ===================================== Internals ======================================== +# +# --------------- misc --------------- +function to_tuple(f::FusionTree) + return ( + leaves(f)..., + arrows(f)..., + root_sector(f), + branch_sectors(f)..., + outer_multiplicity_indices(f)..., + ) +end + +# --------------- SectorProduct helper functions --------------- function outer_multiplicity_kron( sec1, sec2, fused, outer_multiplicity1, outer_multiplicity2 ) - full_space = fusion_product(sec1, sec2) - nsymbol = blocklengths(full_space)[findfirst(==(fused), blocklabels(full_space))] - linear_inds = LinearIndices((nsymbol, outer_multiplicity2)) + n = nsymbol(sec1, sec2, fused) + linear_inds = LinearIndices((n, outer_multiplicity2)) return linear_inds[outer_multiplicity1, outer_multiplicity2] end -function outer_multiplicity_split(sec1, sec2, fused, outer_multiplicity) - args1 = SymmetrySectors.arguments(sec1) - args2 = SymmetrySectors.arguments(sec2) - args12 = SymmetrySectors.arguments(fused) - nsymbols = map(zip(args1, args2, args12)) do (sec1, sec2, sec12) - full_space = fusion_product(sec1, sec2) - return blocklengths(full_space)[findfirst(==(sec12), blocklabels(full_space))] - end - return CartesianIndices(nsymbols)[outer_multiplicity] +# TODO move to GradedUnitRanges +function nsymbol(s1::AbstractSector, s2::AbstractSector, s3::AbstractSector) + full_space = fusion_product(s1, s2) + x = findfirst(==(s3), blocklabels(full_space)) + isnothing(x) && return 0 # OR labelled(0, s3)? + return Int(blocklengths(full_space)[x]) end +function outer_multiplicity_split( + sec1::S, sec2::S, fused::S, outer_mult_index::Integer +) where {S<:SectorProduct} + args1 = arguments(sec1) + args2 = arguments(sec2) + args12 = arguments(fused) + nsymbols = Tuple(map(nsymbol, args1, args2, args12)) # CartesianIndices requires explicit Tuple + return Tuple(CartesianIndices(nsymbols)[outer_mult_index]) +end + +# --------------- Build trees --------------- # zero leg: need S to get sector type information function FusionTree{S}() where {S<:AbstractSector} return FusionTree((), (), trivial(S), (), ()) @@ -142,7 +193,12 @@ end # one leg FusionTree(sect::AbstractSector, arrow::Bool) = FusionTree((sect,), (arrow,), sect, (), ()) -# ===================================== Internals ======================================== +function braid_tuples(t1::Tuple{Vararg{<:Any,N}}, t2::Tuple{Vararg{<:Any,N}}) where {N} + t12 = (t1, t2) + nested = ntuple(i -> getindex.(t12, i), N) + return TensorAlgebra.flatten_tuples(nested) +end + function grow_tree( parent_tree::FusionTree, branch_sector::AbstractSector, @@ -153,7 +209,7 @@ function grow_tree( child_leaves = (leaves(parent_tree)..., branch_sector) child_arrows = (arrows(parent_tree)..., level_arrow) child_branch_sectors = (branch_sectors(parent_tree)..., root_sector(parent_tree)) - child_outer_mul = (outer_multiplicty_indices(parent_tree)..., outer_mult) + child_outer_mul = (outer_multiplicity_indices(parent_tree)..., outer_mult) return FusionTree( child_leaves, child_arrows, child_root_sector, child_branch_sectors, child_outer_mul ) @@ -185,8 +241,78 @@ function build_trees(trees::Vector, ::Tuple{}, ::Tuple{}) end function build_trees( - sectors_to_fuse::NTuple{N,S}, arrows_to_fuse::NTuple{N,Bool} -) where {N,S<:AbstractSector} + sectors_to_fuse::NTuple{N,<:AbstractSector}, arrows_to_fuse::NTuple{N,Bool} +) where {N} trees = [FusionTree(first(sectors_to_fuse), first(arrows_to_fuse))] return build_trees(trees, Base.tail(sectors_to_fuse), Base.tail(arrows_to_fuse)) end + +# --------------- convert to Array --------------- +to_tensor(::FusionTree{<:Any,0}) = ones(1) + +function to_tensor(f::FusionTree) + # init with dummy trivial leg to get arrow correct and deal with size-1 case + cgt1 = clebsch_gordan_tensor( + trivial(sector_type(f)), first(leaves(f)), first(leaves(f)), false, first(arrows(f)), 1 + ) + tree_tensor = cgt1[1, :, :] + return grow_tensor_tree(tree_tensor, f) +end + +#to_tensor(::FusionTree{<:SectorProduct,0}) = ones(1) +function to_tensor(f::FusionTree{<:SectorProduct}) + args = convert.(Array, arguments(f)) + return reduce(_tensor_kron, args) +end + +# LinearAlgebra.kron does not allow input for ndims>2 +function _tensor_kron(a::AbstractArray{<:Any,N}, b::AbstractArray{<:Any,N}) where {N} + t1 = ntuple(_ -> 1, N) + sha = braid_tuples(size(a), t1) + shb = braid_tuples(t1, size(b)) + c = reshape(a, sha) .* reshape(b, shb) + return reshape(c, size(a) .* size(b)) +end + +function contract_clebsch_gordan(tree_tensor::AbstractArray, cgt::AbstractArray) + N = ndims(tree_tensor) + return contract( + (ntuple(identity, N - 1)..., N + 1, N + 2), + tree_tensor, + ntuple(identity, N), + cgt, + (N, N + 1, N + 2), + ) +end + +# specialized code when branch_sector is empty +function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,2}, ::FusionTree{<:Any,1}) + return tree_tensor +end + +function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,N}, f::FusionTree) where {N} + cgt = clebsch_gordan_tensor( + branch_sectors(f)[N - 1], + leaves(f)[N], + branch_sectors(f)[N], + false, + arrows(f)[N], + outer_multiplicity_indices(f)[N - 1], + ) + next_level_tree = contract_clebsch_gordan(tree_tensor, cgt) + return grow_tensor_tree(next_level_tree, f) +end + +function grow_tensor_tree( + tree_tensor::AbstractArray{<:Real,N}, f::FusionTree{<:Any,N} +) where {N} + cgt = clebsch_gordan_tensor( + last(branch_sectors(f)), + last(leaves(f)), + root_sector(f), + false, + last(arrows(f)), + last(outer_multiplicity_indices(f)), + ) + return contract_clebsch_gordan(tree_tensor, cgt) +end diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 04f8957..13fa10d 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -41,54 +41,40 @@ function cast_from_array( end ft = FusionTensor(eltype(blockarray), codomain_legs, domain_legs) - - # TODO cache FusedAxes into FusionTensor - codomain_fused_axes = FusedAxes{sector_type(ft)}(codomain_legs) - domain_fused_axes = FusedAxes{sector_type(ft)}(dual.(domain_legs)) - fill_matrix_blocks!(data_matrix(ft), blockarray, codomain_fused_axes, domain_fused_axes) + for (f1, f2) in keys(trees_block_mapping(ft)) + b = findblock(ft, f1, f2) + charge_block = contract_fusion_trees(blockarray[b], f1, f2) + ft[f1, f2] = reshape(charge_block, size(ft[f1, f2])) + end return ft end function cast_to_array(ft::FusionTensor) - return cast_to_array(data_matrix(ft), codomain_axes(ft), domain_axes(ft)) -end - -function cast_to_array( - data_mat::AbstractMatrix, - codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, -) - bounds = block_dimensions.((codomain_legs..., domain_legs...)) - blockarray = BlockSparseArray{eltype(data_mat)}(blockedrange.(bounds)) - codomain_fused_axes = FusedAxes(codomain_legs) - domain_fused_axes = FusedAxes(dual.(domain_legs)) - fill_blockarray!(blockarray, data_mat, codomain_fused_axes, domain_fused_axes) - return blockarray + bounds = block_dimensions.((codomain_axes(ft)..., domain_axes(ft)...)) + bsa = BlockSparseArray{eltype(ft)}(blockedrange.(bounds)) + for (f1, f2) in keys(trees_block_mapping(ft)) + b = findblock(ft, f1, f2) + bsa[b] = bsa[b] + contract_fusion_trees(ft, f1, f2) # init block when needed + end + return bsa end # ===================================== Internals ======================================== -#------------------------------------ utility tools --------------------------------------- -function split_axes(fa::FusedAxes) - legs = axes(fa) - degens = blocklengths.(legs) - dimensions = broadcast.(quantum_dimension, blocklabels.(legs)) - return legs, degens, dimensions +#----------------------------------- misc tools ---------------------------------------- + +function degen_dims_shape(dense_shape::Tuple{Vararg{Int}}, f::FusionTree) + dims = quantum_dimension.(leaves(f)) + mults = dense_shape .÷ dims + return braid_tuples(mults, dims) end -function split_degen_dims( - array_block::AbstractArray, - codomain_block_degens::Tuple, - codomain_block_dims::Tuple, - domain_block_degens::Tuple, - domain_block_dims::Tuple, -) +function split_degen_dims(array_block::AbstractArray, f1::FusionTree, f2::FusionTree) array_block_split_shape = ( - braid_tuples(codomain_block_degens, codomain_block_dims)..., - braid_tuples(domain_block_degens, domain_block_dims)..., + degen_dims_shape(size(array_block)[begin:length(f1)], f1)..., + degen_dims_shape(size(array_block)[(length(f1) + 1):end], f2)..., ) - split_array_block = reshape(array_block, array_block_split_shape) - return split_array_block + return reshape(array_block, array_block_split_shape) end function merge_degen_dims(split_array_block::AbstractArray) @@ -99,60 +85,9 @@ function merge_degen_dims(split_array_block::AbstractArray) return array_block end -function permute_split_array_block(split_array_block::AbstractArray) - N = ndims(split_array_block) ÷ 2 - array_data_perm = (ntuple(i -> 2 * i - 1, N)..., ntuple(i -> 2 * i, N)...) - permuted_split_array_block = permutedims(split_array_block, array_data_perm) - return permuted_split_array_block -end - -function unpermute_split_array_block(permuted_split_array_block::AbstractArray) - twoN = ndims(permuted_split_array_block) - N = twoN ÷ 2 - inverse_array_data_perm = ntuple(i -> fld1(i, 2) + (1 - i % 2) * N, twoN) - split_array_block = permutedims(permuted_split_array_block, inverse_array_data_perm) - return split_array_block -end - -function reshape_permuted_to_fused( - permuted_split_array_block::AbstractArray, ::Val{N_CO} -) where {N_CO} - N = ndims(permuted_split_array_block) ÷ 2 - permuted_array_shape = size(permuted_split_array_block) - fused_array_block_shape = ( - prod(permuted_array_shape[begin:N_CO]), - prod(permuted_array_shape[(N_CO + 1):N]), - prod(permuted_array_shape[(N + 1):(N + N_CO)]), - prod(permuted_array_shape[(N + N_CO + 1):end]), - ) - fused_array_block = reshape(permuted_split_array_block, fused_array_block_shape) - return fused_array_block -end - -function reshape_fused_to_permuted( - fused_array_block::AbstractArray, - codomain_block_degens::Tuple, - codomain_block_dims::Tuple, - domain_block_degens::Tuple, - domain_block_dims::Tuple, -) - degen_dim_shape = ( - codomain_block_degens..., - domain_block_degens..., - codomain_block_dims..., - domain_block_dims..., - ) - permuted_split_array_block = reshape(fused_array_block, degen_dim_shape) - return permuted_split_array_block -end +#---------------------------------- cast from array --------------------------------------- -function fuse_array_block( - array_block::AbstractArray, - codomain_block_degens::Tuple, - codomain_block_dims::Tuple, - domain_block_degens::Tuple, - domain_block_dims::Tuple, -) +function contract_fusion_trees(array_block::AbstractArray, f1::FusionTree, f2::FusionTree) # start from an array outer block with e.g. N=6 axes divided into N_DO=3 ndims_codomain # and N_CO=3 ndims_domain. Each leg k can be decomposed as a product of external an # multiplicity extk and a quantum dimension dimk @@ -172,359 +107,62 @@ function fuse_array_block( # / \ / \ / \ / \ / \ / \ # ext1 dim1 ext2 dim2 ext3 dim3 ext4 dim4 ext5 dim5 ext6 dim6 # - split_array_block = split_degen_dims( - array_block, - codomain_block_degens, - codomain_block_dims, - domain_block_degens, - domain_block_dims, - ) - - # Now we permute the axes to group together degenearacies on one side and irrep - # dimensions on the other side. This is the bottleneck. - # - # -------------------permuted_split_array_block----------------------------------- - # | | | | | | | | | | | | - # ext1 ext2 ext3 ext4 ext5 ext6 dim1 dim2 dim3 dim4 dim5 dim6 - # - permuted_split_array_block = permute_split_array_block(split_array_block) - - # Finally, it is convenient to merge together legs corresponding to codomain or - # to domain and produce a 4-dims tensor - # - # ---------------------fused_array_block-------------------- - # | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 - # - fused_array_block = reshape_permuted_to_fused( - permuted_split_array_block, Val(length(codomain_block_degens)) - ) - return fused_array_block -end - -function contract_fusion_trees( - fused_array_block::AbstractArray{<:Number,4}, - tree_codomain::AbstractArray{<:Real,3}, - tree_domain::AbstractArray{<:Real,3}, -) - # Input: - # - # ---------------------fused_array_block-------------------- - # | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 - # - # - # - # ---------------tree_codomain------------ - # | | | - # dim1*dim2*dim3 dim_sec struct_sec_codomain - # - # - # ----------------tree_domain----------- - # | | | - # dim4*dim5*dim6 dim_sec struct_sec_domain - # - # in this form, we can apply fusion trees on both the domain and the codomain. - # - - # contract domain tree - # -------------------------data_1tree--------------------------- - # | | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim_sec struct_sec_domain - # - data_1tree = contract( - (1, 2, 3, 5, 6), fused_array_block, (1, 2, 3, 4), tree_domain, (4, 5, 6) - ) - - # contract codomain tree - # -----------------------sym_data---------------------------- - # | | | | - # ext1*ext2*ext3 struct_sec_domain ext4*ext5*ext6 struct_sec_codomain - # - T = promote_type(eltype(fused_array_block), Float64) - dim_sec = size(tree_codomain, 2) - sym_data::Array{T,4} = contract( - (1, 7, 2, 6), # HERE WE SET INNER STRUCTURE FOR MATRIX BLOCKS - data_1tree, - (1, 2, 3, 5, 6), - tree_codomain, - (3, 5, 7), - 1 / dim_sec, # normalization factor - ) - - # ----------------------sym_block_sec--------------- - # | | - # ext1*ext2*ext3*struct_sec_domain ext4*ext5*ext6*struct_sec_codomain - # - sym_shape = (size(sym_data, 1) * size(sym_data, 2), size(sym_data, 3) * size(sym_data, 4)) - sym_block_sec = reshape(sym_data, sym_shape) - return sym_block_sec -end - -#---------------------------------- cast from array --------------------------------------- - -function fill_matrix_blocks!( - data_mat::AbstractBlockSparseMatrix, - blockarray::AbstractBlockArray, - codomain_fused_axes::FusedAxes, - domain_fused_axes::FusedAxes, -) - codomain_legs, codomain_degens, codomain_dims = split_axes(codomain_fused_axes) - domain_legs, domain_degens, domain_dims = split_axes(domain_fused_axes) - - matrix_block_indices = intersect(codomain_fused_axes, domain_fused_axes) - allowed_matrix_blocks = [view!(data_mat, Block(bi)) for bi in matrix_block_indices] - allowed_sectors = blocklabels(codomain_fused_axes)[first.(matrix_block_indices)] - allowed_outer_blocks = allowed_outer_blocks_sectors( - codomain_fused_axes, domain_fused_axes, matrix_block_indices - ) - - # cache computed trees - codomain_tree_tensors_cache = Dict{ - NTuple{ndims(codomain_fused_axes),Int},Vector{Array{Float64,3}} - }() - domain_tree_tensors_cache = Dict{ - NTuple{ndims(domain_fused_axes),Int},Vector{Array{Float64,3}} - }() - - # Below, we loop over every allowed outer block, contract codomain and domain fusion trees - # for each allowed sector and write the result inside a symmetric matrix block - # - # ----------------dim_sec--------- - # | | - # | struct_mult_codomain_sec | struct_mult_domain_sec - # \ / \ / - # \/ \/ - # / / - # / / - # /\ /\ - # / \ / \ - # /\ \ /\ \ - # / \ \ / \ \ - # dim1 dim2 dim3 dim4 dim5 dim6 - # | | | | | | - # ------------------array_block------------- - # | | | | | | - # ext1 ext2 ext3 ext4 ext5 ext6 - - # loop for each allowed outer block - for (outer_block, outer_block_sectors) in allowed_outer_blocks - iter_do = outer_block[begin:ndims(codomain_fused_axes)] - codomain_block_trees = get_fusion_tree_tensors!( - codomain_tree_tensors_cache, iter_do, codomain_legs, allowed_sectors - ) - - iter_co = outer_block[(ndims(codomain_fused_axes) + 1):end] - domain_block_trees = get_fusion_tree_tensors!( - domain_tree_tensors_cache, iter_co, domain_legs, allowed_sectors - ) - - fused_array_block = fuse_array_block( - view(blockarray, Block(iter_do..., iter_co...)), - getindex.(codomain_degens, iter_do), - getindex.(codomain_dims, iter_do), - getindex.(domain_degens, iter_co), - getindex.(domain_dims, iter_co), - ) - - # loop for each symmetry sector allowed in this outer block - for sect in outer_block_sectors - - # actual implementation: legs are conveniently merged - # - # ----------------dim_sec--------- - # | | - # | struct_mult_codomain_sec | struct_mult_domain_sec - # \ / \ / - # \/ \/ - # / / - # | | - # dim1*dim2*dim3 dim4*dim5*dim6 - # | | - # ------------fused_array_block---- - # | | - # ext1*ext2*ext3 ext4*ext5*ext6 - - # contract fusion trees and reshape symmetric block as a matrix - # Note: a final permutedims is needed after the last contract - # therefore cannot efficiently use contract!(allowed_matrix_blocks[...], ...) - i_sec = findfirst(==(sect), allowed_sectors) - sym_block_sec = contract_fusion_trees( - fused_array_block, codomain_block_trees[i_sec], domain_block_trees[i_sec] - ) - - # find outer block location inside this matrix block && write it - row_range = find_block_range(codomain_fused_axes, iter_do, sect) - col_range = find_block_range(domain_fused_axes, iter_co, sect) - @views allowed_matrix_blocks[i_sec][row_range, col_range] = sym_block_sec - end - end -end - -#----------------------------------- cast to array ---------------------------------------- -function add_sector_block!( - fused_array_block::AbstractArray{<:Number,4}, - sym_block_sec::AbstractMatrix, - tree_codomain::AbstractArray{<:Real,3}, - tree_domain::AbstractArray{<:Real,3}, -) - codomain_block_struct_sector = size(tree_codomain, 3) - domain_block_struct_sector = size(tree_domain, 3) - # ----------------------sym_block_sec--------------- - # | | - # ext1*ext2*ext3*struct_sec_domain ext4*ext5*ext6*struct_sec_codomain - # - sym_data_shape = ( - size(sym_block_sec, 1) ÷ codomain_block_struct_sector, - codomain_block_struct_sector, - size(sym_block_sec, 2) ÷ domain_block_struct_sector, - domain_block_struct_sector, - ) - - # -----------------------sym_data---------------------------- - # | | | | - # ext1*ext2*ext3 struct_sec_domain ext4*ext5*ext6 struct_sec_codomain - # - sym_data = reshape(sym_block_sec, sym_data_shape) - - # contract codomain tree - # -----------------------------data_1tree------------------------------ - # | | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 struct_sec_codomain dim1*dim2*dim3 dim_sec - # - data_1tree = contract((1, 2, 6, 3, 5), sym_data, (1, 7, 2, 6), tree_codomain, (3, 5, 7)) + N = ndims(array_block) - # contract domain tree - # ---------------------fused_array_block-------------------- - # | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 - # - return contract!( - fused_array_block, - (1, 2, 3, 4), - data_1tree, - (1, 2, 6, 3, 5), - tree_domain, - (4, 5, 6), - 1.0, - 1.0, - ) -end - -function unfuse_array_block( - fused_array_block::AbstractArray{<:Number,4}, - codomain_block_degens::Tuple, - codomain_block_dims::Tuple, - domain_block_degens::Tuple, - domain_block_dims::Tuple, -) - # ---------------------fused_array_block-------------------- - # | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 - # - - # -------------------permuted_split_array_block----------------------------------- - # | | | | | | | | | | | | - # ext1 ext2 ext3 ext4 ext5 ext6 dim1 dim2 dim3 dim4 dim5 dim6 - # - permuted_split_array_block = reshape_fused_to_permuted( - fused_array_block, - codomain_block_degens, - codomain_block_dims, - domain_block_degens, - domain_block_dims, - ) + split_array_block = split_degen_dims(array_block, f1, f2) + dim_sec = quantum_dimension(root_sector(f1)) + p = contract_singlet_projector(f1, f2) # ------------------------------split_array_block------------------------- # | | | | | | # / \ / \ / \ / \ / \ / \ # / \ / \ / \ / \ / \ / \ # ext1 dim1 ext2 dim2 ext3 dim3 ext4 dim4 ext5 dim5 ext6 dim6 - # - split_array_block = unpermute_split_array_block(permuted_split_array_block) - - # - # ------------------------------array_block------------------------------- - # | | | | | | - # ext1*dim1 ext2*dim2 ext3*dim3 ext4*dim4 ext5*dim5 ext6*dim6 - # - array_block = merge_degen_dims(split_array_block) - return array_block + # \ | | \__ | | + # \________ | | \__ | | + # \| | \ _| | + # \___________| \___________| + # \ | + # \----------------dim_sec---------------- / + return contract( + ntuple(i -> 2 * i - 1, N), + split_array_block, + ntuple(identity, 2 * N), + p, + ntuple(i -> 2 * i, N), + 1 / dim_sec, # normalization factor + ) end -function fill_blockarray!( - blockarray::AbstractBlockArray, - data_mat::AbstractMatrix, - codomain_fused_axes::FusedAxes, - domain_fused_axes::FusedAxes, -) - codomain_legs, codomain_degens, codomain_dims = split_axes(codomain_fused_axes) - domain_legs, domain_degens, domain_dims = split_axes(domain_fused_axes) - - matrix_block_blocks = sort(collect(block_stored_indices(data_mat))) - existing_matrix_blocks = [view(data_mat, b) for b in matrix_block_blocks] - matrix_block_indices = reinterpret(Tuple{Int,Int}, matrix_block_blocks) - existing_sectors = blocklabels(codomain_fused_axes)[first.(matrix_block_indices)] - existing_outer_blocks = allowed_outer_blocks_sectors( - codomain_fused_axes, domain_fused_axes, matrix_block_indices +function contract_singlet_projector(f1::FusionTree, f2::FusionTree) + f1_array = convert(Array, f1) + f2_array = convert(Array, f2) + N_CO = length(f1) + N_DO = length(f2) + return contract( + ntuple(identity, N_CO + N_DO), + f1_array, + (ntuple(identity, N_CO)..., N_CO + N_DO + 1), + f2_array, + (ntuple(i -> i + N_CO, N_DO)..., N_CO + N_DO + 1), ) +end - # cache computed trees - codomain_tree_tensors_cache = Dict{ - NTuple{ndims(codomain_fused_axes),Int},Vector{Array{Float64,3}} - }() - domain_tree_tensors_cache = Dict{ - NTuple{ndims(domain_fused_axes),Int},Vector{Array{Float64,3}} - }() - - # loop for each existing outer block - for (outer_block, outer_block_sectors) in existing_outer_blocks - iter_do = outer_block[begin:ndims(codomain_fused_axes)] - codomain_block_degens = getindex.(codomain_degens, iter_do) - codomain_block_dims = getindex.(codomain_dims, iter_do) - codomain_block_trees = get_fusion_tree_tensors!( - codomain_tree_tensors_cache, iter_do, codomain_legs, existing_sectors - ) - - iter_co = outer_block[(ndims(codomain_fused_axes) + 1):end] - domain_block_degens = getindex.(domain_degens, iter_co) - domain_block_dims = getindex.(domain_dims, iter_co) - domain_block_trees = get_fusion_tree_tensors!( - domain_tree_tensors_cache, iter_co, domain_legs, existing_sectors - ) - - # ---------------------fused_array_block-------------------- - # | | | | - # ext1*ext2*ext3 ext4*ext5*ext6 dim1*dim2*dim3 dim4*dim5*dim6 - # - fused_array_block_shape = ( - prod(codomain_block_degens), - prod(domain_block_degens), - prod(codomain_block_dims), - prod(domain_block_dims), - ) - fused_array_block = zeros(eltype(blockarray), fused_array_block_shape) - - # loop for each symmetry sector inside this configuration - for sect in outer_block_sectors - i_sec = findfirst(==(sect), existing_sectors) - row_range = find_block_range(codomain_fused_axes, iter_do, sect) - col_range = find_block_range(domain_fused_axes, iter_co, sect) - sym_block_sec = view(existing_matrix_blocks[i_sec], row_range, col_range) - add_sector_block!( - fused_array_block, - sym_block_sec, - codomain_block_trees[i_sec], - domain_block_trees[i_sec], - ) - end +#----------------------------------- cast to array ---------------------------------------- +function contract_fusion_trees(ft::FusionTensor, f1::FusionTree, f2::FusionTree) + N = ndims(ft) + charge_block = reshape(view(ft, f1, f2), :, 1) + p = contract_singlet_projector(f1, f2) + + # TODO use contract once it supports outer product + swapped = charge_block * reshape(p, 1, :) + b = findblock(ft, f1, f2) + block_shape = ( + ntuple(i -> blocklengths(axes(ft, i))[Int(Tuple(b)[i])], N)..., + ntuple(i -> quantum_dimension(blocklabels(axes(ft, i))[Int(Tuple(b)[i])]), N)..., + ) + perm = braid_tuples(ntuple(identity, N), ntuple(i -> i + N, N)) + split_array_block = permutedims(reshape(swapped, block_shape), perm) - blockarray[Block(iter_do..., iter_co...)] = unfuse_array_block( - fused_array_block, - codomain_block_degens, - codomain_block_dims, - domain_block_degens, - domain_block_dims, - ) - end + return merge_degen_dims(split_array_block) end diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 3c169ba..df8a7fb 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -53,9 +53,17 @@ Base.Array(ft::FusionTensor) = Array(cast_to_array(ft)) # adjoint is costless: dual axes, swap codomain and domain, take data_matrix adjoint. # data_matrix coeff are not modified (beyond complex conjugation) +transpose_mapping(d::Dict) = Dict([reverse(k) => transpose_mapping(v) for (k, v) in d]) +function transpose_mapping(b::BlockIndexRange{2}) + new_block = Block(reverse(Tuple(Block(b)))) + return new_block[reverse(b.indices)...] +end function Base.adjoint(ft::FusionTensor) return FusionTensor( - adjoint(data_matrix(ft)), dual.(domain_axes(ft)), dual.(codomain_axes(ft)) + adjoint(data_matrix(ft)), + dual.(domain_axes(ft)), + dual.(codomain_axes(ft)), + transpose_mapping(trees_block_mapping(ft)), ) end @@ -88,11 +96,21 @@ function Base.eachindex(::FusionTensor) throw(codomainError("eachindex", "eachindex not defined for FusionTensor")) end -function Base.getindex(ft::FusionTensor, trees::Tuple{<:FusionTree,<:FusionTree}) - return data_matrix(ft)[trees_block_mapping(ft)[trees]] -end +Base.getindex(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = ft[f1f2...] function Base.getindex(ft::FusionTensor, f1::FusionTree, f2::FusionTree) - return ft[(f1, f2)] + return data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] +end + +function Base.setindex!( + ft::FusionTensor, m::AbstractMatrix, f1f2::Tuple{<:FusionTree,<:FusionTree} +) + return setindex!(ft, m, f1f2...) +end +# TBD any way to replace explicit definition with better handling of @views? +function Base.setindex!(ft::FusionTensor, m::AbstractMatrix, f1::FusionTree, f2::FusionTree) + #return setindex!(data_matrix(ft), m, trees_block_mapping(ft)[f1, f2]) + # workaround for setindex(::BlockSparseArray) issue + return data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] .= m end Base.ndims(::FusionTensor{T,N}) where {T,N} = N @@ -124,3 +142,8 @@ function Base.show(io::IO, ::MIME"text/plain", ft::FusionTensor) end Base.size(ft::FusionTensor) = quantum_dimension.(axes(ft)) + +Base.view(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = view(ft, f1f2...) +function Base.view(ft::FusionTensor, f1::FusionTree, f2::FusionTree) + return view(data_matrix(ft), trees_block_mapping(ft)[f1, f2]) +end diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index e356f1d..24e63aa 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -38,20 +38,20 @@ end function FusedAxes{S}( outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} ) where {S<:AbstractSector} - fusion_trees_mult = fusion_trees_external_multiplicites(outer_legs) + fusion_trees_mult = fusion_trees_external_multiplicities(outer_legs) fused_leg, range_mapping = compute_inner_ranges(fusion_trees_mult) return FusedAxes(outer_legs, fused_leg, range_mapping) end -function fusion_trees_external_multiplicites( +function fusion_trees_external_multiplicities( outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} ) N = length(outer_legs) tree_arrows = isdual.(outer_legs) return mapreduce(vcat, CartesianIndices(blocklength.(outer_legs))) do it - block_sectors = ntuple(i -> blocklabels(outer_legs[i])[it[i]], N) - block_mult = prod(ntuple(i -> blocklengths(outer_legs[i])[it[i]], N)) + block_sectors = map((g, i) -> blocklabels(g)[i], outer_legs, Tuple(it)) + block_mult = mapreduce((g, i) -> blocklengths(g)[i], *, outer_legs, Tuple(it); init=1) return build_trees(block_sectors, tree_arrows) .=> block_mult end end diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 0232e19..4e946c4 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -47,7 +47,24 @@ ndims_domain(ft::FusionTensor) = length(domain_axes(ft)) matrix_size(ft::FusionTensor) = quantum_dimension.(axes(data_matrix(ft))) matrix_row_axis(ft::FusionTensor) = first(axes(data_matrix(ft))) matrix_column_axis(ft::FusionTensor) = last(axes(data_matrix(ft))) + +# GradedUnitRanges interface GradedUnitRanges.sector_type(ft::FusionTensor) = sector_type(matrix_row_axis(ft)) +function GradedUnitRanges.findblock(ft::FusionTensor, f1::FusionTree, f2::FusionTree) + # find outer block corresponding to fusion trees + @assert ndims_codomain(ft) == length(f1) + @assert ndims_domain(ft) == length(f2) + @assert sector_type(ft) == sector_type(f1) + @assert sector_type(ft) == sector_type(f2) + b1 = ntuple( + i -> findfirst(==(leaves(f1)[i]), blocklabels(codomain_axes(ft)[i])), ndims_codomain(ft) + ) + b2 = ntuple( + i -> findfirst(==(leaves(f2)[i]), blocklabels(dual(domain_axes(ft)[i]))), + ndims_domain(ft), + ) + return Block(b1..., b2...) +end function sanitize_axes(raw_legs) legs = unify_sector_type(raw_legs) @@ -145,6 +162,7 @@ function initialize_allowed_sectors!(mat::AbstractBlockMatrix) end end +# TODO move to tests function check_data_matrix_axes( mat::BlockSparseMatrix, codomain_legs::Tuple, domain_legs::Tuple ) @@ -157,6 +175,7 @@ function check_data_matrix_axes(mat::Adjoint, codomain_legs::Tuple, domain_legs: return check_data_matrix_axes(adjoint(mat), dual.(domain_legs), dual.(codomain_legs)) end +# TODO move to tests function check_sanity(ft::FusionTensor) nca = ndims_domain(ft) @assert nca == length(domain_axes(ft)) "ndims_domain does not match domain_axes" diff --git a/test/test_array_cast.jl b/test/test_array_cast.jl index 333bc81..9e90afd 100644 --- a/test/test_array_cast.jl +++ b/test/test_array_cast.jl @@ -41,10 +41,10 @@ using SymmetrySectors: O2, SectorProduct, SU2, TrivialSector, U1 g2 = gradedrange([TrivialSector() => 3]) g3 = gradedrange([TrivialSector() => 4]) g4 = gradedrange([TrivialSector() => 2]) - domain_legs = (g1, g2) - codomain_legs = dual.((g3, g4)) + codomain_legs = (g1, g2) + domain_legs = dual.((g3, g4)) t = convert.(Float64, reshape(collect(1:48), (2, 3, 4, 2))) - ft = FusionTensor(t, domain_legs, codomain_legs) + ft = FusionTensor(t, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (6, 8) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[Block(1, 1)] ≈ reshape(t, (6, 8)) @@ -83,10 +83,10 @@ end @testset "2-block identity" begin g = gradedrange([U1(1) => 1, U1(2) => 2]) - domain_legs = (g,) - codomain_legs = (dual(g),) + codomain_legs = (g,) + domain_legs = (dual(g),) dense = Array{Float64}(LinearAlgebra.I(3)) - ft = FusionTensor(dense, domain_legs, codomain_legs) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (3, 3) @test blocksize(data_matrix(ft)) == (2, 2) @test data_matrix(ft)[Block(1, 1)] ≈ ones((1, 1)) @@ -102,10 +102,10 @@ end g2 = gradedrange([U1(2) => 3]) g3 = gradedrange([U1(3) => 4]) g4 = gradedrange([U1(0) => 2]) - domain_legs = dual.((g1, g2)) - codomain_legs = (g3, g4) + codomain_legs = (g1, g2) + domain_legs = dual.((g3, g4)) t = convert.(Float64, reshape(collect(1:48), (2, 3, 4, 2))) - ft = FusionTensor(t, domain_legs, codomain_legs) + ft = FusionTensor(t, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (6, 8) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[Block(1, 1)] ≈ reshape(t, (6, 8)) @@ -119,14 +119,14 @@ end g2 = gradedrange([U1(2) => 3, U1(3) => 2]) g3 = gradedrange([U1(3) => 4, U1(4) => 1]) g4 = gradedrange([U1(0) => 2, U1(2) => 1]) - domain_legs = (g1, g2) - codomain_legs = dual.((g3, g4)) + codomain_legs = (g1, g2) + domain_legs = dual.((g3, g4)) dense = zeros((4, 5, 5, 3)) dense[1:2, 1:3, 1:4, 1:2] .= 1.0 dense[3:4, 1:3, 5:5, 1:2] .= 2.0 dense[1:2, 4:5, 5:5, 1:2] .= 3.0 dense[3:4, 4:5, 1:4, 3:3] .= 4.0 - ft = FusionTensor(dense, domain_legs, codomain_legs) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (20, 15) @test blocksize(data_matrix(ft)) == (3, 4) @test norm(ft) ≈ norm(dense) @@ -140,11 +140,11 @@ end g2 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) g3 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) g4 = gradedrange([U1(-1) => 1, U1(0) => 2, U1(1) => 1]) - domain_legs = (g1,) - codomain_legs = (dual(g2), dual(g3), g4) + codomain_legs = (g1,) + domain_legs = (dual(g2), dual(g3), g4) dense = zeros(ComplexF64, (3, 6, 5, 4)) dense[2:2, 1:1, 1:2, 2:3] .= 1.0im - ft = FusionTensor(dense, domain_legs, codomain_legs) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (3, 120) @test blocksize(data_matrix(ft)) == (3, 8) @test norm(ft) ≈ norm(dense) @@ -186,13 +186,14 @@ end if VERSION < v"1.11" @test_broken FusionTensor(zerodim, (), ()) isa FusionTensor # https://github.com/JuliaLang/julia/issues/52615 else - ft = FusionTensor(zerodim, (), ()) - @test ft isa FusionTensor - @test ndims(ft) == 0 - @test isnothing(check_sanity(ft)) - @test size(data_matrix(ft)) == (1, 1) - @test data_matrix(ft)[1, 1] ≈ 1.0 - @test_broken Array(ft) ≈ zerodim # cannot create zerodim BlockSparseArray + # TODO fix: add specialized method, maybe fix TensorAlgebra + @test_broken FusionTensor(zerodim, (), ()) + #@test ft isa FusionTensor + #@test ndims(ft) == 0 + #@test isnothing(check_sanity(ft)) + #@test size(data_matrix(ft)) == (1, 1) + #@test data_matrix(ft)[1, 1] ≈ 1.0 + #@test_broken Array(ft) ≈ zerodim # cannot create zerodim BlockSparseArray end end end @@ -217,7 +218,7 @@ end # identity id2 = LinearAlgebra.I((2)) - ft = FusionTensor(id2, (g2b,), (g2,)) + ft = FusionTensor(id2, (g2,), (g2b,)) @test norm(ft) ≈ √2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ id2 @@ -233,8 +234,8 @@ end ], (2, 2, 2, 2), ) - dense, domain_legs, codomain_legs = sds22, (g2b, g2b), (g2, g2) - ft = FusionTensor(dense, domain_legs, codomain_legs) + dense, codomain_legs, domain_legs = sds22, (g2, g2), (g2b, g2b) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test norm(ft) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @@ -251,23 +252,23 @@ end (2, 2, 2, 2), ) sds22b_codomain_legs = (g2, g2b) - dense, domain_legs, codomain_legs = sds22b, (g2, g2b), (g2b, g2) - ftb = FusionTensor(dense, domain_legs, codomain_legs) + dense, codomain_legs, domain_legs = sds22b, (g2, g2b), (g2b, g2) + ftb = FusionTensor(dense, codomain_legs, domain_legs) @test norm(ftb) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ftb) ≈ sds22b @test Array(adjoint(ftb)) ≈ sds22b - # no codomain axis - dense, domain_legs, codomain_legs = sds22, (g2b, g2b, g2, g2), () - ft = FusionTensor(dense, domain_legs, codomain_legs) + # no domain axis + dense, codomain_legs, domain_legs = sds22, (g2, g2, g2b, g2b), () + ft = FusionTensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 - # no domain axis - dense, domain_legs, codomain_legs = sds22, (), (g2b, g2b, g2, g2) - ft = FusionTensor(dense, domain_legs, codomain_legs) + # no codomain axis + dense, codomain_legs, domain_legs = sds22, (), (g2, g2, g2b, g2b) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 @@ -294,7 +295,7 @@ end # identity id2 = LinearAlgebra.I((2)) - ft = FusionTensor(id2, (g2b,), (g2,)) + ft = FusionTensor(id2, (g2,), (g2b,)) @test norm(ft) ≈ √2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ id2 @@ -357,7 +358,7 @@ end domain_legs = dual.(codomain_legs) d = 8 dense = reshape(LinearAlgebra.I(d^N), ntuple(_ -> d, 2 * N)) - ft = FusionTensor(dense, domain_legs, codomain_legs) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ dense @test Array(adjoint(ft)) ≈ dense @@ -377,21 +378,21 @@ end end gd = gradedrange([SectorProduct(s, U1(3)) => 1]) - domain_legs = (dual(gd),) + codomain_legs = (dual(gd),) gD = gradedrange([SectorProduct(SU2(0), U1(1)) => 1, SectorProduct(s, U1(0)) => 1]) - codomain_legs = (gD, gD, gD, gD) - ft = FusionTensor(tRVB, domain_legs, codomain_legs) + domain_legs = (gD, gD, gD, gD) + ft = FusionTensor(tRVB, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ tRVB # same with NamedTuples gd_nt = gradedrange([SectorProduct(; S=s, N=U1(3)) => 1]) - domain_legs_nt = (dual(gd_nt),) + codomain_legs_nt = (dual(gd_nt),) gD_nt = gradedrange([ SectorProduct(; S=SU2(0), N=U1(1)) => 1, SectorProduct(; S=s, N=U1(0)) => 1 ]) - codomain_legs_nt = (gD_nt, gD_nt, gD_nt, gD_nt) - ft_nt = FusionTensor(tRVB, domain_legs_nt, codomain_legs_nt) + domain_legs_nt = (gD_nt, gD_nt, gD_nt, gD_nt) + ft_nt = FusionTensor(tRVB, codomain_legs_nt, domain_legs_nt) @test isnothing(check_sanity(ft_nt)) @test Array(ft_nt) ≈ tRVB end From a9dc29f623a68ea7349f7e33674292608dd7850c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Tue, 10 Dec 2024 23:26:02 +0100 Subject: [PATCH 06/52] clean Base --- src/fusiontensor/base_interface.jl | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index df8a7fb..f646181 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -13,9 +13,7 @@ end # impose matching type and number of axes at compile time # impose matching axes at run time function Base.:*(left::FusionTensor, right::FusionTensor) - if !matching_dual(domain_axes(left), codomain_axes(right)) - throw(codomainError("Incompatible tensor axes")) - end + @asssert matching_dual(domain_axes(left), codomain_axes(right)) new_data_matrix = data_matrix(left) * data_matrix(right) return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(right)) end @@ -25,9 +23,7 @@ Base.:+(ft::FusionTensor) = ft # tensor addition is a block data_matrix add. # impose matching axes, allow different eltypes function Base.:+(left::FusionTensor, right::FusionTensor) - if !matching_axes(axes(left), axes(right)) - throw(codomainError("Incompatible tensor axes")) - end + @assert !matching_axes(axes(left), axes(right)) new_data_matrix = data_matrix(left) + data_matrix(right) return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(left)) end @@ -38,9 +34,7 @@ function Base.:-(ft::FusionTensor) end function Base.:-(left::FusionTensor, right::FusionTensor) - if !matching_axes(axes(left), axes(right)) - throw(codomainError("Incompatible tensor axes")) - end + @assert matching_axes(axes(left), axes(right)) new_data_matrix = data_matrix(left) - data_matrix(right) return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(left)) end @@ -70,9 +64,7 @@ end Base.axes(ft::FusionTensor) = (codomain_axes(ft)..., domain_axes(ft)...) # conj is defined as coefficient wise complex conjugation, without axis dual -# same object for real element type -Base.conj(ft::FusionTensor{<:Real}) = ft - +Base.conj(ft::FusionTensor{<:Real}) = ft # same object for real element type function Base.conj(ft::FusionTensor) return FusionTensor(conj(data_matrix(ft)), codomain_axes(ft), domain_axes(ft)) end @@ -92,9 +84,7 @@ function Base.deepcopy(ft::FusionTensor) end # eachindex is automatically defined for AbstractArray. We do not want it. -function Base.eachindex(::FusionTensor) - throw(codomainError("eachindex", "eachindex not defined for FusionTensor")) -end +Base.eachindex(::FusionTensor) = error("eachindex not defined for FusionTensor") Base.getindex(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = ft[f1f2...] function Base.getindex(ft::FusionTensor, f1::FusionTree, f2::FusionTree) From d7edb71a79358458bb78c7cff81bc847ee90fbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 11:14:03 +0100 Subject: [PATCH 07/52] reuse tree_block_mapping in Base operations --- src/fusiontensor/base_interface.jl | 71 +++++++++++++++++++----------- src/fusiontensor/fusiontensor.jl | 2 +- test/test_basics.jl | 5 +-- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index f646181..a446fa6 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -1,19 +1,20 @@ # This files defines Base functions for FusionTensor function Base.:*(x::Number, ft::FusionTensor) - return FusionTensor(x * data_matrix(ft), codomain_axes(ft), domain_axes(ft)) + return FusionTensor( + x * data_matrix(ft), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) + ) end function Base.:*(ft::FusionTensor, x::Number) - return FusionTensor(x * data_matrix(ft), codomain_axes(ft), domain_axes(ft)) + return FusionTensor( + x * data_matrix(ft), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) + ) end # tensor contraction is a block data_matrix product. -# allow to contract with different eltype and let BlockSparseArray ensure compatibility -# impose matching type and number of axes at compile time -# impose matching axes at run time function Base.:*(left::FusionTensor, right::FusionTensor) - @asssert matching_dual(domain_axes(left), codomain_axes(right)) + @assert matching_dual(domain_axes(left), codomain_axes(right)) new_data_matrix = data_matrix(left) * data_matrix(right) return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(right)) end @@ -21,26 +22,33 @@ end Base.:+(ft::FusionTensor) = ft # tensor addition is a block data_matrix add. -# impose matching axes, allow different eltypes function Base.:+(left::FusionTensor, right::FusionTensor) - @assert !matching_axes(axes(left), axes(right)) + @assert matching_axes(axes(left), axes(right)) new_data_matrix = data_matrix(left) + data_matrix(right) - return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(left)) + return FusionTensor( + new_data_matrix, codomain_axes(left), domain_axes(left), trees_block_mapping(left) + ) end function Base.:-(ft::FusionTensor) new_data_matrix = -data_matrix(ft) - return FusionTensor(new_data_matrix, codomain_axes(ft), domain_axes(ft)) + return FusionTensor( + new_data_matrix, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) + ) end function Base.:-(left::FusionTensor, right::FusionTensor) @assert matching_axes(axes(left), axes(right)) new_data_matrix = data_matrix(left) - data_matrix(right) - return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(left)) + return FusionTensor( + new_data_matrix, codomain_axes(left), domain_axes(left), trees_block_mapping(left) + ) end function Base.:/(ft::FusionTensor, x::Number) - return FusionTensor(data_matrix(ft) / x, codomain_axes(ft), domain_axes(ft)) + return FusionTensor( + data_matrix(ft) / x, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) + ) end Base.Array(ft::FusionTensor) = Array(cast_to_array(ft)) @@ -66,21 +74,27 @@ Base.axes(ft::FusionTensor) = (codomain_axes(ft)..., domain_axes(ft)...) # conj is defined as coefficient wise complex conjugation, without axis dual Base.conj(ft::FusionTensor{<:Real}) = ft # same object for real element type function Base.conj(ft::FusionTensor) - return FusionTensor(conj(data_matrix(ft)), codomain_axes(ft), domain_axes(ft)) + return FusionTensor( + conj(data_matrix(ft)), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) + ) end function Base.copy(ft::FusionTensor) - new_data_matrix = copy(data_matrix(ft)) - new_codomain_axes = copy.(codomain_axes(ft)) - new_domain_axes = copy.(domain_axes(ft)) - return FusionTensor(new_data_matrix, new_codomain_axes, new_domain_axes) + return FusionTensor( + copy(data_matrix(ft)), + copy.(codomain_axes(ft)), + copy.(domain_axes(ft)), + copy(trees_block_mapping(ft)), + ) end function Base.deepcopy(ft::FusionTensor) - new_data_matrix = deepcopy(data_matrix(ft)) - new_codomain_axes = deepcopy.(codomain_axes(ft)) - new_domain_axes = deepcopy.(domain_axes(ft)) - return FusionTensor(new_data_matrix, new_codomain_axes, new_domain_axes) + return FusionTensor( + deepcopy(data_matrix(ft)), + deepcopy.(codomain_axes(ft)), + deepcopy.(domain_axes(ft)), + deepcopy(trees_block_mapping(ft)), + ) end # eachindex is automatically defined for AbstractArray. We do not want it. @@ -109,15 +123,20 @@ Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args. function Base.similar(ft::FusionTensor) mat = similar(data_matrix(ft)) - return FusionTensor(mat, codomain_axes(ft), domain_axes(ft)) + initialize_allowed_sectors!(mat) + return FusionTensor(mat, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft)) end -function Base.similar(ft::FusionTensor, elt::Type) - return FusionTensor(elt, codomain_axes(ft), domain_axes(ft)) +function Base.similar(ft::FusionTensor, ::Type{T}) where {T} + # fusion trees have Float64 eltype: need compatible type + @assert promote_type(T, Float64) === T + mat = similar(data_matrix(ft), T) + initialize_allowed_sectors!(mat) + return FusionTensor(mat, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft)) end -function Base.similar(::FusionTensor, elt::Type, new_axes::Tuple{<:Tuple,<:Tuple}) - return FusionTensor(elt, new_axes[1], new_axes[2]) +function Base.similar(::FusionTensor, ::Type{T}, new_axes::Tuple{<:Tuple,<:Tuple}) where {T} + return FusionTensor(T, new_axes[1], new_axes[2]) end Base.show(io::IO, ft::FusionTensor) = print(io, "$(ndims(ft))-dim FusionTensor") diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 4e946c4..eee16df 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -205,7 +205,7 @@ function check_sanity(ft::FusionTensor) return nothing end -matching_dual(axes1::Tuple, axes2::Tuple) = matching_axes(dual.(axes1), axes2) +matching_dual(axes1::Tuple, axes2::Tuple) = matching_axes(axes1, dual.(axes2)) matching_axes(axes1::Tuple, axes2::Tuple) = false function matching_axes(axes1::T, axes2::T) where {T<:Tuple} return all(space_isequal.(axes1, axes2)) diff --git a/test/test_basics.jl b/test/test_basics.jl index bbbf92a..81273aa 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,5 +1,5 @@ @eval module $(gensym()) -using Test: @test, @testset +using Test: @test, @test_throws, @testset using BlockSparseArrays: BlockSparseArray using FusionTensors: @@ -81,8 +81,7 @@ using SymmetrySectors: U1 @test matching_axes(codomain_axes(ft3), codomain_axes(ft1)) @test matching_axes(domain_axes(ft3), domain_axes(ft1)) - ft4 = similar(ft1, Float32) - @test eltype(ft4) == Float64 # promoted + @test_throws AssertionError similar(ft1, Int) ft5 = similar(ft1, ComplexF32, ((g1, g1), (g2,))) @test isnothing(check_sanity(ft5)) From 4a8f9ba27178bb81d12874d803dcf94141af1035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 17:00:55 +0100 Subject: [PATCH 08/52] working permutedims --- src/fusiontensor/array_cast.jl | 3 +- src/fusiontensor/base_interface.jl | 17 +- src/fusiontensor/fusiontensor.jl | 4 + src/permutedims/permutedims.jl | 235 ++--------------------- src/permutedims/unitaries.jl | 291 +++-------------------------- test/test_permutedims.jl | 44 ++--- 6 files changed, 73 insertions(+), 521 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 13fa10d..cb5d082 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -43,8 +43,7 @@ function cast_from_array( ft = FusionTensor(eltype(blockarray), codomain_legs, domain_legs) for (f1, f2) in keys(trees_block_mapping(ft)) b = findblock(ft, f1, f2) - charge_block = contract_fusion_trees(blockarray[b], f1, f2) - ft[f1, f2] = reshape(charge_block, size(ft[f1, f2])) + ft[f1, f2] = contract_fusion_trees(blockarray[b], f1, f2) end return ft end diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index a446fa6..0ddbfdb 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -102,19 +102,17 @@ Base.eachindex(::FusionTensor) = error("eachindex not defined for FusionTensor") Base.getindex(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = ft[f1f2...] function Base.getindex(ft::FusionTensor, f1::FusionTree, f2::FusionTree) - return data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] + charge_matrix = data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] + return reshape(charge_matrix, charge_block_size(ft, f1, f2)) end function Base.setindex!( - ft::FusionTensor, m::AbstractMatrix, f1f2::Tuple{<:FusionTree,<:FusionTree} + ft::FusionTensor, a::AbstractArray, f1f2::Tuple{<:FusionTree,<:FusionTree} ) - return setindex!(ft, m, f1f2...) + return setindex!(ft, a, f1f2...) end -# TBD any way to replace explicit definition with better handling of @views? -function Base.setindex!(ft::FusionTensor, m::AbstractMatrix, f1::FusionTree, f2::FusionTree) - #return setindex!(data_matrix(ft), m, trees_block_mapping(ft)[f1, f2]) - # workaround for setindex(::BlockSparseArray) issue - return data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] .= m +function Base.setindex!(ft::FusionTensor, a::AbstractArray, f1::FusionTree, f2::FusionTree) + return view(ft, f1, f2) .= a end Base.ndims(::FusionTensor{T,N}) where {T,N} = N @@ -154,5 +152,6 @@ Base.size(ft::FusionTensor) = quantum_dimension.(axes(ft)) Base.view(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = view(ft, f1f2...) function Base.view(ft::FusionTensor, f1::FusionTree, f2::FusionTree) - return view(data_matrix(ft), trees_block_mapping(ft)[f1, f2]) + charge_matrix = view(data_matrix(ft), trees_block_mapping(ft)[f1, f2]) + return reshape(charge_matrix, charge_block_size(ft, f1, f2)) end diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index eee16df..75524a5 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -47,6 +47,10 @@ ndims_domain(ft::FusionTensor) = length(domain_axes(ft)) matrix_size(ft::FusionTensor) = quantum_dimension.(axes(data_matrix(ft))) matrix_row_axis(ft::FusionTensor) = first(axes(data_matrix(ft))) matrix_column_axis(ft::FusionTensor) = last(axes(data_matrix(ft))) +function charge_block_size(ft::FusionTensor, f1::FusionTree, f2::FusionTree) + b = Tuple(findblock(ft, f1, f2)) + return ntuple(i -> Int(length(axes(ft)[i][b[i]])), ndims(ft)) +end # GradedUnitRanges interface GradedUnitRanges.sector_type(ft::FusionTensor) = sector_type(matrix_row_axis(ft)) diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl index 72ce211..bd4ea0c 100644 --- a/src/permutedims/permutedims.jl +++ b/src/permutedims/permutedims.jl @@ -1,8 +1,7 @@ # This file defines permutedims for a FusionTensor -# ================================= High level interface ================================= # permutedims with 1 tuple of 2 separate tuples -function fusiontensor_permutedims(ft::FusionTensor, new_leg_indices::Tuple{NTuple,NTuple}) +function fusiontensor_permutedims(ft::FusionTensor, new_leg_indices::Tuple{Tuple,Tuple}) return fusiontensor_permutedims(ft, new_leg_indices...) end @@ -11,7 +10,7 @@ function fusiontensor_permutedims( ft::FusionTensor, new_codomain_indices::Tuple, new_domain_indices::Tuple ) biperm = blockedperm(new_codomain_indices, new_domain_indices) - return permutedims(ft, biperm) + return fusiontensor_permutedims(ft, biperm) end function fusiontensor_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2}) @@ -24,11 +23,9 @@ function fusiontensor_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2 end end - permuted_data_matrix = permute_data_matrix( - data_matrix(ft), codomain_axes(ft), domain_axes(ft), biperm - ) new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) - permuted = FusionTensor(permuted_data_matrix, new_codomain_legs, new_domain_legs) + permuted = FusionTensor(eltype(ft), new_codomain_legs, new_domain_legs) + permute_fusiontensor_data!(permuted, ft, Tuple(biperm)) return permuted end @@ -36,227 +33,19 @@ function naive_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2}) @assert ndims(ft) == length(biperm) new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) - # stupid permute: cast to dense, permutedims, cast to FusionTensor + # naive permute: cast to dense, permutedims, cast to FusionTensor arr = Array(ft) permuted_arr = permutedims(arr, Tuple(biperm)) permuted = FusionTensor(permuted_arr, new_codomain_legs, new_domain_legs) - return permuted end -# ================================= Low level interface ================================== -function permute_data_matrix( - old_data_matrix::AbstractMatrix, - old_codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - old_domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - biperm::BlockedPermutation{2}, -) - # TBD use FusedAxes as input? - - unitaries = compute_unitaries(old_codomain_legs, old_domain_legs, biperm) - - # TODO cache FusedAxes - old_codomain_fused_axes = FusedAxes(old_codomain_legs) - old_domain_fused_axes = FusedAxes(dual.(old_domain_legs)) - new_codomain_legs, new_domain_legs = blockpermute( - (old_codomain_legs..., old_domain_legs...), biperm - ) - new_codomain_fused_axes = FusedAxes(new_codomain_legs) - new_domain_fused_axes = FusedAxes(dual.(new_domain_legs)) - new_data_matrix = initialize_data_matrix( - eltype(old_data_matrix), new_codomain_fused_axes, new_domain_fused_axes - ) - - fill_data_matrix!( - new_data_matrix, - old_data_matrix, - old_codomain_fused_axes, - old_domain_fused_axes, - new_codomain_fused_axes, - new_domain_fused_axes, - biperm, - unitaries, - ) - return new_data_matrix -end - -# ===================================== Internals ======================================== -function add_structural_axes( - biperm::BlockedPermutation{2}, ::Val{OldNDoAxes} -) where {OldNDoAxes} - flat = Tuple(biperm) - extended_perm = ( - OldNDoAxes + 1, length(biperm) + 2, (flat .+ (flat .>= OldNDoAxes + 1))... - ) - return extended_perm -end - -function fill_data_matrix!( - new_data_matrix::AbstractMatrix, - old_data_matrix::AbstractMatrix, - old_codomain_fused_axes::FusedAxes, - old_domain_fused_axes::FusedAxes, - new_codomain_fused_axes::FusedAxes, - new_domain_fused_axes::FusedAxes, - biperm::BlockedPermutation{2}, - unitaries::Dict, -) - @assert ndims(old_codomain_fused_axes) + ndims(old_domain_fused_axes) == length(biperm) - @assert ndims(new_codomain_fused_axes) + ndims(new_domain_fused_axes) == length(biperm) - - old_matrix_block_indices = reinterpret( - Tuple{Int,Int}, sort(collect(block_stored_indices(old_data_matrix))) - ) - old_matrix_blocks = Dict( - blocklabels(old_codomain_fused_axes)[first(b)] => view(old_data_matrix, Block(b)) for - b in old_matrix_block_indices - ) - - new_matrix_block_indices = intersect(new_codomain_fused_axes, new_domain_fused_axes) - new_matrix_blocks = Dict( - blocklabels(new_codomain_fused_axes)[first(b)] => view!(new_data_matrix, Block(b)) for - b in new_matrix_block_indices - ) - - old_existing_outer_blocks = allowed_outer_blocks_sectors( - old_codomain_fused_axes, old_domain_fused_axes, old_matrix_block_indices - ) - - new_existing_outer_blocks = allowed_outer_blocks_sectors( - new_codomain_fused_axes, new_domain_fused_axes, new_matrix_block_indices - ) - - extended_perm = add_structural_axes(biperm, Val(ndims(old_codomain_fused_axes))) - - # loop for each existing outer block TODO parallelize - for old_outer_block_sectors in old_existing_outer_blocks - new_outer_block = map(i -> first(old_outer_block_sectors)[i], Tuple(biperm)) - new_outer_block_sectors = new_outer_block => new_existing_outer_blocks[new_outer_block] - permute_outer_block!( - new_matrix_blocks, - old_matrix_blocks, - old_outer_block_sectors, - new_outer_block_sectors, - unitaries[first(old_outer_block_sectors)], - extended_perm, - old_codomain_fused_axes, - old_domain_fused_axes, - new_codomain_fused_axes, - new_domain_fused_axes, - ) - end -end - -function permute_outer_block!( - new_matrix_blocks::Dict{S,<:AbstractMatrix}, - old_matrix_blocks::Dict{S,<:AbstractMatrix}, - old_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, - new_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, - unitary::AbstractBlockMatrix, - extended_perm::Tuple{Vararg{Int}}, - old_codomain_fused_axes::FusedAxes, - old_domain_fused_axes::FusedAxes, - new_codomain_fused_axes::FusedAxes, - new_domain_fused_axes::FusedAxes, -) where {S<:AbstractSector} - new_outer_array = permute_old_outer_block( - old_matrix_blocks, - old_outer_block_sectors, - old_codomain_fused_axes, - old_domain_fused_axes, - extended_perm, - unitary, - ) - return write_new_outer_block!( - new_matrix_blocks, - new_outer_array, - new_outer_block_sectors, - new_codomain_fused_axes, - new_domain_fused_axes, - ) -end - -function permute_old_outer_block( - old_matrix_blocks::Dict{S,<:AbstractMatrix}, - old_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, - old_codomain_fused_axes::FusedAxes, - old_domain_fused_axes::FusedAxes, - extended_perm::Tuple{Vararg{Int}}, - unitary::AbstractBlockMatrix, -) where {S<:AbstractSector} - old_codomain_block = first(old_outer_block_sectors)[begin:ndims(old_codomain_fused_axes)] - old_domain_block = first(old_outer_block_sectors)[(ndims(old_codomain_fused_axes) + 1):end] - old_codomain_ext_mult = block_external_multiplicities( - old_codomain_fused_axes, old_codomain_block - ) - old_domain_ext_mult = block_external_multiplicities( - old_domain_fused_axes, old_domain_block - ) - return mapreduce(+, enumerate(last(old_outer_block_sectors))) do (i_sec, old_sector) - old_row_range = find_block_range( - old_codomain_fused_axes, old_codomain_block, old_sector - ) - old_col_range = find_block_range(old_domain_fused_axes, old_domain_block, old_sector) - old_sym_block_matrix = view(old_matrix_blocks[old_sector], old_row_range, old_col_range) - old_tensor_shape = ( - old_codomain_ext_mult..., - block_structural_multiplicity( - old_codomain_fused_axes, old_codomain_block, old_sector - ), - old_domain_ext_mult..., - block_structural_multiplicity(old_domain_fused_axes, old_domain_block, old_sector), - ) - old_sym_block_tensor = reshape(old_sym_block_matrix, old_tensor_shape) - unitary_column = unitary[:, Block(i_sec)] # take all new blocks at once - return change_basis_block_sector(old_sym_block_tensor, unitary_column, extended_perm) - end -end - -function change_basis_block_sector( - old_sym_block_tensor::AbstractArray, - unitary_column::AbstractBlockMatrix, - extended_perm::Tuple{Vararg{Int}}, -) - old_permuted = permutedims(old_sym_block_tensor, extended_perm) - new_shape = (size(unitary_column, 2), prod(size(old_permuted)[3:end])) - reshaped = reshape(old_permuted, new_shape) - new_outer_array = unitary_column * reshaped - return new_outer_array -end - -function write_new_outer_block!( - new_matrix_blocks::Dict{S,<:AbstractMatrix}, - new_outer_array::AbstractMatrix, - new_outer_block_sectors::Pair{<:Tuple{Vararg{Int}},Vector{S}}, - new_codomain_fused_axes::FusedAxes, - new_domain_fused_axes::FusedAxes, -) where {S<:AbstractSector} - new_codomain_block = first(new_outer_block_sectors)[begin:ndims(new_codomain_fused_axes)] - new_domain_block = first(new_outer_block_sectors)[(ndims(new_codomain_fused_axes) + 1):end] - new_codomain_ext_mult = block_external_multiplicities( - new_codomain_fused_axes, new_codomain_block - ) - new_domain_ext_mult = block_external_multiplicities( - new_domain_fused_axes, new_domain_block - ) - for (i_sec, new_sector) in enumerate(last(new_outer_block_sectors)) # not worth parallelize - new_shape_4d = ( - block_structural_multiplicity( - new_codomain_fused_axes, new_codomain_block, new_sector - ), - block_structural_multiplicity(new_domain_fused_axes, new_domain_block, new_sector), - prod(new_codomain_ext_mult), - prod(new_domain_ext_mult), - ) - new_block_4d = reshape(view(new_outer_array, Block(i_sec), :), new_shape_4d) - permuted_new_block = permutedims(new_block_4d, (3, 1, 2, 4)) - mat_shape = (new_shape_4d[3] * new_shape_4d[1], new_shape_4d[2] * new_shape_4d[4]) - new_mat = reshape(permuted_new_block, mat_shape) - - new_row_range = find_block_range( - new_codomain_fused_axes, new_codomain_block, new_sector - ) - new_col_range = find_block_range(new_domain_fused_axes, new_domain_block, new_sector) - new_matrix_blocks[new_sector][new_row_range, new_col_range] = new_mat +function permute_fusiontensor_data!( + new_ft::FusionTensor{T,N}, old_ft::FusionTensor{T,N}, flatperm::NTuple{N,Integer} +) where {T,N} + unitary = compute_unitary(new_ft, old_ft, flatperm) + for p in unitary + old_trees, new_trees = first(p) + new_ft[new_trees] += last(p) * permutedims(old_ft[old_trees], flatperm) end end diff --git a/src/permutedims/unitaries.jl b/src/permutedims/unitaries.jl index f9afb4a..ee389aa 100644 --- a/src/permutedims/unitaries.jl +++ b/src/permutedims/unitaries.jl @@ -1,25 +1,12 @@ # This file defines unitaries to be used in permutedims -# Current implementation: -# * Unitary = BlockMatrix -# * no unitary cache - -# Notes: -# - The interface uses AbstractGradedUnitRanges as input for interface simplicity -# however only blocklabels are used and blocklengths are never used. - -using BlockArrays: BlockedOneTo - const unitary_cache = LRU{Any,AbstractMatrix}(; maxsize=10000) # ====================================== Interface ======================================= -function compute_unitaries( - old_codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - old_domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - biperm::BlockedPermutation{2}, -) - @assert length(old_codomain_legs) + length(old_domain_legs) == length(biperm) - return compute_unitaries_clebsch_gordan(old_codomain_legs, old_domain_legs, biperm) +function compute_unitary( + new_ft::FusionTensor{T,N}, old_ft::FusionTensor{T,N}, flatperm::NTuple{N,Int} +) where {T,N} + return compute_unitary_clebsch_gordan(new_ft, old_ft, flatperm) end function unitary_key(codomain_legs, domain_legs, old_outer_block, biperm) @@ -30,260 +17,34 @@ function unitary_key(codomain_legs, domain_legs, old_outer_block, biperm) end # =========================== Constructor from Clebsch-Gordan ============================ -function contract_singlet_space_projector( - codomain_tree_tensor::AbstractArray{<:Real}, - domain_tree_tensor::AbstractArray{<:Real}, - irreps_perm::NTuple{N,Int}, -) where {N} - # compile time values - NCoAxes = ndims(codomain_tree_tensor) - 2 - NDoAxes = ndims(domain_tree_tensor) - 2 - @assert NDoAxes + NCoAxes == N - - irrep_dims_prod = - prod(size(codomain_tree_tensor)[begin:NCoAxes]) * - prod(size(domain_tree_tensor)[begin:NDoAxes]) - - # some trees are empty: return projector on null space - if length(codomain_tree_tensor) == 0 || length(domain_tree_tensor) == 0 - return zeros((irrep_dims_prod, 0)) - end - - labels_codomain = (ntuple(identity, NCoAxes)..., N + 3, N + 1) - labels_domain = (ntuple(i -> i + NCoAxes, NDoAxes)..., N + 3, N + 2) - labels_dest = (irreps_perm..., N + 1, N + 2) - - # ----------------dim_sec--------- - # | | - # | struct_mult_codomain_sec | struct_mult_domain_sec - # \ / \ / - # \/ \/ - # / / - # / / - # /\ /\ - # / \ / \ - # /\ \ /\ \ - # / \ \ / \ \ - # dim1 dim2 dim3 dim4 dim5 dim6 - # - projector = TensorAlgebra.contract( - labels_dest, codomain_tree_tensor, labels_codomain, domain_tree_tensor, labels_domain - ) - - # reshape as a matrix - # ---------------projector_sector---------------- - # | | - # dim1*dim2*...*dim6 struct_mult_codomain_sec*struct_mult_domain_sec - # - return reshape(projector, (irrep_dims_prod, :)) -end - -function intersect_trees( - codomain_tree_tensors::Vector{<:AbstractArray{<:Real}}, - domain_tree_tensors::Vector{<:AbstractArray{<:Real}}, -) - kept_indices = findall( - .!isempty.(codomain_tree_tensors) .* .!isempty.(domain_tree_tensors) - ) - return codomain_tree_tensors[kept_indices] .=> domain_tree_tensors[kept_indices] -end - function overlap_fusion_trees( - old_codomain_block_trees::Vector{<:AbstractArray{<:Real}}, - old_domain_block_trees::Vector{<:AbstractArray{<:Real}}, - new_codomain_block_trees::Vector{<:AbstractArray{<:Real}}, - new_domain_block_trees::Vector{<:AbstractArray{<:Real}}, - irreps_perm::NTuple{N,Int}, -) where {N} - # filter trees that do not share the same sectors - old_trees = intersect_trees(old_codomain_block_trees, old_domain_block_trees) - new_trees = intersect_trees(new_codomain_block_trees, new_domain_block_trees) - return overlap_filtered_fusion_trees(old_trees, new_trees, irreps_perm) -end - -function overlap_filtered_fusion_trees( - old_trees::Vector{<:Pair{<:AbstractArray{<:Real},<:AbstractArray{<:Real}}}, - new_trees::Vector{<:Pair{<:AbstractArray{<:Real},<:AbstractArray{<:Real}}}, - irreps_perm::NTuple{N,Int}, -) where {N} - old_codomain_block_trees = first.(old_trees) - old_domain_block_trees = last.(old_trees) - new_codomain_block_trees = first.(new_trees) - new_domain_block_trees = last.(new_trees) - # compile time - - OldNCoAxes = ndims(eltype(old_codomain_block_trees)) - 2 - OldNDoAxes = ndims(eltype(old_domain_block_trees)) - 2 - NCoAxesNew = ndims(eltype(new_codomain_block_trees)) - 2 - NDoAxesNew = ndims(eltype(new_domain_block_trees)) - 2 - @assert OldNCoAxes + OldNDoAxes == N - @assert NCoAxesNew + NDoAxesNew == N - @assert N > 0 - - # initialize output as a BlockArray with - # blocksize: (n_new_sectors, n_old_sectors) - # blocksizes: (struct_mult_new_block_sectors, struct_mult_old_block_sectors) - block_rows = - size.(new_codomain_block_trees, NCoAxesNew + 2) .* - size.(new_domain_block_trees, NDoAxesNew + 2) - block_cols = - size.(old_codomain_block_trees, OldNCoAxes + 2) .* - size.(old_domain_block_trees, OldNDoAxes + 2) - unitary = BlockArray{Float64}(undef, block_rows, block_cols) - - # contract codomain and domain fusion trees to construct projector on each allowed sector - new_projectors = - contract_singlet_space_projector.( - new_codomain_block_trees, new_domain_block_trees, Ref(ntuple(identity, N)) - ) - - for j in eachindex(block_cols) - old_proj = contract_singlet_space_projector( - old_codomain_block_trees[j], old_domain_block_trees[j], irreps_perm - ) - for (i, new_proj) in enumerate(new_projectors) - - # Contract new and old projectors to construct singlet space basis change matrix. - # Construction is blockwise. One block (i,j) corresponds to fusing old axes - # over sector sec_j and new axes over sector sec_i. It has a 4-dim tensor - # internal structure which is reshaped into a matrix. - # - # --------------dim_sec_j--------- - # | | - # | struct_mult | struct_mult_old_domain_sec_j - # \ / _old_codomain_sec_j \ / - # \/ \/ - # / / - # /\ /\ - # / \ / \ - # /\ \ /\ \ - # / \ \ / \ \ - # dim1 dim2 dim3 dim4 dim5 dim6 - # | | | | | | - # ----------------- irreps_perm ------------ - # | | | | | | - # dim4 dim1 dim2 dim6 dim3 dim5 - # \ / / / \ / - # \ / / / \ / - # \ / / \ - # \ / / \ - # \ / \ - # \ / \ - # \/ \ - # \ \ - # /\ /\ - # / \ / \ - # | struct_mult | struct_mult_new_domain_sec_i - # | _old_codomain_sec_j | - # | | - # -------------dim_sec_i--------- - # - dim_sec_i = size(new_codomain_block_trees[i], NCoAxesNew + 1) - unitary[Block(i, j)] = (new_proj'old_proj) / dim_sec_i - end - end - - return unitary + old_trees::Tuple{FusionTree,FusionTree}, + new_trees::Tuple{FusionTree,FusionTree}, + flatperm::Tuple{Vararg{Integer}}, +) + old_proj = contract_singlet_projector(old_trees...) + new_proj = contract_singlet_projector(new_trees...) + a = contract((), new_proj, flatperm, old_proj, ntuple(identity, ndims(new_proj))) + return a[] / quantum_dimension(root_sector(first(new_trees))) end -function compute_unitaries_clebsch_gordan( - old_codomain_legs::NTuple{OldNDoAxes,AbstractGradedUnitRange}, - old_domain_legs::NTuple{OldNCoAxes,AbstractGradedUnitRange}, - biperm::BlockedPermutation{2,N}, -) where {OldNDoAxes,OldNCoAxes,N} - @assert OldNDoAxes + OldNCoAxes == N - - new_codomain_legs, nondual_new_domain_legs = TensorAlgebra.blockpermute( - (old_codomain_legs..., old_domain_legs...), biperm - ) - new_domain_legs = dual.(nondual_new_domain_legs) - new_row_labels = blocklabels(fusion_product(new_codomain_legs...)) - new_column_labels = blocklabels(fusion_product(new_domain_legs...)) - new_allowed_sectors = new_row_labels[first.( - find_shared_indices(new_row_labels, new_column_labels) - )] - - # TBD use FusedAxes as input? - old_codomain_fused_axes = FusedAxes(old_codomain_legs) - old_domain_fused_axes = FusedAxes(dual.(old_domain_legs)) - old_matrix_block_indices = intersect(old_codomain_fused_axes, old_domain_fused_axes) - old_allowed_sectors = blocklabels(old_codomain_fused_axes)[first.( - old_matrix_block_indices - )] - old_allowed_outer_blocks = allowed_outer_blocks_sectors( - old_codomain_fused_axes, old_domain_fused_axes, old_matrix_block_indices - ) - - # initialize output - unitaries = Dict{ - NTuple{N,Int64}, - BlockMatrix{ # TBD use BlockSparseArray 4-dim? - Float64, - Matrix{Matrix{Float64}}, - Tuple{BlockedOneTo{Int64,Vector{Int64}},BlockedOneTo{Int64,Vector{Int64}}}, - }, +function compute_unitary_clebsch_gordan( + new_ft::FusionTensor{T,N}, old_ft::FusionTensor{T,N}, flatperm::NTuple{N,Int} +) where {T,N} + unitary = Dict{ + Tuple{keytype(trees_block_mapping(old_ft)),keytype(trees_block_mapping(new_ft))},Float64 }() - - # cache computed Clebsch-Gordan trees. - old_codomain_tree_tensors_cache = Dict{ - NTuple{OldNDoAxes,Int},Vector{Array{Float64,OldNDoAxes + 2}} - }() - old_domain_tree_tensors_cache = Dict{ - NTuple{OldNCoAxes,Int},Vector{Array{Float64,OldNCoAxes + 2}} - }() - new_codomain_tree_tensors_cache = Dict{ - NTuple{length(new_codomain_legs),Int}, - Vector{Array{Float64,length(new_codomain_legs) + 2}}, - }() - new_domain_tree_tensors_cache = Dict{ - NTuple{length(new_domain_legs),Int},Vector{Array{Float64,length(new_domain_legs) + 2}} - }() - - flat_permutation = Tuple(biperm) - - # loop over all allowed outer blocks. - for (old_outer_block, _) in old_allowed_outer_blocks - ukey = unitary_key(old_codomain_legs, old_domain_legs, old_outer_block, biperm) - u = get!(unitary_cache, ukey) do - new_codomain_outer_block, new_domain_outer_block = blockpermute(old_outer_block, biperm) - old_codomain_block_trees = get_fusion_tree_tensors!( - old_codomain_tree_tensors_cache, - old_outer_block[begin:OldNDoAxes], - old_codomain_legs, - old_allowed_sectors, - ) - old_domain_block_trees = get_fusion_tree_tensors!( - old_domain_tree_tensors_cache, - old_outer_block[(OldNDoAxes + 1):end], - dual.(old_domain_legs), - old_allowed_sectors, - ) - new_codomain_block_trees = get_fusion_tree_tensors!( - new_codomain_tree_tensors_cache, - new_codomain_outer_block, - new_codomain_legs, - new_allowed_sectors, - ) - new_domain_block_trees = get_fusion_tree_tensors!( - new_domain_tree_tensors_cache, - new_domain_outer_block, - new_domain_legs, - new_allowed_sectors, - ) - - u = overlap_fusion_trees( - old_codomain_block_trees, - old_domain_block_trees, - new_codomain_block_trees, - new_domain_block_trees, - flat_permutation, - ) - return u + for old_trees in keys(trees_block_mapping(old_ft)) + old_outer = Tuple(findblock(old_ft, old_trees...)) + swapped_old_block = Block(getindex.(Ref(Tuple(old_outer)), flatperm)) + for new_trees in keys(trees_block_mapping(new_ft)) + new_outer = findblock(new_ft, new_trees...) + if swapped_old_block == new_outer + unitary[old_trees, new_trees] = overlap_fusion_trees(old_trees, new_trees, flatperm) + end end - unitaries[old_outer_block] = u end - - return unitaries + return unitary end # ================================= Constructor from 6j ================================== diff --git a/test/test_permutedims.jl b/test/test_permutedims.jl index e4042ae..f407b4f 100644 --- a/test/test_permutedims.jl +++ b/test/test_permutedims.jl @@ -23,7 +23,7 @@ using TensorAlgebra: blockedperm g4 = gradedrange([U1(-1) => 1, U1(0) => 1, U1(1) => 1]) for elt in (Float64, ComplexF64) - ft1 = FusionTensor(elt, dual.((g1, g2)), (g3, g4)) + ft1 = FusionTensor(elt, (g1, g2), dual.((g3, g4))) @test isnothing(check_sanity(ft1)) # test permutedims interface @@ -40,9 +40,9 @@ using TensorAlgebra: blockedperm ft3 = permutedims(ft1, (4,), (1, 2, 3)) @test ft3 !== ft1 @test ft3 isa FusionTensor{elt,4} - @test matching_axes(axes(ft3), (g4, dual(g1), dual(g2), g3)) - @test ndims_domain(ft3) == 1 - @test ndims_codomain(ft3) == 3 + @test matching_axes(axes(ft3), (dual(g4), g1, g2, dual(g3))) + @test ndims_domain(ft3) == 3 + @test ndims_codomain(ft3) == 1 @test ndims(ft3) == 4 @test isnothing(check_sanity(ft3)) @@ -59,14 +59,14 @@ using TensorAlgebra: blockedperm g2 = gradedrange([U1(2) => 3, U1(3) => 2]) g3 = gradedrange([U1(3) => 4, U1(4) => 1]) g4 = gradedrange([U1(0) => 2, U1(2) => 1]) - domain_legs = (g1, g2) - codomain_legs = dual.((g3, g4)) + codomain_legs = (g1, g2) + domain_legs = dual.((g3, g4)) arr = zeros(ComplexF64, (4, 5, 5, 3)) arr[1:2, 1:3, 1:4, 1:2] .= 1.0im arr[3:4, 1:3, 5:5, 1:2] .= 2.0 arr[1:2, 4:5, 5:5, 1:2] .= 3.0 arr[3:4, 4:5, 1:4, 3:3] .= 4.0 - ft = FusionTensor(arr, domain_legs, codomain_legs) + ft = FusionTensor(arr, codomain_legs, domain_legs) biperm = blockedperm((3,), (2, 4, 1)) ftp = permutedims(ft, biperm) @@ -84,14 +84,15 @@ using TensorAlgebra: blockedperm @testset "Less than two axes" begin if VERSION >= v"1.11" - ft0 = FusionTensor(ones(()), (), ()) - ft0p = permutedims(ft0, (), ()) - @test ft0p isa FusionTensor{Float64,0} - @test data_matrix(ft0p) ≈ data_matrix(ft0) - @test ft0p ≈ ft0 - - @test permutedims(ft0, ((), ())) isa FusionTensor{Float64,0} - @test permutedims(ft0, blockedperm((), ())) isa FusionTensor{Float64,0} + @test_broken FusionTensor(ones(()), (), ()) isa FusionTensor + #= ft0p = permutedims(ft0, (), ()) + @test ft0p isa FusionTensor{Float64,0} + @test data_matrix(ft0p) ≈ data_matrix(ft0) + @test ft0p ≈ ft0 + + @test permutedims(ft0, ((), ())) isa FusionTensor{Float64,0} + @test permutedims(ft0, blockedperm((), ())) isa FusionTensor{Float64,0} + =# end g = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) @@ -99,12 +100,11 @@ using TensorAlgebra: blockedperm v[1] = 1.0 biperm = blockedperm((), (1,)) ft1 = FusionTensor(v, (g,), ()) - @test_broken permutedims(ft1, biperm) isa FusionTensor - #ft2 = permutedims(ft1, biperm) isa FusionTensor - #@test isnothing(check_sanity(ft2)) - #@test ft2 ≈ naive_permutedims(ft1, biperm) - #ft3 = permutedims(ft2, (1,), ()) - #@test ft1 ≈ ft3 + ft2 = permutedims(ft1, biperm) + @test isnothing(check_sanity(ft2)) + @test ft2 ≈ naive_permutedims(ft1, biperm) + ft3 = permutedims(ft2, (1,), ()) + @test ft1 ≈ ft3 end end @@ -151,7 +151,7 @@ end end for biperm in [blockedperm((1, 2, 3, 4), ()), blockedperm((), (3, 1, 2, 4))] ft = FusionTensor(sds22, (g2, g2), (g2b, g2b)) - @test_broken permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) + @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) end end end From e75d783a61c73d6786dd0b5d6f7290c023885aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 17:11:57 +0100 Subject: [PATCH 09/52] fix contract --- src/FusionTensors.jl | 4 ++-- src/fusiontensor/tensor_algebra_interface.jl | 2 +- test/test_contraction.jl | 19 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/FusionTensors.jl b/src/FusionTensors.jl index c7eb64f..c3c124e 100644 --- a/src/FusionTensors.jl +++ b/src/FusionTensors.jl @@ -52,9 +52,9 @@ using SymmetrySectors: quantum_dimension, trivial using TensorAlgebra: - TensorAlgebra, - Algorithm, BlockedPermutation, + Matricize, + TensorAlgebra, blockedperm, blockpermute, contract, diff --git a/src/fusiontensor/tensor_algebra_interface.jl b/src/fusiontensor/tensor_algebra_interface.jl index ff19bd9..09f2f0c 100644 --- a/src/fusiontensor/tensor_algebra_interface.jl +++ b/src/fusiontensor/tensor_algebra_interface.jl @@ -25,7 +25,7 @@ end # as it calls _mul!, which I should not overload. # TBD I can also overload higher up and do not allow use of different algorithms function TensorAlgebra.contract!( - alg::Algorithm, + alg::Matricize, a_dest::FusionTensor, biperm_dest::BlockedPermutation, a1::FusionTensor, diff --git a/test/test_contraction.jl b/test/test_contraction.jl index 7e799b6..acdd292 100644 --- a/test/test_contraction.jl +++ b/test/test_contraction.jl @@ -22,19 +22,19 @@ using TensorAlgebra: contract ft3 = ft1 * ft2 # tensor contraction @test isnothing(check_sanity(ft3)) - @test domain_axes(ft3) === domain_axes(ft1) - @test codomain_axes(ft3) === codomain_axes(ft2) + @test domain_axes(ft3) === domain_axes(ft2) + @test codomain_axes(ft3) === codomain_axes(ft1) # test LinearAlgebra.mul! with in-place matrix product LinearAlgebra.mul!(ft3, ft1, ft2) @test isnothing(check_sanity(ft3)) - @test domain_axes(ft3) === domain_axes(ft1) - @test codomain_axes(ft3) === codomain_axes(ft2) + @test domain_axes(ft3) === domain_axes(ft2) + @test codomain_axes(ft3) === codomain_axes(ft1) LinearAlgebra.mul!(ft3, ft1, ft2, 1.0, 1.0) @test isnothing(check_sanity(ft2)) - @test domain_axes(ft3) === domain_axes(ft1) - @test codomain_axes(ft3) === codomain_axes(ft2) + @test domain_axes(ft3) === domain_axes(ft2) + @test codomain_axes(ft3) === codomain_axes(ft1) end @testset "TensorAlgebra interface" begin @@ -50,8 +50,8 @@ end ft4, legs = contract(ft1, (1, 2, 3, 4), ft2, (3, 4, 5)) @test legs == (1, 2, 5) @test isnothing(check_sanity(ft4)) - @test domain_axes(ft4) === domain_axes(ft1) - @test codomain_axes(ft4) === codomain_axes(ft2) + @test domain_axes(ft4) === domain_axes(ft2) + @test codomain_axes(ft4) === codomain_axes(ft1) ft5 = contract((1, 2, 5), ft1, (1, 2, 3, 4), ft2, (3, 4, 5)) @test isnothing(check_sanity(ft5)) @@ -59,8 +59,7 @@ end # biperm is not allowed @test_broken contract(((1, 2), (5,)), ft1, (1, 2, 3, 4), ft2, (3, 4, 5)) - # issue with 0 axis - @test_broken permutedims(ft1, (), (1, 2, 3, 4)) * permutedims(ft3, (3, 4, 1, 2), ()) isa + @test permutedims(ft1, (), (1, 2, 3, 4)) * permutedims(ft3, (3, 4, 1, 2), ()) isa FusionTensor{Float64,0} @test_broken contract(ft1, (1, 2, 3, 4), ft3, (3, 4, 1, 2)) end From 705b7aaa955df7eb712b3fb230c6f7ddc0dedae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 18:49:05 +0100 Subject: [PATCH 10/52] put imports on top of files --- src/FusionTensors.jl | 62 +------------------- src/fusion_trees/clebsch_gordan_tensors.jl | 20 ++++++- src/fusion_trees/fusiontree.jl | 7 ++- src/fusiontensor/array_cast.jl | 7 +++ src/fusiontensor/fusedaxes.jl | 5 ++ src/fusiontensor/fusiontensor.jl | 14 ++++- src/fusiontensor/linear_algebra_interface.jl | 14 ++++- src/fusiontensor/tensor_algebra_interface.jl | 8 ++- src/permutedims/permutedims.jl | 4 ++ src/permutedims/unitaries.jl | 5 ++ 10 files changed, 75 insertions(+), 71 deletions(-) diff --git a/src/FusionTensors.jl b/src/FusionTensors.jl index c3c124e..d17d579 100644 --- a/src/FusionTensors.jl +++ b/src/FusionTensors.jl @@ -1,68 +1,7 @@ module FusionTensors -using LinearAlgebra: LinearAlgebra, Adjoint, norm, tr - -using BlockArrays: - AbstractBlockArray, - AbstractBlockMatrix, - Block, - BlockArray, - BlockedArray, - BlockIndexRange, - BlockMatrix, - blockedrange, - blocklength, - blocklengths, - blocks -using LRUCache: LRU - -using BlockSparseArrays: - AbstractBlockSparseMatrix, - BlockSparseArrays, - BlockSparseArray, - BlockSparseMatrix, - stored_indices, - view! -using GradedUnitRanges: - GradedUnitRanges, - AbstractGradedUnitRange, - blocklabels, - blockmergesort, - dual, - findblock, - fusion_product, - gradedrange, - isdual, - labelled_blocks, - sector_type, - space_isequal, - unlabel_blocks -using SymmetrySectors: - ⊗, - AbelianStyle, - AbstractSector, - NotAbelianStyle, - SectorProduct, - SymmetrySectors, - SymmetryStyle, - TrivialSector, - arguments, - block_dimensions, - istrivial, - quantum_dimension, - trivial -using TensorAlgebra: - BlockedPermutation, - Matricize, - TensorAlgebra, - blockedperm, - blockpermute, - contract, - contract! - include("fusion_trees/fusiontree.jl") include("fusion_trees/clebsch_gordan_tensors.jl") - include("fusiontensor/fusedaxes.jl") include("fusiontensor/fusiontensor.jl") include("fusiontensor/base_interface.jl") @@ -71,4 +10,5 @@ include("fusiontensor/linear_algebra_interface.jl") include("fusiontensor/tensor_algebra_interface.jl") include("permutedims/unitaries.jl") include("permutedims/permutedims.jl") + end diff --git a/src/fusion_trees/clebsch_gordan_tensors.jl b/src/fusion_trees/clebsch_gordan_tensors.jl index 10b4781..fd426e6 100644 --- a/src/fusion_trees/clebsch_gordan_tensors.jl +++ b/src/fusion_trees/clebsch_gordan_tensors.jl @@ -5,7 +5,21 @@ using HalfIntegers: half using WignerSymbols: clebschgordan -using SymmetrySectors: ⊗, AbstractSector, O2, SU, sector_label, zero_odd +using GradedUnitRanges: dual, fusion_product +using SymmetrySectors: + ⊗, + AbelianStyle, + AbstractSector, + NotAbelianStyle, + SymmetryStyle, + O2, + SU, + istrivial, + quantum_dimension, + sector_label, + trivial, + zero_odd +using TensorAlgebra: contract function symbol_1j(s::AbstractSector) cgt = clebsch_gordan_tensor(s, dual(s), trivial(s), 1) @@ -23,11 +37,11 @@ function clebsch_gordan_tensor( cgt = clebsch_gordan_tensor(s1, s2, s3, inner_mult_index) if arrow1 flip1 = symbol_1j(s1) - cgt = TensorAlgebra.contract((1, 2, 3), flip1, (4, 1), cgt, (4, 2, 3)) + cgt = contract((1, 2, 3), flip1, (4, 1), cgt, (4, 2, 3)) end if arrow2 flip2 = symbol_1j(s2) - cgt = TensorAlgebra.contract((1, 2, 3), flip2, (4, 2), cgt, (1, 4, 3)) + cgt = contract((1, 2, 3), flip2, (4, 2), cgt, (1, 4, 3)) end return cgt end diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index a05c633..f9e2f08 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -3,6 +3,11 @@ # TBD # compatibility with TensorKit conventions? +using GradedUnitRanges: + AbstractGradedUnitRange, GradedUnitRanges, fusion_product, isdual, sector_type +using SymmetrySectors: AbstractSector, SectorProduct, SymmetrySectors, arguments, trivial +using TensorAlgebra: flatten_tuples + # # A fusion tree fuses N sectors sec1, secN onto one sector fused_sec. A given set of # sectors and arrow directions (as defined by a given outer block) contains several fusion @@ -196,7 +201,7 @@ FusionTree(sect::AbstractSector, arrow::Bool) = FusionTree((sect,), (arrow,), se function braid_tuples(t1::Tuple{Vararg{<:Any,N}}, t2::Tuple{Vararg{<:Any,N}}) where {N} t12 = (t1, t2) nested = ntuple(i -> getindex.(t12, i), N) - return TensorAlgebra.flatten_tuples(nested) + return flatten_tuples(nested) end function grow_tree( diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index cb5d082..4d660f6 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -1,5 +1,12 @@ # This file defines interface to cast from and to generic array +using BlockArrays: AbstractBlockArray, BlockedArray, blockedrange, findblock + +using BlockSparseArrays: BlockSparseArrays +using GradedUnitRanges: AbstractGradedUnitRange +using SymmetrySectors: block_dimensions, quantum_dimension +using TensorAlgebra: contract + # ================================= High level interface ================================= #### cast from array to symmetric diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index 24e63aa..7e508c4 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -1,5 +1,10 @@ # This file defines helper functions to access FusionTensor internal structures +using BlockArrays: Block, BlockIndexRange, blocklength, blocklengths + +using GradedUnitRanges: AbstractGradedUnitRange, blocklabels, blockmergesort, gradedrange +using SymmetrySectors: AbstractSector, trivial + struct FusedAxes{A,B,C} outer_axes::A fused_axis::B diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 75524a5..d4bbc61 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -1,6 +1,14 @@ # This file defines struct FusionTensor and constructors -using BlockSparseArrays: block_stored_indices +using LinearAlgebra: Adjoint + +using BlockArrays: AbstractBlockMatrix, BlockArrays, blocklength, findblock + +using BlockSparseArrays: + AbstractBlockSparseMatrix, BlockSparseArray, BlockSparseMatrix, block_stored_indices +using GradedUnitRanges: + AbstractGradedUnitRange, blocklabels, dual, sector_type, space_isequal +using SymmetrySectors: SectorProduct, TrivialSector struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T,N} data_matrix::Mat @@ -54,7 +62,9 @@ end # GradedUnitRanges interface GradedUnitRanges.sector_type(ft::FusionTensor) = sector_type(matrix_row_axis(ft)) -function GradedUnitRanges.findblock(ft::FusionTensor, f1::FusionTree, f2::FusionTree) + +# BlockArrays interface +function BlockArrays.findblock(ft::FusionTensor, f1::FusionTree, f2::FusionTree) # find outer block corresponding to fusion trees @assert ndims_codomain(ft) == length(f1) @assert ndims_domain(ft) == length(f2) diff --git a/src/fusiontensor/linear_algebra_interface.jl b/src/fusiontensor/linear_algebra_interface.jl index b950ecd..65800c8 100644 --- a/src/fusiontensor/linear_algebra_interface.jl +++ b/src/fusiontensor/linear_algebra_interface.jl @@ -1,5 +1,13 @@ # This file defines linalg for FusionTensor +using LinearAlgebra: LinearAlgebra, mul!, norm, tr + +using BlockArrays: Block, blocks + +using BlockSparseArrays: stored_indices +using GradedUnitRanges: blocklabels +using SymmetrySectors: quantum_dimension + # allow to contract with different eltype and let BlockSparseArray ensure compatibility # impose matching type and number of axes at compile time # impose matching axes at run time @@ -29,7 +37,7 @@ function LinearAlgebra.mul!( if !matching_axes(domain_axes(C), domain_axes(B)) throw(codomainError("Incompatible tensor axes for C and B")) end - LinearAlgebra.mul!(data_matrix(C), data_matrix(A), data_matrix(B), α, β) + mul!(data_matrix(C), data_matrix(A), data_matrix(B), α, β) return C end @@ -57,14 +65,14 @@ function LinearAlgebra.tr(ft::FusionTensor) end function LinearAlgebra.qr(ft::FusionTensor) - qmat, rmat = BlockSparseArrays.block_qr(data_matrix(ft)) + qmat, rmat = block_qr(data_matrix(ft)) qtens = FusionTensor(qmat, codomain_axes(ft), (axes(qmat)[1],)) rtens = FusionTensor(rmat, (axes(rmat)[0],), domain_axes(ft)) return qtens, rtens end function LinearAlgebra.svd(ft::FusionTensor) - umat, s, vmat = BlockSparseArrays.block_svd(data_matrix(ft)) + umat, s, vmat = block_svd(data_matrix(ft)) utens = FusionTensor(umat, codomain_axes(ft), (axes(umat)[1],)) stens = FusionTensor(s, (axes(umat)[1],), (axes(vmat)[0],)) vtens = FusionTensor(vmat, (axes(vmat)[0],), domain_axes(ft)) diff --git a/src/fusiontensor/tensor_algebra_interface.jl b/src/fusiontensor/tensor_algebra_interface.jl index 09f2f0c..cba822c 100644 --- a/src/fusiontensor/tensor_algebra_interface.jl +++ b/src/fusiontensor/tensor_algebra_interface.jl @@ -1,5 +1,11 @@ # This file defines TensorAlgebra interface for a FusionTensor +using LinearAlgebra: mul! + +using BlockArrays: Block + +using TensorAlgebra: BlockedPermutation, Matricize, TensorAlgebra + # TBD how to deal with inner contraction = no ouput axis? function TensorAlgebra.allocate_output( ::typeof(contract), @@ -37,6 +43,6 @@ function TensorAlgebra.contract!( ) a1_perm = permutedims(a1, biperm1) a2_perm = permutedims(a2, biperm2) - LinearAlgebra.mul!(a_dest, a1_perm, a2_perm, α, β) + mul!(a_dest, a1_perm, a2_perm, α, β) return a_dest end diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl index bd4ea0c..b486413 100644 --- a/src/permutedims/permutedims.jl +++ b/src/permutedims/permutedims.jl @@ -1,5 +1,9 @@ # This file defines permutedims for a FusionTensor +using BlockArrays: blocklengths + +using TensorAlgebra: BlockedPermutation, blockedperm, blockpermute + # permutedims with 1 tuple of 2 separate tuples function fusiontensor_permutedims(ft::FusionTensor, new_leg_indices::Tuple{Tuple,Tuple}) return fusiontensor_permutedims(ft, new_leg_indices...) diff --git a/src/permutedims/unitaries.jl b/src/permutedims/unitaries.jl index ee389aa..c3e199d 100644 --- a/src/permutedims/unitaries.jl +++ b/src/permutedims/unitaries.jl @@ -1,5 +1,10 @@ # This file defines unitaries to be used in permutedims +using BlockArrays: Block, findblock +using LRUCache: LRU + +using SymmetrySectors: quantum_dimension + const unitary_cache = LRU{Any,AbstractMatrix}(; maxsize=10000) # ====================================== Interface ======================================= From deaf6b1eaf32877dae23b9db7cefe1bf4b13129c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 19:19:34 +0100 Subject: [PATCH 11/52] move check_sanity to tests --- src/fusiontensor/fusedaxes.jl | 3 +- src/fusiontensor/fusiontensor.jl | 50 ++-------------------------- test/shared.jl | 56 ++++++++++++++++++++++++++++++++ test/test_array_cast.jl | 4 ++- test/test_basics.jl | 5 +-- test/test_contraction.jl | 10 +++--- test/test_fusion_trees.jl | 11 +++---- test/test_linear_algebra.jl | 4 ++- test/test_permutedims.jl | 3 +- 9 files changed, 82 insertions(+), 64 deletions(-) create mode 100644 test/shared.jl diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index 7e508c4..8c5df7d 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -2,7 +2,8 @@ using BlockArrays: Block, BlockIndexRange, blocklength, blocklengths -using GradedUnitRanges: AbstractGradedUnitRange, blocklabels, blockmergesort, gradedrange +using GradedUnitRanges: + AbstractGradedUnitRange, GradedUnitRanges, blocklabels, blockmergesort, gradedrange using SymmetrySectors: AbstractSector, trivial struct FusedAxes{A,B,C} diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index d4bbc61..8163586 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -1,13 +1,10 @@ # This file defines struct FusionTensor and constructors -using LinearAlgebra: Adjoint - using BlockArrays: AbstractBlockMatrix, BlockArrays, blocklength, findblock -using BlockSparseArrays: - AbstractBlockSparseMatrix, BlockSparseArray, BlockSparseMatrix, block_stored_indices +using BlockSparseArrays: AbstractBlockSparseMatrix, BlockSparseArray, block_stored_indices using GradedUnitRanges: - AbstractGradedUnitRange, blocklabels, dual, sector_type, space_isequal + AbstractGradedUnitRange, blocklabels, dual, isdual, sector_type, space_isequal using SymmetrySectors: SectorProduct, TrivialSector struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T,N} @@ -176,49 +173,6 @@ function initialize_allowed_sectors!(mat::AbstractBlockMatrix) end end -# TODO move to tests -function check_data_matrix_axes( - mat::BlockSparseMatrix, codomain_legs::Tuple, domain_legs::Tuple -) - ft0 = FusionTensor(Float64, codomain_legs, domain_legs) - @assert space_isequal(matrix_row_axis(ft0), axes(mat, 1)) - @assert space_isequal(matrix_column_axis(ft0), axes(mat, 2)) -end - -function check_data_matrix_axes(mat::Adjoint, codomain_legs::Tuple, domain_legs::Tuple) - return check_data_matrix_axes(adjoint(mat), dual.(domain_legs), dual.(codomain_legs)) -end - -# TODO move to tests -function check_sanity(ft::FusionTensor) - nca = ndims_domain(ft) - @assert nca == length(domain_axes(ft)) "ndims_domain does not match domain_axes" - @assert nca <= ndims(ft) "invalid ndims_domain" - - nda = ndims_codomain(ft) - @assert nda == length(codomain_axes(ft)) "ndims_codomain does not match codomain_axes" - @assert nda <= ndims(ft) "invalid ndims_codomain" - @assert nda + nca == ndims(ft) "invalid ndims" - - @assert length(axes(ft)) == ndims(ft) "ndims does not match axes" - @assert matching_axes(axes(ft)[begin:nda], codomain_axes(ft)) "axes do not match codomain_axes" - @assert matching_axes(axes(ft)[(nda + 1):end], domain_axes(ft)) "axes do not match domain_axes" - - m = data_matrix(ft) - @assert ndims(m) == 2 "invalid data_matrix ndims" - row_axis = matrix_row_axis(ft) - column_axis = matrix_column_axis(ft) - @assert row_axis === axes(m, 1) "invalid row_axis" - @assert column_axis === axes(m, 2) "invalid column_axis" - check_data_matrix_axes(data_matrix(ft), codomain_axes(ft), domain_axes(ft)) - - for b in block_stored_indices(m) - it = Int.(Tuple(b)) - @assert blocklabels(row_axis)[it[1]] == blocklabels(dual(column_axis))[it[2]] "forbidden block" - end - return nothing -end - matching_dual(axes1::Tuple, axes2::Tuple) = matching_axes(axes1, dual.(axes2)) matching_axes(axes1::Tuple, axes2::Tuple) = false function matching_axes(axes1::T, axes2::T) where {T<:Tuple} diff --git a/test/shared.jl b/test/shared.jl new file mode 100644 index 0000000..ae75b96 --- /dev/null +++ b/test/shared.jl @@ -0,0 +1,56 @@ +using LinearAlgebra: Adjoint + +using BlockSparseArrays: BlockSparseMatrix, block_stored_indices +using FusionTensors: + FusionTensor, + codomain_axes, + data_matrix, + domain_axes, + matching_axes, + matching_dual, + matrix_column_axis, + matrix_row_axis, + ndims_codomain, + ndims_domain +using GradedUnitRanges: blocklabels, dual, space_isequal + +function check_data_matrix_axes( + mat::BlockSparseMatrix, codomain_legs::Tuple, domain_legs::Tuple +) + ft0 = FusionTensor(Float64, codomain_legs, domain_legs) + @assert space_isequal(matrix_row_axis(ft0), axes(mat, 1)) + @assert space_isequal(matrix_column_axis(ft0), axes(mat, 2)) +end + +function check_data_matrix_axes(mat::Adjoint, codomain_legs::Tuple, domain_legs::Tuple) + return check_data_matrix_axes(adjoint(mat), dual.(domain_legs), dual.(codomain_legs)) +end + +function check_sanity(ft::FusionTensor) + nca = ndims_domain(ft) + @assert nca == length(domain_axes(ft)) "ndims_domain does not match domain_axes" + @assert nca <= ndims(ft) "invalid ndims_domain" + + nda = ndims_codomain(ft) + @assert nda == length(codomain_axes(ft)) "ndims_codomain does not match codomain_axes" + @assert nda <= ndims(ft) "invalid ndims_codomain" + @assert nda + nca == ndims(ft) "invalid ndims" + + @assert length(axes(ft)) == ndims(ft) "ndims does not match axes" + @assert matching_axes(axes(ft)[begin:nda], codomain_axes(ft)) "axes do not match codomain_axes" + @assert matching_axes(axes(ft)[(nda + 1):end], domain_axes(ft)) "axes do not match domain_axes" + + m = data_matrix(ft) + @assert ndims(m) == 2 "invalid data_matrix ndims" + row_axis = matrix_row_axis(ft) + column_axis = matrix_column_axis(ft) + @assert row_axis === axes(m, 1) "invalid row_axis" + @assert column_axis === axes(m, 2) "invalid column_axis" + check_data_matrix_axes(data_matrix(ft), codomain_axes(ft), domain_axes(ft)) + + for b in block_stored_indices(m) + it = Int.(Tuple(b)) + @assert blocklabels(row_axis)[it[1]] == blocklabels(dual(column_axis))[it[2]] "forbidden block" + end + return nothing +end diff --git a/test/test_array_cast.jl b/test/test_array_cast.jl index 9e90afd..6dc59b3 100644 --- a/test/test_array_cast.jl +++ b/test/test_array_cast.jl @@ -4,10 +4,12 @@ using Test: @test, @testset, @test_broken using BlockArrays: Block, blocksize -using FusionTensors: FusionTensor, check_sanity, data_matrix +using FusionTensors: FusionTensor, data_matrix using GradedUnitRanges: dual, fusion_product, gradedrange using SymmetrySectors: O2, SectorProduct, SU2, TrivialSector, U1 +include("shared.jl") + @testset "Trivial FusionTensor" begin @testset "trivial matrix" begin g = gradedrange([TrivialSector() => 1]) diff --git a/test/test_basics.jl b/test/test_basics.jl index 81273aa..b6ee3da 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -13,12 +13,13 @@ using FusionTensors: matrix_row_axis, matrix_size, ndims_domain, - ndims_codomain, - check_sanity + ndims_codomain using GradedUnitRanges: blockmergesort, dual, flip, fusion_product, gradedrange, space_isequal using SymmetrySectors: U1 +include("shared.jl") + @testset "Fusion matrix" begin g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) g2 = dual(gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1])) diff --git a/test/test_contraction.jl b/test/test_contraction.jl index acdd292..213f560 100644 --- a/test/test_contraction.jl +++ b/test/test_contraction.jl @@ -1,13 +1,15 @@ @eval module $(gensym()) -using LinearAlgebra: LinearAlgebra +using LinearAlgebra: mul! using Test: @test, @testset, @test_broken using BlockSparseArrays: BlockSparseArray -using FusionTensors: FusionTensor, domain_axes, codomain_axes, check_sanity +using FusionTensors: FusionTensor, domain_axes, codomain_axes using GradedUnitRanges: dual, gradedrange using SymmetrySectors: U1 using TensorAlgebra: contract +include("shared.jl") + @testset "contraction" begin g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) g2 = gradedrange([U1(0) => 2, U1(1) => 2, U1(3) => 1]) @@ -26,12 +28,12 @@ using TensorAlgebra: contract @test codomain_axes(ft3) === codomain_axes(ft1) # test LinearAlgebra.mul! with in-place matrix product - LinearAlgebra.mul!(ft3, ft1, ft2) + mul!(ft3, ft1, ft2) @test isnothing(check_sanity(ft3)) @test domain_axes(ft3) === domain_axes(ft2) @test codomain_axes(ft3) === codomain_axes(ft1) - LinearAlgebra.mul!(ft3, ft1, ft2, 1.0, 1.0) + mul!(ft3, ft1, ft2, 1.0, 1.0) @test isnothing(check_sanity(ft2)) @test domain_axes(ft3) === domain_axes(ft2) @test codomain_axes(ft3) === codomain_axes(ft1) diff --git a/test/test_fusion_trees.jl b/test/test_fusion_trees.jl index fe83376..e095a44 100644 --- a/test/test_fusion_trees.jl +++ b/test/test_fusion_trees.jl @@ -1,12 +1,11 @@ @eval module $(gensym()) using Test: @test, @testset -using LinearAlgebra: LinearAlgebra +using LinearAlgebra: I using BlockArrays: BlockArrays using FusionTensors: fusion_tree_tensors using GradedUnitRanges: blocklabels, fusion_product -using LabelledNumbers: unlabel using SymmetrySectors: SectorProduct, SU, SU2, TrivialSector, U1, Z, quantum_dimension @testset "Trivial fusion trees" begin @@ -56,7 +55,7 @@ end tree_irreps_pairs2 = fusion_tree_tensors((SU2(1 / 2),), (false,)) @test first.(tree_irreps_pairs2) == [SU2(1 / 2)] - @test last.(tree_irreps_pairs2) == [reshape(LinearAlgebra.I(2), (2, 2, 1))] + @test last.(tree_irreps_pairs2) == [reshape(I(2), (2, 2, 1))] tree_irreps_pairs3 = fusion_tree_tensors((SU2(1 / 2),), (true,)) @test first.(tree_irreps_pairs3) == [SU2(1 / 2)] @@ -76,7 +75,7 @@ end (hole, hole, hole, s12), (false, false, false, false) ) @test first.(tree_irreps_pairs1) == [SectorProduct(; N=U1(3), S=SU2(1 / 2))] - @test last.(tree_irreps_pairs1) == [reshape(LinearAlgebra.I(2), (1, 1, 1, 2, 2, 1))] + @test last.(tree_irreps_pairs1) == [reshape(I(2), (1, 1, 1, 2, 2, 1))] s3 = SectorProduct(SU((1, 0)), SU((1,)), U1(1)) irreps = (s3, s3, s3) @@ -86,7 +85,7 @@ end rep = fusion_product(irreps...) @test blocklabels(rep) == tree_irreps @test quantum_dimension.(tree_irreps) == size.(trees, 4) - @test unlabel.(BlockArrays.blocklengths(rep)) == size.(trees, 5) + @test BlockArrays.blocklengths(rep) == size.(trees, 5) s_nt = SectorProduct(; A=SU((1, 0)), B=SU((1,)), C=U1(1)) irreps_nt = (s_nt, s_nt, s_nt) @@ -95,6 +94,6 @@ end rep_nt = reduce(fusion_product, irreps_nt) @test blocklabels(rep_nt) == tree_irreps_nt @test quantum_dimension.(tree_irreps_nt) == size.(trees_nt, 4) - @test unlabel.(BlockArrays.blocklengths(rep_nt)) == size.(trees_nt, 5) + @test BlockArrays.blocklengths(rep_nt) == size.(trees_nt, 5) end end diff --git a/test/test_linear_algebra.jl b/test/test_linear_algebra.jl index 93d2ff3..910a750 100644 --- a/test/test_linear_algebra.jl +++ b/test/test_linear_algebra.jl @@ -5,10 +5,12 @@ using Test: @test, @testset using BlockArrays: BlockArrays using BlockSparseArrays: BlockSparseArrays -using FusionTensors: FusionTensor, check_sanity +using FusionTensors: FusionTensor using GradedUnitRanges: dual, gradedrange using SymmetrySectors: U1, SU2, TrivialSector +include("shared.jl") + @testset "LinearAlgebra interface" begin sds22 = [ 0.25 0.0 0.0 0.0 diff --git a/test/test_permutedims.jl b/test/test_permutedims.jl index f407b4f..b87a910 100644 --- a/test/test_permutedims.jl +++ b/test/test_permutedims.jl @@ -3,7 +3,6 @@ using Test: @test, @testset, @test_broken using FusionTensors: FusionTensor, - check_sanity, data_matrix, matching_axes, matrix_column_axis, @@ -15,6 +14,8 @@ using GradedUnitRanges: dual, gradedrange, space_isequal using SymmetrySectors: O2, U1, SectorProduct, SU2 using TensorAlgebra: blockedperm +include("shared.jl") + @testset "Abelian permutedims" begin @testset "dummy" begin g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) From 8ab786ee25f6c9c5016f1ef19f1c0def51676f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 20:40:39 +0100 Subject: [PATCH 12/52] rewrite test FusionTree --- src/fusion_trees/fusiontree.jl | 2 +- test/test_fusion_trees.jl | 194 +++++++++++++++++++-------------- test/test_unitaries.jl | 54 --------- 3 files changed, 112 insertions(+), 138 deletions(-) delete mode 100644 test/test_unitaries.jl diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index f9e2f08..ace5c7f 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -5,7 +5,7 @@ using GradedUnitRanges: AbstractGradedUnitRange, GradedUnitRanges, fusion_product, isdual, sector_type -using SymmetrySectors: AbstractSector, SectorProduct, SymmetrySectors, arguments, trivial +using SymmetrySectors: ×, AbstractSector, SectorProduct, SymmetrySectors, arguments, trivial using TensorAlgebra: flatten_tuples # diff --git a/test/test_fusion_trees.jl b/test/test_fusion_trees.jl index e095a44..2799b76 100644 --- a/test/test_fusion_trees.jl +++ b/test/test_fusion_trees.jl @@ -4,96 +4,124 @@ using LinearAlgebra: I using BlockArrays: BlockArrays -using FusionTensors: fusion_tree_tensors -using GradedUnitRanges: blocklabels, fusion_product -using SymmetrySectors: SectorProduct, SU, SU2, TrivialSector, U1, Z, quantum_dimension +using FusionTensors: + FusionTree, + arrows, + branch_sectors, + build_trees, + leaves, + outer_multiplicity_indices, + root_sector +using GradedUnitRanges: blocklabels, fusion_product, sector_type +using SymmetrySectors: ×, SectorProduct, SU, SU2, TrivialSector, arguments @testset "Trivial fusion trees" begin - tree_irreps_pairs1 = fusion_tree_tensors((), ()) - @test first.(tree_irreps_pairs1) == [TrivialSector()] - @test last.(tree_irreps_pairs1) == [ones((1, 1))] - - tree_irreps_pairs2 = fusion_tree_tensors( - (TrivialSector(), TrivialSector()), (false, false) - ) - @test first.(tree_irreps_pairs2) == [TrivialSector()] - @test last.(tree_irreps_pairs2) == [ones((1, 1, 1, 1))] - - tree_irreps_pairs3 = fusion_tree_tensors( - (TrivialSector(), TrivialSector(), TrivialSector()), (false, true, true) - ) - @test first.(tree_irreps_pairs3) == [TrivialSector()] - @test last.(tree_irreps_pairs3) == [ones((1, 1, 1, 1, 1))] + q = TrivialSector() + f = FusionTree{TrivialSector}() + @test arrows(f) == () + @test leaves(f) == () + @test root_sector(f) == q + @test branch_sectors(f) == () + @test sector_type(f) == TrivialSector + @test outer_multiplicity_indices(f) == () + @test convert(Array, f) ≈ ones((1,)) + + f = only(build_trees((q,), (true,))) + @test arrows(f) == (true,) + @test leaves(f) == (q,) + @test root_sector(f) == q + @test branch_sectors(f) == () + @test outer_multiplicity_indices(f) == () + @test convert(Array, f) ≈ ones((1, 1)) + + f = only(build_trees((q, q), (true, false))) + @test arrows(f) == (true, false) + @test leaves(f) == (q, q) + @test root_sector(f) == q + @test branch_sectors(f) == (q,) + @test outer_multiplicity_indices(f) == (1,) + @test convert(Array, f) ≈ ones((1, 1, 1)) end -@testset "Abelian fusion trees" begin - tree_irreps_pairs1 = fusion_tree_tensors((U1(0), U1(0)), (false, false)) - @test first.(tree_irreps_pairs1) == [U1(0)] - @test last.(tree_irreps_pairs1) == [ones((1, 1, 1, 1))] - - tree_irreps_pairs2 = fusion_tree_tensors((U1(1), U1(1), U1(-2)), (false, false, false)) - @test first.(tree_irreps_pairs2) == [U1(0)] - @test last.(tree_irreps_pairs2) == [ones((1, 1, 1, 1, 1))] - - s1 = SectorProduct(Z{2}(1), U1(0)) - s2 = SectorProduct(Z{2}(1), U1(1)) - tree_irreps_z2u1 = fusion_tree_tensors((s1, s2), (false, false)) - @test first.(tree_irreps_z2u1) == [SectorProduct(Z{2}(0), U1(1))] - @test last.(tree_irreps_z2u1) == [ones((1, 1, 1, 1))] - - s3 = SectorProduct(; A=Z{2}(1), B=U1(0)) - s4 = SectorProduct(; A=Z{2}(1), B=U1(1)) - tree_irreps_nt = fusion_tree_tensors((s3, s4), (false, false)) - @test first.(tree_irreps_nt) == [SectorProduct(; A=Z{2}(0), B=U1(1))] - @test last.(tree_irreps_nt) == [ones((1, 1, 1, 1))] +@testset "SU(2) FusionTree" begin + j2 = SU2(1//2) + + f = only(build_trees((j2,), (false,))) + @test arrows(f) == (false,) + @test leaves(f) == (j2,) + @test root_sector(f) == j2 + @test branch_sectors(f) == () + @test outer_multiplicity_indices(f) == () + @test sector_type(f) == typeof(j2) + @test convert(Array, f) ≈ I(2) + + f = only(build_trees((j2,), (true,))) + @test arrows(f) == (true,) + @test convert(Array, f) ≈ [0 -1; 1 0] + + trees = build_trees((j2, j2), (false, false)) + @test length(trees) == 2 + f1 = first(trees) + @test root_sector(f1) == SU2(0) + @test branch_sectors(f1) == (SU2(1//2),) + @test outer_multiplicity_indices(f1) == (1,) + @test convert(Array, f1) ≈ [0 1/sqrt(2); -1/sqrt(2) 0] + + f3 = last(trees) + @test root_sector(f3) == SU2(1) + @test branch_sectors(f3) == (SU2(1//2),) + @test outer_multiplicity_indices(f3) == (1,) + t = zeros((2, 2, 3)) + t[1, 1, 1] = 1 + t[1, 2, 2] = 1 / sqrt(2) + t[2, 1, 2] = 1 / sqrt(2) + t[2, 2, 3] = 1 + @test convert(Array, f3) ≈ t + + trees = build_trees((j2, j2, j2), (false, false, false)) + @test length(trees) == 3 + f12, f32, f34 = trees + @test f12 < f32 < f34 + @test root_sector(f12) == SU2(1//2) + @test root_sector(f32) == SU2(1//2) + @test root_sector(f34) == SU2(3//2) + + @test branch_sectors(f12) == (SU2(1//2), SU2(0)) + @test branch_sectors(f32) == (SU2(1//2), SU2(1)) + @test branch_sectors(f34) == (SU2(1//2), SU2(1)) end -@testset "SU(2) fusion trees" begin - tree_irreps_pairs1 = fusion_tree_tensors((SU2(0), SU2(0), SU2(0)), (false, false, false)) - @test first.(tree_irreps_pairs1) == [SU2(0)] - @test last.(tree_irreps_pairs1) == [ones((1, 1, 1, 1, 1))] - - tree_irreps_pairs2 = fusion_tree_tensors((SU2(1 / 2),), (false,)) - @test first.(tree_irreps_pairs2) == [SU2(1 / 2)] - @test last.(tree_irreps_pairs2) == [reshape(I(2), (2, 2, 1))] - - tree_irreps_pairs3 = fusion_tree_tensors((SU2(1 / 2),), (true,)) - @test first.(tree_irreps_pairs3) == [SU2(1 / 2)] - @test last.(tree_irreps_pairs3) ≈ [reshape([0, 1, -1, 0], (2, 2, 1))] - - tree_irreps_pairs4 = fusion_tree_tensors( - (SU2(1 / 2), SU2(1 / 2), SU2(1 / 2)), (false, false, false) - ) - @test first.(tree_irreps_pairs4) == [SU2(1 / 2), SU2(3 / 2)] - @test size.(last.(tree_irreps_pairs4)) == [(2, 2, 2, 2, 2), (2, 2, 2, 4, 1)] +@testset "SU(3) FusionTree" begin + a8 = SU{3}((2, 1)) + trees = build_trees((a8, a8), (false, false)) + @test length(trees) == 6 + f = first(trees) + @test root_sector(f) == SU{3}((0, 0)) + @test sector_type(f) == typeof(a8) + + f8a = trees[2] + f8b = trees[3] + @test root_sector(f8a) == a8 + @test root_sector(f8b) == a8 + @test branch_sectors(f8a) == (a8,) + @test branch_sectors(f8b) == (a8,) + @test outer_multiplicity_indices(f8a) == (1,) + @test outer_multiplicity_indices(f8b) == (2,) end -@testset "SectorProduct fusion trees" begin - hole = SectorProduct(; N=U1(1), S=SU2(0)) - s12 = SectorProduct(; N=U1(0), S=SU2(1 / 2)) - tree_irreps_pairs1 = fusion_tree_tensors( - (hole, hole, hole, s12), (false, false, false, false) - ) - @test first.(tree_irreps_pairs1) == [SectorProduct(; N=U1(3), S=SU2(1 / 2))] - @test last.(tree_irreps_pairs1) == [reshape(I(2), (1, 1, 1, 2, 2, 1))] - - s3 = SectorProduct(SU((1, 0)), SU((1,)), U1(1)) - irreps = (s3, s3, s3) - arrows = (false, false, false) - tree_irreps_pairs2 = fusion_tree_tensors(irreps, arrows) - tree_irreps, trees = first.(tree_irreps_pairs2), last.(tree_irreps_pairs2) - rep = fusion_product(irreps...) - @test blocklabels(rep) == tree_irreps - @test quantum_dimension.(tree_irreps) == size.(trees, 4) - @test BlockArrays.blocklengths(rep) == size.(trees, 5) - - s_nt = SectorProduct(; A=SU((1, 0)), B=SU((1,)), C=U1(1)) - irreps_nt = (s_nt, s_nt, s_nt) - tree_irreps_nt_pairs = fusion_tree_tensors(irreps_nt, arrows) - tree_irreps_nt, trees_nt = first.(tree_irreps_nt_pairs), last.(tree_irreps_nt_pairs) - rep_nt = reduce(fusion_product, irreps_nt) - @test blocklabels(rep_nt) == tree_irreps_nt - @test quantum_dimension.(tree_irreps_nt) == size.(trees_nt, 4) - @test BlockArrays.blocklengths(rep_nt) == size.(trees_nt, 5) +@testset "SU(2)×SU(3) FusionTree" begin + j2 = SU2(1//2) + a8 = SU{3}((2, 1)) + s = j2 × a8 + trees = build_trees((s, s), (false, false)) + @test length(trees) == 12 + f = first(trees) + @test sector_type(f) == typeof(s) + argument_trees = arguments(f) + @test length(argument_trees) == 2 + f2, f3 = argument_trees + @test sector_type(f2) == typeof(j2) + @test sector_type(f3) == typeof(a8) + @test f == f2 × f3 end end diff --git a/test/test_unitaries.jl b/test/test_unitaries.jl deleted file mode 100644 index c47c261..0000000 --- a/test/test_unitaries.jl +++ /dev/null @@ -1,54 +0,0 @@ -@eval module $(gensym()) -using Test: @test, @testset, @test_broken -using LinearAlgebra: LinearAlgebra - -using BlockArrays: AbstractBlockMatrix, blocksize - -using FusionTensors: compute_unitaries_clebsch_gordan -using GradedUnitRanges: gradedrange, dual, GradedUnitRanges -using SymmetrySectors: SymmetrySectors, SU, SU2, TrivialSector, U1, Z -using TensorAlgebra: blockedperm, TensorAlgebra - -@testset "Trivial unitaries" begin - function check_trivial_unitary(u) - @test u isa AbstractBlockMatrix - @test blocksize(u) == (1, 1) - @test size(u) == (1, 1) - @test u[1, 1] == 1.0 - end - - g = gradedrange([TrivialSector() => 1]) - old_domain_legs = (g, g) - old_codomain_legs = (g,) - biperm = blockedperm((2,), (3, 1)) - u_d = compute_unitaries_clebsch_gordan(old_domain_legs, old_codomain_legs, biperm) - @test collect(keys(u_d)) == [(1, 1, 1)] - check_trivial_unitary(u_d[1, 1, 1]) - - g = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) - for biperm in [ - blockedperm((1, 2), (3, 4, 5)), - blockedperm((2, 1), (5, 3, 4)), - blockedperm((3, 1, 4, 5), (2,)), - ] - u_d = compute_unitaries_clebsch_gordan((g, g), (dual(g), dual(g), g), biperm) - @test length(u_d) == 45 - check_trivial_unitary.(values(u_d)) - end - for biperm in [blockedperm((), (5, 2, 1, 3, 4)), blockedperm((5, 2, 1, 3, 4), ())] - @test_broken compute_unitaries_clebsch_gordan((g, g), (dual(g), dual(g), g), biperm) isa - Dict - end -end - -@testset "SU(2) unitaries" begin - g12 = gradedrange([SU2(1 / 2) => 1]) - g1 = gradedrange([SU2(1) => 1]) - biperm = blockedperm((2,), (3, 1)) - u_d = compute_unitaries_clebsch_gordan((g12, g12), (g1,), biperm) - @test collect(keys(u_d)) == [(1, 1, 1)] - @test u_d[1, 1, 1] ≈ -√(3 / 2) * ones((1, 1)) -end - -@testset "SectorProduct unitaries" begin end -end From 563bae17ff7610e1f4a05bac2825590d558e6639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 11 Dec 2024 22:56:22 +0100 Subject: [PATCH 13/52] cleaning --- src/fusiontensor/array_cast.jl | 6 +++--- src/fusiontensor/fusedaxes.jl | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 4d660f6..86482b8 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -1,9 +1,9 @@ # This file defines interface to cast from and to generic array -using BlockArrays: AbstractBlockArray, BlockedArray, blockedrange, findblock +using BlockArrays: AbstractBlockArray, BlockedArray, blockedrange, blocklengths, findblock -using BlockSparseArrays: BlockSparseArrays -using GradedUnitRanges: AbstractGradedUnitRange +using BlockSparseArrays: BlockSparseArrays, BlockSparseArray +using GradedUnitRanges: AbstractGradedUnitRange, blocklabels using SymmetrySectors: block_dimensions, quantum_dimension using TensorAlgebra: contract diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index 8c5df7d..fcdf58d 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -2,6 +2,7 @@ using BlockArrays: Block, BlockIndexRange, blocklength, blocklengths +using BlockSparseArrays: to_block_indices using GradedUnitRanges: AbstractGradedUnitRange, GradedUnitRanges, blocklabels, blockmergesort, gradedrange using SymmetrySectors: AbstractSector, trivial @@ -68,7 +69,7 @@ function compute_inner_ranges( fused_leg = blockmergesort( gradedrange(root_sector.(first.(fusion_trees_mult)) .=> last.(fusion_trees_mult)) ) - range_mapping = Dict{typeof(first(first(fusion_trees_mult))),typeof(Block(1)[1:1])}() + range_mapping = Dict{fieldtype(eltype(fusion_trees_mult), 1),typeof(Block(1)[1:1])}() fused_sectors = blocklabels(fused_leg) shifts = ones(Int, blocklength(fused_leg)) for (f, m) in fusion_trees_mult @@ -82,7 +83,7 @@ end function to_blockindexrange(b1::BlockIndexRange{1}, b2::BlockIndexRange{1}) t = (b1, b2) - return Block(Block.(t))[BlockSparseArrays.to_block_indices.(t)...] + return Block(Block.(t))[to_block_indices.(t)...] end function intersect_sectors(left::FusedAxes, right::FusedAxes) From b35757bd2b14f20ef80fdb8bec279e48f752381d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 13 Dec 2024 12:22:09 +0100 Subject: [PATCH 14/52] cache unitary coeff --- src/permutedims/unitaries.jl | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/permutedims/unitaries.jl b/src/permutedims/unitaries.jl index c3e199d..11fde7f 100644 --- a/src/permutedims/unitaries.jl +++ b/src/permutedims/unitaries.jl @@ -5,7 +5,11 @@ using LRUCache: LRU using SymmetrySectors: quantum_dimension -const unitary_cache = LRU{Any,AbstractMatrix}(; maxsize=10000) +const unitary_cache = LRU{ + Tuple{FusionTree,FusionTree,FusionTree,FusionTree,Tuple{Vararg{Int}}},Float64 +}(; + maxsize=10000, # TBD size +) # ====================================== Interface ======================================= function compute_unitary( @@ -14,25 +18,28 @@ function compute_unitary( return compute_unitary_clebsch_gordan(new_ft, old_ft, flatperm) end -function unitary_key(codomain_legs, domain_legs, old_outer_block, biperm) - legs = (codomain_legs..., domain_legs...) - old_arrows = isdual.(legs) - old_sectors = ntuple(i -> blocklabels(legs[i])[old_outer_block[i]], length(legs)) - return (old_arrows, old_sectors, length(codomain_legs), biperm) -end - # =========================== Constructor from Clebsch-Gordan ============================ function overlap_fusion_trees( - old_trees::Tuple{FusionTree,FusionTree}, - new_trees::Tuple{FusionTree,FusionTree}, + old_trees::Tuple{FusionTree{S},FusionTree{S}}, + new_trees::Tuple{FusionTree{S},FusionTree{S}}, flatperm::Tuple{Vararg{Integer}}, -) +) where {S} old_proj = contract_singlet_projector(old_trees...) new_proj = contract_singlet_projector(new_trees...) a = contract((), new_proj, flatperm, old_proj, ntuple(identity, ndims(new_proj))) return a[] / quantum_dimension(root_sector(first(new_trees))) end +function cached_unitary_coeff( + old_trees::Tuple{FusionTree{S},FusionTree{S}}, + new_trees::Tuple{FusionTree{S},FusionTree{S}}, + flatperm::Tuple{Vararg{Integer}}, +) where {S} + get!(unitary_cache, (old_trees..., new_trees..., flatperm)) do + overlap_fusion_trees(old_trees, new_trees, flatperm) + end +end + function compute_unitary_clebsch_gordan( new_ft::FusionTensor{T,N}, old_ft::FusionTensor{T,N}, flatperm::NTuple{N,Int} ) where {T,N} @@ -43,10 +50,8 @@ function compute_unitary_clebsch_gordan( old_outer = Tuple(findblock(old_ft, old_trees...)) swapped_old_block = Block(getindex.(Ref(Tuple(old_outer)), flatperm)) for new_trees in keys(trees_block_mapping(new_ft)) - new_outer = findblock(new_ft, new_trees...) - if swapped_old_block == new_outer - unitary[old_trees, new_trees] = overlap_fusion_trees(old_trees, new_trees, flatperm) - end + swapped_old_block != findblock(new_ft, new_trees...) && continue + unitary[old_trees, new_trees] = cached_unitary_coeff(old_trees, new_trees, flatperm) end end return unitary From 0299e4be1b2e8e3dde0aa2e4bd59dee04ba1d3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 13 Dec 2024 14:27:45 +0100 Subject: [PATCH 15/52] fix tests --- src/fusion_trees/fusiontree.jl | 5 ++--- src/fusiontensor/fusiontensor.jl | 28 +++++++++++++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index ace5c7f..bfd8840 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -73,8 +73,7 @@ Base.length(::FusionTree{<:Any,N}) where {N} = N # GradedUnitRanges interface GradedUnitRanges.sector_type(::FusionTree{S}) where {S} = S -function build_trees(legs::Vararg{AbstractGradedUnitRange{LA}}) where {LA} - # TBD when to impose LA to be the same for every leg? +function build_trees(legs::Vararg{AbstractGradedUnitRange}) tree_arrows = isdual.(legs) sectors = blocklabels.(legs) return mapreduce(vcat, CartesianIndices(blocklength.(legs))) do it @@ -198,7 +197,7 @@ end # one leg FusionTree(sect::AbstractSector, arrow::Bool) = FusionTree((sect,), (arrow,), sect, (), ()) -function braid_tuples(t1::Tuple{Vararg{<:Any,N}}, t2::Tuple{Vararg{<:Any,N}}) where {N} +function braid_tuples(t1::Tuple{Vararg{Any,N}}, t2::Tuple{Vararg{Any,N}}) where {N} t12 = (t1, t2) nested = ntuple(i -> getindex.(t12, i), N) return flatten_tuples(nested) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 8163586..6670c9a 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -77,17 +77,30 @@ function BlockArrays.findblock(ft::FusionTensor, f1::FusionTree, f2::FusionTree) return Block(b1..., b2...) end -function sanitize_axes(raw_legs) - legs = unify_sector_type(raw_legs) +sanitize_axes(::Tuple{}) = () +function sanitize_axes(raw_legs::Tuple{Vararg{AbstractGradedUnitRange}}) + legs = unify_sector_type(typeof(first(raw_legs)), raw_legs) @assert all(check_unique_blocklabels.(legs)) return legs end -function unify_sector_type(legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}}) where {LA} # nothing to do +function check_unique_blocklabels(g::AbstractGradedUnitRange) + return length(unique(blocklabels(g))) == blocklength(g) +end + +function unify_sector_type( + ::Type{<:AbstractGradedUnitRange{LA}}, legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}} +) where {LA} # nothing to do return legs end -check_unique_blocklabels(g) = length(unique(blocklabels(g))) == blocklength(g) +function unify_sector_type( + ::Type{<:AbstractGradedUnitRange}, legs::Tuple{Vararg{AbstractGradedUnitRange}} +) + T = find_common_sector_type(legs) + unified_legs = map(g -> unify_sector_type(T, g), legs) + return unified_legs +end # TODO move this to SymmetrySectors or GradedUnitRanges # merge with SymmetrySectors.map_blocklabels @@ -97,12 +110,6 @@ function find_common_sector_type(sector_or_axes_enum) return label_type(fusion_product(trivial.(sector_or_axes_enum)...)) end -function unify_sector_type(legs::Tuple{Vararg{AbstractGradedUnitRange}}) - T = find_common_sector_type(legs) - unified_legs = map(g -> unify_sector_type(T, g), legs) - return unified_legs -end - function unify_sector_type(T::Type{<:SectorProduct}, g::AbstractGradedUnitRange) # fuse with trivial to insert all missing arguments inside each GradedAxis # avoid depending on SymmetrySectors internals @@ -150,7 +157,6 @@ function FusionTensor(data_type::Type, ::Tuple{}, ::Tuple{}) return FusionTensor(mat, (), (), tree_to_block_mapping) end -# init data_matrix function initialize_data_matrix( data_type::Type{<:Number}, codomain_fused_axes::FusedAxes, domain_fused_axes::FusedAxes ) From 46470304434e6395e10f0061d2dc66138cf70fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 13 Dec 2024 14:58:07 +0100 Subject: [PATCH 16/52] simplify cast_to_array --- src/fusiontensor/array_cast.jl | 10 +++------- test/test_array_cast.jl | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 86482b8..0ff77e8 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -157,16 +157,12 @@ end #----------------------------------- cast to array ---------------------------------------- function contract_fusion_trees(ft::FusionTensor, f1::FusionTree, f2::FusionTree) N = ndims(ft) - charge_block = reshape(view(ft, f1, f2), :, 1) + charge_block = view(ft, f1, f2) p = contract_singlet_projector(f1, f2) # TODO use contract once it supports outer product - swapped = charge_block * reshape(p, 1, :) - b = findblock(ft, f1, f2) - block_shape = ( - ntuple(i -> blocklengths(axes(ft, i))[Int(Tuple(b)[i])], N)..., - ntuple(i -> quantum_dimension(blocklabels(axes(ft, i))[Int(Tuple(b)[i])]), N)..., - ) + swapped = reshape(charge_block, :, 1) * reshape(p, 1, :) + block_shape = (size(charge_block)..., size(p)...) perm = braid_tuples(ntuple(identity, N), ntuple(i -> i + N, N)) split_array_block = permutedims(reshape(swapped, block_shape), perm) diff --git a/test/test_array_cast.jl b/test/test_array_cast.jl index 6dc59b3..7d49f9f 100644 --- a/test/test_array_cast.jl +++ b/test/test_array_cast.jl @@ -313,8 +313,8 @@ end ], (2, 2, 2, 2), ) - dense, domain_legs, codomain_legs = sds22, (g2b, g2b), (g2, g2) - ft = FusionTensor(dense, domain_legs, codomain_legs) + dense, codomain_legs, domain_legs = sds22, (g2, g2), (g2b, g2b) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test norm(ft) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @@ -331,23 +331,23 @@ end (2, 2, 2, 2), ) sds22b_codomain_legs = (g2, g2b) - dense, domain_legs, codomain_legs = sds22b, (g2, g2b), (g2b, g2) - ftb = FusionTensor(dense, domain_legs, codomain_legs) + dense, codomain_legs, domain_legs = sds22b, (g2, g2b), (g2b, g2) + ftb = FusionTensor(dense, codomain_legs, domain_legs) @test norm(ftb) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ftb) ≈ sds22b @test Array(adjoint(ftb)) ≈ sds22b - # no codomain axis - dense, domain_legs, codomain_legs = sds22, (g2b, g2b, g2, g2), () - ft = FusionTensor(dense, domain_legs, codomain_legs) + # no domain axis + dense, codomain_legs, domain_legs = sds22, (g2b, g2b, g2, g2), () + ft = FusionTensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 - # no domain axis - dense, domain_legs, codomain_legs = sds22, (), (g2b, g2b, g2, g2) - ft = FusionTensor(dense, domain_legs, codomain_legs) + # no codomain axis + dense, codomain_legs, domain_legs = sds22, (), (g2b, g2b, g2, g2) + ft = FusionTensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 From e53bffd47acd182bf7727db36ff2f9de7e213b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Tue, 17 Dec 2024 22:55:32 +0100 Subject: [PATCH 17/52] update to BlockSparseArrays 0.2.0 --- Project.toml | 12 +++++------- src/fusiontensor/fusiontensor.jl | 8 ++++---- src/fusiontensor/linear_algebra_interface.jl | 10 +++++----- test/shared.jl | 10 +++++----- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Project.toml b/Project.toml index cb9f147..63926ab 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FusionTensors" uuid = "e16ca583-1f51-4df0-8e12-57d32947d33e" authors = ["ITensor developers and contributors"] -version = "0.1.0" +version = "0.2.0" [deps] BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" @@ -12,7 +12,6 @@ HalfIntegers = "f0d1745a-41c9-11e9-1dd9-e5d34d218721" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LabelledNumbers = "f856a3a6-4152-4ec4-b2a7-02c1a55d7993" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -NestedPermutedDimsArrays = "2c2a8ec4-3cfc-4276-aa3e-1307b4294e58" SparseArraysBase = "0d5efcca-f356-4864-8770-e1ed8d78f208" SymmetrySectors = "f8a8ad64-adbc-4fce-92f7-ffe2bb36a86e" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" @@ -22,16 +21,15 @@ WignerSymbols = "9f57e263-0b3d-5e2e-b1be-24f2bb48858b" [compat] Aqua = "0.8.9" BlockArrays = "1.2.0" -BlockSparseArrays = "0.1.0" +BlockSparseArrays = "0.2.0" BroadcastMapConversion = "0.1.0" -GradedUnitRanges = "0.1.0" +GradedUnitRanges = "0.1.1" HalfIntegers = "1.6.0" LRUCache = "1.6.1" LabelledNumbers = "0.1.0" LinearAlgebra = "1.11.0" -NestedPermutedDimsArrays = "0.1.0" -SparseArraysBase = "0.1.0" -SymmetrySectors = "0.1.0" +SparseArraysBase = "0.2.0" +SymmetrySectors = "0.1.1" TensorAlgebra = "0.1.0" Test = "1.10" TypeParameterAccessors = "0.1.0" diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 6670c9a..a805bb9 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -2,7 +2,7 @@ using BlockArrays: AbstractBlockMatrix, BlockArrays, blocklength, findblock -using BlockSparseArrays: AbstractBlockSparseMatrix, BlockSparseArray, block_stored_indices +using BlockSparseArrays: AbstractBlockSparseMatrix, BlockSparseArray, eachblockstoredindex using GradedUnitRanges: AbstractGradedUnitRange, blocklabels, dual, isdual, sector_type, space_isequal using SymmetrySectors: SectorProduct, TrivialSector @@ -125,9 +125,9 @@ function FusionTensor( domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, ) ft = FusionTensor(eltype(mat), codomain_legs, domain_legs) - for b in block_stored_indices(mat) - @assert last(b) in block_stored_indices(data_matrix(ft)) - data_matrix(ft)[last(b)] = mat[last(b)] + for b in eachblockstoredindex(mat) + @assert b in eachblockstoredindex(data_matrix(ft)) + data_matrix(ft)[b] = mat[b] end return ft end diff --git a/src/fusiontensor/linear_algebra_interface.jl b/src/fusiontensor/linear_algebra_interface.jl index 65800c8..fa70e0c 100644 --- a/src/fusiontensor/linear_algebra_interface.jl +++ b/src/fusiontensor/linear_algebra_interface.jl @@ -4,7 +4,7 @@ using LinearAlgebra: LinearAlgebra, mul!, norm, tr using BlockArrays: Block, blocks -using BlockSparseArrays: stored_indices +using BlockSparseArrays: eachblockstoredindex using GradedUnitRanges: blocklabels using SymmetrySectors: quantum_dimension @@ -45,9 +45,9 @@ function LinearAlgebra.norm(ft::FusionTensor) m = data_matrix(ft) row_sectors = blocklabels(matrix_row_axis(ft)) n2 = mapreduce( - idx -> quantum_dimension(row_sectors[idx[1]]) * norm(m[Block(Tuple(idx))])^2, + b -> quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * norm(m[b])^2, +, - stored_indices(blocks(m)); + eachblockstoredindex(m); init=0.0, ) return sqrt(n2) @@ -57,9 +57,9 @@ function LinearAlgebra.tr(ft::FusionTensor) m = data_matrix(ft) row_sectors = blocklabels(matrix_row_axis(ft)) return mapreduce( - idx -> quantum_dimension(row_sectors[idx[1]]) * tr(m[Block(Tuple(idx))]), + b -> quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * tr(m[b]), +, - stored_indices(blocks(m)); + eachblockstoredindex(m); init=eltype(ft)(0), ) end diff --git a/test/shared.jl b/test/shared.jl index ae75b96..6592ec0 100644 --- a/test/shared.jl +++ b/test/shared.jl @@ -1,6 +1,6 @@ using LinearAlgebra: Adjoint -using BlockSparseArrays: BlockSparseMatrix, block_stored_indices +using BlockSparseArrays: BlockSparseMatrix, eachblockstoredindex using FusionTensors: FusionTensor, codomain_axes, @@ -46,11 +46,11 @@ function check_sanity(ft::FusionTensor) column_axis = matrix_column_axis(ft) @assert row_axis === axes(m, 1) "invalid row_axis" @assert column_axis === axes(m, 2) "invalid column_axis" - check_data_matrix_axes(data_matrix(ft), codomain_axes(ft), domain_axes(ft)) + check_data_matrix_axes(m, codomain_axes(ft), domain_axes(ft)) - for b in block_stored_indices(m) - it = Int.(Tuple(b)) - @assert blocklabels(row_axis)[it[1]] == blocklabels(dual(column_axis))[it[2]] "forbidden block" + for b in eachblockstoredindex(m) + ir, ic = Int.(Tuple(b)) + @assert blocklabels(row_axis)[ir] == blocklabels(dual(column_axis))[ic] "forbidden block" end return nothing end From 2794d604c310f64ea0d3633ab8f023a728a47150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 11:14:45 +0100 Subject: [PATCH 18/52] use map_blocklabels --- src/fusiontensor/fusiontensor.jl | 36 +++++++++++++++----------------- test/test_basics.jl | 32 ++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index a805bb9..86b1cd6 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -4,7 +4,13 @@ using BlockArrays: AbstractBlockMatrix, BlockArrays, blocklength, findblock using BlockSparseArrays: AbstractBlockSparseMatrix, BlockSparseArray, eachblockstoredindex using GradedUnitRanges: - AbstractGradedUnitRange, blocklabels, dual, isdual, sector_type, space_isequal + AbstractGradedUnitRange, + blocklabels, + dual, + isdual, + map_blocklabels, + sector_type, + space_isequal using SymmetrySectors: SectorProduct, TrivialSector struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T,N} @@ -79,7 +85,7 @@ end sanitize_axes(::Tuple{}) = () function sanitize_axes(raw_legs::Tuple{Vararg{AbstractGradedUnitRange}}) - legs = unify_sector_type(typeof(first(raw_legs)), raw_legs) + legs = promote_sectors(typeof(first(raw_legs)), raw_legs) @assert all(check_unique_blocklabels.(legs)) return legs end @@ -88,35 +94,27 @@ function check_unique_blocklabels(g::AbstractGradedUnitRange) return length(unique(blocklabels(g))) == blocklength(g) end -function unify_sector_type( +function promote_sectors( ::Type{<:AbstractGradedUnitRange{LA}}, legs::Tuple{Vararg{AbstractGradedUnitRange{LA}}} ) where {LA} # nothing to do return legs end -function unify_sector_type( +function promote_sectors( ::Type{<:AbstractGradedUnitRange}, legs::Tuple{Vararg{AbstractGradedUnitRange}} ) - T = find_common_sector_type(legs) - unified_legs = map(g -> unify_sector_type(T, g), legs) + T = promote_sector_type(legs) + # fuse with trivial to insert all missing arguments inside each GradedAxis + # avoid depending on SymmetrySectors internals + s0 = trivial(T) + unified_legs = map_blocklabels.(s -> only(blocklabels(fusion_product(s0, s))), legs) return unified_legs end -# TODO move this to SymmetrySectors or GradedUnitRanges -# merge with SymmetrySectors.map_blocklabels -function find_common_sector_type(sector_or_axes_enum) +function promote_sector_type(legs) # fuse trivial sectors to produce unified type # avoid depending on SymmetrySectors internals - return label_type(fusion_product(trivial.(sector_or_axes_enum)...)) -end - -function unify_sector_type(T::Type{<:SectorProduct}, g::AbstractGradedUnitRange) - # fuse with trivial to insert all missing arguments inside each GradedAxis - # avoid depending on SymmetrySectors internals - glabels = map(s -> only(blocklabels(fusion_product(trivial(T), s))), blocklabels(g)) - # use labelled_blocks to preserve GradedUnitRange - unified_g = labelled_blocks(unlabel_blocks(g), glabels) - return isdual(g) ? flip(unified_g) : unified_g + return sector_type(fusion_product(trivial.(legs)...)) end function FusionTensor( diff --git a/test/test_basics.jl b/test/test_basics.jl index b6ee3da..0ebd584 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -15,8 +15,8 @@ using FusionTensors: ndims_domain, ndims_codomain using GradedUnitRanges: - blockmergesort, dual, flip, fusion_product, gradedrange, space_isequal -using SymmetrySectors: U1 + blockmergesort, dual, flip, fusion_product, gradedrange, sector_type, space_isequal +using SymmetrySectors: U1, SU2, SectorProduct, Z include("shared.jl") @@ -46,6 +46,7 @@ include("shared.jl") @test space_isequal(matrix_column_axis(ft1), g2) @test isnothing(check_sanity(ft0)) @test isnothing(check_sanity(ft1)) + @test sector_type(ft1) === U1{Int} # Base methods @test eltype(ft1) === Float64 @@ -232,4 +233,31 @@ end @test space_isequal(dual(g4), codomain_axes(ad)[2]) @test isnothing(check_sanity(ad)) end + +@testset "mising SectorProduct" begin + g1 = gradedrange([SectorProduct(U1(1)) => 1]) + g2 = gradedrange([SectorProduct(U1(1), SU2(1//2)) => 1]) + g3 = gradedrange([SectorProduct(U1(1), SU2(1//2), Z{2}(1)) => 1]) + S = sector_type(g3) + + ft = FusionTensor(Float64, (g1,), (dual(g2), dual(g3))) + @test sector_type(ft) === S + gr = gradedrange([SectorProduct(U1(1), SU2(0), Z{2}(0)) => 1]) + @test space_isequal(matrix_row_axis(ft), gr) + gc = gradedrange([ + SectorProduct(U1(2), SU2(0), Z{2}(1)) => 1, SectorProduct(U1(2), SU2(1), Z{2}(1)) => 1 + ]) + @test space_isequal(matrix_column_axis(ft), dual(gc)) + + gA = gradedrange([SectorProduct(; A=U1(1)) => 1]) + gB = gradedrange([SectorProduct(; B=SU2(1//2)) => 1]) + gC = gradedrange([SectorProduct(; C=Z{2}(0)) => 1]) + gABC = fusion_product(fusion_product(gA, gB), gC) + S = sector_type(gABC) + + ft = FusionTensor(Float64, (gA, gB), (dual(gA), dual(gB), gC)) + @test sector_type(ft) === S + @test space_isequal(matrix_row_axis(ft), gABC) + @test space_isequal(matrix_column_axis(ft), dual(gABC)) +end end From a23939c4d4253b253a19fefd725e5fb0499b41b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 11:20:38 +0100 Subject: [PATCH 19/52] fix signatures for fusiontensor_permutedims --- src/permutedims/permutedims.jl | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl index b486413..2c5a179 100644 --- a/src/permutedims/permutedims.jl +++ b/src/permutedims/permutedims.jl @@ -4,20 +4,31 @@ using BlockArrays: blocklengths using TensorAlgebra: BlockedPermutation, blockedperm, blockpermute +function naive_permutedims(ft, biperm::BlockedPermutation{2}) + @assert ndims(ft) == length(biperm) + new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) + + # naive permute: cast to dense, permutedims, cast to FusionTensor + arr = Array(ft) + permuted_arr = permutedims(arr, Tuple(biperm)) + permuted = FusionTensor(permuted_arr, new_codomain_legs, new_domain_legs) + return permuted +end + # permutedims with 1 tuple of 2 separate tuples -function fusiontensor_permutedims(ft::FusionTensor, new_leg_indices::Tuple{Tuple,Tuple}) +function fusiontensor_permutedims(ft, new_leg_indices::Tuple{Tuple,Tuple}) return fusiontensor_permutedims(ft, new_leg_indices...) end # permutedims with 2 separate tuples function fusiontensor_permutedims( - ft::FusionTensor, new_codomain_indices::Tuple, new_domain_indices::Tuple + ft, new_codomain_indices::Tuple, new_domain_indices::Tuple ) biperm = blockedperm(new_codomain_indices, new_domain_indices) return fusiontensor_permutedims(ft, biperm) end -function fusiontensor_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2}) +function fusiontensor_permutedims(ft, biperm::BlockedPermutation{2}) @assert ndims(ft) == length(biperm) # early return for identity operation. Do not copy. Also handle tricky 0-dim case. @@ -29,22 +40,11 @@ function fusiontensor_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2 new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) permuted = FusionTensor(eltype(ft), new_codomain_legs, new_domain_legs) - permute_fusiontensor_data!(permuted, ft, Tuple(biperm)) - return permuted -end - -function naive_permutedims(ft::FusionTensor, biperm::BlockedPermutation{2}) - @assert ndims(ft) == length(biperm) - new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) - - # naive permute: cast to dense, permutedims, cast to FusionTensor - arr = Array(ft) - permuted_arr = permutedims(arr, Tuple(biperm)) - permuted = FusionTensor(permuted_arr, new_codomain_legs, new_domain_legs) + fusiontensor_permutedims!(permuted, ft, Tuple(biperm)) return permuted end -function permute_fusiontensor_data!( +function fusiontensor_permutedims!( new_ft::FusionTensor{T,N}, old_ft::FusionTensor{T,N}, flatperm::NTuple{N,Integer} ) where {T,N} unitary = compute_unitary(new_ft, old_ft, flatperm) From f9b0daa56d38a6e40ee14dc96bc77cf820a2f3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 12:19:54 +0100 Subject: [PATCH 20/52] define unsafe_cast_from_array --- src/fusiontensor/array_cast.jl | 25 ++++++++++++++++++++++--- test/test_array_cast.jl | 17 +++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 0ff77e8..3c1cffd 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -37,16 +37,35 @@ end function cast_from_array( blockarray::AbstractBlockArray, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}; + tol::Float64=1e-12, ) # input validation if length(codomain_legs) + length(domain_legs) != ndims(blockarray) # compile time - throw(codomainError("legs are incompatible with array ndims")) + throw(DomainError("legs are incompatible with array ndims")) end if quantum_dimension.((codomain_legs..., domain_legs...)) != size(blockarray) - throw(codomainError("legs dimensions are incompatible with array")) + throw(DomainError("legs dimensions are incompatible with array")) end + ft = unsafe_cast_from_array(blockarray, codomain_legs, domain_legs) + + # if blockarray is not G-invariant, norm(ft) < norm(blockarray) + if abs(norm(ft) - norm(blockarray)) > tol + throw( + InexactError( + :FusionTensor, typeof(blockarray), typeof(codomain_legs), typeof(domain_legs) + ), + ) + end + return ft +end + +function unsafe_cast_from_array( + blockarray::AbstractBlockArray, + codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, +) ft = FusionTensor(eltype(blockarray), codomain_legs, domain_legs) for (f1, f2) in keys(trees_block_mapping(ft)) b = findblock(ft, f1, f2) diff --git a/test/test_array_cast.jl b/test/test_array_cast.jl index 7d49f9f..03bfc4d 100644 --- a/test/test_array_cast.jl +++ b/test/test_array_cast.jl @@ -1,8 +1,8 @@ @eval module $(gensym()) using LinearAlgebra: LinearAlgebra, norm -using Test: @test, @testset, @test_broken +using Test: @test, @test_broken, @test_throws, @testset -using BlockArrays: Block, blocksize +using BlockArrays: Block, BlockedArray, blocksize using FusionTensors: FusionTensor, data_matrix using GradedUnitRanges: dual, fusion_product, gradedrange @@ -97,6 +97,19 @@ end @test isnothing(check_sanity(ft)) @test Array(ft) ≈ dense @test Array(adjoint(ft)) ≈ adjoint(dense) + + @test_throws BoundsError FusionTensor( + dense, (gradedrange([U1(1) => 1, U1(2) => 3]),), domain_legs + ) + @test_throws MethodError FusionTensor(dense, (g, g), domain_legs) + + ba = BlockedArray(dense, [1, 2], [1, 2]) + @test_throws DomainError FusionTensor( + ba, (gradedrange([U1(1) => 1, U1(2) => 3]),), domain_legs + ) + @test_throws DomainError FusionTensor(ba, (g, g), domain_legs) + dense[1, 2] = 1 # forbidden + @test_throws InexactError FusionTensor(dense, codomain_legs, domain_legs) end @testset "several axes, one block" begin From d816193f2de570fd71f0087b4701fd4245b8c2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 11:23:51 +0100 Subject: [PATCH 21/52] use @strided --- Project.toml | 2 ++ src/permutedims/permutedims.jl | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 63926ab..6fca567 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LabelledNumbers = "f856a3a6-4152-4ec4-b2a7-02c1a55d7993" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" SparseArraysBase = "0d5efcca-f356-4864-8770-e1ed8d78f208" +Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" SymmetrySectors = "f8a8ad64-adbc-4fce-92f7-ffe2bb36a86e" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" TypeParameterAccessors = "7e5a90cf-f82e-492e-a09b-e3e26432c138" @@ -29,6 +30,7 @@ LRUCache = "1.6.1" LabelledNumbers = "0.1.0" LinearAlgebra = "1.11.0" SparseArraysBase = "0.2.0" +Strided = "2.2.0" SymmetrySectors = "0.1.1" TensorAlgebra = "0.1.0" Test = "1.10" diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl index 2c5a179..4052467 100644 --- a/src/permutedims/permutedims.jl +++ b/src/permutedims/permutedims.jl @@ -1,6 +1,7 @@ # This file defines permutedims for a FusionTensor using BlockArrays: blocklengths +using Strided: Strided, @strided using TensorAlgebra: BlockedPermutation, blockedperm, blockpermute @@ -39,9 +40,9 @@ function fusiontensor_permutedims(ft, biperm::BlockedPermutation{2}) end new_codomain_legs, new_domain_legs = blockpermute(axes(ft), biperm) - permuted = FusionTensor(eltype(ft), new_codomain_legs, new_domain_legs) - fusiontensor_permutedims!(permuted, ft, Tuple(biperm)) - return permuted + new_ft = FusionTensor(eltype(ft), new_codomain_legs, new_domain_legs) + fusiontensor_permutedims!(new_ft, ft, Tuple(biperm)) + return new_ft end function fusiontensor_permutedims!( @@ -50,6 +51,8 @@ function fusiontensor_permutedims!( unitary = compute_unitary(new_ft, old_ft, flatperm) for p in unitary old_trees, new_trees = first(p) - new_ft[new_trees] += last(p) * permutedims(old_ft[old_trees], flatperm) + new_block = view(new_ft, new_trees) + old_block = view(old_ft, old_trees) + @strided new_block .+= last(p) .* permutedims(old_block, flatperm) end end From 899bed80d7d64e10b27a0158bbd10cc581eaef70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 16:39:23 +0100 Subject: [PATCH 22/52] rename FusionTree SectorFusionTree --- src/fusion_trees/fusiontree.jl | 76 ++++++++++++++++-------------- src/fusiontensor/array_cast.jl | 14 ++++-- src/fusiontensor/base_interface.jl | 18 ++++--- src/fusiontensor/fusedaxes.jl | 6 +-- src/fusiontensor/fusiontensor.jl | 7 +-- src/permutedims/unitaries.jl | 13 +++-- test/test_fusion_trees.jl | 10 ++-- 7 files changed, 81 insertions(+), 63 deletions(-) diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index bfd8840..110f735 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -35,7 +35,7 @@ using TensorAlgebra: flatten_tuples # The interface uses AbstractGradedUnitRanges as input for interface simplicity # however only blocklabels are used and blocklengths are never read. -struct FusionTree{S,N,M} +struct SectorFusionTree{S,N,M} leaves::NTuple{N,S} # TBD rename outer_sectors or leave_sectors? arrows::NTuple{N,Bool} root_sector::S @@ -46,7 +46,7 @@ struct FusionTree{S,N,M} # currently first(branch_sectors) == first(leaves) # redundant but allows for simpler, generic grow_tree code - function FusionTree( + function SectorFusionTree( leaves, arrows, root_sector, branch_sectors, outer_multiplicity_indices ) N = length(leaves) @@ -59,19 +59,19 @@ struct FusionTree{S,N,M} end # getters -arrows(f::FusionTree) = f.arrows -leaves(f::FusionTree) = f.leaves -root_sector(f::FusionTree) = f.root_sector -branch_sectors(f::FusionTree) = f.branch_sectors -outer_multiplicity_indices(f::FusionTree) = f.outer_multiplicity_indices +arrows(f::SectorFusionTree) = f.arrows +leaves(f::SectorFusionTree) = f.leaves +root_sector(f::SectorFusionTree) = f.root_sector +branch_sectors(f::SectorFusionTree) = f.branch_sectors +outer_multiplicity_indices(f::SectorFusionTree) = f.outer_multiplicity_indices # Base interface -Base.convert(T::Type{<:Array}, f::FusionTree) = convert(T, to_tensor(f)) -Base.isless(f1::FusionTree, f2::FusionTree) = isless(to_tuple(f1), to_tuple(f2)) -Base.length(::FusionTree{<:Any,N}) where {N} = N +Base.convert(T::Type{<:Array}, f::SectorFusionTree) = convert(T, to_tensor(f)) +Base.isless(f1::SectorFusionTree, f2::SectorFusionTree) = isless(to_tuple(f1), to_tuple(f2)) +Base.length(::SectorFusionTree{<:Any,N}) where {N} = N # GradedUnitRanges interface -GradedUnitRanges.sector_type(::FusionTree{S}) where {S} = S +GradedUnitRanges.sector_type(::SectorFusionTree{S}) where {S} = S function build_trees(legs::Vararg{AbstractGradedUnitRange}) tree_arrows = isdual.(legs) @@ -83,7 +83,7 @@ function build_trees(legs::Vararg{AbstractGradedUnitRange}) end # SymmetrySectors interface -function SymmetrySectors.:×(f1::FusionTree, f2::FusionTree) +function SymmetrySectors.:×(f1::SectorFusionTree, f2::SectorFusionTree) @assert arrows(f1) == arrows(f2) product_leaves = .×(leaves(f1), leaves(f2)) product_root_sector = root_sector(f1) × root_sector(f2) @@ -96,7 +96,7 @@ function SymmetrySectors.:×(f1::FusionTree, f2::FusionTree) outer_multiplicity_indices(f1), outer_multiplicity_indices(f2), ) - return FusionTree( + return SectorFusionTree( product_leaves, arrows(f1), product_root_sector, @@ -105,7 +105,7 @@ function SymmetrySectors.:×(f1::FusionTree, f2::FusionTree) ) end -function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct}) +function SymmetrySectors.arguments(f::SectorFusionTree{<:SectorProduct}) transposed_indices = outer_multiplicity_split.( Base.tail(leaves(f)), @@ -119,7 +119,7 @@ function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct}) # TODO way to avoid explicit ntuple? # works fine for Tuple and NamedTuple SectorProduct return ntuple( - i -> FusionTree( + i -> SectorFusionTree( getindex.(arguments_leaves, i), arrows(f), arguments_root[i], @@ -130,16 +130,16 @@ function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct}) ) end -function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct,0}) - return map(arg -> FusionTree((), (), arg, (), ()), arguments(root_sector(f))) +function SymmetrySectors.arguments(f::SectorFusionTree{<:SectorProduct,0}) + return map(arg -> SectorFusionTree((), (), arg, (), ()), arguments(root_sector(f))) end -function SymmetrySectors.arguments(f::FusionTree{<:SectorProduct,1}) +function SymmetrySectors.arguments(f::SectorFusionTree{<:SectorProduct,1}) arguments_root = arguments(root_sector(f)) arguments_leave = arguments(only(leaves(f))) # use map(keys) to stay agnostic with respect to SectorProduct implementation return map( - k -> FusionTree((arguments_leave[k],), arrows(f), arguments_root[k], (), ()), + k -> SectorFusionTree((arguments_leave[k],), arrows(f), arguments_root[k], (), ()), keys(arguments_root), ) end @@ -148,7 +148,7 @@ end # ===================================== Internals ======================================== # # --------------- misc --------------- -function to_tuple(f::FusionTree) +function to_tuple(f::SectorFusionTree) return ( leaves(f)..., arrows(f)..., @@ -187,15 +187,17 @@ end # --------------- Build trees --------------- # zero leg: need S to get sector type information -function FusionTree{S}() where {S<:AbstractSector} - return FusionTree((), (), trivial(S), (), ()) +function SectorFusionTree{S}() where {S<:AbstractSector} + return SectorFusionTree((), (), trivial(S), (), ()) end -function FusionTree{S}(::Tuple{}, ::Tuple{}) where {S<:AbstractSector} - return FusionTree((), (), trivial(S), (), ()) +function SectorFusionTree{S}(::Tuple{}, ::Tuple{}) where {S<:AbstractSector} + return SectorFusionTree((), (), trivial(S), (), ()) end # one leg -FusionTree(sect::AbstractSector, arrow::Bool) = FusionTree((sect,), (arrow,), sect, (), ()) +function SectorFusionTree(sect::AbstractSector, arrow::Bool) + return SectorFusionTree((sect,), (arrow,), sect, (), ()) +end function braid_tuples(t1::Tuple{Vararg{Any,N}}, t2::Tuple{Vararg{Any,N}}) where {N} t12 = (t1, t2) @@ -204,7 +206,7 @@ function braid_tuples(t1::Tuple{Vararg{Any,N}}, t2::Tuple{Vararg{Any,N}}) where end function grow_tree( - parent_tree::FusionTree, + parent_tree::SectorFusionTree, branch_sector::AbstractSector, level_arrow::Bool, child_root_sector, @@ -214,13 +216,13 @@ function grow_tree( child_arrows = (arrows(parent_tree)..., level_arrow) child_branch_sectors = (branch_sectors(parent_tree)..., root_sector(parent_tree)) child_outer_mul = (outer_multiplicity_indices(parent_tree)..., outer_mult) - return FusionTree( + return SectorFusionTree( child_leaves, child_arrows, child_root_sector, child_branch_sectors, child_outer_mul ) end function grow_tree( - parent_tree::FusionTree, branch_sector::AbstractSector, level_arrow::Bool + parent_tree::SectorFusionTree, branch_sector::AbstractSector, level_arrow::Bool ) new_space = fusion_product(root_sector(parent_tree), branch_sector) return mapreduce(vcat, zip(blocklabels(new_space), blocklengths(new_space))) do (la, n) @@ -247,14 +249,14 @@ end function build_trees( sectors_to_fuse::NTuple{N,<:AbstractSector}, arrows_to_fuse::NTuple{N,Bool} ) where {N} - trees = [FusionTree(first(sectors_to_fuse), first(arrows_to_fuse))] + trees = [SectorFusionTree(first(sectors_to_fuse), first(arrows_to_fuse))] return build_trees(trees, Base.tail(sectors_to_fuse), Base.tail(arrows_to_fuse)) end # --------------- convert to Array --------------- -to_tensor(::FusionTree{<:Any,0}) = ones(1) +to_tensor(::SectorFusionTree{<:Any,0}) = ones(1) -function to_tensor(f::FusionTree) +function to_tensor(f::SectorFusionTree) # init with dummy trivial leg to get arrow correct and deal with size-1 case cgt1 = clebsch_gordan_tensor( trivial(sector_type(f)), first(leaves(f)), first(leaves(f)), false, first(arrows(f)), 1 @@ -263,8 +265,8 @@ function to_tensor(f::FusionTree) return grow_tensor_tree(tree_tensor, f) end -#to_tensor(::FusionTree{<:SectorProduct,0}) = ones(1) -function to_tensor(f::FusionTree{<:SectorProduct}) +#to_tensor(::SectorFusionTree{<:SectorProduct,0}) = ones(1) +function to_tensor(f::SectorFusionTree{<:SectorProduct}) args = convert.(Array, arguments(f)) return reduce(_tensor_kron, args) end @@ -290,11 +292,13 @@ function contract_clebsch_gordan(tree_tensor::AbstractArray, cgt::AbstractArray) end # specialized code when branch_sector is empty -function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,2}, ::FusionTree{<:Any,1}) +function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,2}, ::SectorFusionTree{<:Any,1}) return tree_tensor end -function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,N}, f::FusionTree) where {N} +function grow_tensor_tree( + tree_tensor::AbstractArray{<:Real,N}, f::SectorFusionTree +) where {N} cgt = clebsch_gordan_tensor( branch_sectors(f)[N - 1], leaves(f)[N], @@ -308,7 +312,7 @@ function grow_tensor_tree(tree_tensor::AbstractArray{<:Real,N}, f::FusionTree) w end function grow_tensor_tree( - tree_tensor::AbstractArray{<:Real,N}, f::FusionTree{<:Any,N} + tree_tensor::AbstractArray{<:Real,N}, f::SectorFusionTree{<:Any,N} ) where {N} cgt = clebsch_gordan_tensor( last(branch_sectors(f)), diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 3c1cffd..f48966c 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -88,13 +88,15 @@ end #----------------------------------- misc tools ---------------------------------------- -function degen_dims_shape(dense_shape::Tuple{Vararg{Int}}, f::FusionTree) +function degen_dims_shape(dense_shape::Tuple{Vararg{Int}}, f::SectorFusionTree) dims = quantum_dimension.(leaves(f)) mults = dense_shape .÷ dims return braid_tuples(mults, dims) end -function split_degen_dims(array_block::AbstractArray, f1::FusionTree, f2::FusionTree) +function split_degen_dims( + array_block::AbstractArray, f1::SectorFusionTree, f2::SectorFusionTree +) array_block_split_shape = ( degen_dims_shape(size(array_block)[begin:length(f1)], f1)..., degen_dims_shape(size(array_block)[(length(f1) + 1):end], f2)..., @@ -112,7 +114,9 @@ end #---------------------------------- cast from array --------------------------------------- -function contract_fusion_trees(array_block::AbstractArray, f1::FusionTree, f2::FusionTree) +function contract_fusion_trees( + array_block::AbstractArray, f1::SectorFusionTree, f2::SectorFusionTree +) # start from an array outer block with e.g. N=6 axes divided into N_DO=3 ndims_codomain # and N_CO=3 ndims_domain. Each leg k can be decomposed as a product of external an # multiplicity extk and a quantum dimension dimk @@ -159,7 +163,7 @@ function contract_fusion_trees(array_block::AbstractArray, f1::FusionTree, f2::F ) end -function contract_singlet_projector(f1::FusionTree, f2::FusionTree) +function contract_singlet_projector(f1::SectorFusionTree, f2::SectorFusionTree) f1_array = convert(Array, f1) f2_array = convert(Array, f2) N_CO = length(f1) @@ -174,7 +178,7 @@ function contract_singlet_projector(f1::FusionTree, f2::FusionTree) end #----------------------------------- cast to array ---------------------------------------- -function contract_fusion_trees(ft::FusionTensor, f1::FusionTree, f2::FusionTree) +function contract_fusion_trees(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) N = ndims(ft) charge_block = view(ft, f1, f2) p = contract_singlet_projector(f1, f2) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 0ddbfdb..f98529d 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -100,18 +100,22 @@ end # eachindex is automatically defined for AbstractArray. We do not want it. Base.eachindex(::FusionTensor) = error("eachindex not defined for FusionTensor") -Base.getindex(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = ft[f1f2...] -function Base.getindex(ft::FusionTensor, f1::FusionTree, f2::FusionTree) +function Base.getindex(ft::FusionTensor, f1f2::Tuple{<:SectorFusionTree,<:SectorFusionTree}) + return ft[f1f2...] +end +function Base.getindex(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) charge_matrix = data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] return reshape(charge_matrix, charge_block_size(ft, f1, f2)) end function Base.setindex!( - ft::FusionTensor, a::AbstractArray, f1f2::Tuple{<:FusionTree,<:FusionTree} + ft::FusionTensor, a::AbstractArray, f1f2::Tuple{<:SectorFusionTree,<:SectorFusionTree} ) return setindex!(ft, a, f1f2...) end -function Base.setindex!(ft::FusionTensor, a::AbstractArray, f1::FusionTree, f2::FusionTree) +function Base.setindex!( + ft::FusionTensor, a::AbstractArray, f1::SectorFusionTree, f2::SectorFusionTree +) return view(ft, f1, f2) .= a end @@ -150,8 +154,10 @@ end Base.size(ft::FusionTensor) = quantum_dimension.(axes(ft)) -Base.view(ft::FusionTensor, f1f2::Tuple{<:FusionTree,<:FusionTree}) = view(ft, f1f2...) -function Base.view(ft::FusionTensor, f1::FusionTree, f2::FusionTree) +function Base.view(ft::FusionTensor, f1f2::Tuple{<:SectorFusionTree,<:SectorFusionTree}) + return view(ft, f1f2...) +end +function Base.view(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) charge_matrix = view(data_matrix(ft), trees_block_mapping(ft)[f1, f2]) return reshape(charge_matrix, charge_block_size(ft, f1, f2)) end diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index fcdf58d..0b4623c 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -15,7 +15,7 @@ struct FusedAxes{A,B,C} function FusedAxes( outer_legs::NTuple{N,AbstractGradedUnitRange{LA}}, fused_axis::AbstractGradedUnitRange{LA}, - trees_to_ranges_mapping::Dict{<:FusionTree{<:AbstractSector,N}}, + trees_to_ranges_mapping::Dict{<:SectorFusionTree{<:AbstractSector,N}}, ) where {N,LA} return new{typeof(outer_legs),typeof(fused_axis),typeof(trees_to_ranges_mapping)}( outer_legs, fused_axis, trees_to_ranges_mapping @@ -38,7 +38,7 @@ GradedUnitRanges.blocklabels(fa::FusedAxes) = blocklabels(fused_axis(fa)) # constructors function FusedAxes{S}(::Tuple{}) where {S<:AbstractSector} fused_axis = gradedrange([trivial(S) => 1]) - trees_to_ranges_mapping = Dict([FusionTree{S}() => Block(1)[1:1]]) + trees_to_ranges_mapping = Dict([SectorFusionTree{S}() => Block(1)[1:1]]) return FusedAxes((), fused_axis, trees_to_ranges_mapping) end @@ -64,7 +64,7 @@ function fusion_trees_external_multiplicities( end function compute_inner_ranges( - fusion_trees_mult::AbstractVector{<:Pair{<:FusionTree,<:Integer}} + fusion_trees_mult::AbstractVector{<:Pair{<:SectorFusionTree,<:Integer}} ) fused_leg = blockmergesort( gradedrange(root_sector.(first.(fusion_trees_mult)) .=> last.(fusion_trees_mult)) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 86b1cd6..444d70a 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -29,7 +29,8 @@ struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T, ) S = sector_type(axes(mat, 1)) @assert sector_type(axes(mat, 2)) === S - @assert keytype(trees_block_mapping) <: Tuple{<:FusionTree{S},<:FusionTree{S}} + @assert keytype(trees_block_mapping) <: + Tuple{<:SectorFusionTree{S},<:SectorFusionTree{S}} @assert all(sector_type.(codomain_legs) .=== S) @assert all(sector_type.(domain_legs) .=== S) return new{ @@ -58,7 +59,7 @@ ndims_domain(ft::FusionTensor) = length(domain_axes(ft)) matrix_size(ft::FusionTensor) = quantum_dimension.(axes(data_matrix(ft))) matrix_row_axis(ft::FusionTensor) = first(axes(data_matrix(ft))) matrix_column_axis(ft::FusionTensor) = last(axes(data_matrix(ft))) -function charge_block_size(ft::FusionTensor, f1::FusionTree, f2::FusionTree) +function charge_block_size(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) b = Tuple(findblock(ft, f1, f2)) return ntuple(i -> Int(length(axes(ft)[i][b[i]])), ndims(ft)) end @@ -67,7 +68,7 @@ end GradedUnitRanges.sector_type(ft::FusionTensor) = sector_type(matrix_row_axis(ft)) # BlockArrays interface -function BlockArrays.findblock(ft::FusionTensor, f1::FusionTree, f2::FusionTree) +function BlockArrays.findblock(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) # find outer block corresponding to fusion trees @assert ndims_codomain(ft) == length(f1) @assert ndims_domain(ft) == length(f2) diff --git a/src/permutedims/unitaries.jl b/src/permutedims/unitaries.jl index 11fde7f..5e9dbf0 100644 --- a/src/permutedims/unitaries.jl +++ b/src/permutedims/unitaries.jl @@ -6,7 +6,10 @@ using LRUCache: LRU using SymmetrySectors: quantum_dimension const unitary_cache = LRU{ - Tuple{FusionTree,FusionTree,FusionTree,FusionTree,Tuple{Vararg{Int}}},Float64 + Tuple{ + SectorFusionTree,SectorFusionTree,SectorFusionTree,SectorFusionTree,Tuple{Vararg{Int}} + }, + Float64, }(; maxsize=10000, # TBD size ) @@ -20,8 +23,8 @@ end # =========================== Constructor from Clebsch-Gordan ============================ function overlap_fusion_trees( - old_trees::Tuple{FusionTree{S},FusionTree{S}}, - new_trees::Tuple{FusionTree{S},FusionTree{S}}, + old_trees::Tuple{SectorFusionTree{S},SectorFusionTree{S}}, + new_trees::Tuple{SectorFusionTree{S},SectorFusionTree{S}}, flatperm::Tuple{Vararg{Integer}}, ) where {S} old_proj = contract_singlet_projector(old_trees...) @@ -31,8 +34,8 @@ function overlap_fusion_trees( end function cached_unitary_coeff( - old_trees::Tuple{FusionTree{S},FusionTree{S}}, - new_trees::Tuple{FusionTree{S},FusionTree{S}}, + old_trees::Tuple{SectorFusionTree{S},SectorFusionTree{S}}, + new_trees::Tuple{SectorFusionTree{S},SectorFusionTree{S}}, flatperm::Tuple{Vararg{Integer}}, ) where {S} get!(unitary_cache, (old_trees..., new_trees..., flatperm)) do diff --git a/test/test_fusion_trees.jl b/test/test_fusion_trees.jl index 2799b76..9043e0e 100644 --- a/test/test_fusion_trees.jl +++ b/test/test_fusion_trees.jl @@ -5,7 +5,7 @@ using LinearAlgebra: I using BlockArrays: BlockArrays using FusionTensors: - FusionTree, + SectorFusionTree, arrows, branch_sectors, build_trees, @@ -17,7 +17,7 @@ using SymmetrySectors: ×, SectorProduct, SU, SU2, TrivialSector, arguments @testset "Trivial fusion trees" begin q = TrivialSector() - f = FusionTree{TrivialSector}() + f = SectorFusionTree{TrivialSector}() @test arrows(f) == () @test leaves(f) == () @test root_sector(f) == q @@ -43,7 +43,7 @@ using SymmetrySectors: ×, SectorProduct, SU, SU2, TrivialSector, arguments @test convert(Array, f) ≈ ones((1, 1, 1)) end -@testset "SU(2) FusionTree" begin +@testset "SU(2) SectorFusionTree" begin j2 = SU2(1//2) f = only(build_trees((j2,), (false,))) @@ -91,7 +91,7 @@ end @test branch_sectors(f34) == (SU2(1//2), SU2(1)) end -@testset "SU(3) FusionTree" begin +@testset "SU(3) SectorFusionTree" begin a8 = SU{3}((2, 1)) trees = build_trees((a8, a8), (false, false)) @test length(trees) == 6 @@ -109,7 +109,7 @@ end @test outer_multiplicity_indices(f8b) == (2,) end -@testset "SU(2)×SU(3) FusionTree" begin +@testset "SU(2)×SU(3) SectorFusionTree" begin j2 = SU2(1//2) a8 = SU{3}((2, 1)) s = j2 × a8 From 3ef076d39da9bdeb9985f8c25ed8cd36b1ae58d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 17:06:54 +0100 Subject: [PATCH 23/52] use @view! --- src/fusiontensor/base_interface.jl | 4 +++- test/test_permutedims.jl | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index f98529d..319c5b0 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -1,5 +1,7 @@ # This files defines Base functions for FusionTensor +using BlockSparseArrays: @view! + function Base.:*(x::Number, ft::FusionTensor) return FusionTensor( x * data_matrix(ft), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) @@ -158,6 +160,6 @@ function Base.view(ft::FusionTensor, f1f2::Tuple{<:SectorFusionTree,<:SectorFusi return view(ft, f1f2...) end function Base.view(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) - charge_matrix = view(data_matrix(ft), trees_block_mapping(ft)[f1, f2]) + charge_matrix = @view! data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] return reshape(charge_matrix, charge_block_size(ft, f1, f2)) end diff --git a/test/test_permutedims.jl b/test/test_permutedims.jl index b87a910..bf2e2db 100644 --- a/test/test_permutedims.jl +++ b/test/test_permutedims.jl @@ -146,7 +146,7 @@ end @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) @test permutedims(adjoint(ft), biperm) ≈ naive_permutedims(adjoint(ft), biperm) - ft = FusionTensor(sds22b, (g2, g2b), (g2, g2b)) + ft = FusionTensor(sds22b, (g2, g2b), (g2b, g2)) @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) @test permutedims(adjoint(ft), biperm) ≈ naive_permutedims(adjoint(ft), biperm) end From cdd3a2f345b6b7009361f580f825c2a38c6e974b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 18 Dec 2024 17:18:53 +0100 Subject: [PATCH 24/52] use fusiontensor when data_matrix is provided --- src/fusiontensor/base_interface.jl | 2 +- src/fusiontensor/fusiontensor.jl | 4 +++- test/test_basics.jl | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 319c5b0..6e8389e 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -18,7 +18,7 @@ end function Base.:*(left::FusionTensor, right::FusionTensor) @assert matching_dual(domain_axes(left), codomain_axes(right)) new_data_matrix = data_matrix(left) * data_matrix(right) - return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(right)) + return fusiontensor(new_data_matrix, codomain_axes(left), domain_axes(right)) end Base.:+(ft::FusionTensor) = ft diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 444d70a..5339c33 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -118,11 +118,13 @@ function promote_sector_type(legs) return sector_type(fusion_product(trivial.(legs)...)) end -function FusionTensor( +# initialize with already computed data_matrix +function fusiontensor( mat::AbstractBlockSparseMatrix, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, ) + # init with empty data_matrix to construct trees_block_mapping ft = FusionTensor(eltype(mat), codomain_legs, domain_legs) for b in eachblockstoredindex(mat) @assert b in eachblockstoredindex(data_matrix(ft)) diff --git a/test/test_basics.jl b/test/test_basics.jl index 0ebd584..66cd0f1 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -4,9 +4,10 @@ using Test: @test, @test_throws, @testset using BlockSparseArrays: BlockSparseArray using FusionTensors: FusionTensor, - domain_axes, codomain_axes, data_matrix, + domain_axes, + fusiontensor, matching_axes, matching_dual, matrix_column_axis, @@ -30,7 +31,7 @@ include("shared.jl") @test space_isequal(matrix_column_axis(ft0), g2) m = BlockSparseArray{Float64}(g1, g2) - ft1 = FusionTensor(m, (g1,), (g2,)) + ft1 = fusiontensor(m, (g1,), (g2,)) # getters @test data_matrix(ft1) == m @@ -100,7 +101,7 @@ end gr = fusion_product(g1, g2) gc = dual(fusion_product(dual(g3), dual(g4))) m2 = BlockSparseArray{Float64}(gr, gc) - ft = FusionTensor(m2, (g1, g2), (g3, g4)) + ft = fusiontensor(m2, (g1, g2), (g3, g4)) @test data_matrix(ft) == m2 @test matching_axes(codomain_axes(ft), (g1, g2)) From 4f746b80eca03b497a8608e8cf5b65881ab1c98a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 19 Dec 2024 14:05:30 +0100 Subject: [PATCH 25/52] move tests --- Project.toml | 2 -- test/{shared.jl => basics/setup.jl} | 0 test/{ => basics}/test_array_cast.jl | 2 +- test/basics/test_basics.jl | 2 +- test/{ => basics}/test_fusion_trees.jl | 0 test/{ => basics}/test_linear_algebra.jl | 2 +- test/{ => basics}/test_permutedims.jl | 2 +- 7 files changed, 4 insertions(+), 6 deletions(-) rename test/{shared.jl => basics/setup.jl} (100%) rename test/{ => basics}/test_array_cast.jl (99%) rename test/{ => basics}/test_fusion_trees.jl (100%) rename test/{ => basics}/test_linear_algebra.jl (97%) rename test/{ => basics}/test_permutedims.jl (99%) diff --git a/Project.toml b/Project.toml index e799e01..1984f4d 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,6 @@ TypeParameterAccessors = "7e5a90cf-f82e-492e-a09b-e3e26432c138" WignerSymbols = "9f57e263-0b3d-5e2e-b1be-24f2bb48858b" [compat] -Aqua = "0.8.9" BlockArrays = "1.2.0" BlockSparseArrays = "0.2.0" BroadcastMapConversion = "0.1.0" @@ -33,7 +32,6 @@ SparseArraysBase = "0.2.0" Strided = "2.2.0" SymmetrySectors = "0.1.1" TensorAlgebra = "0.1.0" -Test = "1.10" TypeParameterAccessors = "0.1.0" WignerSymbols = "2.0.0" julia = "1.10" diff --git a/test/shared.jl b/test/basics/setup.jl similarity index 100% rename from test/shared.jl rename to test/basics/setup.jl diff --git a/test/test_array_cast.jl b/test/basics/test_array_cast.jl similarity index 99% rename from test/test_array_cast.jl rename to test/basics/test_array_cast.jl index 03bfc4d..ea34b3e 100644 --- a/test/test_array_cast.jl +++ b/test/basics/test_array_cast.jl @@ -8,7 +8,7 @@ using FusionTensors: FusionTensor, data_matrix using GradedUnitRanges: dual, fusion_product, gradedrange using SymmetrySectors: O2, SectorProduct, SU2, TrivialSector, U1 -include("shared.jl") +include("setup.jl") @testset "Trivial FusionTensor" begin @testset "trivial matrix" begin diff --git a/test/basics/test_basics.jl b/test/basics/test_basics.jl index c6eb1ec..095ee7a 100644 --- a/test/basics/test_basics.jl +++ b/test/basics/test_basics.jl @@ -18,7 +18,7 @@ using GradedUnitRanges: blockmergesort, dual, flip, fusion_product, gradedrange, sector_type, space_isequal using SymmetrySectors: U1, SU2, SectorProduct, Z -include("shared.jl") +include("setup.jl") @testset "Fusion matrix" begin g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) diff --git a/test/test_fusion_trees.jl b/test/basics/test_fusion_trees.jl similarity index 100% rename from test/test_fusion_trees.jl rename to test/basics/test_fusion_trees.jl diff --git a/test/test_linear_algebra.jl b/test/basics/test_linear_algebra.jl similarity index 97% rename from test/test_linear_algebra.jl rename to test/basics/test_linear_algebra.jl index 910a750..6fc33b0 100644 --- a/test/test_linear_algebra.jl +++ b/test/basics/test_linear_algebra.jl @@ -9,7 +9,7 @@ using FusionTensors: FusionTensor using GradedUnitRanges: dual, gradedrange using SymmetrySectors: U1, SU2, TrivialSector -include("shared.jl") +include("setup.jl") @testset "LinearAlgebra interface" begin sds22 = [ diff --git a/test/test_permutedims.jl b/test/basics/test_permutedims.jl similarity index 99% rename from test/test_permutedims.jl rename to test/basics/test_permutedims.jl index bf2e2db..6bf0162 100644 --- a/test/test_permutedims.jl +++ b/test/basics/test_permutedims.jl @@ -14,7 +14,7 @@ using GradedUnitRanges: dual, gradedrange, space_isequal using SymmetrySectors: O2, U1, SectorProduct, SU2 using TensorAlgebra: blockedperm -include("shared.jl") +include("setup.jl") @testset "Abelian permutedims" begin @testset "dummy" begin From 6528b6302cf456d6a2e1177732b270d4d257d557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 19 Dec 2024 14:48:51 +0100 Subject: [PATCH 26/52] check matrix axes in fusiontensor --- src/fusiontensor/fusiontensor.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 5339c33..3529bb2 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -120,14 +120,16 @@ end # initialize with already computed data_matrix function fusiontensor( - mat::AbstractBlockSparseMatrix, + mat::AbstractMatrix, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, ) # init with empty data_matrix to construct trees_block_mapping ft = FusionTensor(eltype(mat), codomain_legs, domain_legs) + @assert space_isequal(matrix_row_axis(ft), axes(mat, 1)) + @assert space_isequal(matrix_column_axis(ft), axes(mat, 2)) for b in eachblockstoredindex(mat) - @assert b in eachblockstoredindex(data_matrix(ft)) + @assert b in eachblockstoredindex(data_matrix(ft)) # check matrix block is allowed data_matrix(ft)[b] = mat[b] end return ft @@ -170,7 +172,7 @@ function initialize_data_matrix( return mat end -function initialize_allowed_sectors!(mat::AbstractBlockMatrix) +function initialize_allowed_sectors!(mat::AbstractMatrix) row_sectors = blocklabels(axes(mat, 1)) col_sectors = blocklabels(dual(axes(mat, 2))) row_block_indices = findall(in(col_sectors), row_sectors) From cc9c7dcc64040f7cc31c348db1099633da8be763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 19 Dec 2024 15:00:00 +0100 Subject: [PATCH 27/52] remove unused variable --- src/fusiontensor/fusedaxes.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl index 0b4623c..f3fa152 100644 --- a/src/fusiontensor/fusedaxes.jl +++ b/src/fusiontensor/fusedaxes.jl @@ -54,7 +54,6 @@ end function fusion_trees_external_multiplicities( outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} ) - N = length(outer_legs) tree_arrows = isdual.(outer_legs) return mapreduce(vcat, CartesianIndices(blocklength.(outer_legs))) do it block_sectors = map((g, i) -> blocklabels(g)[i], outer_legs, Tuple(it)) From 067403b188b41a66315e340c0ead8a3118d3b993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 19 Dec 2024 18:44:11 +0100 Subject: [PATCH 28/52] fix tests --- test/Project.toml | 6 ++++++ test/{ => basics}/test_contraction.jl | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) rename test/{ => basics}/test_contraction.jl (99%) diff --git a/test/Project.toml b/test/Project.toml index e9c291e..3472834 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,7 +1,13 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" +BlockSparseArrays = "2c9a651f-6452-4ace-a6ac-809f4280fbb4" +GradedUnitRanges = "e2de450a-8a67-46c7-b59c-01d5a3d041c5" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +SymmetrySectors = "f8a8ad64-adbc-4fce-92f7-ffe2bb36a86e" +TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] diff --git a/test/test_contraction.jl b/test/basics/test_contraction.jl similarity index 99% rename from test/test_contraction.jl rename to test/basics/test_contraction.jl index 213f560..3342a8d 100644 --- a/test/test_contraction.jl +++ b/test/basics/test_contraction.jl @@ -8,7 +8,7 @@ using GradedUnitRanges: dual, gradedrange using SymmetrySectors: U1 using TensorAlgebra: contract -include("shared.jl") +include("setup.jl") @testset "contraction" begin g1 = gradedrange([U1(0) => 1, U1(1) => 2, U1(2) => 3]) From ee06e3fe97f3ee5b7da9bd885a1cf6397de8b68a Mon Sep 17 00:00:00 2001 From: Matt Fishman Date: Thu, 19 Dec 2024 13:14:24 -0500 Subject: [PATCH 29/52] Loosen LinearAlgebra compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1984f4d..9b5720e 100644 --- a/Project.toml +++ b/Project.toml @@ -27,7 +27,7 @@ GradedUnitRanges = "0.1.1" HalfIntegers = "1.6.0" LRUCache = "1.6.1" LabelledNumbers = "0.1.0" -LinearAlgebra = "1.11.0" +LinearAlgebra = "1.10.0" SparseArraysBase = "0.2.0" Strided = "2.2.0" SymmetrySectors = "0.1.1" From 837e15145f8aee6e714f716a7f06cb595464f6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Thu, 19 Dec 2024 20:34:37 +0100 Subject: [PATCH 30/52] fix names in array_cast --- src/fusiontensor/array_cast.jl | 32 ++++++++++++++++++++---------- src/fusiontensor/base_interface.jl | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index f48966c..3af23a6 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -15,30 +15,34 @@ function FusionTensor( codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, ) - return cast_from_array(array, codomain_legs, domain_legs) + return to_fusiontensor(array, codomain_legs, domain_legs) end #### cast from symmetric to array function BlockSparseArrays.BlockSparseArray(ft::FusionTensor) - return cast_to_array(ft) + return to_array(ft) end # ================================= Low level interface ================================== -function cast_from_array( +function to_fusiontensor( array::AbstractArray, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, ) bounds = block_dimensions.((codomain_legs..., domain_legs...)) blockarray = BlockedArray(array, bounds...) - return cast_from_array(blockarray, codomain_legs, domain_legs) + return to_fusiontensor(blockarray, codomain_legs, domain_legs) end -function cast_from_array( +get_tol(a::AbstractArray) = get_tol(real(eltype(a))) +get_tol(T::Type{<:Integer}) = get_tol(Float64) +get_tol(T::Type{<:Real}) = 10 * eps(T) + +function to_fusiontensor( blockarray::AbstractBlockArray, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}; - tol::Float64=1e-12, + tol::Real=get_tol(blockarray), ) # input validation if length(codomain_legs) + length(domain_legs) != ndims(blockarray) # compile time @@ -48,20 +52,26 @@ function cast_from_array( throw(DomainError("legs dimensions are incompatible with array")) end - ft = unsafe_cast_from_array(blockarray, codomain_legs, domain_legs) + ft = to_fusiontensor_no_checknorm(blockarray, codomain_legs, domain_legs) # if blockarray is not G-invariant, norm(ft) < norm(blockarray) - if abs(norm(ft) - norm(blockarray)) > tol + checknorm(ft, blockarray, tol) + return ft +end + +function checknorm(ft::FusionTensor, a::AbstractArray, tol::Real) + n0 = norm(a) + if abs(norm(ft) - n0) > tol * n0 throw( InexactError( - :FusionTensor, typeof(blockarray), typeof(codomain_legs), typeof(domain_legs) + :FusionTensor, typeof(a), typeof(codomain_axes(ft)), typeof(domain_axes(ft)) ), ) end return ft end -function unsafe_cast_from_array( +function to_fusiontensor_no_checknorm( blockarray::AbstractBlockArray, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, @@ -74,7 +84,7 @@ function unsafe_cast_from_array( return ft end -function cast_to_array(ft::FusionTensor) +function to_array(ft::FusionTensor) bounds = block_dimensions.((codomain_axes(ft)..., domain_axes(ft)...)) bsa = BlockSparseArray{eltype(ft)}(blockedrange.(bounds)) for (f1, f2) in keys(trees_block_mapping(ft)) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 6e8389e..827b7f2 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -53,7 +53,7 @@ function Base.:/(ft::FusionTensor, x::Number) ) end -Base.Array(ft::FusionTensor) = Array(cast_to_array(ft)) +Base.Array(ft::FusionTensor) = Array(to_array(ft)) # adjoint is costless: dual axes, swap codomain and domain, take data_matrix adjoint. # data_matrix coeff are not modified (beyond complex conjugation) From 423b2751106325c3ddd7375fefaeb24cbc660018 Mon Sep 17 00:00:00 2001 From: mtfishman Date: Sun, 22 Dec 2024 17:41:20 -0500 Subject: [PATCH 31/52] Bump TypeParameterAccessors to v0.2 --- Project.toml | 2 +- test/Project.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9b5720e..be8012f 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,6 @@ SparseArraysBase = "0.2.0" Strided = "2.2.0" SymmetrySectors = "0.1.1" TensorAlgebra = "0.1.0" -TypeParameterAccessors = "0.1.0" +TypeParameterAccessors = "0.2.0" WignerSymbols = "2.0.0" julia = "1.10" diff --git a/test/Project.toml b/test/Project.toml index 3472834..110a927 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,6 +2,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" BlockSparseArrays = "2c9a651f-6452-4ace-a6ac-809f4280fbb4" +FusionTensors = "e16ca583-1f51-4df0-8e12-57d32947d33e" GradedUnitRanges = "e2de450a-8a67-46c7-b59c-01d5a3d041c5" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" From 7decaa48db11a618cea89f9ee74d19b9146515f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 20 Dec 2024 17:13:21 +0100 Subject: [PATCH 32/52] remove getindex(::Tuple) --- src/fusiontensor/base_interface.jl | 11 ----------- src/permutedims/permutedims.jl | 4 ++-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 827b7f2..7dc4fca 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -102,19 +102,11 @@ end # eachindex is automatically defined for AbstractArray. We do not want it. Base.eachindex(::FusionTensor) = error("eachindex not defined for FusionTensor") -function Base.getindex(ft::FusionTensor, f1f2::Tuple{<:SectorFusionTree,<:SectorFusionTree}) - return ft[f1f2...] -end function Base.getindex(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) charge_matrix = data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] return reshape(charge_matrix, charge_block_size(ft, f1, f2)) end -function Base.setindex!( - ft::FusionTensor, a::AbstractArray, f1f2::Tuple{<:SectorFusionTree,<:SectorFusionTree} -) - return setindex!(ft, a, f1f2...) -end function Base.setindex!( ft::FusionTensor, a::AbstractArray, f1::SectorFusionTree, f2::SectorFusionTree ) @@ -156,9 +148,6 @@ end Base.size(ft::FusionTensor) = quantum_dimension.(axes(ft)) -function Base.view(ft::FusionTensor, f1f2::Tuple{<:SectorFusionTree,<:SectorFusionTree}) - return view(ft, f1f2...) -end function Base.view(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) charge_matrix = @view! data_matrix(ft)[trees_block_mapping(ft)[f1, f2]] return reshape(charge_matrix, charge_block_size(ft, f1, f2)) diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl index 4052467..2bc7031 100644 --- a/src/permutedims/permutedims.jl +++ b/src/permutedims/permutedims.jl @@ -51,8 +51,8 @@ function fusiontensor_permutedims!( unitary = compute_unitary(new_ft, old_ft, flatperm) for p in unitary old_trees, new_trees = first(p) - new_block = view(new_ft, new_trees) - old_block = view(old_ft, old_trees) + new_block = view(new_ft, new_trees...) + old_block = view(old_ft, old_trees...) @strided new_block .+= last(p) .* permutedims(old_block, flatperm) end end From 3eb6a8ec18598fe745f0cd116bd0f0829ba2429d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 20 Dec 2024 17:16:54 +0100 Subject: [PATCH 33/52] remove ndims --- src/fusiontensor/base_interface.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 7dc4fca..daf53ba 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -113,8 +113,6 @@ function Base.setindex!( return view(ft, f1, f2) .= a end -Base.ndims(::FusionTensor{T,N}) where {T,N} = N - Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args...) function Base.similar(ft::FusionTensor) From 62e09efcbc57a54617bac2e9fca8b1fb69be0926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 20 Dec 2024 17:19:12 +0100 Subject: [PATCH 34/52] simplify similar --- src/fusiontensor/base_interface.jl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index daf53ba..2c94736 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -115,12 +115,7 @@ end Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args...) -function Base.similar(ft::FusionTensor) - mat = similar(data_matrix(ft)) - initialize_allowed_sectors!(mat) - return FusionTensor(mat, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft)) -end - +Base.similar(ft::FusionTensor) = similar(ft, eltype(ft)) function Base.similar(ft::FusionTensor, ::Type{T}) where {T} # fusion trees have Float64 eltype: need compatible type @assert promote_type(T, Float64) === T @@ -138,8 +133,7 @@ Base.show(io::IO, ft::FusionTensor) = print(io, "$(ndims(ft))-dim FusionTensor") function Base.show(io::IO, ::MIME"text/plain", ft::FusionTensor) println(io, "$(ndims(ft))-dim FusionTensor with axes:") for ax in axes(ft) - display(ax) - println(io) + println(io, ax) end return nothing end From 4057fe8d88826169f7e450e74625746bbbeb11ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Fri, 20 Dec 2024 17:44:43 +0100 Subject: [PATCH 35/52] rewrite linear_algebra --- src/fusiontensor/fusiontensor.jl | 12 ++++----- src/fusiontensor/linear_algebra_interface.jl | 28 +++++++++----------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 3529bb2..bfa84ae 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -137,7 +137,7 @@ end # empty matrix function FusionTensor( - data_type::Type, + elt::Type, codomain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, ) @@ -147,24 +147,24 @@ function FusionTensor( domain_legs = legs[(length(codomain_legs_raw) + 1):end] codomain_fused_axes = FusedAxes{S}(codomain_legs) domain_fused_axes = FusedAxes{S}(dual.(domain_legs)) - mat = initialize_data_matrix(data_type, codomain_fused_axes, domain_fused_axes) + mat = initialize_data_matrix(elt, codomain_fused_axes, domain_fused_axes) tree_to_block_mapping = intersect_sectors(codomain_fused_axes, domain_fused_axes) return FusionTensor(mat, codomain_legs, domain_legs, tree_to_block_mapping) end -function FusionTensor(data_type::Type, ::Tuple{}, ::Tuple{}) +function FusionTensor(elt::Type, ::Tuple{}, ::Tuple{}) codomain_fused_axes = FusedAxes{TrivialSector}(()) domain_fused_axes = FusedAxes{TrivialSector}(()) - mat = initialize_data_matrix(data_type, codomain_fused_axes, domain_fused_axes) + mat = initialize_data_matrix(elt, codomain_fused_axes, domain_fused_axes) tree_to_block_mapping = intersect_sectors(codomain_fused_axes, domain_fused_axes) return FusionTensor(mat, (), (), tree_to_block_mapping) end function initialize_data_matrix( - data_type::Type{<:Number}, codomain_fused_axes::FusedAxes, domain_fused_axes::FusedAxes + elt::Type{<:Number}, codomain_fused_axes::FusedAxes, domain_fused_axes::FusedAxes ) # fusion trees have Float64 eltype: need compatible type - promoted = promote_type(data_type, Float64) + promoted = promote_type(elt, Float64) mat_row_axis = fused_axis(codomain_fused_axes) mat_col_axis = dual(fused_axis(domain_fused_axes)) mat = BlockSparseArray{promoted}(mat_row_axis, mat_col_axis) diff --git a/src/fusiontensor/linear_algebra_interface.jl b/src/fusiontensor/linear_algebra_interface.jl index fa70e0c..6b48314 100644 --- a/src/fusiontensor/linear_algebra_interface.jl +++ b/src/fusiontensor/linear_algebra_interface.jl @@ -44,11 +44,10 @@ end function LinearAlgebra.norm(ft::FusionTensor) m = data_matrix(ft) row_sectors = blocklabels(matrix_row_axis(ft)) - n2 = mapreduce( - b -> quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * norm(m[b])^2, - +, - eachblockstoredindex(m); - init=0.0, + n2 = sum( + quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * norm(m[b])^2 for + b in eachblockstoredindex(m); + init=zero(real(eltype(ft))), ) return sqrt(n2) end @@ -56,25 +55,24 @@ end function LinearAlgebra.tr(ft::FusionTensor) m = data_matrix(ft) row_sectors = blocklabels(matrix_row_axis(ft)) - return mapreduce( - b -> quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * tr(m[b]), - +, - eachblockstoredindex(m); - init=eltype(ft)(0), + return sum( + quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * tr(m[b]) for + b in eachblockstoredindex(m); + init=zero(eltype(ft)), ) end function LinearAlgebra.qr(ft::FusionTensor) qmat, rmat = block_qr(data_matrix(ft)) - qtens = FusionTensor(qmat, codomain_axes(ft), (axes(qmat)[1],)) - rtens = FusionTensor(rmat, (axes(rmat)[0],), domain_axes(ft)) + qtens = FusionTensor(qmat, codomain_axes(ft), (axes(qmat, 2),)) + rtens = FusionTensor(rmat, (axes(rmat, 1),), domain_axes(ft)) return qtens, rtens end function LinearAlgebra.svd(ft::FusionTensor) umat, s, vmat = block_svd(data_matrix(ft)) - utens = FusionTensor(umat, codomain_axes(ft), (axes(umat)[1],)) - stens = FusionTensor(s, (axes(umat)[1],), (axes(vmat)[0],)) - vtens = FusionTensor(vmat, (axes(vmat)[0],), domain_axes(ft)) + utens = FusionTensor(umat, codomain_axes(ft), (axes(umat, 2),)) + stens = FusionTensor(s, (axes(umat, 1),), (axes(vmat, 2),)) + vtens = FusionTensor(vmat, (axes(vmat, 1),), domain_axes(ft)) return utens, stens, vtens end From f663d05dc75794fb4c08d1f42ced955f508967d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 11:51:45 -0500 Subject: [PATCH 36/52] use do block --- src/fusiontensor/linear_algebra_interface.jl | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/fusiontensor/linear_algebra_interface.jl b/src/fusiontensor/linear_algebra_interface.jl index 6b48314..771ad89 100644 --- a/src/fusiontensor/linear_algebra_interface.jl +++ b/src/fusiontensor/linear_algebra_interface.jl @@ -44,22 +44,18 @@ end function LinearAlgebra.norm(ft::FusionTensor) m = data_matrix(ft) row_sectors = blocklabels(matrix_row_axis(ft)) - n2 = sum( - quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * norm(m[b])^2 for - b in eachblockstoredindex(m); - init=zero(real(eltype(ft))), - ) + n2 = sum(eachblockstoredindex(m); init=zero(real(eltype(ft)))) do b + return quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * norm(m[b])^2 + end return sqrt(n2) end function LinearAlgebra.tr(ft::FusionTensor) m = data_matrix(ft) row_sectors = blocklabels(matrix_row_axis(ft)) - return sum( - quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * tr(m[b]) for - b in eachblockstoredindex(m); - init=zero(eltype(ft)), - ) + return sum(eachblockstoredindex(m); init=zero(eltype(ft))) do b + return quantum_dimension(row_sectors[Int(first(Tuple(b)))]) * tr(m[b]) + end end function LinearAlgebra.qr(ft::FusionTensor) From 03080e31ed32d707f3690546921cb0526ab5f1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 12:21:50 -0500 Subject: [PATCH 37/52] use Accessors --- Project.toml | 2 ++ src/fusiontensor/base_interface.jl | 44 +++++++----------------------- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/Project.toml b/Project.toml index be8012f..ae16ecc 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["ITensor developers and contributors"] version = "0.2.0" [deps] +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" BlockSparseArrays = "2c9a651f-6452-4ace-a6ac-809f4280fbb4" BroadcastMapConversion = "4a4adec5-520f-4750-bb37-d5e66b4ddeb2" @@ -20,6 +21,7 @@ TypeParameterAccessors = "7e5a90cf-f82e-492e-a09b-e3e26432c138" WignerSymbols = "9f57e263-0b3d-5e2e-b1be-24f2bb48858b" [compat] +Accessors = "0.1.39" BlockArrays = "1.2.0" BlockSparseArrays = "0.2.0" BroadcastMapConversion = "0.1.0" diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 2c94736..cd6358e 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -1,18 +1,13 @@ # This files defines Base functions for FusionTensor +using Accessors: @set + using BlockSparseArrays: @view! -function Base.:*(x::Number, ft::FusionTensor) - return FusionTensor( - x * data_matrix(ft), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) - ) -end +set_data_matrix(ft::FusionTensor, data_matrix) = @set ft.data_matrix = data_matrix -function Base.:*(ft::FusionTensor, x::Number) - return FusionTensor( - x * data_matrix(ft), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) - ) -end +Base.:*(x::Number, ft::FusionTensor) = set_data_matrix(ft, x * data_matrix(ft)) +Base.:*(ft::FusionTensor, x::Number) = set_data_matrix(ft, x * data_matrix(ft)) # tensor contraction is a block data_matrix product. function Base.:*(left::FusionTensor, right::FusionTensor) @@ -26,32 +21,17 @@ Base.:+(ft::FusionTensor) = ft # tensor addition is a block data_matrix add. function Base.:+(left::FusionTensor, right::FusionTensor) @assert matching_axes(axes(left), axes(right)) - new_data_matrix = data_matrix(left) + data_matrix(right) - return FusionTensor( - new_data_matrix, codomain_axes(left), domain_axes(left), trees_block_mapping(left) - ) + return set_data_matrix(left, data_matrix(left) + data_matrix(right)) end -function Base.:-(ft::FusionTensor) - new_data_matrix = -data_matrix(ft) - return FusionTensor( - new_data_matrix, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) - ) -end +Base.:-(ft::FusionTensor) = set_data_matrix(ft, -data_matrix(ft)) function Base.:-(left::FusionTensor, right::FusionTensor) @assert matching_axes(axes(left), axes(right)) - new_data_matrix = data_matrix(left) - data_matrix(right) - return FusionTensor( - new_data_matrix, codomain_axes(left), domain_axes(left), trees_block_mapping(left) - ) + return set_data_matrix(left, data_matrix(left) - data_matrix(right)) end -function Base.:/(ft::FusionTensor, x::Number) - return FusionTensor( - data_matrix(ft) / x, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) - ) -end +Base.:/(ft::FusionTensor, x::Number) = set_data_matrix(ft, data_matrix(ft) / x) Base.Array(ft::FusionTensor) = Array(to_array(ft)) @@ -75,11 +55,7 @@ Base.axes(ft::FusionTensor) = (codomain_axes(ft)..., domain_axes(ft)...) # conj is defined as coefficient wise complex conjugation, without axis dual Base.conj(ft::FusionTensor{<:Real}) = ft # same object for real element type -function Base.conj(ft::FusionTensor) - return FusionTensor( - conj(data_matrix(ft)), codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft) - ) -end +Base.conj(ft::FusionTensor) = set_data_matrix(ft, conj(data_matrix(ft))) function Base.copy(ft::FusionTensor) return FusionTensor( From 9247074421921eec9c66984c9d71013550db4c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 12:25:28 -0500 Subject: [PATCH 38/52] use AbstractArray similar(::FusionTensor) --- src/fusiontensor/base_interface.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index cd6358e..5013534 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -91,7 +91,6 @@ end Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args...) -Base.similar(ft::FusionTensor) = similar(ft, eltype(ft)) function Base.similar(ft::FusionTensor, ::Type{T}) where {T} # fusion trees have Float64 eltype: need compatible type @assert promote_type(T, Float64) === T From 22c5e51326dd8d9bd67a19626052b6d7e190bbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 12:58:55 -0500 Subject: [PATCH 39/52] replace matching_axes with checkaxes --- src/fusiontensor/base_interface.jl | 6 +-- src/fusiontensor/fusiontensor.jl | 11 ++++-- src/fusiontensor/linear_algebra_interface.jl | 12 ++---- test/basics/setup.jl | 8 ++-- test/basics/test_basics.jl | 39 +++++++++++--------- test/basics/test_permutedims.jl | 6 +-- 6 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 5013534..7df5a4d 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -11,7 +11,7 @@ Base.:*(ft::FusionTensor, x::Number) = set_data_matrix(ft, x * data_matrix(ft)) # tensor contraction is a block data_matrix product. function Base.:*(left::FusionTensor, right::FusionTensor) - @assert matching_dual(domain_axes(left), codomain_axes(right)) + checkaxes_dual(domain_axes(left), codomain_axes(right)) new_data_matrix = data_matrix(left) * data_matrix(right) return fusiontensor(new_data_matrix, codomain_axes(left), domain_axes(right)) end @@ -20,14 +20,14 @@ Base.:+(ft::FusionTensor) = ft # tensor addition is a block data_matrix add. function Base.:+(left::FusionTensor, right::FusionTensor) - @assert matching_axes(axes(left), axes(right)) + checkaxes(axes(left), axes(right)) return set_data_matrix(left, data_matrix(left) + data_matrix(right)) end Base.:-(ft::FusionTensor) = set_data_matrix(ft, -data_matrix(ft)) function Base.:-(left::FusionTensor, right::FusionTensor) - @assert matching_axes(axes(left), axes(right)) + checkaxes(axes(left), axes(right)) return set_data_matrix(left, data_matrix(left) - data_matrix(right)) end diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index bfa84ae..8132820 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -182,8 +182,11 @@ function initialize_allowed_sectors!(mat::AbstractMatrix) end end -matching_dual(axes1::Tuple, axes2::Tuple) = matching_axes(axes1, dual.(axes2)) -matching_axes(axes1::Tuple, axes2::Tuple) = false -function matching_axes(axes1::T, axes2::T) where {T<:Tuple} - return all(space_isequal.(axes1, axes2)) +checkaxes_dual(axes1, axes2) = checkaxes(axes1, dual.(axes2)) +function checkaxes(ax1, ax2) + return checkaxes(Bool, ax1, ax2) || + throw(DimensionMismatch(lazy"$ax1 does not match $ax2")) +end +function checkaxes(::Type{Bool}, axes1, axes2) + return length(axes1) == length(axes2) && all(space_isequal.(axes1, axes2)) end diff --git a/src/fusiontensor/linear_algebra_interface.jl b/src/fusiontensor/linear_algebra_interface.jl index 771ad89..7edee18 100644 --- a/src/fusiontensor/linear_algebra_interface.jl +++ b/src/fusiontensor/linear_algebra_interface.jl @@ -28,15 +28,9 @@ function LinearAlgebra.mul!( end # input validation - if !matching_dual(domain_axes(A), codomain_axes(B)) - throw(codomainError("Incompatible tensor axes for A and B")) - end - if !matching_axes(codomain_axes(C), codomain_axes(A)) - throw(codomainError("Incompatible tensor axes for C and A")) - end - if !matching_axes(domain_axes(C), domain_axes(B)) - throw(codomainError("Incompatible tensor axes for C and B")) - end + checkaxes_dual(domain_axes(A), codomain_axes(B)) + checkaxes(codomain_axes(C), codomain_axes(A)) + checkaxes(domain_axes(C), domain_axes(B)) mul!(data_matrix(C), data_matrix(A), data_matrix(B), α, β) return C end diff --git a/test/basics/setup.jl b/test/basics/setup.jl index 6592ec0..70a0025 100644 --- a/test/basics/setup.jl +++ b/test/basics/setup.jl @@ -6,8 +6,8 @@ using FusionTensors: codomain_axes, data_matrix, domain_axes, - matching_axes, - matching_dual, + checkaxes, + checkaxes_dual, matrix_column_axis, matrix_row_axis, ndims_codomain, @@ -37,8 +37,8 @@ function check_sanity(ft::FusionTensor) @assert nda + nca == ndims(ft) "invalid ndims" @assert length(axes(ft)) == ndims(ft) "ndims does not match axes" - @assert matching_axes(axes(ft)[begin:nda], codomain_axes(ft)) "axes do not match codomain_axes" - @assert matching_axes(axes(ft)[(nda + 1):end], domain_axes(ft)) "axes do not match domain_axes" + checkaxes(axes(ft)[begin:nda], codomain_axes(ft)) + checkaxes(axes(ft)[(nda + 1):end], domain_axes(ft)) m = data_matrix(ft) @assert ndims(m) == 2 "invalid data_matrix ndims" diff --git a/test/basics/test_basics.jl b/test/basics/test_basics.jl index 095ee7a..f644f9c 100644 --- a/test/basics/test_basics.jl +++ b/test/basics/test_basics.jl @@ -7,8 +7,8 @@ using FusionTensors: data_matrix, domain_axes, fusiontensor, - matching_axes, - matching_dual, + checkaxes, + checkaxes_dual, matrix_column_axis, matrix_row_axis, matrix_size, @@ -34,11 +34,11 @@ include("setup.jl") # getters @test data_matrix(ft1) == m - @test matching_axes(codomain_axes(ft1), (g1,)) - @test matching_axes(domain_axes(ft1), (g2,)) + @test checkaxes(codomain_axes(ft1), (g1,)) + @test checkaxes(domain_axes(ft1), (g2,)) # misc - @test matching_axes(axes(ft1), (g1, g2)) + @test checkaxes(axes(ft1), (g1, g2)) @test ndims_codomain(ft1) == 1 @test ndims_domain(ft1) == 1 @test matrix_size(ft1) == (6, 5) @@ -60,36 +60,36 @@ include("setup.jl") @test ft2 !== ft1 @test data_matrix(ft2) == data_matrix(ft1) @test data_matrix(ft2) !== data_matrix(ft1) - @test matching_axes(codomain_axes(ft2), codomain_axes(ft1)) - @test matching_axes(domain_axes(ft2), domain_axes(ft1)) + @test checkaxes(codomain_axes(ft2), codomain_axes(ft1)) + @test checkaxes(domain_axes(ft2), domain_axes(ft1)) ft2 = deepcopy(ft1) @test ft2 !== ft1 @test data_matrix(ft2) == data_matrix(ft1) @test data_matrix(ft2) !== data_matrix(ft1) - @test matching_axes(codomain_axes(ft2), codomain_axes(ft1)) - @test matching_axes(domain_axes(ft2), domain_axes(ft1)) + @test checkaxes(codomain_axes(ft2), codomain_axes(ft1)) + @test checkaxes(domain_axes(ft2), domain_axes(ft1)) # similar ft2 = similar(ft1) @test isnothing(check_sanity(ft2)) @test eltype(ft2) == Float64 - @test matching_axes(codomain_axes(ft2), codomain_axes(ft1)) - @test matching_axes(domain_axes(ft2), domain_axes(ft1)) + @test checkaxes(codomain_axes(ft2), codomain_axes(ft1)) + @test checkaxes(domain_axes(ft2), domain_axes(ft1)) ft3 = similar(ft1, ComplexF64) @test isnothing(check_sanity(ft3)) @test eltype(ft3) == ComplexF64 - @test matching_axes(codomain_axes(ft3), codomain_axes(ft1)) - @test matching_axes(domain_axes(ft3), domain_axes(ft1)) + @test checkaxes(codomain_axes(ft3), codomain_axes(ft1)) + @test checkaxes(domain_axes(ft3), domain_axes(ft1)) @test_throws AssertionError similar(ft1, Int) ft5 = similar(ft1, ComplexF32, ((g1, g1), (g2,))) @test isnothing(check_sanity(ft5)) @test eltype(ft5) == ComplexF64 - @test matching_axes(codomain_axes(ft5), (g1, g1)) - @test matching_axes(domain_axes(ft5), (g2,)) + @test checkaxes(codomain_axes(ft5), (g1, g1)) + @test checkaxes(domain_axes(ft5), (g2,)) end @testset "More than 2 axes" begin @@ -103,8 +103,8 @@ end ft = fusiontensor(m2, (g1, g2), (g3, g4)) @test data_matrix(ft) == m2 - @test matching_axes(codomain_axes(ft), (g1, g2)) - @test matching_axes(domain_axes(ft), (g3, g4)) + @test checkaxes(codomain_axes(ft), (g1, g2)) + @test checkaxes(domain_axes(ft), (g3, g4)) @test axes(ft) == (g1, g2, g3, g4) @test ndims_codomain(ft) == 2 @@ -232,6 +232,11 @@ end @test space_isequal(dual(g3), codomain_axes(ad)[1]) @test space_isequal(dual(g4), codomain_axes(ad)[2]) @test isnothing(check_sanity(ad)) + + ft7 = FusionTensor(Float64, (g1,), (g2, g3, g4)) + @test_throws DimensionMismatch ft7 + ft3 + @test_throws DimensionMismatch ft7 - ft3 + @test_throws DimensionMismatch ft7 * ft3 end @testset "mising SectorProduct" begin diff --git a/test/basics/test_permutedims.jl b/test/basics/test_permutedims.jl index 6bf0162..12c7695 100644 --- a/test/basics/test_permutedims.jl +++ b/test/basics/test_permutedims.jl @@ -4,7 +4,7 @@ using Test: @test, @testset, @test_broken using FusionTensors: FusionTensor, data_matrix, - matching_axes, + checkaxes, matrix_column_axis, matrix_row_axis, naive_permutedims, @@ -41,14 +41,14 @@ include("setup.jl") ft3 = permutedims(ft1, (4,), (1, 2, 3)) @test ft3 !== ft1 @test ft3 isa FusionTensor{elt,4} - @test matching_axes(axes(ft3), (dual(g4), g1, g2, dual(g3))) + @test checkaxes(axes(ft3), (dual(g4), g1, g2, dual(g3))) @test ndims_domain(ft3) == 3 @test ndims_codomain(ft3) == 1 @test ndims(ft3) == 4 @test isnothing(check_sanity(ft3)) ft4 = permutedims(ft3, (2, 3), (4, 1)) - @test matching_axes(axes(ft1), axes(ft4)) + @test checkaxes(axes(ft1), axes(ft4)) @test space_isequal(matrix_column_axis(ft1), matrix_column_axis(ft4)) @test space_isequal(matrix_row_axis(ft1), matrix_row_axis(ft4)) @test ft4 ≈ ft1 From ab9a76a9fa04062a747ac02b31f92639c8ce72ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 14:11:32 -0500 Subject: [PATCH 40/52] rename to_array --- src/fusion_trees/fusiontree.jl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index 110f735..6d2294d 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -66,7 +66,7 @@ branch_sectors(f::SectorFusionTree) = f.branch_sectors outer_multiplicity_indices(f::SectorFusionTree) = f.outer_multiplicity_indices # Base interface -Base.convert(T::Type{<:Array}, f::SectorFusionTree) = convert(T, to_tensor(f)) +Base.convert(T::Type{<:Array}, f::SectorFusionTree) = convert(T, to_array(f)) Base.isless(f1::SectorFusionTree, f2::SectorFusionTree) = isless(to_tuple(f1), to_tuple(f2)) Base.length(::SectorFusionTree{<:Any,N}) where {N} = N @@ -138,10 +138,9 @@ function SymmetrySectors.arguments(f::SectorFusionTree{<:SectorProduct,1}) arguments_root = arguments(root_sector(f)) arguments_leave = arguments(only(leaves(f))) # use map(keys) to stay agnostic with respect to SectorProduct implementation - return map( - k -> SectorFusionTree((arguments_leave[k],), arrows(f), arguments_root[k], (), ()), - keys(arguments_root), - ) + return map(keys(arguments_root)) do k + return SectorFusionTree((arguments_leave[k],), arrows(f), arguments_root[k], (), ()) + end end # @@ -254,9 +253,9 @@ function build_trees( end # --------------- convert to Array --------------- -to_tensor(::SectorFusionTree{<:Any,0}) = ones(1) +to_array(::SectorFusionTree{<:Any,0}) = ones(1) -function to_tensor(f::SectorFusionTree) +function to_array(f::SectorFusionTree) # init with dummy trivial leg to get arrow correct and deal with size-1 case cgt1 = clebsch_gordan_tensor( trivial(sector_type(f)), first(leaves(f)), first(leaves(f)), false, first(arrows(f)), 1 @@ -265,8 +264,7 @@ function to_tensor(f::SectorFusionTree) return grow_tensor_tree(tree_tensor, f) end -#to_tensor(::SectorFusionTree{<:SectorProduct,0}) = ones(1) -function to_tensor(f::SectorFusionTree{<:SectorProduct}) +function to_array(f::SectorFusionTree{<:SectorProduct}) args = convert.(Array, arguments(f)) return reduce(_tensor_kron, args) end From aaac06ad271f32abd5f8b907509ba5191a60c300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 14:51:06 -0500 Subject: [PATCH 41/52] define atol and rtol for array_cast --- src/fusiontensor/array_cast.jl | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 3af23a6..afd31da 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -34,15 +34,24 @@ function to_fusiontensor( return to_fusiontensor(blockarray, codomain_legs, domain_legs) end -get_tol(a::AbstractArray) = get_tol(real(eltype(a))) -get_tol(T::Type{<:Integer}) = get_tol(Float64) -get_tol(T::Type{<:Real}) = 10 * eps(T) +rtoldefault(a::AbstractArray) = rtoldefault(eltype(a)) +rtoldefault(arrayt::Type{<:AbstractArray}) = rtoldefault(eltype(arrayt)) +rtoldefault(elt::Type{<:Number}) = 10 * eps(real(float(elt))) + +function checknorm(ft::FusionTensor, a::AbstractArray, atol::Real, rtol::Real) + return isapprox(norm(ft), norm(a); atol=atol, rtol=rtol) || throw( + InexactError( + :FusionTensor, typeof(a), typeof(codomain_axes(ft)), typeof(domain_axes(ft)) + ), + ) +end function to_fusiontensor( blockarray::AbstractBlockArray, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}; - tol::Real=get_tol(blockarray), + atol::Real=0, + rtol::Real=rtoldefault(blockarray), ) # input validation if length(codomain_legs) + length(domain_legs) != ndims(blockarray) # compile time @@ -55,19 +64,7 @@ function to_fusiontensor( ft = to_fusiontensor_no_checknorm(blockarray, codomain_legs, domain_legs) # if blockarray is not G-invariant, norm(ft) < norm(blockarray) - checknorm(ft, blockarray, tol) - return ft -end - -function checknorm(ft::FusionTensor, a::AbstractArray, tol::Real) - n0 = norm(a) - if abs(norm(ft) - n0) > tol * n0 - throw( - InexactError( - :FusionTensor, typeof(a), typeof(codomain_axes(ft)), typeof(domain_axes(ft)) - ), - ) - end + checknorm(ft, blockarray, atol, rtol) return ft end From 591bcbc73662e08e2bd16b837332943b6e1011af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 15:22:20 -0500 Subject: [PATCH 42/52] fix sector_type --- src/fusiontensor/fusiontensor.jl | 14 +++++++++++++- test/basics/test_basics.jl | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 8132820..3fa9e8f 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -65,7 +65,19 @@ function charge_block_size(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFus end # GradedUnitRanges interface -GradedUnitRanges.sector_type(ft::FusionTensor) = sector_type(matrix_row_axis(ft)) +function GradedUnitRanges.sector_type( + ::Type{<:FusionTensor{<:Any,<:Any,CoDomainAxes}} +) where {CoDomainAxes} + return sector_type(fieldtype(CoDomainAxes, 1)) +end +function GradedUnitRanges.sector_type( + ::Type{<:FusionTensor{<:Any,<:Any,Tuple{},DomainAxes}} +) where {DomainAxes} + return sector_type(fieldtype(DomainAxes, 1)) +end +function GradedUnitRanges.sector_type(::Type{<:FusionTensor{<:Any,0}}) + return TrivialSector +end # BlockArrays interface function BlockArrays.findblock(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) diff --git a/test/basics/test_basics.jl b/test/basics/test_basics.jl index f644f9c..f4f4b63 100644 --- a/test/basics/test_basics.jl +++ b/test/basics/test_basics.jl @@ -16,7 +16,7 @@ using FusionTensors: ndims_codomain using GradedUnitRanges: blockmergesort, dual, flip, fusion_product, gradedrange, sector_type, space_isequal -using SymmetrySectors: U1, SU2, SectorProduct, Z +using SymmetrySectors: U1, SU2, SectorProduct, TrivialSector, Z include("setup.jl") @@ -129,6 +129,7 @@ end @test size(ft1) == (6,) @test size(data_matrix(ft1)) == (6, 1) @test isnothing(check_sanity(ft1)) + @test sector_type(ft1) === sector_type(g1) # one column axis ft2 = FusionTensor(Float64, (), (g1,)) @@ -138,6 +139,7 @@ end @test size(ft2) == (6,) @test size(data_matrix(ft2)) == (1, 6) @test isnothing(check_sanity(ft2)) + @test sector_type(ft2) === sector_type(g1) # zero axis ft3 = FusionTensor(Float64, (), ()) @@ -147,6 +149,7 @@ end @test size(ft3) == () @test size(data_matrix(ft3)) == (1, 1) @test isnothing(check_sanity(ft3)) + @test sector_type(ft3) === TrivialSector end @testset "Base operations" begin From 192fe4761d944d06f7970d81db9dab6b3642e18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 15:38:11 -0500 Subject: [PATCH 43/52] use sector_type(FusionTree) --- src/fusion_trees/fusiontree.jl | 2 +- src/fusiontensor/fusiontensor.jl | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index 6d2294d..8021dc9 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -71,7 +71,7 @@ Base.isless(f1::SectorFusionTree, f2::SectorFusionTree) = isless(to_tuple(f1), t Base.length(::SectorFusionTree{<:Any,N}) where {N} = N # GradedUnitRanges interface -GradedUnitRanges.sector_type(::SectorFusionTree{S}) where {S} = S +GradedUnitRanges.sector_type(::Type{<:SectorFusionTree{S}}) where {S} = S function build_trees(legs::Vararg{AbstractGradedUnitRange}) tree_arrows = isdual.(legs) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 3fa9e8f..0d532e2 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -66,17 +66,9 @@ end # GradedUnitRanges interface function GradedUnitRanges.sector_type( - ::Type{<:FusionTensor{<:Any,<:Any,CoDomainAxes}} -) where {CoDomainAxes} - return sector_type(fieldtype(CoDomainAxes, 1)) -end -function GradedUnitRanges.sector_type( - ::Type{<:FusionTensor{<:Any,<:Any,Tuple{},DomainAxes}} -) where {DomainAxes} - return sector_type(fieldtype(DomainAxes, 1)) -end -function GradedUnitRanges.sector_type(::Type{<:FusionTensor{<:Any,0}}) - return TrivialSector + ::Type{<:FusionTensor{<:Any,<:Any,<:Any,<:Any,<:Any,<:Dict{<:Tuple{<:Any,F}}}} +) where {F} + return sector_type(F) end # BlockArrays interface From 5f2452d54835f1e80fc80c173d21a0b467f61d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 16:00:36 -0500 Subject: [PATCH 44/52] remove to_tuple --- src/fusion_trees/fusiontree.jl | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index 8021dc9..09825c7 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -67,7 +67,15 @@ outer_multiplicity_indices(f::SectorFusionTree) = f.outer_multiplicity_indices # Base interface Base.convert(T::Type{<:Array}, f::SectorFusionTree) = convert(T, to_array(f)) -Base.isless(f1::SectorFusionTree, f2::SectorFusionTree) = isless(to_tuple(f1), to_tuple(f2)) + +function Base.isless(f1::SectorFusionTree, f2::SectorFusionTree) + return isless(leaves(f1), leaves(f2)) || + isless(arrows(f1), arrows(f2)) || + isless(root_sector(f1), root_sector(f2)) || + isless(branch_sectors(f1), branch_sectors(f2)) || + isless(outer_multiplicity_indices(f1), outer_multiplicity_indices(f2)) +end + Base.length(::SectorFusionTree{<:Any,N}) where {N} = N # GradedUnitRanges interface @@ -146,16 +154,6 @@ end # # ===================================== Internals ======================================== # -# --------------- misc --------------- -function to_tuple(f::SectorFusionTree) - return ( - leaves(f)..., - arrows(f)..., - root_sector(f), - branch_sectors(f)..., - outer_multiplicity_indices(f)..., - ) -end # --------------- SectorProduct helper functions --------------- function outer_multiplicity_kron( From 4e943672f032f30e1725a9aea7cc17b6712c3b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Mon, 6 Jan 2025 17:32:57 -0500 Subject: [PATCH 45/52] define fusiontree_eltype --- src/fusion_trees/fusiontree.jl | 3 +++ src/fusiontensor/base_interface.jl | 4 ++-- src/fusiontensor/fusiontensor.jl | 4 ++-- test/basics/test_fusion_trees.jl | 4 ++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/fusion_trees/fusiontree.jl b/src/fusion_trees/fusiontree.jl index 09825c7..2f48757 100644 --- a/src/fusion_trees/fusiontree.jl +++ b/src/fusion_trees/fusiontree.jl @@ -151,6 +151,9 @@ function SymmetrySectors.arguments(f::SectorFusionTree{<:SectorProduct,1}) end end +# TBD change type depending on AbelianStyle? +fusiontree_eltype(::Type{<:AbstractSector}) = Float64 + # # ===================================== Internals ======================================== # diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 7df5a4d..7dbdafd 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -92,8 +92,8 @@ end Base.permutedims(ft::FusionTensor, args...) = fusiontensor_permutedims(ft, args...) function Base.similar(ft::FusionTensor, ::Type{T}) where {T} - # fusion trees have Float64 eltype: need compatible type - @assert promote_type(T, Float64) === T + # some fusion trees have Float64 eltype: need compatible type + @assert promote_type(T, fusiontree_eltype(sector_type(ft))) === T mat = similar(data_matrix(ft), T) initialize_allowed_sectors!(mat) return FusionTensor(mat, codomain_axes(ft), domain_axes(ft), trees_block_mapping(ft)) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 0d532e2..b5b2986 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -167,10 +167,10 @@ end function initialize_data_matrix( elt::Type{<:Number}, codomain_fused_axes::FusedAxes, domain_fused_axes::FusedAxes ) - # fusion trees have Float64 eltype: need compatible type - promoted = promote_type(elt, Float64) mat_row_axis = fused_axis(codomain_fused_axes) mat_col_axis = dual(fused_axis(domain_fused_axes)) + # non-abelian fusion trees have float eltype: need compatible type + promoted = promote_type(elt, fusiontree_eltype(sector_type(mat_row_axis))) mat = BlockSparseArray{promoted}(mat_row_axis, mat_col_axis) initialize_allowed_sectors!(mat) return mat diff --git a/test/basics/test_fusion_trees.jl b/test/basics/test_fusion_trees.jl index 9043e0e..7b343de 100644 --- a/test/basics/test_fusion_trees.jl +++ b/test/basics/test_fusion_trees.jl @@ -9,6 +9,7 @@ using FusionTensors: arrows, branch_sectors, build_trees, + fusiontree_eltype, leaves, outer_multiplicity_indices, root_sector @@ -41,6 +42,8 @@ using SymmetrySectors: ×, SectorProduct, SU, SU2, TrivialSector, arguments @test branch_sectors(f) == (q,) @test outer_multiplicity_indices(f) == (1,) @test convert(Array, f) ≈ ones((1, 1, 1)) + + @test fusiontree_eltype(sector_type(f)) === eltype(convert(Array, f)) end @testset "SU(2) SectorFusionTree" begin @@ -54,6 +57,7 @@ end @test outer_multiplicity_indices(f) == () @test sector_type(f) == typeof(j2) @test convert(Array, f) ≈ I(2) + @test fusiontree_eltype(sector_type(f)) === eltype(convert(Array, f)) f = only(build_trees((j2,), (true,))) @test arrows(f) == (true,) From 20a3119d22c988ebed6c4c570d4ded01d868bbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Tue, 7 Jan 2025 14:13:51 -0500 Subject: [PATCH 46/52] avoid code dupplication --- src/fusiontensor/base_interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 7dbdafd..dec7eb1 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -7,7 +7,7 @@ using BlockSparseArrays: @view! set_data_matrix(ft::FusionTensor, data_matrix) = @set ft.data_matrix = data_matrix Base.:*(x::Number, ft::FusionTensor) = set_data_matrix(ft, x * data_matrix(ft)) -Base.:*(ft::FusionTensor, x::Number) = set_data_matrix(ft, x * data_matrix(ft)) +Base.:*(ft::FusionTensor, x::Number) = x * ft # tensor contraction is a block data_matrix product. function Base.:*(left::FusionTensor, right::FusionTensor) From 260760fe6a424af417def2ae0849e6afe0495e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Tue, 7 Jan 2025 15:01:59 -0500 Subject: [PATCH 47/52] remove FusedAxes --- src/FusionTensors.jl | 1 - src/fusiontensor/fusedaxes.jl | 98 --------------------------- src/fusiontensor/fusiontensor.jl | 109 ++++++++++++++++++++++++------- 3 files changed, 87 insertions(+), 121 deletions(-) delete mode 100644 src/fusiontensor/fusedaxes.jl diff --git a/src/FusionTensors.jl b/src/FusionTensors.jl index d17d579..9be8837 100644 --- a/src/FusionTensors.jl +++ b/src/FusionTensors.jl @@ -2,7 +2,6 @@ module FusionTensors include("fusion_trees/fusiontree.jl") include("fusion_trees/clebsch_gordan_tensors.jl") -include("fusiontensor/fusedaxes.jl") include("fusiontensor/fusiontensor.jl") include("fusiontensor/base_interface.jl") include("fusiontensor/array_cast.jl") diff --git a/src/fusiontensor/fusedaxes.jl b/src/fusiontensor/fusedaxes.jl deleted file mode 100644 index f3fa152..0000000 --- a/src/fusiontensor/fusedaxes.jl +++ /dev/null @@ -1,98 +0,0 @@ -# This file defines helper functions to access FusionTensor internal structures - -using BlockArrays: Block, BlockIndexRange, blocklength, blocklengths - -using BlockSparseArrays: to_block_indices -using GradedUnitRanges: - AbstractGradedUnitRange, GradedUnitRanges, blocklabels, blockmergesort, gradedrange -using SymmetrySectors: AbstractSector, trivial - -struct FusedAxes{A,B,C} - outer_axes::A - fused_axis::B - trees_to_ranges_mapping::C - - function FusedAxes( - outer_legs::NTuple{N,AbstractGradedUnitRange{LA}}, - fused_axis::AbstractGradedUnitRange{LA}, - trees_to_ranges_mapping::Dict{<:SectorFusionTree{<:AbstractSector,N}}, - ) where {N,LA} - return new{typeof(outer_legs),typeof(fused_axis),typeof(trees_to_ranges_mapping)}( - outer_legs, fused_axis, trees_to_ranges_mapping - ) - end -end - -# getters -fused_axis(fa::FusedAxes) = fa.fused_axis -fusion_trees(fa::FusedAxes) = keys(trees_to_ranges_mapping(fa)) -trees_to_ranges_mapping(fa::FusedAxes) = fa.trees_to_ranges_mapping - -# Base interface -Base.axes(fa::FusedAxes) = fa.outer_axes -Base.ndims(fa::FusedAxes) = length(axes(fa)) - -# GradedUnitRanges interface -GradedUnitRanges.blocklabels(fa::FusedAxes) = blocklabels(fused_axis(fa)) - -# constructors -function FusedAxes{S}(::Tuple{}) where {S<:AbstractSector} - fused_axis = gradedrange([trivial(S) => 1]) - trees_to_ranges_mapping = Dict([SectorFusionTree{S}() => Block(1)[1:1]]) - return FusedAxes((), fused_axis, trees_to_ranges_mapping) -end - -function FusedAxes{S}( - outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} -) where {S<:AbstractSector} - fusion_trees_mult = fusion_trees_external_multiplicities(outer_legs) - - fused_leg, range_mapping = compute_inner_ranges(fusion_trees_mult) - return FusedAxes(outer_legs, fused_leg, range_mapping) -end - -function fusion_trees_external_multiplicities( - outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} -) - tree_arrows = isdual.(outer_legs) - return mapreduce(vcat, CartesianIndices(blocklength.(outer_legs))) do it - block_sectors = map((g, i) -> blocklabels(g)[i], outer_legs, Tuple(it)) - block_mult = mapreduce((g, i) -> blocklengths(g)[i], *, outer_legs, Tuple(it); init=1) - return build_trees(block_sectors, tree_arrows) .=> block_mult - end -end - -function compute_inner_ranges( - fusion_trees_mult::AbstractVector{<:Pair{<:SectorFusionTree,<:Integer}} -) - fused_leg = blockmergesort( - gradedrange(root_sector.(first.(fusion_trees_mult)) .=> last.(fusion_trees_mult)) - ) - range_mapping = Dict{fieldtype(eltype(fusion_trees_mult), 1),typeof(Block(1)[1:1])}() - fused_sectors = blocklabels(fused_leg) - shifts = ones(Int, blocklength(fused_leg)) - for (f, m) in fusion_trees_mult - s = root_sector(f) - i = findfirst(==(s), fused_sectors) - range_mapping[f] = Block(i)[shifts[i]:(shifts[i] + m - 1)] - shifts[i] += m - end - return fused_leg, range_mapping -end - -function to_blockindexrange(b1::BlockIndexRange{1}, b2::BlockIndexRange{1}) - t = (b1, b2) - return Block(Block.(t))[to_block_indices.(t)...] -end - -function intersect_sectors(left::FusedAxes, right::FusedAxes) - return Dict( - map( - t -> first.(t) => to_blockindexrange(last.(t)...), - Iterators.filter( - t -> root_sector(first(t[1])) == root_sector(first(t[2])), - Iterators.product(trees_to_ranges_mapping(left), trees_to_ranges_mapping(right)), - ), - ), - ) -end diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index b5b2986..1371f31 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -1,12 +1,15 @@ # This file defines struct FusionTensor and constructors -using BlockArrays: AbstractBlockMatrix, BlockArrays, blocklength, findblock +using BlockArrays: AbstractBlockMatrix, BlockArrays, BlockIndexRange, blocklength, findblock -using BlockSparseArrays: AbstractBlockSparseMatrix, BlockSparseArray, eachblockstoredindex +using BlockSparseArrays: + AbstractBlockSparseMatrix, BlockSparseArray, eachblockstoredindex, to_block_indices using GradedUnitRanges: AbstractGradedUnitRange, blocklabels, + blockmergesort, dual, + gradedrange, isdual, map_blocklabels, sector_type, @@ -20,7 +23,6 @@ struct FusionTensor{T,N,CoDomainAxes,DomainAxes,Mat,Mapping} <: AbstractArray{T, trees_block_mapping::Mapping # inner constructor to impose constraints on types - # TBD replace codomain_legs with FusedAxes(codomain_legs)? function FusionTensor( mat::AbstractMatrix, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, @@ -88,12 +90,22 @@ function BlockArrays.findblock(ft::FusionTensor, f1::SectorFusionTree, f2::Secto return Block(b1..., b2...) end -sanitize_axes(::Tuple{}) = () function sanitize_axes(raw_legs::Tuple{Vararg{AbstractGradedUnitRange}}) legs = promote_sectors(typeof(first(raw_legs)), raw_legs) @assert all(check_unique_blocklabels.(legs)) return legs end +sanitize_axes(::Tuple{}, ::Tuple{}) = TrivialSector, (), () +function sanitize_axes( + codomain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, + domain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, +) + legs = sanitize_axes((codomain_legs_raw..., domain_legs_raw...)) + S = sector_type(first(legs)) + codomain_legs = legs[begin:length(codomain_legs_raw)] + domain_legs = legs[(length(codomain_legs_raw) + 1):end] + return S, domain_legs, codomain_legs +end function check_unique_blocklabels(g::AbstractGradedUnitRange) return length(unique(blocklabels(g))) == blocklength(g) @@ -145,33 +157,86 @@ function FusionTensor( codomain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs_raw::Tuple{Vararg{AbstractGradedUnitRange}}, ) - legs = sanitize_axes((codomain_legs_raw..., domain_legs_raw...)) - S = sector_type(first(legs)) - codomain_legs = legs[begin:length(codomain_legs_raw)] - domain_legs = legs[(length(codomain_legs_raw) + 1):end] - codomain_fused_axes = FusedAxes{S}(codomain_legs) - domain_fused_axes = FusedAxes{S}(dual.(domain_legs)) - mat = initialize_data_matrix(elt, codomain_fused_axes, domain_fused_axes) - tree_to_block_mapping = intersect_sectors(codomain_fused_axes, domain_fused_axes) + S, domain_legs, codomain_legs = sanitize_axes(codomain_legs_raw, domain_legs_raw) + + row_axis, codomain_trees_to_ranges_mapping = fuse_axes(S, codomain_legs) + nondual_col_axis, domain_trees_to_ranges_mapping = fuse_axes(S, dual.(domain_legs)) + + mat = initialize_data_matrix(elt, row_axis, nondual_col_axis) + tree_to_block_mapping = intersect_sectors( + codomain_trees_to_ranges_mapping, domain_trees_to_ranges_mapping + ) return FusionTensor(mat, codomain_legs, domain_legs, tree_to_block_mapping) end -function FusionTensor(elt::Type, ::Tuple{}, ::Tuple{}) - codomain_fused_axes = FusedAxes{TrivialSector}(()) - domain_fused_axes = FusedAxes{TrivialSector}(()) - mat = initialize_data_matrix(elt, codomain_fused_axes, domain_fused_axes) - tree_to_block_mapping = intersect_sectors(codomain_fused_axes, domain_fused_axes) - return FusionTensor(mat, (), (), tree_to_block_mapping) +function fuse_axes(::Type{S}, ::Tuple{}) where {S<:AbstractSector} + fused_axis = gradedrange([trivial(S) => 1]) + trees_to_ranges_mapping = Dict([SectorFusionTree{S}() => Block(1)[1:1]]) + return fused_axis, trees_to_ranges_mapping +end +function fuse_axes(::Type, outer_legs::Tuple{Vararg{AbstractGradedUnitRange}}) + fusion_trees_mult = fusion_trees_external_multiplicities(outer_legs) + fused_leg, trees_to_ranges_mapping = compute_inner_ranges(fusion_trees_mult) + return fused_leg, trees_to_ranges_mapping +end + +function fusion_trees_external_multiplicities( + outer_legs::Tuple{Vararg{AbstractGradedUnitRange}} +) + tree_arrows = isdual.(outer_legs) + return mapreduce(vcat, CartesianIndices(blocklength.(outer_legs))) do it + block_sectors = map((g, i) -> blocklabels(g)[i], outer_legs, Tuple(it)) + block_mult = mapreduce((g, i) -> blocklengths(g)[i], *, outer_legs, Tuple(it); init=1) + return build_trees(block_sectors, tree_arrows) .=> block_mult + end +end + +function compute_inner_ranges( + fusion_trees_mult::AbstractVector{<:Pair{<:SectorFusionTree,<:Integer}} +) + fused_leg = blockmergesort( + gradedrange(root_sector.(first.(fusion_trees_mult)) .=> last.(fusion_trees_mult)) + ) + range_mapping = Dict{fieldtype(eltype(fusion_trees_mult), 1),typeof(Block(1)[1:1])}() + fused_sectors = blocklabels(fused_leg) + shifts = ones(Int, blocklength(fused_leg)) + for (f, m) in fusion_trees_mult + s = root_sector(f) + i = findfirst(==(s), fused_sectors) + range_mapping[f] = Block(i)[shifts[i]:(shifts[i] + m - 1)] + shifts[i] += m + end + return fused_leg, range_mapping +end + +function to_blockindexrange(b1::BlockIndexRange{1}, b2::BlockIndexRange{1}) + t = (b1, b2) + return Block(Block.(t))[to_block_indices.(t)...] +end + +function intersect_sectors( + codomain_trees_to_ranges_mapping::Dict{<:SectorFusionTree,<:BlockIndexRange{1}}, + domain_trees_to_ranges_mapping::Dict{<:SectorFusionTree,<:BlockIndexRange{1}}, +) + return Dict( + map( + t -> first.(t) => to_blockindexrange(last.(t)...), + Iterators.filter( + t -> root_sector(first(t[1])) == root_sector(first(t[2])), + Iterators.product(codomain_trees_to_ranges_mapping, domain_trees_to_ranges_mapping), + ), + ), + ) end function initialize_data_matrix( - elt::Type{<:Number}, codomain_fused_axes::FusedAxes, domain_fused_axes::FusedAxes + elt::Type{<:Number}, + mat_row_axis::AbstractGradedUnitRange, + nondual_col_axis::AbstractGradedUnitRange, ) - mat_row_axis = fused_axis(codomain_fused_axes) - mat_col_axis = dual(fused_axis(domain_fused_axes)) # non-abelian fusion trees have float eltype: need compatible type promoted = promote_type(elt, fusiontree_eltype(sector_type(mat_row_axis))) - mat = BlockSparseArray{promoted}(mat_row_axis, mat_col_axis) + mat = BlockSparseArray{promoted}(mat_row_axis, dual(nondual_col_axis)) initialize_allowed_sectors!(mat) return mat end From 734247764ce29bc166a66ccc68713f9ba4d7b33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Tue, 7 Jan 2025 15:07:05 -0500 Subject: [PATCH 48/52] better names --- src/fusiontensor/base_interface.jl | 2 +- src/fusiontensor/fusiontensor.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index dec7eb1..78d2af6 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -13,7 +13,7 @@ Base.:*(ft::FusionTensor, x::Number) = x * ft function Base.:*(left::FusionTensor, right::FusionTensor) checkaxes_dual(domain_axes(left), codomain_axes(right)) new_data_matrix = data_matrix(left) * data_matrix(right) - return fusiontensor(new_data_matrix, codomain_axes(left), domain_axes(right)) + return to_fusiontensor(new_data_matrix, codomain_axes(left), domain_axes(right)) end Base.:+(ft::FusionTensor) = ft diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 1371f31..76e537f 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -135,7 +135,7 @@ function promote_sector_type(legs) end # initialize with already computed data_matrix -function fusiontensor( +function to_fusiontensor( mat::AbstractMatrix, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, @@ -163,7 +163,7 @@ function FusionTensor( nondual_col_axis, domain_trees_to_ranges_mapping = fuse_axes(S, dual.(domain_legs)) mat = initialize_data_matrix(elt, row_axis, nondual_col_axis) - tree_to_block_mapping = intersect_sectors( + tree_to_block_mapping = intersect_codomain_domain( codomain_trees_to_ranges_mapping, domain_trees_to_ranges_mapping ) return FusionTensor(mat, codomain_legs, domain_legs, tree_to_block_mapping) @@ -214,7 +214,7 @@ function to_blockindexrange(b1::BlockIndexRange{1}, b2::BlockIndexRange{1}) return Block(Block.(t))[to_block_indices.(t)...] end -function intersect_sectors( +function intersect_codomain_domain( codomain_trees_to_ranges_mapping::Dict{<:SectorFusionTree,<:BlockIndexRange{1}}, domain_trees_to_ranges_mapping::Dict{<:SectorFusionTree,<:BlockIndexRange{1}}, ) From cb15d6328be103fd5cc7fc9572e20d148b53ce61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 8 Jan 2025 09:56:47 -0500 Subject: [PATCH 49/52] replace FusionTensor with to_fusiontensor --- src/fusiontensor/array_cast.jl | 2 +- src/fusiontensor/fusiontensor.jl | 19 +++----- test/basics/test_array_cast.jl | 74 ++++++++++++++++---------------- 3 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index afd31da..1a63861 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -10,7 +10,7 @@ using TensorAlgebra: contract # ================================= High level interface ================================= #### cast from array to symmetric -function FusionTensor( +function to_fusiontensor( array::AbstractArray, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 76e537f..ebe39fe 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -76,19 +76,14 @@ end # BlockArrays interface function BlockArrays.findblock(ft::FusionTensor, f1::SectorFusionTree, f2::SectorFusionTree) # find outer block corresponding to fusion trees - @assert ndims_codomain(ft) == length(f1) - @assert ndims_domain(ft) == length(f2) - @assert sector_type(ft) == sector_type(f1) - @assert sector_type(ft) == sector_type(f2) - b1 = ntuple( - i -> findfirst(==(leaves(f1)[i]), blocklabels(codomain_axes(ft)[i])), ndims_codomain(ft) - ) - b2 = ntuple( - i -> findfirst(==(leaves(f2)[i]), blocklabels(dual(domain_axes(ft)[i]))), - ndims_domain(ft), - ) + @assert typeof((f1, f2)) === keytype(trees_block_mapping(ft)) + b1 = find_sector_block.(leaves(f1), codomain_axes(ft)) + b2 = find_sector_block.(leaves(f2), dual.(domain_axes(ft))) return Block(b1..., b2...) end +function find_sector_block(s::AbstractSector, l::AbstractGradedUnitRange) + return findfirst(==(s), blocklabels(l)) +end function sanitize_axes(raw_legs::Tuple{Vararg{AbstractGradedUnitRange}}) legs = promote_sectors(typeof(first(raw_legs)), raw_legs) @@ -135,7 +130,7 @@ function promote_sector_type(legs) end # initialize with already computed data_matrix -function to_fusiontensor( +function FusionTensor( mat::AbstractMatrix, codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, diff --git a/test/basics/test_array_cast.jl b/test/basics/test_array_cast.jl index ea34b3e..031502b 100644 --- a/test/basics/test_array_cast.jl +++ b/test/basics/test_array_cast.jl @@ -4,7 +4,7 @@ using Test: @test, @test_broken, @test_throws, @testset using BlockArrays: Block, BlockedArray, blocksize -using FusionTensors: FusionTensor, data_matrix +using FusionTensors: FusionTensor, data_matrix, to_fusiontensor using GradedUnitRanges: dual, fusion_product, gradedrange using SymmetrySectors: O2, SectorProduct, SU2, TrivialSector, U1 @@ -15,7 +15,7 @@ include("setup.jl") g = gradedrange([TrivialSector() => 1]) gb = dual(g) m = ones((1, 1)) - ft = FusionTensor(m, (g,), (gb,)) + ft = to_fusiontensor(m, (g,), (gb,)) @test size(data_matrix(ft)) == (1, 1) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[1, 1] ≈ 1.0 @@ -25,14 +25,14 @@ include("setup.jl") for elt in (Int, UInt32, Float32) m = ones(elt, (1, 1)) - ft = FusionTensor(m, (g,), (gb,)) + ft = to_fusiontensor(m, (g,), (gb,)) @test eltype(ft) === Float64 @test Array(ft) ≈ m end for elt in (ComplexF32, ComplexF64) m = ones(elt, (1, 1)) - ft = FusionTensor(m, (g,), (gb,)) + ft = to_fusiontensor(m, (g,), (gb,)) @test eltype(ft) === ComplexF64 @test Array(ft) ≈ m end @@ -46,7 +46,7 @@ include("setup.jl") codomain_legs = (g1, g2) domain_legs = dual.((g3, g4)) t = convert.(Float64, reshape(collect(1:48), (2, 3, 4, 2))) - ft = FusionTensor(t, codomain_legs, domain_legs) + ft = to_fusiontensor(t, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (6, 8) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[Block(1, 1)] ≈ reshape(t, (6, 8)) @@ -61,7 +61,7 @@ end g = gradedrange([U1(0) => 1]) gb = dual(g) m = ones((1, 1)) - ft = FusionTensor(m, (g,), (gb,)) + ft = to_fusiontensor(m, (g,), (gb,)) @test size(data_matrix(ft)) == (1, 1) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[1, 1] ≈ 1.0 @@ -74,7 +74,7 @@ end g = gradedrange([U1(1) => 2]) gb = dual(g) m = ones((2, 2)) - ft = FusionTensor(m, (g,), (gb,)) + ft = to_fusiontensor(m, (g,), (gb,)) @test size(data_matrix(ft)) == (2, 2) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[Block(1, 1)] ≈ m @@ -88,7 +88,7 @@ end codomain_legs = (g,) domain_legs = (dual(g),) dense = Array{Float64}(LinearAlgebra.I(3)) - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (3, 3) @test blocksize(data_matrix(ft)) == (2, 2) @test data_matrix(ft)[Block(1, 1)] ≈ ones((1, 1)) @@ -98,18 +98,18 @@ end @test Array(ft) ≈ dense @test Array(adjoint(ft)) ≈ adjoint(dense) - @test_throws BoundsError FusionTensor( + @test_throws BoundsError to_fusiontensor( dense, (gradedrange([U1(1) => 1, U1(2) => 3]),), domain_legs ) - @test_throws MethodError FusionTensor(dense, (g, g), domain_legs) + @test_throws MethodError to_fusiontensor(dense, (g, g), domain_legs) ba = BlockedArray(dense, [1, 2], [1, 2]) - @test_throws DomainError FusionTensor( + @test_throws DomainError to_fusiontensor( ba, (gradedrange([U1(1) => 1, U1(2) => 3]),), domain_legs ) - @test_throws DomainError FusionTensor(ba, (g, g), domain_legs) + @test_throws DomainError to_fusiontensor(ba, (g, g), domain_legs) dense[1, 2] = 1 # forbidden - @test_throws InexactError FusionTensor(dense, codomain_legs, domain_legs) + @test_throws InexactError to_fusiontensor(dense, codomain_legs, domain_legs) end @testset "several axes, one block" begin @@ -120,7 +120,7 @@ end codomain_legs = (g1, g2) domain_legs = dual.((g3, g4)) t = convert.(Float64, reshape(collect(1:48), (2, 3, 4, 2))) - ft = FusionTensor(t, codomain_legs, domain_legs) + ft = to_fusiontensor(t, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (6, 8) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[Block(1, 1)] ≈ reshape(t, (6, 8)) @@ -141,7 +141,7 @@ end dense[3:4, 1:3, 5:5, 1:2] .= 2.0 dense[1:2, 4:5, 5:5, 1:2] .= 3.0 dense[3:4, 4:5, 1:4, 3:3] .= 4.0 - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (20, 15) @test blocksize(data_matrix(ft)) == (3, 4) @test norm(ft) ≈ norm(dense) @@ -159,7 +159,7 @@ end domain_legs = (dual(g2), dual(g3), g4) dense = zeros(ComplexF64, (3, 6, 5, 4)) dense[2:2, 1:1, 1:2, 2:3] .= 1.0im - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test size(data_matrix(ft)) == (3, 120) @test blocksize(data_matrix(ft)) == (3, 8) @test norm(ft) ≈ norm(dense) @@ -173,36 +173,36 @@ end v = zeros((6,)) v[1] = 1.0 - ft1 = FusionTensor(v, (g,), ()) + ft1 = to_fusiontensor(v, (g,), ()) @test isnothing(check_sanity(ft1)) @test ndims(ft1) == 1 @test vec(Array(data_matrix(ft1))) ≈ v @test Array(ft1) ≈ v @test Array(adjoint(ft1)) ≈ v - ft2 = FusionTensor(v, (), (dual(g),)) + ft2 = to_fusiontensor(v, (), (dual(g),)) @test isnothing(check_sanity(ft2)) @test ndims(ft2) == 1 @test vec(Array(data_matrix(ft2))) ≈ v @test Array(ft2) ≈ v @test Array(adjoint(ft2)) ≈ v - ft3 = FusionTensor(v, (dual(g),), ()) + ft3 = to_fusiontensor(v, (dual(g),), ()) @test isnothing(check_sanity(ft3)) @test Array(ft3) ≈ v @test Array(adjoint(ft3)) ≈ v - ft4 = FusionTensor(v, (), (g,)) + ft4 = to_fusiontensor(v, (), (g,)) @test isnothing(check_sanity(ft4)) @test Array(ft4) ≈ v @test Array(adjoint(ft4)) ≈ v zerodim = ones(()) if VERSION < v"1.11" - @test_broken FusionTensor(zerodim, (), ()) isa FusionTensor # https://github.com/JuliaLang/julia/issues/52615 + @test_broken to_fusiontensor(zerodim, (), ()) isa FusionTensor # https://github.com/JuliaLang/julia/issues/52615 else # TODO fix: add specialized method, maybe fix TensorAlgebra - @test_broken FusionTensor(zerodim, (), ()) + @test_broken to_fusiontensor(zerodim, (), ()) #@test ft isa FusionTensor #@test ndims(ft) == 0 #@test isnothing(check_sanity(ft)) @@ -218,7 +218,7 @@ end g = gradedrange([O2(0) => 1]) gb = dual(g) m = ones((1, 1)) - ft = FusionTensor(m, (gb,), (g,)) + ft = to_fusiontensor(m, (gb,), (g,)) @test size(data_matrix(ft)) == (1, 1) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[1, 1] ≈ 1.0 @@ -233,7 +233,7 @@ end # identity id2 = LinearAlgebra.I((2)) - ft = FusionTensor(id2, (g2,), (g2b,)) + ft = to_fusiontensor(id2, (g2,), (g2b,)) @test norm(ft) ≈ √2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ id2 @@ -250,7 +250,7 @@ end (2, 2, 2, 2), ) dense, codomain_legs, domain_legs = sds22, (g2, g2), (g2b, g2b) - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test norm(ft) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @@ -268,7 +268,7 @@ end ) sds22b_codomain_legs = (g2, g2b) dense, codomain_legs, domain_legs = sds22b, (g2, g2b), (g2b, g2) - ftb = FusionTensor(dense, codomain_legs, domain_legs) + ftb = to_fusiontensor(dense, codomain_legs, domain_legs) @test norm(ftb) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ftb) ≈ sds22b @@ -276,14 +276,14 @@ end # no domain axis dense, codomain_legs, domain_legs = sds22, (g2, g2, g2b, g2b), () - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 # no codomain axis dense, codomain_legs, domain_legs = sds22, (), (g2, g2, g2b, g2b) - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 @@ -295,7 +295,7 @@ end g = gradedrange([SU2(0) => 1]) gb = dual(g) m = ones((1, 1)) - ft = FusionTensor(m, (gb,), (g,)) + ft = to_fusiontensor(m, (gb,), (g,)) @test size(data_matrix(ft)) == (1, 1) @test blocksize(data_matrix(ft)) == (1, 1) @test data_matrix(ft)[1, 1] ≈ 1.0 @@ -310,7 +310,7 @@ end # identity id2 = LinearAlgebra.I((2)) - ft = FusionTensor(id2, (g2,), (g2b,)) + ft = to_fusiontensor(id2, (g2,), (g2b,)) @test norm(ft) ≈ √2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ id2 @@ -327,7 +327,7 @@ end (2, 2, 2, 2), ) dense, codomain_legs, domain_legs = sds22, (g2, g2), (g2b, g2b) - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test norm(ft) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @@ -345,7 +345,7 @@ end ) sds22b_codomain_legs = (g2, g2b) dense, codomain_legs, domain_legs = sds22b, (g2, g2b), (g2b, g2) - ftb = FusionTensor(dense, codomain_legs, domain_legs) + ftb = to_fusiontensor(dense, codomain_legs, domain_legs) @test norm(ftb) ≈ √3 / 2 @test isnothing(check_sanity(ft)) @test Array(ftb) ≈ sds22b @@ -353,14 +353,14 @@ end # no domain axis dense, codomain_legs, domain_legs = sds22, (g2b, g2b, g2, g2), () - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 # no codomain axis dense, codomain_legs, domain_legs = sds22, (), (g2b, g2b, g2, g2) - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ sds22 @test Array(adjoint(ft)) ≈ sds22 @@ -373,7 +373,7 @@ end domain_legs = dual.(codomain_legs) d = 8 dense = reshape(LinearAlgebra.I(d^N), ntuple(_ -> d, 2 * N)) - ft = FusionTensor(dense, codomain_legs, domain_legs) + ft = to_fusiontensor(dense, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ dense @test Array(adjoint(ft)) ≈ dense @@ -396,7 +396,7 @@ end codomain_legs = (dual(gd),) gD = gradedrange([SectorProduct(SU2(0), U1(1)) => 1, SectorProduct(s, U1(0)) => 1]) domain_legs = (gD, gD, gD, gD) - ft = FusionTensor(tRVB, codomain_legs, domain_legs) + ft = to_fusiontensor(tRVB, codomain_legs, domain_legs) @test isnothing(check_sanity(ft)) @test Array(ft) ≈ tRVB @@ -407,7 +407,7 @@ end SectorProduct(; S=SU2(0), N=U1(1)) => 1, SectorProduct(; S=s, N=U1(0)) => 1 ]) domain_legs_nt = (gD_nt, gD_nt, gD_nt, gD_nt) - ft_nt = FusionTensor(tRVB, codomain_legs_nt, domain_legs_nt) + ft_nt = to_fusiontensor(tRVB, codomain_legs_nt, domain_legs_nt) @test isnothing(check_sanity(ft_nt)) @test Array(ft_nt) ≈ tRVB end From b751f9eb1c0f46d272a78e05cb7efdb79b328c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 8 Jan 2025 12:42:59 -0500 Subject: [PATCH 50/52] fix to_fusiontensor name --- src/fusiontensor/array_cast.jl | 7 ------- src/fusiontensor/base_interface.jl | 2 +- src/fusiontensor/fusiontensor.jl | 1 + src/permutedims/permutedims.jl | 2 +- test/basics/test_basics.jl | 6 +++--- test/basics/test_linear_algebra.jl | 4 ++-- test/basics/test_permutedims.jl | 15 ++++++++------- 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/fusiontensor/array_cast.jl b/src/fusiontensor/array_cast.jl index 1a63861..75aeb54 100644 --- a/src/fusiontensor/array_cast.jl +++ b/src/fusiontensor/array_cast.jl @@ -10,13 +10,6 @@ using TensorAlgebra: contract # ================================= High level interface ================================= #### cast from array to symmetric -function to_fusiontensor( - array::AbstractArray, - codomain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, - domain_legs::Tuple{Vararg{AbstractGradedUnitRange}}, -) - return to_fusiontensor(array, codomain_legs, domain_legs) -end #### cast from symmetric to array function BlockSparseArrays.BlockSparseArray(ft::FusionTensor) diff --git a/src/fusiontensor/base_interface.jl b/src/fusiontensor/base_interface.jl index 78d2af6..ca29fc8 100644 --- a/src/fusiontensor/base_interface.jl +++ b/src/fusiontensor/base_interface.jl @@ -13,7 +13,7 @@ Base.:*(ft::FusionTensor, x::Number) = x * ft function Base.:*(left::FusionTensor, right::FusionTensor) checkaxes_dual(domain_axes(left), codomain_axes(right)) new_data_matrix = data_matrix(left) * data_matrix(right) - return to_fusiontensor(new_data_matrix, codomain_axes(left), domain_axes(right)) + return FusionTensor(new_data_matrix, codomain_axes(left), domain_axes(right)) end Base.:+(ft::FusionTensor) = ft diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index ebe39fe..08cd528 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -81,6 +81,7 @@ function BlockArrays.findblock(ft::FusionTensor, f1::SectorFusionTree, f2::Secto b2 = find_sector_block.(leaves(f2), dual.(domain_axes(ft))) return Block(b1..., b2...) end +# TBD move to GradedUnitRanges? rename findfirst_sector? function find_sector_block(s::AbstractSector, l::AbstractGradedUnitRange) return findfirst(==(s), blocklabels(l)) end diff --git a/src/permutedims/permutedims.jl b/src/permutedims/permutedims.jl index 2bc7031..0118ef4 100644 --- a/src/permutedims/permutedims.jl +++ b/src/permutedims/permutedims.jl @@ -12,7 +12,7 @@ function naive_permutedims(ft, biperm::BlockedPermutation{2}) # naive permute: cast to dense, permutedims, cast to FusionTensor arr = Array(ft) permuted_arr = permutedims(arr, Tuple(biperm)) - permuted = FusionTensor(permuted_arr, new_codomain_legs, new_domain_legs) + permuted = to_fusiontensor(permuted_arr, new_codomain_legs, new_domain_legs) return permuted end diff --git a/test/basics/test_basics.jl b/test/basics/test_basics.jl index f4f4b63..0312ed6 100644 --- a/test/basics/test_basics.jl +++ b/test/basics/test_basics.jl @@ -6,7 +6,7 @@ using FusionTensors: codomain_axes, data_matrix, domain_axes, - fusiontensor, + FusionTensor, checkaxes, checkaxes_dual, matrix_column_axis, @@ -30,7 +30,7 @@ include("setup.jl") @test space_isequal(matrix_column_axis(ft0), g2) m = BlockSparseArray{Float64}(g1, g2) - ft1 = fusiontensor(m, (g1,), (g2,)) + ft1 = FusionTensor(m, (g1,), (g2,)) # getters @test data_matrix(ft1) == m @@ -100,7 +100,7 @@ end gr = fusion_product(g1, g2) gc = dual(fusion_product(dual(g3), dual(g4))) m2 = BlockSparseArray{Float64}(gr, gc) - ft = fusiontensor(m2, (g1, g2), (g3, g4)) + ft = FusionTensor(m2, (g1, g2), (g3, g4)) @test data_matrix(ft) == m2 @test checkaxes(codomain_axes(ft), (g1, g2)) diff --git a/test/basics/test_linear_algebra.jl b/test/basics/test_linear_algebra.jl index 6fc33b0..3850317 100644 --- a/test/basics/test_linear_algebra.jl +++ b/test/basics/test_linear_algebra.jl @@ -5,7 +5,7 @@ using Test: @test, @testset using BlockArrays: BlockArrays using BlockSparseArrays: BlockSparseArrays -using FusionTensors: FusionTensor +using FusionTensors: FusionTensor, to_fusiontensor using GradedUnitRanges: dual, gradedrange using SymmetrySectors: U1, SU2, TrivialSector @@ -30,7 +30,7 @@ include("setup.jl") @test norm(ft0) == 0 @test tr(ft0) == 0 - ft = FusionTensor(sdst, (g, g), (dual(g), dual(g))) + ft = to_fusiontensor(sdst, (g, g), (dual(g), dual(g))) @test isnothing(check_sanity(ft)) @test norm(ft) ≈ √3 / 2 @test isapprox(tr(ft), 0; atol=eps(Float64)) diff --git a/test/basics/test_permutedims.jl b/test/basics/test_permutedims.jl index 12c7695..2bad837 100644 --- a/test/basics/test_permutedims.jl +++ b/test/basics/test_permutedims.jl @@ -9,7 +9,8 @@ using FusionTensors: matrix_row_axis, naive_permutedims, ndims_domain, - ndims_codomain + ndims_codomain, + to_fusiontensor using GradedUnitRanges: dual, gradedrange, space_isequal using SymmetrySectors: O2, U1, SectorProduct, SU2 using TensorAlgebra: blockedperm @@ -67,7 +68,7 @@ include("setup.jl") arr[3:4, 1:3, 5:5, 1:2] .= 2.0 arr[1:2, 4:5, 5:5, 1:2] .= 3.0 arr[3:4, 4:5, 1:4, 3:3] .= 4.0 - ft = FusionTensor(arr, codomain_legs, domain_legs) + ft = to_fusiontensor(arr, codomain_legs, domain_legs) biperm = blockedperm((3,), (2, 4, 1)) ftp = permutedims(ft, biperm) @@ -85,7 +86,7 @@ include("setup.jl") @testset "Less than two axes" begin if VERSION >= v"1.11" - @test_broken FusionTensor(ones(()), (), ()) isa FusionTensor + @test_broken to_fusiontensor(ones(()), (), ()) isa FusionTensor #= ft0p = permutedims(ft0, (), ()) @test ft0p isa FusionTensor{Float64,0} @test data_matrix(ft0p) ≈ data_matrix(ft0) @@ -100,7 +101,7 @@ include("setup.jl") v = zeros((6,)) v[1] = 1.0 biperm = blockedperm((), (1,)) - ft1 = FusionTensor(v, (g,), ()) + ft1 = to_fusiontensor(v, (g,), ()) ft2 = permutedims(ft1, biperm) @test isnothing(check_sanity(ft2)) @test ft2 ≈ naive_permutedims(ft1, biperm) @@ -142,16 +143,16 @@ end for biperm in [ blockedperm((2, 1), (3, 4)), blockedperm((3, 1), (2, 4)), blockedperm((3, 1, 4), (2,)) ] - ft = FusionTensor(sds22, (g2, g2), (g2b, g2b)) + ft = to_fusiontensor(sds22, (g2, g2), (g2b, g2b)) @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) @test permutedims(adjoint(ft), biperm) ≈ naive_permutedims(adjoint(ft), biperm) - ft = FusionTensor(sds22b, (g2, g2b), (g2b, g2)) + ft = to_fusiontensor(sds22b, (g2, g2b), (g2b, g2)) @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) @test permutedims(adjoint(ft), biperm) ≈ naive_permutedims(adjoint(ft), biperm) end for biperm in [blockedperm((1, 2, 3, 4), ()), blockedperm((), (3, 1, 2, 4))] - ft = FusionTensor(sds22, (g2, g2), (g2b, g2b)) + ft = to_fusiontensor(sds22, (g2, g2), (g2b, g2b)) @test permutedims(ft, biperm) ≈ naive_permutedims(ft, biperm) end end From 1dc72ae3529a2a924df87ef2edb1d135140074aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 8 Jan 2025 14:34:07 -0500 Subject: [PATCH 51/52] use do block --- src/fusiontensor/fusiontensor.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 08cd528..2cde550 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -216,12 +216,13 @@ function intersect_codomain_domain( ) return Dict( map( - t -> first.(t) => to_blockindexrange(last.(t)...), Iterators.filter( t -> root_sector(first(t[1])) == root_sector(first(t[2])), Iterators.product(codomain_trees_to_ranges_mapping, domain_trees_to_ranges_mapping), ), - ), + ) do t + return first.(t) => to_blockindexrange(last.(t)...) + end, ) end From d3b624e016c11205b37cc2c847c9d84e2560fd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Gauth=C3=A9?= Date: Wed, 8 Jan 2025 14:36:58 -0500 Subject: [PATCH 52/52] inline return --- src/fusiontensor/fusiontensor.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fusiontensor/fusiontensor.jl b/src/fusiontensor/fusiontensor.jl index 2cde550..7bc2628 100644 --- a/src/fusiontensor/fusiontensor.jl +++ b/src/fusiontensor/fusiontensor.jl @@ -120,8 +120,7 @@ function promote_sectors( # fuse with trivial to insert all missing arguments inside each GradedAxis # avoid depending on SymmetrySectors internals s0 = trivial(T) - unified_legs = map_blocklabels.(s -> only(blocklabels(fusion_product(s0, s))), legs) - return unified_legs + return map_blocklabels.(s -> only(blocklabels(fusion_product(s0, s))), legs) end function promote_sector_type(legs)