Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "GradedArrays"
uuid = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2"
version = "0.13.5"
version = "0.13.6"
authors = ["ITensor developers <support@itensor.org> and contributors"]

[workspace]
Expand All @@ -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"
Expand All @@ -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"
22 changes: 22 additions & 0 deletions ext/GradedArraysTensorKitExt/GradedArraysTensorKitExt.jl
Original file line number Diff line number Diff line change
@@ -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
48 changes: 43 additions & 5 deletions src/gradedoneto.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 4 additions & 6 deletions src/sectorrange.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
48 changes: 48 additions & 0 deletions test/test_tensorkitext.jl
Original file line number Diff line number Diff line change
@@ -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
Loading