From 46c9059d1e9b58b005944b76dfe44f15c8bee4a5 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 6 Aug 2026 15:02:01 -0400 Subject: [PATCH 01/10] Support GradedArrays 0.16 and TensorAlgebra 0.19 Bumps the compat bounds to the GradedArrays 0.16 and TensorAlgebra 0.19 breaking releases, which needs no source changes. Co-Authored-By: Claude Opus 4.8 --- Project.toml | 8 ++++---- docs/Project.toml | 2 +- test/Project.toml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index b4f3ec1..9581961 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.13.13" +version = "0.13.14" authors = ["ITensor developers and contributors"] [workspace] @@ -12,6 +12,7 @@ Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" ArrayLayouts = "4c555306-a7a7-4459-81d9-ec55ddd5c99a" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +GradedArrays = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -25,7 +26,6 @@ WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" [weakdeps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -GradedArrays = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2" Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" @@ -45,14 +45,14 @@ Adapt = "4.1.1" ArrayLayouts = "1.11" Combinatorics = "1" ConstructionBase = "1.6" -GradedArrays = "0.15" +GradedArrays = "0.16" LinearAlgebra = "1.10" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" Mooncake = "0.4.202, 0.5" OMEinsumContractionOrders = "1.3" Random = "1.10" SimpleTraits = "0.9.4" -TensorAlgebra = "0.18" +TensorAlgebra = "0.19" TensorKit = "0.17" TensorKitSectors = "0.3.9" TermInterface = "2" diff --git a/docs/Project.toml b/docs/Project.toml index 03074f5..c7278c7 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,5 +16,5 @@ ITensorBase = "0.13" ITensorFormatter = "0.2.27" Literate = "2" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" -TensorAlgebra = "0.18" +TensorAlgebra = "0.19" Test = "1.10" diff --git a/test/Project.toml b/test/Project.toml index 4f0da76..2ea7498 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -32,7 +32,7 @@ AbstractTrees = "0.4.5" Adapt = "4" Aqua = "0.8.9" Combinatorics = "1" -GradedArrays = "0.15" +GradedArrays = "0.16" ITensorBase = "0.13" ITensorPkgSkeleton = "0.3.42" JLArrays = "0.2, 0.3" @@ -44,7 +44,7 @@ Random = "1.10" SafeTestsets = "0.1" StableRNGs = "1" Suppressor = "0.2" -TensorAlgebra = "0.18" +TensorAlgebra = "0.19" TensorKit = "0.17" TensorKitSectors = "0.3.9" TermInterface = "2" From d341faa6b1aa1557396d33632820cfb8cccab7de Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 6 Aug 2026 16:55:21 -0400 Subject: [PATCH 02/10] Pin GradedArrays branch so CI can resolve 0.16 Draft-only [sources] pin to the unregistered GradedArrays 0.16 branch. Remove once it registers. Co-Authored-By: Claude Opus 4.8 --- Project.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Project.toml b/Project.toml index 9581961..40cc1dc 100644 --- a/Project.toml +++ b/Project.toml @@ -31,6 +31,10 @@ OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" +[sources.GradedArrays] +rev = "mf/delete-abeliangradedarray" +url = "https://github.com/ITensor/GradedArrays.jl" + [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseGradedArraysExt = ["GradedArrays", "TensorKitSectors"] From 88ec8b14fc4b341d0d0d324fda7a84f0ebff89cb Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Sun, 16 Aug 2026 15:37:38 -0400 Subject: [PATCH 03/10] Simplify named-tensor broadcasting Named-tensor broadcasting now unwraps names and forwards to unnamed broadcasting, with linear-combination allocation handled by TensorAlgebra's broadcast `similar` instead of a prototype threaded through ITensorBase. --- Project.toml | 2 +- docs/Project.toml | 2 +- src/broadcast.jl | 51 ++++++++++++++--------------------------------- test/Project.toml | 2 +- 4 files changed, 18 insertions(+), 39 deletions(-) diff --git a/Project.toml b/Project.toml index 40cc1dc..c002596 100644 --- a/Project.toml +++ b/Project.toml @@ -56,7 +56,7 @@ Mooncake = "0.4.202, 0.5" OMEinsumContractionOrders = "1.3" Random = "1.10" SimpleTraits = "0.9.4" -TensorAlgebra = "0.19" +TensorAlgebra = "0.19.3" TensorKit = "0.17" TensorKitSectors = "0.3.9" TermInterface = "2" diff --git a/docs/Project.toml b/docs/Project.toml index c7278c7..22c7796 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,5 +16,5 @@ ITensorBase = "0.13" ITensorFormatter = "0.2.27" Literate = "2" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" -TensorAlgebra = "0.19" +TensorAlgebra = "0.19.3" Test = "1.10" diff --git a/src/broadcast.jl b/src/broadcast.jl index 5f97907..0858ccb 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -45,14 +45,6 @@ function broadcasted_unnamed(bc::Broadcasted, names) return broadcasted(bc.f, Base.Fix2(broadcasted_unnamed, names).(bc.args)...) end -# A bare (unnamed) array operand, used as an allocation prototype so a broadcast -# result inherits the operands' backend (e.g. graded) rather than a lazy permuted -# wrapper's `similar` (which can drop the backend). -unnamed_prototype(bc::Broadcasted) = unnamed_prototype(bc.args...) -unnamed_prototype(arg::AbstractNamedTensor, args...) = unnamed(arg) -unnamed_prototype(arg::Broadcasted, args...) = unnamed_prototype(arg.args..., args...) -unnamed_prototype(arg, args...) = unnamed_prototype(args...) - # Skip Base's shape-combination step: named broadcasts don't need the `NamedUnitRange` axis # machinery. Name compatibility is handled by the per-operand alignment in `broadcasted_unnamed` # (via `getperm`), and unnamed-shape compatibility by TensorAlgebra. @@ -65,38 +57,24 @@ _dimnames(bc::Broadcasted, args...) = _dimnames(bc.args..., args...) _dimnames(_, args...) = _dimnames(args...) dimnames(bc::Broadcasted) = _dimnames(bc.args...) -# The result element type of a linear combination, from the concrete unnamed leaves at runtime. -# `eltype(::LinearBroadcasted)` uses `Base.promote_op`, which runs a live inference call here -# because the leaves wrap a named tensor's (non-inferrable) backing array, so promote the -# concrete `eltype`s instead. -_lineareltype(a::AbstractArray) = eltype(a) -function _lineareltype(s::TA.ScaledBroadcasted) - return promote_type(typeof(TA.coeff(s)), _lineareltype(TA.unscaled(s))) -end -_lineareltype(s::TA.AddBroadcasted) = promote_type(map(_lineareltype, TA.addends(s))...) - function Base.copy(bc::Broadcasted{<:AbstractNamedTensorStyle}) nms = dimnames(bc) - dest_unnamed = _copy_unnamed(broadcasted_unnamed(bc, nms), unnamed_prototype(bc)) - return nameddims(dest_unnamed, nms) + return nameddims(_copy_unnamed(broadcasted_unnamed(bc, nms)), nms) end -# Function barrier: `broadcasted_unnamed` and `unnamed_prototype` produce concretely-typed -# values whose *inferred* types are abstract (the named backing array is abstract), so this -# call re-specializes on the concrete runtime types and everything below is type-stable -# (`eltype(lb)` is now inferrable, no runtime `promote_op`). Inlining the body into `copy` -# instead costs one extra allocation per call. +# Function barrier: `broadcasted_unnamed` builds a concretely-valued but abstractly-typed +# `Broadcasted` (a named tensor's backing array is abstractly typed), so re-dispatching on the +# concrete runtime type here keeps the materialize below type-stable. # -# Allocate from `axes(lb)`, the flattened expression's axes, rather than the prototype's own: -# an axis-changing operand (a `conj` leaf dualizes its axes) makes them differ, and the -# destination must match the expression. All axes go in the codomain (empty domain), the -# all-codomain output convention `@tensor` uses for an unbipartitioned left-hand side; on a -# non-bipartitioned backend (a dense array) `similar_map` with an empty domain is a plain -# `similar` over `axes(lb)`. -function _copy_unnamed(bc_unnamed, prototype) +# A linear combination folds to a `LinearBroadcasted` and materializes through `copy(lb)`, whose +# allocation (`similar(lb)`) is the unnamed backend's own broadcast-style `similar` — so the result +# inherits the backend (dense, graded, ...) with no prototype bookkeeping here. A non-linear +# expression falls to Base's generic broadcast; that path's design (which reorderings it should +# support, whether to route strided operands through Strided.jl) is deliberately unresolved. +function _copy_unnamed(bc_unnamed) lb = TA.tryflattenlinear(bc_unnamed) isnothing(lb) && return copy(bc_unnamed) - return copyto!(TA.similar_map(prototype, eltype(lb), axes(lb), ()), lb) + return copy(lb) end # `Base.Broadcast.materialize!` otherwise reconstructs the broadcast over `axes(dest)` and @@ -119,9 +97,10 @@ function Base.copyto!( return dest end -# Function barrier mirroring `_copy_unnamed`: `unnamed(dest)` and `broadcasted_unnamed` -# have abstract inferred types (the named backing array is abstract), so this call -# re-specializes on the concrete runtime types and the flatten/lower below is type-stable. +# Function barrier mirroring `_copy_unnamed`: `unnamed(dest)` and `broadcasted_unnamed` have +# abstract inferred types (the named backing array is abstract), so re-dispatching on the concrete +# runtime type here keeps the `copyto!` below type-stable. Linear folds to a `LinearBroadcasted`; +# non-linear falls to Base's generic in-place broadcast. function _copyto_unnamed!(dest_unnamed, bc_unnamed) lb = TA.tryflattenlinear(bc_unnamed) isnothing(lb) && return copyto!(dest_unnamed, bc_unnamed) diff --git a/test/Project.toml b/test/Project.toml index 2ea7498..2450000 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -44,7 +44,7 @@ Random = "1.10" SafeTestsets = "0.1" StableRNGs = "1" Suppressor = "0.2" -TensorAlgebra = "0.19" +TensorAlgebra = "0.19.3" TensorKit = "0.17" TensorKitSectors = "0.3.9" TermInterface = "2" From 6fe133f8e4168c7def2ed1567426e194e87a1ecf Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Sun, 16 Aug 2026 16:43:00 -0400 Subject: [PATCH 04/10] Point GradedArrays [sources] pin at main --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index c002596..a0a3846 100644 --- a/Project.toml +++ b/Project.toml @@ -32,7 +32,7 @@ TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" [sources.GradedArrays] -rev = "mf/delete-abeliangradedarray" +rev = "main" url = "https://github.com/ITensor/GradedArrays.jl" [extensions] From 24d03554bb9dbac401b93397eb89aa0c8ac68a47 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Sun, 16 Aug 2026 20:06:00 -0400 Subject: [PATCH 05/10] Keep GradedArrays a weakdep, not a direct dependency --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index a0a3846..63cad51 100644 --- a/Project.toml +++ b/Project.toml @@ -12,7 +12,6 @@ Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" ArrayLayouts = "4c555306-a7a7-4459-81d9-ec55ddd5c99a" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" -GradedArrays = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -26,6 +25,7 @@ WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" [weakdeps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +GradedArrays = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2" Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" From 6f5b1ed527d4529b4429251762017612bd935411 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Sun, 16 Aug 2026 22:06:46 -0400 Subject: [PATCH 06/10] Move GradedArrays [sources] pin to the test project --- Project.toml | 4 ---- test/Project.toml | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 63cad51..fdded47 100644 --- a/Project.toml +++ b/Project.toml @@ -31,10 +31,6 @@ OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" -[sources.GradedArrays] -rev = "main" -url = "https://github.com/ITensor/GradedArrays.jl" - [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseGradedArraysExt = ["GradedArrays", "TensorKitSectors"] diff --git a/test/Project.toml b/test/Project.toml index 2450000..f195685 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -24,6 +24,10 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" +[sources.GradedArrays] +rev = "main" +url = "https://github.com/ITensor/GradedArrays.jl" + [sources.ITensorBase] path = ".." From 0854e64cb9a7c858d94b2863c4ee14396d0c8ec0 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 17 Aug 2026 21:13:42 -0400 Subject: [PATCH 07/10] Flatten named broadcasts to a linear expression before unnaming A 1-arg linear broadcast (`2 .* a`, `a ./ norm(a)`) now preserves the operand's codomain/domain split, and named broadcasting is now linear-only. A non-linear `f.(a)` errors. Co-Authored-By: Claude Opus 4.8 --- Project.toml | 2 +- src/broadcast.jl | 74 ++++++++++++++++++++------------------- test/test_tensorkitext.jl | 4 +-- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Project.toml b/Project.toml index fdded47..b249cb5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.13.14" +version = "0.14.0" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/broadcast.jl b/src/broadcast.jl index 0858ccb..4e32000 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -21,12 +21,31 @@ end # `AbstractArray`); without this the default `broadcastable` wraps it in a `Ref`. BC.broadcastable(a::AbstractNamedTensor) = a -broadcasted_unnamed(x::Number, names) = x -function broadcasted_unnamed(a::AbstractNamedTensor, names) - # An operand already aligned to the destination names (the first operand always, and the - # common case for the rest) needs no permutation, avoiding a `getperm` allocation and the - # identity `permuteddims` wrapper. Skipping it makes a small add several times slower. - dimnames(a) == names && return unnamed(a) +# Unname a flattened named `LinearBroadcasted` preserving the operand's codomain/domain split. Valid +# only for a single-operand expression (`2 .* a`, `conj.(a)`): the sole leaf defines the output names, +# so it unnames to its bare backing. A sum has no single split, so its addends are aligned instead. +# (Flattening distributes scaling/conjugation over `+`, so a `Scaled`/`Conj` node never wraps an `Add`.) +function unnamed_split(a::TA.ScaledBroadcasted, names) + return TA.linearbroadcasted(*, TA.coeff(a), unnamed_split(TA.unscaled(a), names)) +end +function unnamed_split(a::TA.ConjBroadcasted, names) + return TA.linearbroadcasted(conj, unnamed_split(parent(a), names)) +end +unnamed_split(a::TA.AddBroadcasted, names) = unnamed_aligned(a, names) +unnamed_split(a::AbstractNamedTensor, names) = unnamed(a) + +# Unname aligning every leaf to `names` through the `PermutedDims` wrapper (all-codomain output). Used +# for a sum's addends and for every in-place `copyto!` (aligned to the destination). +function unnamed_aligned(a::TA.ScaledBroadcasted, names) + return TA.linearbroadcasted(*, TA.coeff(a), unnamed_aligned(TA.unscaled(a), names)) +end +function unnamed_aligned(a::TA.ConjBroadcasted, names) + return TA.linearbroadcasted(conj, unnamed_aligned(parent(a), names)) +end +function unnamed_aligned(a::TA.AddBroadcasted, names) + return TA.linearbroadcasted(+, map(x -> unnamed_aligned(x, names), TA.addends(a))...) +end +function unnamed_aligned(a::AbstractNamedTensor, names) return _broadcast_permuteddims(unnamed(a), getperm(dimnames(a), names)) end # Broadcasting-only alignment: unlike the public `unnamed(a, names)` (which returns a @@ -41,12 +60,8 @@ end @noinline function _broadcast_permuteddims(array, perm) return TA.PermutedDims(array, ntuple(i -> perm[i], Val(TA.ndims(array)))) end -function broadcasted_unnamed(bc::Broadcasted, names) - return broadcasted(bc.f, Base.Fix2(broadcasted_unnamed, names).(bc.args)...) -end - # Skip Base's shape-combination step: named broadcasts don't need the `NamedUnitRange` axis -# machinery. Name compatibility is handled by the per-operand alignment in `broadcasted_unnamed` +# machinery. Name compatibility is handled by the per-operand alignment in `unnamed_aligned` # (via `getperm`), and unnamed-shape compatibility by TensorAlgebra. BC.instantiate(bc::Broadcasted{<:AbstractNamedTensorStyle}) = bc @@ -59,22 +74,15 @@ dimnames(bc::Broadcasted) = _dimnames(bc.args...) function Base.copy(bc::Broadcasted{<:AbstractNamedTensorStyle}) nms = dimnames(bc) - return nameddims(_copy_unnamed(broadcasted_unnamed(bc, nms)), nms) + return nameddims(_copy_unnamed(bc, nms), nms) end -# Function barrier: `broadcasted_unnamed` builds a concretely-valued but abstractly-typed -# `Broadcasted` (a named tensor's backing array is abstractly typed), so re-dispatching on the -# concrete runtime type here keeps the materialize below type-stable. -# -# A linear combination folds to a `LinearBroadcasted` and materializes through `copy(lb)`, whose -# allocation (`similar(lb)`) is the unnamed backend's own broadcast-style `similar` — so the result -# inherits the backend (dense, graded, ...) with no prototype bookkeeping here. A non-linear -# expression falls to Base's generic broadcast; that path's design (which reorderings it should -# support, whether to route strided operands through Strided.jl) is deliberately unresolved. -function _copy_unnamed(bc_unnamed) - lb = TA.tryflattenlinear(bc_unnamed) - isnothing(lb) && return copy(bc_unnamed) - return copy(lb) +# Function barrier: `bc`'s named leaves are abstractly typed, so re-dispatching on the concrete `bc` +# here keeps the flatten/unname/materialize below type-stable. `copy(lb)` allocates through the unnamed +# backend's own broadcast-style `similar`, so the result inherits the backend (dense, graded, ...); +# `unnamed_split` keeps a single scaled/conjugated operand's codomain/domain split. +@noinline function _copy_unnamed(bc, nms) + return copy(unnamed_split(TA.flattenlinear(bc), nms)) end # `Base.Broadcast.materialize!` otherwise reconstructs the broadcast over `axes(dest)` and @@ -93,18 +101,13 @@ function Base.copyto!( dest::AbstractNamedTensor, bc::Broadcasted{<:AbstractNamedTensorStyle} ) - _copyto_unnamed!(unnamed(dest), broadcasted_unnamed(bc, dimnames(dest))) + _copyto_unnamed!(unnamed(dest), bc, dimnames(dest)) return dest end -# Function barrier mirroring `_copy_unnamed`: `unnamed(dest)` and `broadcasted_unnamed` have -# abstract inferred types (the named backing array is abstract), so re-dispatching on the concrete -# runtime type here keeps the `copyto!` below type-stable. Linear folds to a `LinearBroadcasted`; -# non-linear falls to Base's generic in-place broadcast. -function _copyto_unnamed!(dest_unnamed, bc_unnamed) - lb = TA.tryflattenlinear(bc_unnamed) - isnothing(lb) && return copyto!(dest_unnamed, bc_unnamed) - return copyto!(dest_unnamed, lb) +# Function barrier mirroring `_copy_unnamed`. In place, so every operand aligns to `dest`. +@noinline function _copyto_unnamed!(dest_unnamed, bc, nms) + return copyto!(dest_unnamed, unnamed_aligned(TA.flattenlinear(bc), nms)) end # Operator-preserving broadcasting. @@ -148,8 +151,7 @@ end # Reinterpret an operator-style `Broadcasted` under `NamedTensorStyle`, the broadcast # over the operators' states, so the shared `NamedTensorStyle` implementation runs (its -# `broadcasted_unnamed` already peels each operator operand to its `state` via -# `unnamed`). +# `unnamed_split`/`unnamed_aligned` peel each operator operand to its `state` via `unnamed`). function statebroadcasted(bc::Broadcasted{<:NamedTensorOperatorStyle}) return Broadcasted{NamedTensorStyle{Any}}(bc.f, bc.args, bc.axes) end diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl index ebe6258..00b77fc 100644 --- a/test/test_tensorkitext.jl +++ b/test/test_tensorkitext.jl @@ -66,12 +66,12 @@ using Test: @test, @test_throws, @testset @test TK.space(ref) == TK.space(gc) @test ref ≈ gc - # Linear-combination broadcast lowers to `bipermutedimsopadd!`; element-wise errors. + # Named broadcasting is linear-only; a non-linear element-wise `f.(a)` errors. b2 = randn(rng, elt, i, j) @test unnamed(a + b2) ≈ unnamed(a) + unnamed(b2) @test unnamed(2 * a) ≈ 2 * unnamed(a) @test unnamed(a .- 3 .* b2) ≈ unnamed(a) - 3 * unnamed(b2) - @test_throws ErrorException sin.(a) + @test_throws ArgumentError sin.(a) # Factorizations reconstruct the tensor (lowered through matricize / MatrixAlgebraKit). # Checked by full contraction to a scalar, which is bipartition-independent. From 79171ffecee3264951d8b582dc7c813b9d11242f Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 17 Aug 2026 21:43:47 -0400 Subject: [PATCH 08/10] Unname linear broadcasts via the term interface Replaces the per-node unname recursion with a generic `operation`/`arguments` tree-walk over `LinearBroadcasted`, so a new node type needs no new unname method. Co-Authored-By: Claude Opus 4.8 --- src/broadcast.jl | 56 +++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/broadcast.jl b/src/broadcast.jl index 4e32000..f6e2aaa 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -21,33 +21,31 @@ end # `AbstractArray`); without this the default `broadcastable` wraps it in a `Ref`. BC.broadcastable(a::AbstractNamedTensor) = a -# Unname a flattened named `LinearBroadcasted` preserving the operand's codomain/domain split. Valid -# only for a single-operand expression (`2 .* a`, `conj.(a)`): the sole leaf defines the output names, -# so it unnames to its bare backing. A sum has no single split, so its addends are aligned instead. -# (Flattening distributes scaling/conjugation over `+`, so a `Scaled`/`Conj` node never wraps an `Add`.) -function unnamed_split(a::TA.ScaledBroadcasted, names) - return TA.linearbroadcasted(*, TA.coeff(a), unnamed_split(TA.unscaled(a), names)) -end -function unnamed_split(a::TA.ConjBroadcasted, names) - return TA.linearbroadcasted(conj, unnamed_split(parent(a), names)) -end -unnamed_split(a::TA.AddBroadcasted, names) = unnamed_aligned(a, names) -unnamed_split(a::AbstractNamedTensor, names) = unnamed(a) - -# Unname aligning every leaf to `names` through the `PermutedDims` wrapper (all-codomain output). Used -# for a sum's addends and for every in-place `copyto!` (aligned to the destination). -function unnamed_aligned(a::TA.ScaledBroadcasted, names) - return TA.linearbroadcasted(*, TA.coeff(a), unnamed_aligned(TA.unscaled(a), names)) -end -function unnamed_aligned(a::TA.ConjBroadcasted, names) - return TA.linearbroadcasted(conj, unnamed_aligned(parent(a), names)) -end -function unnamed_aligned(a::TA.AddBroadcasted, names) - return TA.linearbroadcasted(+, map(x -> unnamed_aligned(x, names), TA.addends(a))...) +# Unname a flattened named `LinearBroadcasted`. A single scaled/conjugated operand is already aligned to +# the output names, so no permutation is needed and its codomain/domain split is kept; only a sum needs +# its addends aligned. (Flattening distributes scaling/conjugation over `+`, so a `Scaled`/`Conj` node +# never wraps an `Add`, and the no-permutation recursion below never reaches one.) +unnamed_linear(a::TA.LinearBroadcasted, names) = unnamed_linear(a) +unnamed_linear(a::TA.AddBroadcasted, names) = unnamed_linear_aligned(a, names) + +# No permutation: strip names down the expression tree via the `operation`/`arguments` term interface. +function unnamed_linear(a::TA.LinearBroadcasted) + return TA.linearbroadcasted(TA.operation(a), map(unnamed_linear, TA.arguments(a))...) +end +unnamed_linear(a::AbstractNamedTensor) = unnamed(a) +unnamed_linear(a::Number) = a + +# Align every leaf to `names` through the `PermutedDims` wrapper (all-codomain output). Used for a sum's +# addends and for every in-place `copyto!` (aligned to the destination). +function unnamed_linear_aligned(a::TA.LinearBroadcasted, names) + return TA.linearbroadcasted( + TA.operation(a), map(x -> unnamed_linear_aligned(x, names), TA.arguments(a))... + ) end -function unnamed_aligned(a::AbstractNamedTensor, names) +function unnamed_linear_aligned(a::AbstractNamedTensor, names) return _broadcast_permuteddims(unnamed(a), getperm(dimnames(a), names)) end +unnamed_linear_aligned(a::Number, names) = a # Broadcasting-only alignment: unlike the public `unnamed(a, names)` (which returns a # `Base.PermutedDimsArray`, a full array), this wraps in `TensorAlgebra.PermutedDims`, which stores # the permutation in a field rather than a type parameter, so it builds cheaply and type-stably @@ -61,7 +59,7 @@ end return TA.PermutedDims(array, ntuple(i -> perm[i], Val(TA.ndims(array)))) end # Skip Base's shape-combination step: named broadcasts don't need the `NamedUnitRange` axis -# machinery. Name compatibility is handled by the per-operand alignment in `unnamed_aligned` +# machinery. Name compatibility is handled by the per-operand alignment in `unnamed_linear_aligned` # (via `getperm`), and unnamed-shape compatibility by TensorAlgebra. BC.instantiate(bc::Broadcasted{<:AbstractNamedTensorStyle}) = bc @@ -80,9 +78,9 @@ end # Function barrier: `bc`'s named leaves are abstractly typed, so re-dispatching on the concrete `bc` # here keeps the flatten/unname/materialize below type-stable. `copy(lb)` allocates through the unnamed # backend's own broadcast-style `similar`, so the result inherits the backend (dense, graded, ...); -# `unnamed_split` keeps a single scaled/conjugated operand's codomain/domain split. +# `unnamed_linear` keeps a single scaled/conjugated operand's codomain/domain split. @noinline function _copy_unnamed(bc, nms) - return copy(unnamed_split(TA.flattenlinear(bc), nms)) + return copy(unnamed_linear(TA.flattenlinear(bc), nms)) end # `Base.Broadcast.materialize!` otherwise reconstructs the broadcast over `axes(dest)` and @@ -107,7 +105,7 @@ end # Function barrier mirroring `_copy_unnamed`. In place, so every operand aligns to `dest`. @noinline function _copyto_unnamed!(dest_unnamed, bc, nms) - return copyto!(dest_unnamed, unnamed_aligned(TA.flattenlinear(bc), nms)) + return copyto!(dest_unnamed, unnamed_linear_aligned(TA.flattenlinear(bc), nms)) end # Operator-preserving broadcasting. @@ -151,7 +149,7 @@ end # Reinterpret an operator-style `Broadcasted` under `NamedTensorStyle`, the broadcast # over the operators' states, so the shared `NamedTensorStyle` implementation runs (its -# `unnamed_split`/`unnamed_aligned` peel each operator operand to its `state` via `unnamed`). +# `unnamed_linear`/`unnamed_linear_aligned` peel each operator operand to its `state` via `unnamed`). function statebroadcasted(bc::Broadcasted{<:NamedTensorOperatorStyle}) return Broadcasted{NamedTensorStyle{Any}}(bc.f, bc.args, bc.axes) end From 471e454fb62288e744fb5967c851c3b02ca89f8f Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 17 Aug 2026 22:09:38 -0400 Subject: [PATCH 09/10] Keep named broadcasting non-linear-capable Restore the non-linear fallback (`unnamed_broadcasted`) so a general element-wise `f.(a)` still routes through Base's generic broadcast, and drop the version to a non-breaking patch bump. --- Project.toml | 2 +- src/broadcast.jl | 31 +++++++++++++++++++++++++------ test/test_tensorkitext.jl | 5 +++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Project.toml b/Project.toml index b249cb5..fdded47 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.14.0" +version = "0.13.14" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/broadcast.jl b/src/broadcast.jl index f6e2aaa..2e11211 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -46,6 +46,18 @@ function unnamed_linear_aligned(a::AbstractNamedTensor, names) return _broadcast_permuteddims(unnamed(a), getperm(dimnames(a), names)) end unnamed_linear_aligned(a::Number, names) = a + +# Non-linear fallback: unname a general `Broadcasted` by aligning each operand to `names`, so Base's +# generic broadcast can run (all-codomain output). Only the linear path preserves the split. +unnamed_broadcasted(x::Number, names) = x +function unnamed_broadcasted(a::AbstractNamedTensor, names) + # An operand already aligned to `names` needs no permutation, skipping the identity wrapper. + dimnames(a) == names && return unnamed(a) + return _broadcast_permuteddims(unnamed(a), getperm(dimnames(a), names)) +end +function unnamed_broadcasted(bc::Broadcasted, names) + return broadcasted(bc.f, Base.Fix2(unnamed_broadcasted, names).(bc.args)...) +end # Broadcasting-only alignment: unlike the public `unnamed(a, names)` (which returns a # `Base.PermutedDimsArray`, a full array), this wraps in `TensorAlgebra.PermutedDims`, which stores # the permutation in a field rather than a type parameter, so it builds cheaply and type-stably @@ -76,11 +88,15 @@ function Base.copy(bc::Broadcasted{<:AbstractNamedTensorStyle}) end # Function barrier: `bc`'s named leaves are abstractly typed, so re-dispatching on the concrete `bc` -# here keeps the flatten/unname/materialize below type-stable. `copy(lb)` allocates through the unnamed -# backend's own broadcast-style `similar`, so the result inherits the backend (dense, graded, ...); -# `unnamed_linear` keeps a single scaled/conjugated operand's codomain/domain split. +# here keeps the flatten/unname/materialize below type-stable. A linear expression folds to a +# `LinearBroadcasted` and materializes through `copy(lb)`, whose allocation (`similar(lb)`) is the +# unnamed backend's own broadcast-style `similar`, so the result inherits the backend (dense, graded, +# ...) and `unnamed_linear` keeps a single scaled/conjugated operand's codomain/domain split. A +# non-linear expression falls back to unnaming the raw `Broadcasted` and Base's generic broadcast. @noinline function _copy_unnamed(bc, nms) - return copy(unnamed_linear(TA.flattenlinear(bc), nms)) + lb = TA.tryflattenlinear(bc) + isnothing(lb) && return copy(unnamed_broadcasted(bc, nms)) + return copy(unnamed_linear(lb, nms)) end # `Base.Broadcast.materialize!` otherwise reconstructs the broadcast over `axes(dest)` and @@ -103,9 +119,12 @@ function Base.copyto!( return dest end -# Function barrier mirroring `_copy_unnamed`. In place, so every operand aligns to `dest`. +# Function barrier mirroring `_copy_unnamed`. In place, so every operand aligns to `dest`; non-linear +# falls back to Base's generic in-place broadcast. @noinline function _copyto_unnamed!(dest_unnamed, bc, nms) - return copyto!(dest_unnamed, unnamed_linear_aligned(TA.flattenlinear(bc), nms)) + lb = TA.tryflattenlinear(bc) + isnothing(lb) && return copyto!(dest_unnamed, unnamed_broadcasted(bc, nms)) + return copyto!(dest_unnamed, unnamed_linear_aligned(lb, nms)) end # Operator-preserving broadcasting. diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl index 00b77fc..45341be 100644 --- a/test/test_tensorkitext.jl +++ b/test/test_tensorkitext.jl @@ -66,12 +66,13 @@ using Test: @test, @test_throws, @testset @test TK.space(ref) == TK.space(gc) @test ref ≈ gc - # Named broadcasting is linear-only; a non-linear element-wise `f.(a)` errors. + # Linear-combination broadcast lowers to `bipermutedimsopadd!`; a non-linear element-wise + # `f.(a)` on a graded tensor errors (graded broadcasting is linear-only). b2 = randn(rng, elt, i, j) @test unnamed(a + b2) ≈ unnamed(a) + unnamed(b2) @test unnamed(2 * a) ≈ 2 * unnamed(a) @test unnamed(a .- 3 .* b2) ≈ unnamed(a) - 3 * unnamed(b2) - @test_throws ArgumentError sin.(a) + @test_throws ErrorException sin.(a) # Factorizations reconstruct the tensor (lowered through matricize / MatrixAlgebraKit). # Checked by full contraction to a scalar, which is bipartition-independent. From 2f53b069702c10f6125cfe308b97c800e8df9f1d Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 18 Aug 2026 11:53:31 -0400 Subject: [PATCH 10/10] Test graded named broadcasting across backends Adds named-tensor graded broadcasting tests: a within-split leg reorder that still adds correctly, a cross-split add that errors, and GradedArrays.jl-backend coverage (flat and matricized) alongside the TensorMap-backed cases. --- test/test_gradedarraysext.jl | 30 +++++++++++++++++++++++++++++- test/test_tensorkitext.jl | 12 ++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/test/test_gradedarraysext.jl b/test/test_gradedarraysext.jl index 6e7095e..43867cf 100644 --- a/test/test_gradedarraysext.jl +++ b/test/test_gradedarraysext.jl @@ -1,5 +1,5 @@ using GradedArrays: U1, sectors -using ITensorBase: ITensorBase, Index, inds, prime, space +using ITensorBase: ITensorBase, Index, aligndims, inds, prime, space, unnamed using StableRNGs: StableRNG using TensorAlgebra: TensorAlgebra, isdual, project, project_aux, tryproject, tryproject_aux, unchecked_project, unchecked_project_aux @@ -76,6 +76,34 @@ using Test: @test, @test_throws, @testset @test length(inds(fill(elt(2), U1(1), (), (j,)))) == 2 end +# Broadcasting over graded (GradedArrays.jl) indices routes the named expression through the +# `GradedArray` / matricized `FusedGradedMatrix` backend. Linear combinations add block-wise; a sum +# flattens all-codomain, so a within-split reorder is compared at a common split via `aligndims`. +@testset "GradedArraysExt broadcasting (eltype = $elt)" for elt in (Float64, ComplexF64) + rng = StableRNG(1234) + i = Index([U1(0) => 2, U1(1) => 3]; tags = "i") + j = Index([U1(0) => 1, U1(1) => 2]; tags = "j") + k = Index([U1(-1) => 1, U1(0) => 2]; tags = "k") + + # Flat form (all-codomain, `GradedArray`-backed). + a = randn(rng, elt, i, j) + b = randn(rng, elt, i, j) + @test unnamed(a .+ b) ≈ unnamed(a) + unnamed(b) + @test unnamed(2 .* a) ≈ 2 * unnamed(a) + @test unnamed(a .- 3 .* b) ≈ unnamed(a) - 3 * unnamed(b) + + # Map form (codomain/domain split, matricized `FusedGradedMatrix` storage). + m = randn(rng, elt, (i,), (j,)) + n = randn(rng, elt, (i,), (j,)) + @test unnamed(m .+ n) ≈ unnamed(m) + unnamed(n) + + # Within-split reorder still adds correctly (the sum is all-codomain, compared via `aligndims`). + mr1 = randn(rng, elt, (i, j), (k,)) + mr2 = randn(rng, elt, (j, i), (k,)) + @test unnamed(aligndims(mr1 .+ mr2, (i, j), (k,))) ≈ + unnamed(mr1) + unnamed(aligndims(mr2, (i, j), (k,))) +end + # `project_aux` and its siblings derive a named auxiliary leg carrying the operator's flux, so a # charge-shifting operator stays symmetry-allowed instead of being projected away. Strict `project` # instead projects into exactly the given indices and rejects a surplus axis. diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl index 45341be..c439454 100644 --- a/test/test_tensorkitext.jl +++ b/test/test_tensorkitext.jl @@ -74,6 +74,18 @@ using Test: @test, @test_throws, @testset @test unnamed(a .- 3 .* b2) ≈ unnamed(a) - 3 * unnamed(b2) @test_throws ErrorException sin.(a) + # Named broadcasting aligns operands by name within their codomain/domain split, so a within-split + # reorder of a multi-leg operand still adds correctly (compared at a common split via `aligndims`). + mr1 = randn(rng, elt, (i, j), (k,)) + mr2 = randn(rng, elt, (j, i), (k,)) + @test unnamed(aligndims(mr1 .+ mr2, (i, j), (k,))) ≈ + unnamed(mr1) + unnamed(aligndims(mr2, (i, j), (k,))) + # Adding across an incompatible split (a shared leg in the codomain of one operand and the domain + # of the other) has mismatched axes and errors. + cs1 = randn(rng, elt, (i,), (j,)) + cs2 = randn(rng, elt, (j,), (i,)) + @test_throws DimensionMismatch cs1 .+ cs2 + # Factorizations reconstruct the tensor (lowered through matricize / MatrixAlgebraKit). # Checked by full contraction to a scalar, which is bipartition-independent. a3 = randn(rng, elt, i, j, k)