diff --git a/Project.toml b/Project.toml index f0b72efe..cbaf99c5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "GradedArrays" uuid = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2" -version = "0.13.5" +version = "0.13.6" authors = ["ITensor developers and contributors"] [workspace] @@ -24,9 +24,11 @@ TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" [weakdeps] SUNRepresentations = "1a50b95c-7aac-476d-a9ce-2bfc675fc617" +TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" [extensions] GradedArraysSUNRepresentationsExt = "SUNRepresentations" +GradedArraysTensorKitExt = "TensorKit" [compat] ArrayLayouts = "1" @@ -43,5 +45,6 @@ SparseArraysBase = "0.10" SplitApplyCombine = "1.2.3" StridedViews = "0.5" TensorAlgebra = "0.16" +TensorKit = "0.17" TensorKitSectors = "0.3" julia = "1.10" diff --git a/ext/GradedArraysTensorKitExt/GradedArraysTensorKitExt.jl b/ext/GradedArraysTensorKitExt/GradedArraysTensorKitExt.jl new file mode 100644 index 00000000..e9e2d1c0 --- /dev/null +++ b/ext/GradedArraysTensorKitExt/GradedArraysTensorKitExt.jl @@ -0,0 +1,22 @@ +module GradedArraysTensorKitExt + +using GradedArrays: GradedArrays +using TensorKit: TensorKit, ElementarySpace, Vect + +# Non-abelian `sector => multiplicity` pairs have no block-sparse `GradedOneTo` representation, +# so `GradedArrays.to_range` routes them here to build a native TensorKit `GradedSpace`. A raw +# TensorKit sector carries no arrow, so this is the non-dual builder. It is the entry point +# both for the `SectorRange` routing in GradedArrays and for a user-supplied list of TensorKit +# sectors passed to `to_range`. `Vect[S]` takes the pairs as a single iterable (rather than +# splatting), so a long sector list does not build a large tuple or hit vararg dispatch. +function GradedArrays.to_tensorkit_space( + space::AbstractVector{<:Pair{S}} + ) where {S <: TensorKit.Sector} + return Vect[S](space) +end + +# A TensorKit space is a first-class graded axis under the direct-wrap design, so `dual` on +# one flips its arrow. This is the whole-space dual the `SectorRange` routing applies. +GradedArrays.dual(V::ElementarySpace) = TensorKit.dual(V) + +end diff --git a/src/gradedoneto.jl b/src/gradedoneto.jl index 7b636b36..34282e13 100644 --- a/src/gradedoneto.jl +++ b/src/gradedoneto.jl @@ -261,11 +261,49 @@ function gradedrange( end # Build a graded range from a vector of sector-to-multiplicity pairs, e.g. -# `to_range([U1(0) => 2, U1(1) => 3])`. Defined over each key type separately -# rather than a `Union` so each method stays specific enough not to capture -# unrelated `Pair` vectors. +# `to_range([U1(0) => 2, U1(1) => 3])`, routed by symmetry: abelian sectors build a +# block-sparse `GradedOneTo`, while non-abelian sectors have no block-sparse representation +# and build a native TensorKit `GradedSpace` via `to_tensorkit_space`. Defined over each +# key type separately rather than a `Union` so each method stays specific enough not to +# capture unrelated `Pair` vectors. for S in (:(TKS.Sector), :SectorRange) - @eval function TensorAlgebra.to_range(space::AbstractVector{<:Pair{<:$S, <:Integer}}) - return gradedrange(space) + @eval function TensorAlgebra.to_range( + space::AbstractVector{<:Pair{K, <:Integer}} + ) where {K <: $S} + return if SymmetryStyle(K) === AbelianStyle() + gradedrange(space) + else + to_tensorkit_space(space) + end end end + +""" + to_tensorkit_space(sectors) + +Convert a vector of non-abelian `sector => multiplicity` pairs into a native TensorKit +`GradedSpace`. Non-abelian symmetries have no block-sparse (`GradedOneTo`) representation, +so `to_range` routes them here. Requires TensorKit to be loaded; the method that builds the +space lives in the GradedArrays–TensorKit extension. +""" +function to_tensorkit_space(space) + return throw( + ArgumentError( + "building a native non-abelian graded space from $(space) requires TensorKit; \ + run `using TensorKit` to enable it" + ) + ) +end + +# `SectorRange` sector-pairs carry an arrow: strip to sector labels, check the shared arrow, +# and apply it as a whole-space `dual` (distinct from a space of dual sectors, and the form a +# dual index must take for contraction). The label-keyed `to_tensorkit_space` builder and +# `dual` on the resulting space come from the GradedArrays–TensorKit extension. +function to_tensorkit_space(space::AbstractVector{<:Pair{S}}) where {S <: SectorRange} + arrows = isdual.(first.(space)) + allequal(arrows) || + throw(ArgumentError("All sectors must have the same isdual flag")) + labels_space = [label(first(p)) => last(p) for p in space] + nondual_space = to_tensorkit_space(labels_space) + return first(arrows) ? dual(nondual_space) : nondual_space +end diff --git a/src/sectorrange.jl b/src/sectorrange.jl index dfdf00a1..8a6d3a31 100644 --- a/src/sectorrange.jl +++ b/src/sectorrange.jl @@ -13,6 +13,7 @@ struct SectorRange{I <: TKS.Sector} <: AbstractUnitRange{Int} end SectorRange{I}(label) where {I} = SectorRange{I}(label, false) SectorRange(label::TKS.Sector) = SectorRange(label, false) +SectorRange(r::SectorRange) = r label(r::SectorRange) = r.label isdual(r::SectorRange) = r.isdual @@ -141,13 +142,10 @@ SymmetryStyle(x) = SymmetryStyle(typeof(x)) # allows for abelian-like slicing style for GradedOneTo: assume length(::label) = 1 # and preserve labels in any slicing operation SymmetryStyle(T::Type) = AbelianStyle() -function SymmetryStyle(::Type{T}) where {T <: SectorRange} - if TKS.FusionStyle(T) === TKS.UniqueFusion() - return AbelianStyle() - else - return NotAbelianStyle() - end +function SymmetryStyle(::Type{T}) where {T <: TKS.Sector} + return TKS.FusionStyle(T) === TKS.UniqueFusion() ? AbelianStyle() : NotAbelianStyle() end +SymmetryStyle(::Type{SectorRange{I}}) where {I} = SymmetryStyle(I) SymmetryStyle(G::Type{<:AbstractUnitRange}) = SymmetryStyle(sectortype(G)) combine_styles(::AbelianStyle, ::AbelianStyle) = AbelianStyle() diff --git a/test/Project.toml b/test/Project.toml index fd26809f..1038a3d1 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -12,6 +12,7 @@ SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SparseArraysBase = "0d5efcca-f356-4864-8770-e1ed8d78f208" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" +TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" @@ -33,6 +34,7 @@ SafeTestsets = "0.1" SparseArraysBase = "0.10" Suppressor = "0.2.8" TensorAlgebra = "0.16" +TensorKit = "0.17" TensorKitSectors = "0.3" Test = "1.10" TestExtras = "0.3.1" diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl new file mode 100644 index 00000000..2e8b75ba --- /dev/null +++ b/test/test_tensorkitext.jl @@ -0,0 +1,48 @@ +using GradedArrays: GradedOneTo, SU2, U1, dual, gradedrange +using TensorAlgebra: to_range +using TensorKit: TensorKit, GradedSpace, SU2Irrep, Vect, dim +using Test: @test, @test_throws, @testset + +# `to_range` on `sector => multiplicity` pairs is routed by symmetry. Abelian sectors keep +# the block-sparse `GradedOneTo` backend; non-abelian sectors have no block-sparse +# representation and build a native TensorKit `GradedSpace` (this extension). This lets +# `Index([SU2(0) => 1, …])` reach the same `TensorMap`-backed `ITensor` as passing a native +# TensorKit space to `Index`. +@testset "GradedArraysTensorKitExt" begin + # Abelian sectors are untouched by the extension: still a block-sparse `GradedOneTo`. + @testset "abelian stays block-sparse" begin + r = to_range([U1(0) => 2, U1(1) => 3]) + @test r isa GradedOneTo + @test !(r isa GradedSpace) + @test r == gradedrange([U1(0) => 2, U1(1) => 3]) + end + + # Non-abelian sectors build a native TensorKit `GradedSpace` equal to the one produced + # by TensorKit's own constructor. + @testset "non-abelian builds a native GradedSpace" begin + r = to_range([SU2(0) => 1, SU2(1) => 2]) + @test r isa GradedSpace + @test r == Vect[SU2Irrep](0 => 1, 1 => 2) + @test dim(r) == 1 * 1 + 3 * 2 + end + + # A raw list of TensorKit sectors (no `SectorRange` wrapper) is a valid input and builds + # the same non-dual space. + @testset "raw TensorKit sectors" begin + r = to_range([SU2Irrep(0) => 1, SU2Irrep(1) => 2]) + @test r isa GradedSpace + @test r == Vect[SU2Irrep](0 => 1, 1 => 2) + end + + # The sector arrow rides inside the space: a shared dual flag makes a dual space. + @testset "dual arrow rides inside the space" begin + r = to_range([dual(SU2(0)) => 1, dual(SU2(1)) => 2]) + @test r isa GradedSpace + @test r == TensorKit.dual(Vect[SU2Irrep](0 => 1, 1 => 2)) + end + + # A native TensorKit space has a single arrow, so mixed arrows have no representation. + @testset "mixed arrows error" begin + @test_throws ArgumentError to_range([SU2(0) => 1, dual(SU2(1)) => 2]) + end +end