From a35dc6b4b5db08a27e6b0778714d11a699a517e0 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 10 Jul 2026 12:55:31 -0400 Subject: [PATCH 1/9] Vendor the concatenate interface and add directsum Move the generic concatenate machinery into TensorAlgebra as the single source of truth, included flat (no Concatenate submodule): the lazy Concatenated{Style,Dims,Args}, concatenate/cat/cat!, the cat_axes/cat_axis/cat_style helpers, and the generic copyto!(::Concatenated{Nothing}) block-placement fallback. Reuse TensorAlgebra's own unval and zero!, and rename the internal dims/style accessors to concatenated_dims and concatenated_style to avoid occupying those generic names at the top level. Add directsum along given dims. Its FusionStyle is resolved by directsum_style, which defaults to ReshapeFusion for every backend (a straight cat: a valid direct sum for a dense or AbelianGradedArray backing, no basis rotation). A backend overloads directsum_style to opt its arrays into a fusing or rotating variant such as SectorFusion, or the style can be passed explicitly, like matricize(style, ...). directsum_style is deliberately distinct from FusionStyle(a) (which matricize uses) and from cat_style (the Concatenated broadcast style). Downstream packages (SparseArraysBase, GradedArrays) will consolidate onto this copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 2 +- src/TensorAlgebra.jl | 4 +- src/concatenate.jl | 194 +++++++++++++++++++++++++++++++++++++++ src/directsum.jl | 18 ++++ test/test_concatenate.jl | 52 +++++++++++ test/test_exports.jl | 8 +- 6 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 src/concatenate.jl create mode 100644 src/directsum.jl create mode 100644 test/test_concatenate.jl diff --git a/Project.toml b/Project.toml index ba2eff7..2a54cf1 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "TensorAlgebra" uuid = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" -version = "0.17.4" +version = "0.17.5" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index a24d6c8..6285925 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -9,7 +9,7 @@ export contract, contract!, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public biperm, bipartition, contractopadd!, data, datatype, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" + "public biperm, bipartition, cat, cat!, concatenate, concatenated, Concatenated, contractopadd!, data, datatype, directsum, directsum_style, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" ) ) end @@ -21,6 +21,8 @@ include("MatrixAlgebra.jl") include("bituple.jl") include("permutedimsadd.jl") include("matricize.jl") +include("concatenate.jl") +include("directsum.jl") include("diagonal.jl") include("to_range.jl") include("contract/contractalgorithm.jl") diff --git a/src/concatenate.jl b/src/concatenate.jl new file mode 100644 index 0000000..e98247e --- /dev/null +++ b/src/concatenate.jl @@ -0,0 +1,194 @@ +# Concatenation interface, vendored flat from the `Concatenate` module that previously lived in +# SparseArraysBase / FunctionImplementations (both carried identical copies). TensorAlgebra owns it +# here as the single source of truth: `cat`/`cat!`/`concatenate` and the lazy `Concatenated` object, +# with a generic `copyto!(::Concatenated{Nothing})` fallback. Backends (SparseArraysBase, +# GradedArrays, a TensorKit ext) specialize `copyto!(dest, ::Concatenated{<:TheirStyle})` for +# block-level placement without scalar indexing. +# +# Alternative implementation for `Base.cat` through `cat`/`cat!`. This is mostly a copy of the Base +# implementation, with the main difference being that the destination is chosen based on all inputs +# instead of just the first. There is an intermediate representation in terms of a `Concatenated` +# object, reminiscent of how Broadcast works. Destination selection can be customized through +# `Base.similar(::Concatenated{Style}, ::Type{T}, axes)`, and the operation itself through +# `Base.copy`/`Base.copyto!` on a `Concatenated`. + +import Base.Broadcast as BC +using Base: promote_eltypeof + +function _Concatenated end + +# Lazy representation of the concatenation of various `Args` along `Dims`, in order to +# provide hooks to customize the implementation. +struct Concatenated{Style, Dims, Args <: Tuple} + style::Style + dims::Val{Dims} + args::Args + global @inline function _Concatenated( + style::Style, dims::Val{Dims}, args::Args + ) where {Style, Dims, Args <: Tuple} + return new{Style, Dims, Args}(style, dims, args) + end +end + +function Concatenated( + style::Union{BC.AbstractArrayStyle, Nothing}, dims::Val, args::Tuple + ) + return _Concatenated(style, dims, args) +end +function Concatenated(dims::Val, args::Tuple) + return Concatenated(cat_style(dims, args...), dims, args) +end +function Concatenated{Style}( + dims::Val, args::Tuple + ) where {Style <: Union{BC.AbstractArrayStyle, Nothing}} + return Concatenated(Style(), dims, args) +end + +concatenated_dims(::Concatenated{<:Any, D}) where {D} = D +concatenated_style(concat::Concatenated) = getfield(concat, :style) + +concatenated(dims, args...) = concatenated(Val(dims), args...) +concatenated(dims::Val, args...) = Concatenated(dims, args) + +function Base.convert( + ::Type{Concatenated{NewStyle}}, concat::Concatenated{<:Any, Dims, Args} + ) where {NewStyle, Dims, Args} + return Concatenated{NewStyle}( + concat.dims, concat.args + )::Concatenated{NewStyle, Dims, Args} +end + +# allocating the destination container +# ------------------------------------ +Base.similar(concat::Concatenated) = similar(concat, eltype(concat)) +Base.similar(concat::Concatenated, ::Type{T}) where {T} = similar(concat, T, axes(concat)) +function Base.similar(concat::Concatenated, ax) + return similar(concat, eltype(concat), ax) +end + +function Base.similar(concat::Concatenated, ::Type{T}, ax) where {T} + # Convert to a broadcasted to leverage its similar implementation. + bc = BC.Broadcasted(concatenated_style(concat), identity, concat.args, ax) + return similar(bc, T) +end + +function cat_axis( + a1::AbstractUnitRange, a2::AbstractUnitRange, a_rest::AbstractUnitRange... + ) + return cat_axis(cat_axis(a1, a2), a_rest...) +end +function cat_axis(a1::AbstractUnitRange, a2::AbstractUnitRange) + first(a1) == first(a2) == 1 || throw(ArgumentError("Concatenated axes must start at 1")) + return Base.OneTo(length(a1) + length(a2)) +end + +function cat_ndims(dims, as::AbstractArray...) + return max(maximum(dims), maximum(ndims, as)) +end +function cat_ndims(dims::Val, as::AbstractArray...) + return cat_ndims(unval(dims), as...) +end + +function cat_axes(dims, a::AbstractArray, as::AbstractArray...) + return ntuple(cat_ndims(dims, a, as...)) do dim + return if dim in dims + cat_axis(map(Base.Fix2(axes, dim), (a, as...))...) + else + axes(a, dim) + end + end +end +function cat_axes(dims::Val, as::AbstractArray...) + return cat_axes(unval(dims), as...) +end + +function cat_style(dims, as::AbstractArray...) + N = cat_ndims(dims, as...) + return typeof(BC.combine_styles(as...))(Val(N)) +end + +Base.eltype(concat::Concatenated) = promote_eltypeof(concat.args...) +Base.axes(concat::Concatenated) = cat_axes(concatenated_dims(concat), concat.args...) +Base.size(concat::Concatenated) = length.(axes(concat)) +Base.ndims(concat::Concatenated) = cat_ndims(concatenated_dims(concat), concat.args...) + +# Main logic +# ---------- +# Concatenate the supplied `args` along dimensions `dims`. +concatenate(dims, args...) = Base.materialize(concatenated(dims, args...)) + +# Concatenate the supplied `args` along dimensions `dims`. +cat(args...; dims) = concatenate(dims, args...) +Base.materialize(concat::Concatenated) = copy(concat) + +# Concatenate the supplied `args` along dimensions `dims`, placing the result into `dest`. +function cat!(dest, args...; dims) + Base.materialize!(dest, concatenated(dims, args...)) + return dest +end +Base.materialize!(dest, concat::Concatenated) = copyto!(dest, concat) + +Base.copy(concat::Concatenated) = copyto!(similar(concat), concat) + +# The following is largely copied from the Base implementation of `Base.cat`, see: +# https://github.com/JuliaLang/julia/blob/885b1cd875f101f227b345f681cc36879124d80d/base/abstractarray.jl#L1778-L1887 +_copy_or_fill!(A, inds, x) = fill!(view(A, inds...), x) +_copy_or_fill!(A, inds, x::AbstractArray) = (A[inds...] = x) + +cat_size(A) = (1,) +cat_size(A::AbstractArray) = size(A) +cat_size(A, d) = 1 +cat_size(A::AbstractArray, d) = size(A, d) + +cat_indices(A, d) = Base.OneTo(1) +cat_indices(A::AbstractArray, d) = axes(A, d) + +function __cat!(A, shape, catdims, X...) + return __cat_offset!(A, shape, catdims, ntuple(zero, length(shape)), X...) +end +function __cat_offset!(A, shape, catdims, offsets, x, X...) + # splitting the "work" on x from X... may reduce latency (fewer costly specializations) + newoffsets = __cat_offset1!(A, shape, catdims, offsets, x) + return __cat_offset!(A, shape, catdims, newoffsets, X...) +end +__cat_offset!(A, shape, catdims, offsets) = A +function __cat_offset1!(A, shape, catdims, offsets, x) + inds = ntuple(length(offsets)) do i + return if (i <= length(catdims) && catdims[i]) + offsets[i] .+ cat_indices(x, i) + else + 1:shape[i] + end + end + _copy_or_fill!(A, inds, x) + newoffsets = ntuple(length(offsets)) do i + return if (i <= length(catdims) && catdims[i]) + offsets[i] + cat_size(x, i) + else + offsets[i] + end + end + return newoffsets +end + +dims2cat(dims::Val) = dims2cat(unval(dims)) +function dims2cat(dims) + if any(≤(0), dims) + throw(ArgumentError("All cat dimensions must be positive integers, but got $dims")) + end + return ntuple(in(dims), maximum(dims)) +end + +# default falls back to replacing style with Nothing +# this permits specializing on typeof(dest) without ambiguities +# Note: this needs to be defined for AbstractArray specifically to avoid ambiguities with Base. +@inline function Base.copyto!(dest::AbstractArray, concat::Concatenated) + return copyto!(dest, convert(Concatenated{Nothing}, concat)) +end + +function Base.copyto!(dest::AbstractArray, concat::Concatenated{Nothing}) + catdims = dims2cat(concatenated_dims(concat)) + shape = size(concat) + count(!iszero, catdims)::Int > 1 && zero!(dest) + return __cat!(dest, shape, catdims, concat.args...) +end diff --git a/src/directsum.jl b/src/directsum.jl new file mode 100644 index 0000000..22d4279 --- /dev/null +++ b/src/directsum.jl @@ -0,0 +1,18 @@ +# `directsum` — the direct sum along `dims`, parameterized by a `FusionStyle` (the same trait +# `matricize` uses) so the fuse/rotate behavior is selectable. The style is resolved by +# `directsum_style`, which defaults to `ReshapeFusion` for every backend: a straight `cat` +# (block-concatenation), a valid direct sum for a dense or an `AbelianGradedArray` backing +# (concat-order axes, no basis rotation). A backend opts its arrays into a fusing/rotating direct +# sum by overloading `directsum_style` (e.g. to `SectorFusion`, which merges+sorts the summed +# sectors onto a single canonical basis) — that path is a deferred opt-in. The style can also be +# passed explicitly to override, exactly like `matricize(style, ...)`. +# +# `directsum_style` is deliberately distinct from `FusionStyle(a)`: a graded array reports +# `SectorFusion` there (what `matricize` needs), but its `directsum` default is a plain `cat`. It is +# also distinct from `cat_style`, which resolves the `Concatenated` *broadcast* style (which +# `copyto!` placement fires), an orthogonal axis. +directsum_style(as...) = ReshapeFusion() + +function directsum end +directsum(as...; dims) = directsum(directsum_style(as...), as...; dims) +directsum(::ReshapeFusion, as...; dims) = cat(as...; dims) diff --git a/test/test_concatenate.jl b/test/test_concatenate.jl new file mode 100644 index 0000000..31a33f0 --- /dev/null +++ b/test/test_concatenate.jl @@ -0,0 +1,52 @@ +using TensorAlgebra: + TensorAlgebra, Concatenated, ReshapeFusion, cat, cat!, concatenate, directsum +using Test: @test, @testset + +@testset "cat / concatenate" begin + a = reshape(collect(1.0:6.0), 2, 3) + b = reshape(collect(7.0:12.0), 2, 3) + + # Single-dim concatenation matches `Base.cat`. + @test cat(a, b; dims = 1) == vcat(a, b) + @test cat(a, b; dims = 2) == hcat(a, b) + @test concatenate(1, a, b) == vcat(a, b) + + # Multi-dim concatenation is the block-diagonal placement (off-diagonal blocks zeroed). + c = cat(a, b; dims = (1, 2)) + ref = zeros(4, 6) + ref[1:2, 1:3] .= a + ref[3:4, 4:6] .= b + @test c == ref + + # `cat!` writes into a provided destination. + dest = zeros(4, 6) + @test cat!(dest, a, b; dims = (1, 2)) === dest + @test dest == ref + + # Element type is promoted across all inputs, not taken from the first. + @test eltype(cat(a, b .+ 0im; dims = 1)) == ComplexF64 + + # The lazy representation carries the concatenated axes. + @test axes(Concatenated(Val((1, 2)), (a, b))) == (Base.OneTo(4), Base.OneTo(6)) +end + +@testset "directsum (dense: ReshapeFusion -> cat)" begin + a = reshape(collect(1.0:6.0), 2, 3) + b = reshape(collect(7.0:12.0), 2, 3) + + # Dense arrays carry `ReshapeFusion`, so `directsum` is exactly `cat` (no rotation needed). + @test TensorAlgebra.FusionStyle(a) === ReshapeFusion() + @test directsum(a, b; dims = (1, 2)) == cat(a, b; dims = (1, 2)) + @test directsum(a, b; dims = 1) == vcat(a, b) + + # N-ary: each summand lands in its own diagonal hyper-block. + d = reshape(collect(1.0:8.0), 2, 4) + s = directsum(a, b, d; dims = (1, 2)) + @test size(s) == (6, 10) + @test s[1:2, 1:3] == a + @test s[3:4, 4:6] == b + @test s[5:6, 7:10] == d + + # Passing the style explicitly matches the resolved-from-array path. + @test directsum(ReshapeFusion(), a, b; dims = (1, 2)) == directsum(a, b; dims = (1, 2)) +end diff --git a/test/test_exports.jl b/test/test_exports.jl index 7975506..d468494 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -38,9 +38,11 @@ using Test: @test, @testset append!( exports, [ - :biperm, :bipartition, :contractopadd!, :data, :datatype, :flattenlinear, - :label_type, :matricizeopperm, :permutedims, :permutedims!, :scalar, - :similar_map, :to_range, :tr, :tryflattenlinear, :ungrade, :zero!, :scale!, + :biperm, :bipartition, :cat, :cat!, :concatenate, :concatenated, + :Concatenated, :contractopadd!, :data, :datatype, :directsum, + :directsum_style, :flattenlinear, :label_type, + :matricizeopperm, :permutedims, :permutedims!, :scalar, :similar_map, + :to_range, :tr, :tryflattenlinear, :ungrade, :zero!, :scale!, :permuteddims, :PermutedDims, ] ) From ba144b9bd670c01954b0b1009d4788e3e807aa1c Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 13 Jul 2026 21:00:00 -0400 Subject: [PATCH 2/9] Make directsum_style a pairwise-reduction trait `directsum_style` now resolves the direct-sum style over several arguments by folding a per-argument `directsum_style(a)` with a binary `directsum_style(s1, s2)` combine, mirroring how `Base.Broadcast` folds `BroadcastStyle`. Two of the same style combine to that style and any mismatch falls back to `ReshapeFusion`, so heterogeneous inputs resolve to a well-defined combined style. This replaces the `directsum_style(as...) = ReshapeFusion()` vararg stub, which ignored its arguments and so was no better than plain vararg dispatch. The default is still `ReshapeFusion` for every backend, so behavior is unchanged. Its binary combine is kept independent of `FusionStyle`'s so the two traits can diverge. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/directsum.jl | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/directsum.jl b/src/directsum.jl index 22d4279..628e7a0 100644 --- a/src/directsum.jl +++ b/src/directsum.jl @@ -1,18 +1,36 @@ # `directsum` — the direct sum along `dims`, parameterized by a `FusionStyle` (the same trait -# `matricize` uses) so the fuse/rotate behavior is selectable. The style is resolved by -# `directsum_style`, which defaults to `ReshapeFusion` for every backend: a straight `cat` +# `matricize` uses) so the fuse/rotate behavior is selectable. The style for a direct sum over +# several arguments is resolved by folding the per-argument `directsum_style` with the binary +# `directsum_style(s1, s2)` combine, mirroring how `Base.Broadcast` folds `BroadcastStyle`. +# +# `directsum_style` defaults to `ReshapeFusion` for every backend: a straight `cat` # (block-concatenation), a valid direct sum for a dense or an `AbelianGradedArray` backing # (concat-order axes, no basis rotation). A backend opts its arrays into a fusing/rotating direct -# sum by overloading `directsum_style` (e.g. to `SectorFusion`, which merges+sorts the summed -# sectors onto a single canonical basis) — that path is a deferred opt-in. The style can also be -# passed explicitly to override, exactly like `matricize(style, ...)`. +# sum by overloading `directsum_style(::Type)` (e.g. to `SectorFusion`, which merges and sorts the +# summed sectors onto a single canonical basis). The style can also be passed explicitly to +# override, exactly like `matricize(style, ...)`. # # `directsum_style` is deliberately distinct from `FusionStyle(a)`: a graded array reports -# `SectorFusion` there (what `matricize` needs), but its `directsum` default is a plain `cat`. It is -# also distinct from `cat_style`, which resolves the `Concatenated` *broadcast* style (which +# `SectorFusion` there (what `matricize` needs), but its `directsum` default is a plain `cat`. Its +# binary combine is likewise kept independent of `FusionStyle`'s so the two traits can diverge. It +# is also distinct from `cat_style`, which resolves the `Concatenated` broadcast style (which # `copyto!` placement fires), an orthogonal axis. -directsum_style(as...) = ReshapeFusion() + +# Per-argument direct-sum style. +directsum_style(x) = directsum_style(typeof(x)) +directsum_style(::Type{<:AbstractArray}) = ReshapeFusion() + +# Binary combine of two styles, associative and commutative like `FusionStyle`'s: two of the same +# style combine to that style, and any mismatch falls back to `ReshapeFusion`. +directsum_style(style1::Style, style2::Style) where {Style <: FusionStyle} = Style() +directsum_style(style1::FusionStyle, style2::FusionStyle) = ReshapeFusion() + +# Fold the per-argument styles pairwise with the binary combine. +combine_directsum_style(a) = directsum_style(a) +function combine_directsum_style(a, as...) + return directsum_style(directsum_style(a), combine_directsum_style(as...)) +end function directsum end -directsum(as...; dims) = directsum(directsum_style(as...), as...; dims) +directsum(as...; dims) = directsum(combine_directsum_style(as...), as...; dims) directsum(::ReshapeFusion, as...; dims) = cat(as...; dims) From 89f397a2fdaa422d4391f8f78dada52327af50c2 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 13 Jul 2026 21:31:43 -0400 Subject: [PATCH 3/9] Replace the Concatenated wrapper with a function interface The concatenation interface no longer builds a lazy `Concatenated{Style,Dims,Args}` object. Concatenation does not fuse, so there was nothing for the wrapper to defer, and its only real jobs (choosing the destination and the placement per style, and computing the result axes) are expressed directly as functions. `cat`/`cat!`/`concatenate` now resolve the combined `cat_style`, allocate with `cat_similar(style, T, ax, args...)`, and place with `cat_copyto!(dest, style, dims, args...)`. Backends customize concatenation by overloading `cat_similar`/`cat_copyto!` on their style, replacing the previous overloads of `Base.similar`/`Base.copyto!` on `Concatenated{<:TheirStyle}`. The default `cat_copyto!` strips the style to `nothing`, which keeps the "specialize on the destination type without ambiguity against the style dispatch" property the `Concatenated{Nothing}` conversion used to provide. `Concatenated` and `concatenated` are dropped from the public interface. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/TensorAlgebra.jl | 2 +- src/concatenate.jl | 130 ++++++++++++--------------------------- test/test_concatenate.jl | 6 +- test/test_exports.jl | 4 +- 4 files changed, 45 insertions(+), 97 deletions(-) diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index 6285925..33198d9 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -9,7 +9,7 @@ export contract, contract!, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public biperm, bipartition, cat, cat!, concatenate, concatenated, Concatenated, contractopadd!, data, datatype, directsum, directsum_style, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" + "public biperm, bipartition, cat, cat!, cat_copyto!, cat_similar, concatenate, contractopadd!, data, datatype, directsum, directsum_style, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" ) ) end diff --git a/src/concatenate.jl b/src/concatenate.jl index e98247e..f3251c8 100644 --- a/src/concatenate.jl +++ b/src/concatenate.jl @@ -1,77 +1,23 @@ # Concatenation interface, vendored flat from the `Concatenate` module that previously lived in -# SparseArraysBase / FunctionImplementations (both carried identical copies). TensorAlgebra owns it -# here as the single source of truth: `cat`/`cat!`/`concatenate` and the lazy `Concatenated` object, -# with a generic `copyto!(::Concatenated{Nothing})` fallback. Backends (SparseArraysBase, -# GradedArrays, a TensorKit ext) specialize `copyto!(dest, ::Concatenated{<:TheirStyle})` for -# block-level placement without scalar indexing. +# SparseArraysBase / FunctionImplementations. TensorAlgebra owns it here as the single source of +# truth: `cat`/`cat!`/`concatenate`, plus the backend hooks `cat_similar` (destination allocation) +# and `cat_copyto!` (placement). Both dispatch on the combined `cat_style` of the arguments, so a +# backend (SparseArraysBase, GradedArrays, a TensorKit ext) customizes concatenation by overloading +# `cat_similar`/`cat_copyto!` on its style, without scalar indexing. # -# Alternative implementation for `Base.cat` through `cat`/`cat!`. This is mostly a copy of the Base -# implementation, with the main difference being that the destination is chosen based on all inputs -# instead of just the first. There is an intermediate representation in terms of a `Concatenated` -# object, reminiscent of how Broadcast works. Destination selection can be customized through -# `Base.similar(::Concatenated{Style}, ::Type{T}, axes)`, and the operation itself through -# `Base.copy`/`Base.copyto!` on a `Concatenated`. +# This is a function interface (no lazy `Concatenated` wrapper): concatenation does not fuse, so +# there is nothing for a lazy object to defer. `cat`/`cat!` resolve the style with `cat_style`, +# allocate with `cat_similar`, and place with `cat_copyto!`. Mostly a copy of Base's `cat`, except +# the destination is chosen from all inputs instead of just the first. import Base.Broadcast as BC using Base: promote_eltypeof -function _Concatenated end - -# Lazy representation of the concatenation of various `Args` along `Dims`, in order to -# provide hooks to customize the implementation. -struct Concatenated{Style, Dims, Args <: Tuple} - style::Style - dims::Val{Dims} - args::Args - global @inline function _Concatenated( - style::Style, dims::Val{Dims}, args::Args - ) where {Style, Dims, Args <: Tuple} - return new{Style, Dims, Args}(style, dims, args) - end -end - -function Concatenated( - style::Union{BC.AbstractArrayStyle, Nothing}, dims::Val, args::Tuple - ) - return _Concatenated(style, dims, args) -end -function Concatenated(dims::Val, args::Tuple) - return Concatenated(cat_style(dims, args...), dims, args) -end -function Concatenated{Style}( - dims::Val, args::Tuple - ) where {Style <: Union{BC.AbstractArrayStyle, Nothing}} - return Concatenated(Style(), dims, args) -end - -concatenated_dims(::Concatenated{<:Any, D}) where {D} = D -concatenated_style(concat::Concatenated) = getfield(concat, :style) - -concatenated(dims, args...) = concatenated(Val(dims), args...) -concatenated(dims::Val, args...) = Concatenated(dims, args) - -function Base.convert( - ::Type{Concatenated{NewStyle}}, concat::Concatenated{<:Any, Dims, Args} - ) where {NewStyle, Dims, Args} - return Concatenated{NewStyle}( - concat.dims, concat.args - )::Concatenated{NewStyle, Dims, Args} -end - -# allocating the destination container -# ------------------------------------ -Base.similar(concat::Concatenated) = similar(concat, eltype(concat)) -Base.similar(concat::Concatenated, ::Type{T}) where {T} = similar(concat, T, axes(concat)) -function Base.similar(concat::Concatenated, ax) - return similar(concat, eltype(concat), ax) -end - -function Base.similar(concat::Concatenated, ::Type{T}, ax) where {T} - # Convert to a broadcasted to leverage its similar implementation. - bc = BC.Broadcasted(concatenated_style(concat), identity, concat.args, ax) - return similar(bc, T) -end +_valdims(dims::Val) = dims +_valdims(dims) = Val(dims) +# Concatenation axes and style, computed directly from `dims` and the arguments. +# ------------------------------------------------------------------------------ function cat_axis( a1::AbstractUnitRange, a2::AbstractUnitRange, a_rest::AbstractUnitRange... ) @@ -107,28 +53,33 @@ function cat_style(dims, as::AbstractArray...) return typeof(BC.combine_styles(as...))(Val(N)) end -Base.eltype(concat::Concatenated) = promote_eltypeof(concat.args...) -Base.axes(concat::Concatenated) = cat_axes(concatenated_dims(concat), concat.args...) -Base.size(concat::Concatenated) = length.(axes(concat)) -Base.ndims(concat::Concatenated) = cat_ndims(concatenated_dims(concat), concat.args...) +# Allocate the destination container. +# ----------------------------------- +# Allocate the destination for concatenating `args` along the combined `style`, with element type +# `T` and axes `ax`. Backends override on their style; the default reuses broadcast's `similar` by +# building a `Broadcasted` with the same style and axes. +function cat_similar(style, ::Type{T}, ax, args...) where {T} + return similar(BC.Broadcasted(style, identity, args, ax), T) +end -# Main logic -# ---------- +# Main logic. +# ----------- # Concatenate the supplied `args` along dimensions `dims`. -concatenate(dims, args...) = Base.materialize(concatenated(dims, args...)) +concatenate(dims, args...) = concatenate(_valdims(dims), args...) +function concatenate(dims::Val, args...) + style = cat_style(dims, args...) + dest = cat_similar(style, promote_eltypeof(args...), cat_axes(dims, args...), args...) + return cat_copyto!(dest, style, dims, args...) +end # Concatenate the supplied `args` along dimensions `dims`. cat(args...; dims) = concatenate(dims, args...) -Base.materialize(concat::Concatenated) = copy(concat) # Concatenate the supplied `args` along dimensions `dims`, placing the result into `dest`. function cat!(dest, args...; dims) - Base.materialize!(dest, concatenated(dims, args...)) - return dest + d = _valdims(dims) + return cat_copyto!(dest, cat_style(d, args...), d, args...) end -Base.materialize!(dest, concat::Concatenated) = copyto!(dest, concat) - -Base.copy(concat::Concatenated) = copyto!(similar(concat), concat) # The following is largely copied from the Base implementation of `Base.cat`, see: # https://github.com/JuliaLang/julia/blob/885b1cd875f101f227b345f681cc36879124d80d/base/abstractarray.jl#L1778-L1887 @@ -179,16 +130,13 @@ function dims2cat(dims) return ntuple(in(dims), maximum(dims)) end -# default falls back to replacing style with Nothing -# this permits specializing on typeof(dest) without ambiguities -# Note: this needs to be defined for AbstractArray specifically to avoid ambiguities with Base. -@inline function Base.copyto!(dest::AbstractArray, concat::Concatenated) - return copyto!(dest, convert(Concatenated{Nothing}, concat)) -end - -function Base.copyto!(dest::AbstractArray, concat::Concatenated{Nothing}) - catdims = dims2cat(concatenated_dims(concat)) - shape = size(concat) +# Materialize the concatenation into `dest`. Backends override on their `style`; the default strips +# the style to `nothing` (which lets a backend instead specialize on `typeof(dest)` without +# ambiguity against the style dispatch) and runs the generic offset placement. +cat_copyto!(dest, style, dims, args...) = cat_copyto!(dest, nothing, dims, args...) +function cat_copyto!(dest, ::Nothing, dims, args...) + catdims = dims2cat(dims) + shape = map(length, cat_axes(dims, args...)) count(!iszero, catdims)::Int > 1 && zero!(dest) - return __cat!(dest, shape, catdims, concat.args...) + return __cat!(dest, shape, catdims, args...) end diff --git a/test/test_concatenate.jl b/test/test_concatenate.jl index 31a33f0..f3f3ff6 100644 --- a/test/test_concatenate.jl +++ b/test/test_concatenate.jl @@ -1,5 +1,5 @@ using TensorAlgebra: - TensorAlgebra, Concatenated, ReshapeFusion, cat, cat!, concatenate, directsum + TensorAlgebra, ReshapeFusion, cat, cat!, cat_axes, concatenate, directsum using Test: @test, @testset @testset "cat / concatenate" begin @@ -26,8 +26,8 @@ using Test: @test, @testset # Element type is promoted across all inputs, not taken from the first. @test eltype(cat(a, b .+ 0im; dims = 1)) == ComplexF64 - # The lazy representation carries the concatenated axes. - @test axes(Concatenated(Val((1, 2)), (a, b))) == (Base.OneTo(4), Base.OneTo(6)) + # `cat_axes` computes the concatenated axes from the arguments. + @test cat_axes(Val((1, 2)), a, b) == (Base.OneTo(4), Base.OneTo(6)) end @testset "directsum (dense: ReshapeFusion -> cat)" begin diff --git a/test/test_exports.jl b/test/test_exports.jl index d468494..dbb65ac 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -38,8 +38,8 @@ using Test: @test, @testset append!( exports, [ - :biperm, :bipartition, :cat, :cat!, :concatenate, :concatenated, - :Concatenated, :contractopadd!, :data, :datatype, :directsum, + :biperm, :bipartition, :cat, :cat!, :cat_copyto!, :cat_similar, + :concatenate, :contractopadd!, :data, :datatype, :directsum, :directsum_style, :flattenlinear, :label_type, :matricizeopperm, :permutedims, :permutedims!, :scalar, :similar_map, :to_range, :tr, :tryflattenlinear, :ungrade, :zero!, :scale!, From a0a1ea48a344f8a16683ee713da23f0344cedbba Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 13 Jul 2026 21:43:01 -0400 Subject: [PATCH 4/9] Forward directsum to cat, dropping the directsum_style trait For now `directsum(as...; dims)` is just `cat(as...; dims)`. The `directsum_style` trait (per-argument style plus pairwise combine) had no user yet, since every backend resolved to `ReshapeFusion` and there is no fusing/rotating direct sum implemented, so it was speculative infrastructure. The style selector will come back the same way `matricize` takes a `FusionStyle` once a backend needs the canonicalizing (merge-and-sort) direct sum. `directsum_style` is dropped from the public interface. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/TensorAlgebra.jl | 2 +- src/directsum.jl | 41 ++++++---------------------------------- test/test_concatenate.jl | 11 +++-------- test/test_exports.jl | 2 +- 4 files changed, 11 insertions(+), 45 deletions(-) diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index 33198d9..5d07843 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -9,7 +9,7 @@ export contract, contract!, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public biperm, bipartition, cat, cat!, cat_copyto!, cat_similar, concatenate, contractopadd!, data, datatype, directsum, directsum_style, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" + "public biperm, bipartition, cat, cat!, cat_copyto!, cat_similar, concatenate, contractopadd!, data, datatype, directsum, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" ) ) end diff --git a/src/directsum.jl b/src/directsum.jl index 628e7a0..e87c1cb 100644 --- a/src/directsum.jl +++ b/src/directsum.jl @@ -1,36 +1,7 @@ -# `directsum` — the direct sum along `dims`, parameterized by a `FusionStyle` (the same trait -# `matricize` uses) so the fuse/rotate behavior is selectable. The style for a direct sum over -# several arguments is resolved by folding the per-argument `directsum_style` with the binary -# `directsum_style(s1, s2)` combine, mirroring how `Base.Broadcast` folds `BroadcastStyle`. -# -# `directsum_style` defaults to `ReshapeFusion` for every backend: a straight `cat` -# (block-concatenation), a valid direct sum for a dense or an `AbelianGradedArray` backing -# (concat-order axes, no basis rotation). A backend opts its arrays into a fusing/rotating direct -# sum by overloading `directsum_style(::Type)` (e.g. to `SectorFusion`, which merges and sorts the -# summed sectors onto a single canonical basis). The style can also be passed explicitly to -# override, exactly like `matricize(style, ...)`. -# -# `directsum_style` is deliberately distinct from `FusionStyle(a)`: a graded array reports -# `SectorFusion` there (what `matricize` needs), but its `directsum` default is a plain `cat`. Its -# binary combine is likewise kept independent of `FusionStyle`'s so the two traits can diverge. It -# is also distinct from `cat_style`, which resolves the `Concatenated` broadcast style (which -# `copyto!` placement fires), an orthogonal axis. - -# Per-argument direct-sum style. -directsum_style(x) = directsum_style(typeof(x)) -directsum_style(::Type{<:AbstractArray}) = ReshapeFusion() - -# Binary combine of two styles, associative and commutative like `FusionStyle`'s: two of the same -# style combine to that style, and any mismatch falls back to `ReshapeFusion`. -directsum_style(style1::Style, style2::Style) where {Style <: FusionStyle} = Style() -directsum_style(style1::FusionStyle, style2::FusionStyle) = ReshapeFusion() - -# Fold the per-argument styles pairwise with the binary combine. -combine_directsum_style(a) = directsum_style(a) -function combine_directsum_style(a, as...) - return directsum_style(directsum_style(a), combine_directsum_style(as...)) -end - +# `directsum` — the direct sum along `dims`. For now it is exactly `cat`: block-concatenation with +# concat-order axes and no basis rotation, which is a valid direct sum for dense and +# `AbelianGradedArray` backings alike. A fusing/rotating variant (merging and sorting the summed +# sectors onto one canonical basis) will be added behind a style selector when a backend needs it, +# the same way `matricize` takes a `FusionStyle`. function directsum end -directsum(as...; dims) = directsum(combine_directsum_style(as...), as...; dims) -directsum(::ReshapeFusion, as...; dims) = cat(as...; dims) +directsum(as...; dims) = cat(as...; dims) diff --git a/test/test_concatenate.jl b/test/test_concatenate.jl index f3f3ff6..76b7566 100644 --- a/test/test_concatenate.jl +++ b/test/test_concatenate.jl @@ -1,5 +1,4 @@ -using TensorAlgebra: - TensorAlgebra, ReshapeFusion, cat, cat!, cat_axes, concatenate, directsum +using TensorAlgebra: TensorAlgebra, cat, cat!, cat_axes, concatenate, directsum using Test: @test, @testset @testset "cat / concatenate" begin @@ -30,12 +29,11 @@ using Test: @test, @testset @test cat_axes(Val((1, 2)), a, b) == (Base.OneTo(4), Base.OneTo(6)) end -@testset "directsum (dense: ReshapeFusion -> cat)" begin +@testset "directsum (forwards to cat)" begin a = reshape(collect(1.0:6.0), 2, 3) b = reshape(collect(7.0:12.0), 2, 3) - # Dense arrays carry `ReshapeFusion`, so `directsum` is exactly `cat` (no rotation needed). - @test TensorAlgebra.FusionStyle(a) === ReshapeFusion() + # `directsum` is exactly `cat`: block-concatenation, no basis rotation. @test directsum(a, b; dims = (1, 2)) == cat(a, b; dims = (1, 2)) @test directsum(a, b; dims = 1) == vcat(a, b) @@ -46,7 +44,4 @@ end @test s[1:2, 1:3] == a @test s[3:4, 4:6] == b @test s[5:6, 7:10] == d - - # Passing the style explicitly matches the resolved-from-array path. - @test directsum(ReshapeFusion(), a, b; dims = (1, 2)) == directsum(a, b; dims = (1, 2)) end diff --git a/test/test_exports.jl b/test/test_exports.jl index dbb65ac..82a4c68 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -40,7 +40,7 @@ using Test: @test, @testset [ :biperm, :bipartition, :cat, :cat!, :cat_copyto!, :cat_similar, :concatenate, :contractopadd!, :data, :datatype, :directsum, - :directsum_style, :flattenlinear, :label_type, + :flattenlinear, :label_type, :matricizeopperm, :permutedims, :permutedims!, :scalar, :similar_map, :to_range, :tr, :tryflattenlinear, :ungrade, :zero!, :scale!, :permuteddims, :PermutedDims, From aa99d8272b9f2ffe3ea48154e38a96bf22fce7ec Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 14 Jul 2026 10:39:46 -0400 Subject: [PATCH 5/9] Make Val the canonical dims form in the concatenate helpers, trim comments `cat_ndims`, `cat_axes`, and `dims2cat` now implement on their `Val` method and have the plain-value method forward with `Val(dims)`, matching the direction `concatenate` already normalizes. The forward can call `Val` directly rather than through a `to_val` helper, since dispatch guarantees the plain method never receives a `Val`. Also cut the comments down to the non-obvious rationale (the difference from `Base.cat`, the `Broadcasted` reuse, the style-to-`nothing` fallback, the Base attribution). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/concatenate.jl | 67 ++++++++++++++-------------------------------- src/directsum.jl | 7 ++--- 2 files changed, 22 insertions(+), 52 deletions(-) diff --git a/src/concatenate.jl b/src/concatenate.jl index f3251c8..33091d8 100644 --- a/src/concatenate.jl +++ b/src/concatenate.jl @@ -1,23 +1,10 @@ -# Concatenation interface, vendored flat from the `Concatenate` module that previously lived in -# SparseArraysBase / FunctionImplementations. TensorAlgebra owns it here as the single source of -# truth: `cat`/`cat!`/`concatenate`, plus the backend hooks `cat_similar` (destination allocation) -# and `cat_copyto!` (placement). Both dispatch on the combined `cat_style` of the arguments, so a -# backend (SparseArraysBase, GradedArrays, a TensorKit ext) customizes concatenation by overloading -# `cat_similar`/`cat_copyto!` on its style, without scalar indexing. -# -# This is a function interface (no lazy `Concatenated` wrapper): concatenation does not fuse, so -# there is nothing for a lazy object to defer. `cat`/`cat!` resolve the style with `cat_style`, -# allocate with `cat_similar`, and place with `cat_copyto!`. Mostly a copy of Base's `cat`, except -# the destination is chosen from all inputs instead of just the first. +# `cat`/`cat!`/`concatenate` over arrays, with the destination chosen from all inputs rather than +# just the first (unlike `Base.cat`). Backends customize by overloading `cat_similar` (destination) +# and `cat_copyto!` (placement) on the combined `cat_style` of the arguments. import Base.Broadcast as BC using Base: promote_eltypeof -_valdims(dims::Val) = dims -_valdims(dims) = Val(dims) - -# Concatenation axes and style, computed directly from `dims` and the arguments. -# ------------------------------------------------------------------------------ function cat_axis( a1::AbstractUnitRange, a2::AbstractUnitRange, a_rest::AbstractUnitRange... ) @@ -28,60 +15,46 @@ function cat_axis(a1::AbstractUnitRange, a2::AbstractUnitRange) return Base.OneTo(length(a1) + length(a2)) end -function cat_ndims(dims, as::AbstractArray...) - return max(maximum(dims), maximum(ndims, as)) -end +cat_ndims(dims, as::AbstractArray...) = cat_ndims(Val(dims), as...) function cat_ndims(dims::Val, as::AbstractArray...) - return cat_ndims(unval(dims), as...) + return max(maximum(unval(dims)), maximum(ndims, as)) end -function cat_axes(dims, a::AbstractArray, as::AbstractArray...) +cat_axes(dims, as::AbstractArray...) = cat_axes(Val(dims), as...) +function cat_axes(dims::Val, a::AbstractArray, as::AbstractArray...) return ntuple(cat_ndims(dims, a, as...)) do dim - return if dim in dims + return if dim in unval(dims) cat_axis(map(Base.Fix2(axes, dim), (a, as...))...) else axes(a, dim) end end end -function cat_axes(dims::Val, as::AbstractArray...) - return cat_axes(unval(dims), as...) -end function cat_style(dims, as::AbstractArray...) N = cat_ndims(dims, as...) return typeof(BC.combine_styles(as...))(Val(N)) end -# Allocate the destination container. -# ----------------------------------- -# Allocate the destination for concatenating `args` along the combined `style`, with element type -# `T` and axes `ax`. Backends override on their style; the default reuses broadcast's `similar` by -# building a `Broadcasted` with the same style and axes. +# Default destination: reuse broadcast's `similar` for the style by wrapping in a `Broadcasted`. function cat_similar(style, ::Type{T}, ax, args...) where {T} return similar(BC.Broadcasted(style, identity, args, ax), T) end -# Main logic. -# ----------- -# Concatenate the supplied `args` along dimensions `dims`. -concatenate(dims, args...) = concatenate(_valdims(dims), args...) +concatenate(dims, args...) = concatenate(Val(dims), args...) function concatenate(dims::Val, args...) style = cat_style(dims, args...) dest = cat_similar(style, promote_eltypeof(args...), cat_axes(dims, args...), args...) return cat_copyto!(dest, style, dims, args...) end -# Concatenate the supplied `args` along dimensions `dims`. cat(args...; dims) = concatenate(dims, args...) -# Concatenate the supplied `args` along dimensions `dims`, placing the result into `dest`. function cat!(dest, args...; dims) - d = _valdims(dims) - return cat_copyto!(dest, cat_style(d, args...), d, args...) + return cat_copyto!(dest, cat_style(dims, args...), dims, args...) end -# The following is largely copied from the Base implementation of `Base.cat`, see: +# The offset placement below is adapted from Base's `cat`: # https://github.com/JuliaLang/julia/blob/885b1cd875f101f227b345f681cc36879124d80d/base/abstractarray.jl#L1778-L1887 _copy_or_fill!(A, inds, x) = fill!(view(A, inds...), x) _copy_or_fill!(A, inds, x::AbstractArray) = (A[inds...] = x) @@ -122,17 +95,17 @@ function __cat_offset1!(A, shape, catdims, offsets, x) return newoffsets end -dims2cat(dims::Val) = dims2cat(unval(dims)) -function dims2cat(dims) - if any(≤(0), dims) - throw(ArgumentError("All cat dimensions must be positive integers, but got $dims")) +dims2cat(dims) = dims2cat(Val(dims)) +function dims2cat(dims::Val) + d = unval(dims) + if any(≤(0), d) + throw(ArgumentError("All cat dimensions must be positive integers, but got $d")) end - return ntuple(in(dims), maximum(dims)) + return ntuple(in(d), maximum(d)) end -# Materialize the concatenation into `dest`. Backends override on their `style`; the default strips -# the style to `nothing` (which lets a backend instead specialize on `typeof(dest)` without -# ambiguity against the style dispatch) and runs the generic offset placement. +# The default strips the style to `nothing`, so a backend can instead specialize on `typeof(dest)` +# without ambiguity against the style dispatch. cat_copyto!(dest, style, dims, args...) = cat_copyto!(dest, nothing, dims, args...) function cat_copyto!(dest, ::Nothing, dims, args...) catdims = dims2cat(dims) diff --git a/src/directsum.jl b/src/directsum.jl index e87c1cb..990c979 100644 --- a/src/directsum.jl +++ b/src/directsum.jl @@ -1,7 +1,4 @@ -# `directsum` — the direct sum along `dims`. For now it is exactly `cat`: block-concatenation with -# concat-order axes and no basis rotation, which is a valid direct sum for dense and -# `AbelianGradedArray` backings alike. A fusing/rotating variant (merging and sorting the summed -# sectors onto one canonical basis) will be added behind a style selector when a backend needs it, -# the same way `matricize` takes a `FusionStyle`. +# `directsum` is a plain `cat` for now, kept as its own entry point so a fusing/rotating variant can +# later be selected by style, the way `matricize` takes a `FusionStyle`. function directsum end directsum(as...; dims) = cat(as...; dims) From f5c9805c00dca0c14ae152e8308af9f2005d2d71 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 14 Jul 2026 11:07:40 -0400 Subject: [PATCH 6/9] Drop TensorAlgebra.cat, rename cat! to concatenate! The keyword-dims entry point is Base.cat, routed to the concatenation interface through the Base._cat overloads that backends define. Keeping a separate TensorAlgebra.cat only shadowed Base.cat, so it is removed. The positional-dims TensorAlgebra names are now concatenate and concatenate!. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/TensorAlgebra.jl | 2 +- src/concatenate.jl | 6 ++---- src/directsum.jl | 6 +++--- test/test_concatenate.jl | 23 +++++++++++------------ test/test_exports.jl | 4 ++-- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index 5d07843..bdb5c99 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -9,7 +9,7 @@ export contract, contract!, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public biperm, bipartition, cat, cat!, cat_copyto!, cat_similar, concatenate, contractopadd!, data, datatype, directsum, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" + "public biperm, bipartition, cat_copyto!, cat_similar, concatenate, concatenate!, contractopadd!, data, datatype, directsum, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" ) ) end diff --git a/src/concatenate.jl b/src/concatenate.jl index 33091d8..dbc3f0c 100644 --- a/src/concatenate.jl +++ b/src/concatenate.jl @@ -1,4 +1,4 @@ -# `cat`/`cat!`/`concatenate` over arrays, with the destination chosen from all inputs rather than +# `concatenate`/`concatenate!` over arrays, with the destination chosen from all inputs rather than # just the first (unlike `Base.cat`). Backends customize by overloading `cat_similar` (destination) # and `cat_copyto!` (placement) on the combined `cat_style` of the arguments. @@ -48,9 +48,7 @@ function concatenate(dims::Val, args...) return cat_copyto!(dest, style, dims, args...) end -cat(args...; dims) = concatenate(dims, args...) - -function cat!(dest, args...; dims) +function concatenate!(dest, args...; dims) return cat_copyto!(dest, cat_style(dims, args...), dims, args...) end diff --git a/src/directsum.jl b/src/directsum.jl index 990c979..378b93f 100644 --- a/src/directsum.jl +++ b/src/directsum.jl @@ -1,4 +1,4 @@ -# `directsum` is a plain `cat` for now, kept as its own entry point so a fusing/rotating variant can -# later be selected by style, the way `matricize` takes a `FusionStyle`. +# `directsum` is a plain concatenation for now, kept as its own entry point so a fusing/rotating +# variant can later be selected by style, the way `matricize` takes a `FusionStyle`. function directsum end -directsum(as...; dims) = cat(as...; dims) +directsum(as...; dims) = concatenate(dims, as...) diff --git a/test/test_concatenate.jl b/test/test_concatenate.jl index 76b7566..45daf90 100644 --- a/test/test_concatenate.jl +++ b/test/test_concatenate.jl @@ -1,40 +1,39 @@ -using TensorAlgebra: TensorAlgebra, cat, cat!, cat_axes, concatenate, directsum +using TensorAlgebra: TensorAlgebra, cat_axes, concatenate, concatenate!, directsum using Test: @test, @testset -@testset "cat / concatenate" begin +@testset "concatenate" begin a = reshape(collect(1.0:6.0), 2, 3) b = reshape(collect(7.0:12.0), 2, 3) - # Single-dim concatenation matches `Base.cat`. - @test cat(a, b; dims = 1) == vcat(a, b) - @test cat(a, b; dims = 2) == hcat(a, b) + # Single-dim concatenation matches `vcat`/`hcat`. @test concatenate(1, a, b) == vcat(a, b) + @test concatenate(2, a, b) == hcat(a, b) # Multi-dim concatenation is the block-diagonal placement (off-diagonal blocks zeroed). - c = cat(a, b; dims = (1, 2)) + c = concatenate((1, 2), a, b) ref = zeros(4, 6) ref[1:2, 1:3] .= a ref[3:4, 4:6] .= b @test c == ref - # `cat!` writes into a provided destination. + # `concatenate!` writes into a provided destination. dest = zeros(4, 6) - @test cat!(dest, a, b; dims = (1, 2)) === dest + @test concatenate!(dest, a, b; dims = (1, 2)) === dest @test dest == ref # Element type is promoted across all inputs, not taken from the first. - @test eltype(cat(a, b .+ 0im; dims = 1)) == ComplexF64 + @test eltype(concatenate(1, a, b .+ 0im)) == ComplexF64 # `cat_axes` computes the concatenated axes from the arguments. @test cat_axes(Val((1, 2)), a, b) == (Base.OneTo(4), Base.OneTo(6)) end -@testset "directsum (forwards to cat)" begin +@testset "directsum (forwards to concatenate)" begin a = reshape(collect(1.0:6.0), 2, 3) b = reshape(collect(7.0:12.0), 2, 3) - # `directsum` is exactly `cat`: block-concatenation, no basis rotation. - @test directsum(a, b; dims = (1, 2)) == cat(a, b; dims = (1, 2)) + # `directsum` is exactly `concatenate`: block-concatenation, no basis rotation. + @test directsum(a, b; dims = (1, 2)) == concatenate((1, 2), a, b) @test directsum(a, b; dims = 1) == vcat(a, b) # N-ary: each summand lands in its own diagonal hyper-block. diff --git a/test/test_exports.jl b/test/test_exports.jl index 82a4c68..d0fd3a2 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -38,8 +38,8 @@ using Test: @test, @testset append!( exports, [ - :biperm, :bipartition, :cat, :cat!, :cat_copyto!, :cat_similar, - :concatenate, :contractopadd!, :data, :datatype, :directsum, + :biperm, :bipartition, :cat_copyto!, :cat_similar, + :concatenate, :concatenate!, :contractopadd!, :data, :datatype, :directsum, :flattenlinear, :label_type, :matricizeopperm, :permutedims, :permutedims!, :scalar, :similar_map, :to_range, :tr, :tryflattenlinear, :ungrade, :zero!, :scale!, From 2fbb6a7d5ed444a0669ec3bfdb7b9fa96bc0533c Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 14 Jul 2026 12:00:23 -0400 Subject: [PATCH 7/9] Make concatenate! the style-dispatched placement hook Fold the separate placement hook into `concatenate!` by leading with the style: `concatenate!(::BroadcastStyle, dest, dims, args...)` is the generic placement, and `concatenate!(dest, args...; dims)` forwards to it after computing `cat_style`. A backend customizes placement by overloading `concatenate!` on its own style, alongside `cat_similar` for allocation. This drops the `cat_copyto!` name and its style-stripping fallback. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/TensorAlgebra.jl | 2 +- src/concatenate.jl | 13 ++++++------- test/test_exports.jl | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index bdb5c99..445d687 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -9,7 +9,7 @@ export contract, contract!, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public biperm, bipartition, cat_copyto!, cat_similar, concatenate, concatenate!, contractopadd!, data, datatype, directsum, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" + "public biperm, bipartition, cat_similar, concatenate, concatenate!, contractopadd!, data, datatype, directsum, flattenlinear, label_type, matricizeopperm, permutedims, permutedims!, scalar, similar_map, to_range, tr, tryflattenlinear, ungrade, zero!, scale!, permuteddims, PermutedDims" ) ) end diff --git a/src/concatenate.jl b/src/concatenate.jl index dbc3f0c..c80f5ee 100644 --- a/src/concatenate.jl +++ b/src/concatenate.jl @@ -1,6 +1,6 @@ # `concatenate`/`concatenate!` over arrays, with the destination chosen from all inputs rather than # just the first (unlike `Base.cat`). Backends customize by overloading `cat_similar` (destination) -# and `cat_copyto!` (placement) on the combined `cat_style` of the arguments. +# and `concatenate!` (placement) on the combined `cat_style` of the arguments. import Base.Broadcast as BC using Base: promote_eltypeof @@ -45,11 +45,11 @@ concatenate(dims, args...) = concatenate(Val(dims), args...) function concatenate(dims::Val, args...) style = cat_style(dims, args...) dest = cat_similar(style, promote_eltypeof(args...), cat_axes(dims, args...), args...) - return cat_copyto!(dest, style, dims, args...) + return concatenate!(style, dest, dims, args...) end function concatenate!(dest, args...; dims) - return cat_copyto!(dest, cat_style(dims, args...), dims, args...) + return concatenate!(cat_style(dims, args...), dest, dims, args...) end # The offset placement below is adapted from Base's `cat`: @@ -102,10 +102,9 @@ function dims2cat(dims::Val) return ntuple(in(d), maximum(d)) end -# The default strips the style to `nothing`, so a backend can instead specialize on `typeof(dest)` -# without ambiguity against the style dispatch. -cat_copyto!(dest, style, dims, args...) = cat_copyto!(dest, nothing, dims, args...) -function cat_copyto!(dest, ::Nothing, dims, args...) +# Generic placement, dispatched on the abstract style so a backend can override on its own +# `cat_style`. The style itself is only used for dispatch here. +function concatenate!(::BC.BroadcastStyle, dest, dims, args...) catdims = dims2cat(dims) shape = map(length, cat_axes(dims, args...)) count(!iszero, catdims)::Int > 1 && zero!(dest) diff --git a/test/test_exports.jl b/test/test_exports.jl index d0fd3a2..183c3f5 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -38,7 +38,7 @@ using Test: @test, @testset append!( exports, [ - :biperm, :bipartition, :cat_copyto!, :cat_similar, + :biperm, :bipartition, :cat_similar, :concatenate, :concatenate!, :contractopadd!, :data, :datatype, :directsum, :flattenlinear, :label_type, :matricizeopperm, :permutedims, :permutedims!, :scalar, :similar_map, From f7d6fdca6344f8d77d59d1f4e57160a7525299de Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 14 Jul 2026 12:51:10 -0400 Subject: [PATCH 8/9] Dispatch concatenate! on the destination, not the style By placement time `cat_similar` has already used the combined `cat_style` to build the destination, so `concatenate!` can dispatch on the destination type and does not need the style. This drops the leading style argument and the keyword forwarding method, leaving `concatenate!(dest, dims, args...)` with `dims` positional, matching `concatenate(dims, args...)`. Allocation still dispatches on the style through `cat_similar`. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/concatenate.jl | 16 ++++++---------- test/test_concatenate.jl | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/concatenate.jl b/src/concatenate.jl index c80f5ee..170d66c 100644 --- a/src/concatenate.jl +++ b/src/concatenate.jl @@ -1,6 +1,7 @@ # `concatenate`/`concatenate!` over arrays, with the destination chosen from all inputs rather than -# just the first (unlike `Base.cat`). Backends customize by overloading `cat_similar` (destination) -# and `concatenate!` (placement) on the combined `cat_style` of the arguments. +# just the first (unlike `Base.cat`). A backend customizes allocation by overloading `cat_similar` on +# the combined `cat_style` of the arguments, and placement by overloading `concatenate!` on the +# destination type. import Base.Broadcast as BC using Base: promote_eltypeof @@ -45,11 +46,7 @@ concatenate(dims, args...) = concatenate(Val(dims), args...) function concatenate(dims::Val, args...) style = cat_style(dims, args...) dest = cat_similar(style, promote_eltypeof(args...), cat_axes(dims, args...), args...) - return concatenate!(style, dest, dims, args...) -end - -function concatenate!(dest, args...; dims) - return concatenate!(cat_style(dims, args...), dest, dims, args...) + return concatenate!(dest, dims, args...) end # The offset placement below is adapted from Base's `cat`: @@ -102,9 +99,8 @@ function dims2cat(dims::Val) return ntuple(in(d), maximum(d)) end -# Generic placement, dispatched on the abstract style so a backend can override on its own -# `cat_style`. The style itself is only used for dispatch here. -function concatenate!(::BC.BroadcastStyle, dest, dims, args...) +# Generic placement; a backend overrides `concatenate!` on its destination type. +function concatenate!(dest, dims, args...) catdims = dims2cat(dims) shape = map(length, cat_axes(dims, args...)) count(!iszero, catdims)::Int > 1 && zero!(dest) diff --git a/test/test_concatenate.jl b/test/test_concatenate.jl index 45daf90..2ae3a39 100644 --- a/test/test_concatenate.jl +++ b/test/test_concatenate.jl @@ -18,7 +18,7 @@ using Test: @test, @testset # `concatenate!` writes into a provided destination. dest = zeros(4, 6) - @test concatenate!(dest, a, b; dims = (1, 2)) === dest + @test concatenate!(dest, (1, 2), a, b) === dest @test dest == ref # Element type is promoted across all inputs, not taken from the first. From 2ccc83f93b3179d59f56ef831794d33afdfe0cf1 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 14 Jul 2026 13:03:41 -0400 Subject: [PATCH 9/9] Take dims positionally in directsum `directsum(dims, as...)` matches `concatenate(dims, as...)` and the rest of the interface, and a positional signature composes better for later customization, whether a leading style argument or dispatch on the summed arrays. Also drops the empty `function directsum end` stub, since the fallback method already defines the function. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/directsum.jl | 3 +-- test/test_concatenate.jl | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/directsum.jl b/src/directsum.jl index 378b93f..9a51eec 100644 --- a/src/directsum.jl +++ b/src/directsum.jl @@ -1,4 +1,3 @@ # `directsum` is a plain concatenation for now, kept as its own entry point so a fusing/rotating # variant can later be selected by style, the way `matricize` takes a `FusionStyle`. -function directsum end -directsum(as...; dims) = concatenate(dims, as...) +directsum(dims, as...) = concatenate(dims, as...) diff --git a/test/test_concatenate.jl b/test/test_concatenate.jl index 2ae3a39..d0056f8 100644 --- a/test/test_concatenate.jl +++ b/test/test_concatenate.jl @@ -33,12 +33,12 @@ end b = reshape(collect(7.0:12.0), 2, 3) # `directsum` is exactly `concatenate`: block-concatenation, no basis rotation. - @test directsum(a, b; dims = (1, 2)) == concatenate((1, 2), a, b) - @test directsum(a, b; dims = 1) == vcat(a, b) + @test directsum((1, 2), a, b) == concatenate((1, 2), a, b) + @test directsum(1, a, b) == vcat(a, b) # N-ary: each summand lands in its own diagonal hyper-block. d = reshape(collect(1.0:8.0), 2, 4) - s = directsum(a, b, d; dims = (1, 2)) + s = directsum((1, 2), a, b, d) @test size(s) == (6, 10) @test s[1:2, 1:3] == a @test s[3:4, 4:6] == b