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
2 changes: 1 addition & 1 deletion src/abeliangradedarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Base.axes(a::AbelianGradedArray) = a.axes
function blocktype(
::Type{<:AbelianGradedArray{T, S, N, D}}
) where {T, S, N, D}
return AbelianSectorArray{T, S, N, D, N, 0}
return AbelianSectorArray{T, S, N, N, 0, D}
end
blocktype(a::AbelianGradedArray) = blocktype(typeof(a))

Expand Down
38 changes: 19 additions & 19 deletions src/abeliansectorarray.jl
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
"""
AbelianSectorArray{T,S,N,A,NC,ND} <: AbstractSectorArray{T, S, N}
AbelianSectorArray{T,S,N,NC,ND,A} <: AbstractSectorArray{T,S,N}

Unfused N-D data tensor for abelian symmetries. Stores a dense data array plus one `SectorRange`
per axis with a codomain/domain split (`NC` codomain legs, `ND` domain legs, `NC + ND == N`).
Implements the Wigner-Eckart decomposition: the full tensor is the Kronecker product of the
structural [`AbelianSectorDelta`](@ref) (`sector`) with the data array (reduced matrix elements).
The all-codomain case (`NC == N`) is the block an `AbelianGradedArray` yields.
"""
struct AbelianSectorArray{T, S <: SectorRange, N, A <: AbstractArray{T, N}, NC, ND} <:
struct AbelianSectorArray{T, S <: SectorRange, N, NC, ND, A <: AbstractArray{T, N}} <:
AbstractSectorArray{T, S, N}
data::A
sectors_codomain::NTuple{NC, S}
sectors_domain::NTuple{ND, S}
function AbelianSectorArray{T, S, N, A, NC, ND}(
function AbelianSectorArray{T, S, N, NC, ND, A}(
data::A, sectors_codomain::NTuple{NC, S}, sectors_domain::NTuple{ND, S}
) where {T, S <: SectorRange, N, A <: AbstractArray{T, N}, NC, ND}
) where {T, S <: SectorRange, N, NC, ND, A <: AbstractArray{T, N}}
NC + ND == N ||
throw(ArgumentError("codomain ($NC) + domain ($ND) legs must equal N ($N)"))
return new{T, S, N, A, NC, ND}(data, sectors_codomain, sectors_domain)
return new{T, S, N, NC, ND, A}(data, sectors_codomain, sectors_domain)
end
end

Expand All @@ -30,7 +30,7 @@ function AbelianSectorArray(
data::AbstractArray{T, N},
sectors_codomain::NTuple{NC, S}, sectors_domain::NTuple{ND, S}
) where {T, S <: SectorRange, N, NC, ND}
return AbelianSectorArray{T, S, N, typeof(data), NC, ND}(
return AbelianSectorArray{T, S, N, NC, ND, typeof(data)}(
data, sectors_codomain, sectors_domain
)
end
Expand All @@ -42,7 +42,7 @@ function AbelianSectorArray{T, S, N}(
data::AbstractArray{T, N},
sectors_codomain::NTuple{NC, S}, sectors_domain::NTuple{ND, S}
) where {T, S <: SectorRange, N, NC, ND}
return AbelianSectorArray{T, S, N, typeof(data), NC, ND}(
return AbelianSectorArray{T, S, N, NC, ND, typeof(data)}(
data, sectors_codomain, sectors_domain
)
end
Expand All @@ -61,7 +61,7 @@ end
function AbelianSectorArray(
data::AbstractArray{T}, delta::AbelianSectorDelta{<:Any, S, N, NC, ND}
) where {T, S, N, NC, ND}
return AbelianSectorArray{T, S, N, typeof(data), NC, ND}(
return AbelianSectorArray{T, S, N, NC, ND, typeof(data)}(
data, delta.sectors_codomain, delta.sectors_domain
)
end
Expand All @@ -73,25 +73,25 @@ function AbelianSectorArray{T}(
) where {T}
N = length(axs)
S = sectortype(eltype(axs))
return AbelianSectorArray{T, S, N, Array{T, N}, N, 0}(
return AbelianSectorArray{T, S, N, N, 0, Array{T, N}}(
similar(Array{T, N}, data.(axs)), sector.(axs), ()
)
end

const AbelianSectorVector{T, S <: SectorRange, A <: AbstractVector{T}} =
AbelianSectorArray{T, S, 1, A, NC, ND} where {NC, ND}
const AbelianSectorMatrix{T, S <: SectorRange, A <: AbstractMatrix{T}} =
AbelianSectorArray{T, S, 2, A, NC, ND} where {NC, ND}
const AbelianSectorVector{T, S <: SectorRange, NC, ND, A <: AbstractVector{T}} =
AbelianSectorArray{T, S, 1, NC, ND, A}
const AbelianSectorMatrix{T, S <: SectorRange, NC, ND, A <: AbstractMatrix{T}} =
AbelianSectorArray{T, S, 2, NC, ND, A}

# Accessors

# Kronecker factor decomposition: AbelianSectorArray = sector ⊗ data. `sector` wraps the stored
# codomain/domain sector tuples in a delta, so `sector_kron(sector(a), data(a)) === a`.
function sector(sa::AbelianSectorArray{T, S, N, A, NC, ND}) where {T, S, N, A, NC, ND}
function sector(sa::AbelianSectorArray{T, S, N, NC, ND, A}) where {T, S, N, NC, ND, A}
return AbelianSectorDelta{T, S, N, NC, ND}(sa.sectors_codomain, sa.sectors_domain)
end

datatype(::Type{<:AbelianSectorArray{T, S, N, A, NC, ND}}) where {T, S, N, A, NC, ND} = A
datatype(::Type{<:AbelianSectorArray{T, S, N, NC, ND, A}}) where {T, S, N, NC, ND, A} = A

function Base.copy(a::AbelianSectorArray)
return AbelianSectorArray(copy(data(a)), a.sectors_codomain, a.sectors_domain)
Expand All @@ -108,9 +108,9 @@ function Base.similar(
end

function Base.convert(
::Type{AbelianSectorArray{T₁, S, N, A, NC, ND}},
x::AbelianSectorArray{T₂, S, N, B, NC, ND}
)::AbelianSectorArray{T₁, S, N, A, NC, ND} where {T₁, T₂, S, N, A, B, NC, ND}
::Type{AbelianSectorArray{T₁, S, N, NC, ND, A}},
x::AbelianSectorArray{T₂, S, N, NC, ND, B}
)::AbelianSectorArray{T₁, S, N, NC, ND, A} where {T₁, T₂, S, N, NC, ND, A, B}
A === B && return x
return AbelianSectorArray(convert(A, data(x)), sector(x))
end
Expand All @@ -137,7 +137,7 @@ end
# dualized is filled by a single `op = conj` permute-add over the identity biperm; the fermionic
# leg-reversal sign rides `bipermutedimsopadd!` (folded in by `fermion_permutation_phase`), which a
# bare data `conj` would drop.
function Base.conj(a::AbelianSectorArray{T, S, N, <:Any, NC, ND}) where {T, S, N, NC, ND}
function Base.conj(a::AbelianSectorArray{T, S, N, NC, ND}) where {T, S, N, NC, ND}
dest = AbelianSectorArray{T, S, N}(
similar(data(a)), map(dual, a.sectors_codomain), map(dual, a.sectors_domain)
)
Expand Down
2 changes: 1 addition & 1 deletion src/abeliansectordelta.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
AbelianSectorDelta{T,S<:SectorRange,N,NC,ND} <: AbstractSectorDelta{T, S, N}
AbelianSectorDelta{T,S<:SectorRange,N,NC,ND} <: AbstractSectorDelta{T,S,N}

Unfused N-D structural tensor for abelian symmetries. Stores one `SectorRange` per axis,
split into `NC` codomain legs and `ND` domain legs (`NC + ND == N`); the all-codomain case
Expand Down
37 changes: 22 additions & 15 deletions src/fusionarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ using Random: Random, AbstractRNG
using TensorKit: TensorKit as TK

"""
FusionArray{T,S,N} <: AbstractGradedArray{T,S,N}
FusionArray{T,S,N,NC,ND,M} <: AbstractGradedArray{T,S,N}

Always-fused symmetric array: an `N`-dimensional graded array with a codomain/domain split,
backed by a matricized [`FusedGradedMatrix`](@ref). The external axes are `GradedOneTo` and may be
unfused or unsorted (a sector repeated, or out of `SectorRange` order); the `matricized` backing is
always over the fused-sorted coupled space, and the per-leg sort permutation relates the two.
Always-fused symmetric array: an `N`-dimensional graded array split into `NC` codomain and `ND`
domain legs (`NC + ND == N`), backed by a matricized [`FusedGradedMatrix`](@ref). The external axes
are `GradedOneTo` and may be unfused or unsorted (a sector repeated, or out of `SectorRange` order);
the `matricized` backing is always over the fused-sorted coupled space, and the per-leg sort
permutation relates the two.
"""
struct FusionArray{
T, S, N, M <: FusedGradedMatrix{T, S}, NC, ND,
T, S, N, NC, ND, M <: FusedGradedMatrix{T, S},
} <: AbstractGradedArray{T, S, N}
matricized::M
axes_codomain::NTuple{NC, GradedOneTo{S}}
Expand All @@ -27,7 +28,7 @@ struct FusionArray{
axes_codomain::NTuple{NC, GradedOneTo{S}},
axes_domain::NTuple{ND, GradedOneTo{S}}
) where {T, S, NC, ND}
return new{T, S, NC + ND, typeof(matricized), NC, ND}(
return new{T, S, NC + ND, NC, ND, typeof(matricized)}(
matricized, axes_codomain, axes_domain
)
end
Expand All @@ -50,13 +51,19 @@ ndims_domain(fa::FusionArray) = length(axes_domain(fa))
# matrix directly (see `matricize(::FusionArrayFusionStyle, …)` for re-splitting to another).
TensorAlgebra.matricize(fa::FusionArray) = fa.matricized

# Rank aliases (any codomain/domain split), mirroring `AbelianSectorVector`/`AbelianSectorMatrix`.
const FusionVector{T, S, NC, ND, M <: FusedGradedMatrix{T, S}} =
FusionArray{T, S, 1, NC, ND, M}
const FusionMatrix{T, S, NC, ND, M <: FusedGradedMatrix{T, S}} =
FusionArray{T, S, 2, NC, ND, M}

# ============================ block indexing (unique fusion) ============================
# Unique fusion only: for non-abelian symmetry a `Block`'s external leg sectors don't pin down the
# block. The returned `AbelianSectorArray` is a view into the block's strided data, so get/set writes
# back in place.

function viewblock(
a::FusionArray{T, S, N, <:Any, NC, ND},
a::FusionArray{T, S, N, NC, ND},
I::Block{N}
) where {T, S, N, NC, ND}
require_unique_fusion(a)
Expand Down Expand Up @@ -103,8 +110,8 @@ end
# hardcoding `Array{T, N}`, so non-`Array` storage (GPU, etc.) is preserved — e.g. via
# `Base.promote_op` on `view(A, ::Block)` (the actual returned block type). Also only well-defined
# for unique fusion (blocks are `AbelianSectorArray` only then); tie it to that guard.
function blocktype(::Type{<:FusionArray{T, S, N, <:Any, NC, ND}}) where {T, S, N, NC, ND}
return AbelianSectorArray{T, S, N, Array{T, N}, NC, ND}
function blocktype(::Type{<:FusionArray{T, S, N, NC, ND}}) where {T, S, N, NC, ND}
return AbelianSectorArray{T, S, N, NC, ND, Array{T, N}}
end
blocktype(a::FusionArray) = blocktype(typeof(a))

Expand Down Expand Up @@ -232,7 +239,7 @@ end
# ============================ matrix product ============================
# Matrix-matrix product as a contraction over the shared leg, mirroring `AbelianGradedMatrix`. The
# generic `LinearAlgebra` matmul scalar-indexes, which errors on forbidden blocks.
function Base.:*(a::FusionArray{<:Any, <:Any, 2}, b::FusionArray{<:Any, <:Any, 2})
function Base.:*(a::FusionMatrix, b::FusionMatrix)
return TensorAlgebra.contract((1, 3), a, (1, 2), b, (2, 3))
end

Expand All @@ -243,14 +250,14 @@ end
# are not matrices: their dense form is split-dependent, so `Array(a')` would not be `adjoint(Array(a))`
# and the Gram product would not typecheck. This is a working matrix `adjoint` where
# `AbelianGradedArray` has none.
function Base.adjoint(fa::FusionArray{<:Any, <:Any, 2, <:Any, 1, 1})
function Base.adjoint(fa::FusionArray{<:Any, <:Any, 2, 1, 1})
return FusionArray(adjoint(matricize(fa)), axes_domain(fa), axes_codomain(fa))
end

# A 2-index `FusionArray` with any other split is not a matrix. It already errors through the generic
# `AbstractGradedArray` fallback rather than building a broken lazy `Adjoint`, but give a clearer
# message naming the `(1, 1)` requirement.
function Base.adjoint(fa::FusionArray{<:Any, <:Any, 2})
function Base.adjoint(fa::FusionMatrix)
return error(
"`adjoint` of a `FusionArray` requires a (1, 1) codomain/domain split; this matrix has \
split ($(ndims_codomain(fa)), $(ndims_domain(fa))). Bend it to a matrix first."
Expand All @@ -263,7 +270,7 @@ end
# same-split destination (per-leg axes dualized) with a single `op = conj` permute-add over the
# identity biperm, so the TensorKit-backed transform folds in the leg-reversal fermion sign and the
# non-abelian recoupling that a bare block conjugation would drop.
function Base.conj(fa::FusionArray{<:Any, <:Any, <:Any, <:Any, NC, ND}) where {NC, ND}
function Base.conj(fa::FusionArray{<:Any, <:Any, <:Any, NC, ND}) where {NC, ND}
dest = TensorAlgebra.similar_map(
fa, map(dual, axes_codomain(fa)), map(dual, axes_domain(fa))
)
Expand All @@ -280,7 +287,7 @@ end
# that include point. Avoids MatrixAlgebraKit's native path, which scalar-indexes and hits a
# forbidden block.
for f in BARE_MATRIX_FACTORIZATIONS
@eval function MAK.$f(m::FusionArray{<:Any, <:Any, 2}; kwargs...)
@eval function MAK.$f(m::FusionMatrix; kwargs...)
return TensorAlgebra.$f(m, (1,), (2,); kwargs...)
end
end
Expand Down
2 changes: 1 addition & 1 deletion src/sectoridentity.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
SectorIdentity{T,S<:SectorRange} <: AbstractSectorDelta{T, S, 2}
SectorIdentity{T,S<:SectorRange} <: AbstractSectorDelta{T,S,2}

Fused 2D structural factor for a single coupled sector. By Schur's lemma, the
structural part of each block in the fused (matricized) basis is the identity
Expand Down
2 changes: 1 addition & 1 deletion src/sectoronesvector.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
SectorOnesVector{T,S<:SectorRange} <: AbstractSectorDelta{T, S, 1}
SectorOnesVector{T,S<:SectorRange} <: AbstractSectorDelta{T,S,1}

Fused 1-D structural factor for a single coupled sector: the all-ones vector whose length is the
sector's quantum dimension. It is the diagonal of the [`SectorIdentity`](@ref) that a
Expand Down
9 changes: 5 additions & 4 deletions test/test_sectorarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ using Test: @test, @test_throws, @testset
@testset "Construction from SectorRange tuples" begin
data = [1.0 2.0; 3.0 4.0]
sa = AbelianSectorArray(data, (U1(1), conj(U1(-1))))
@test sa isa AbelianSectorArray{Float64, U1, 2, Matrix{Float64}}
@test sa isa AbelianSectorArray{Float64, U1, 2, <:Any, <:Any, Matrix{Float64}}
@test sa isa AbstractArray{Float64, 2}
end

Expand Down Expand Up @@ -67,7 +67,7 @@ using Test: @test, @test_throws, @testset
@testset "rank-0 (scalar) array" begin
# A rank-0 array has an empty `sectors` tuple, so `sector` and the delta/data
# constructor take the sector type from the type rather than inferring it.
sa = AbelianSectorArray{Float64, U1, 0, Array{Float64, 0}, 0, 0}(fill(2.0), (), ())
sa = AbelianSectorArray{Float64, U1, 0, 0, 0, Array{Float64, 0}}(fill(2.0), (), ())
@test ndims(sa) == 0
@test sectortype(sa) === U1
@test sa[] == 2.0
Expand All @@ -77,7 +77,8 @@ using Test: @test, @test_throws, @testset
@test sectortype(sd) === U1

rebuilt = AbelianSectorArray(fill(5.0), sd)
@test rebuilt isa AbelianSectorArray{Float64, U1, 0, Array{Float64, 0}}
@test rebuilt isa
AbelianSectorArray{Float64, U1, 0, <:Any, <:Any, Array{Float64, 0}}
@test rebuilt[] == 5.0

# The convenience constructors infer `S` from the axes/sectors, which is
Expand Down Expand Up @@ -114,7 +115,7 @@ using Test: @test, @test_throws, @testset
@testset "convert" begin
data = [1 2; 3 4]
sa = AbelianSectorArray(data, (U1(0), U1(1)))
T = AbelianSectorArray{Float64, U1, 2, Matrix{Float64}, 2, 0}
T = AbelianSectorArray{Float64, U1, 2, 2, 0, Matrix{Float64}}
sa2 = convert(T, sa)
@test eltype(sa2) == Float64
@test sa2[1, 1] === 1.0
Expand Down