From 318ff29f44d0cc61eab9f6bfe55ba3694829e387 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 3 Aug 2026 13:34:27 -0400 Subject: [PATCH 1/3] Adopt TensorAlgebra 0.18's strict `project` and `project_aux` Follow TensorAlgebra 0.18's `project`/`project_aux` split at the named-index layer: `project` projects into exactly the given indices, while `project_aux` derives and appends the flux-carrying auxiliary index for a charge-shifting operator or non-invariant state. Also moves to GradedArrays 0.15. Co-Authored-By: Claude Opus 4.8 --- Project.toml | 10 +++- docs/Project.toml | 2 +- src/tensoralgebra.jl | 104 +++++++++++++++++++++++++++++------ test/Project.toml | 14 ++++- test/test_gradedarraysext.jl | 41 ++++++++------ 5 files changed, 131 insertions(+), 40 deletions(-) diff --git a/Project.toml b/Project.toml index 34bf4aee..b42692bc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.13.12" +version = "0.13.13" authors = ["ITensor developers and contributors"] [workspace] @@ -31,6 +31,10 @@ OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" +[sources.TensorAlgebra] +rev = "mf/project-aux-interface" +url = "https://github.com/ITensor/TensorAlgebra.jl" + [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseGradedArraysExt = ["GradedArrays", "TensorKitSectors"] @@ -45,14 +49,14 @@ Adapt = "4.1.1" ArrayLayouts = "1.11" Combinatorics = "1" ConstructionBase = "1.6" -GradedArrays = "0.14.6" +GradedArrays = "0.15" 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.17.10" +TensorAlgebra = "0.18" TensorKit = "0.17" TensorKitSectors = "0.3.9" TermInterface = "2" diff --git a/docs/Project.toml b/docs/Project.toml index 7273ead0..03074f54 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.17" +TensorAlgebra = "0.18" Test = "1.10" diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index 2e9285c6..4911616e 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -793,12 +793,11 @@ end # Projection into a symmetry-restricted named tensor. # -# Attach `input_names` to the leading axes of the `projected` array and mint a fresh unique name for -# a trailing surplus axis if the backend derived one (it appends at most one, as the last domain -# axis). A surplus axis is an auxiliary leg the backend adds so the result is symmetry-allowed (for -# example a flux-canceling charge leg for a charge-shifting operator), and naming it returns it as a -# dimension the caller can read off the result. A `nothing` (from `tryproject`) passes straight -# through. +# Attach `input_names` to the projected array's axes, minting a fresh unique name for each trailing +# axis beyond them. The strict `project` verbs add none, so this just reattaches the given names; +# the `*_aux` verbs append one derived auxiliary leg (as the last domain axis) carrying the flux of +# a charge-shifting operator or non-invariant state, and naming it returns it as a dimension the +# caller can read off the result. A `nothing` (from the nullable verbs) passes straight through. function name_projected(projected, input_names) isnothing(projected) && return nothing aux_names = ntuple( @@ -809,8 +808,8 @@ function name_projected(projected, input_names) end # Each `_nameddims` runs the named-index layer of a `TensorAlgebra` verb: strip the axes to -# their unnamed ranges, lower to the dense verb, and reattach the names, minting a name for the aux -# leg the backend may append (see `name_projected`). The one body also covers an empty codomain, +# their unnamed ranges, lower to the unnamed verb, and reattach the names (the `*_aux` verbs also +# name the derived auxiliary leg, see `name_projected`). The one body also covers an empty codomain, # since `unnamed.(())` and `name.(())` are both `()`, so the all-domain (co-state) case needs no # separate path. function project_nameddims(a, codomain_inds, domain_inds; kwargs...) @@ -827,18 +826,45 @@ function unchecked_project_nameddims(a, codomain_inds, domain_inds; kwargs...) return name_projected(projected, (name.(codomain_inds)..., name.(domain_inds)...)) end -# Shared body for the named-index `project` family docstrings. Each function's summary states its -# own verification behavior; this describes what all three have in common. +# The `*_aux` workers derive and append the flux-carrying auxiliary leg, which `name_projected` +# names. Same named-index layer as the strict workers above, lowered to the `*_aux` unnamed verbs. +function project_aux_nameddims(a, codomain_inds, domain_inds; kwargs...) + projected = TA.project_aux(a, unnamed.(codomain_inds), unnamed.(domain_inds); kwargs...) + return name_projected(projected, (name.(codomain_inds)..., name.(domain_inds)...)) +end +function tryproject_aux_nameddims(a, codomain_inds, domain_inds; kwargs...) + projected = + TA.tryproject_aux(a, unnamed.(codomain_inds), unnamed.(domain_inds); kwargs...) + return name_projected(projected, (name.(codomain_inds)..., name.(domain_inds)...)) +end +function unchecked_project_aux_nameddims(a, codomain_inds, domain_inds; kwargs...) + projected = + TA.unchecked_project_aux( + a, + unnamed.(codomain_inds), + unnamed.(domain_inds); + kwargs... + ) + return name_projected(projected, (name.(codomain_inds)..., name.(domain_inds)...)) +end + +# Shared body for the named-index `project` and `project_aux` family docstrings. Each function's +# summary states its own verification behavior; this describes the forms and backend selection they +# all share. const _project_named_body = """ The three-argument form takes an explicit codomain/domain split (an operator), and the two-argument form a flat list of indices (a state, i.e. an empty domain). The index axes select the backend: dense ranges give an `Array`, graded ranges a block-sparse array, and TensorKit spaces a `TensorMap`. `a` is indexed positionally in the order `(codomain_inds..., domain_inds...)`. +""" -When `a` carries one more axis than `codomain_inds` and `domain_inds` account for, that trailing -surplus axis is an auxiliary leg the backend derived to make the result symmetry-allowed (for -example a flux-canceling charge leg for a charge-shifting operator). It is returned as a named -dimension with a freshly generated name the caller can read off the result. +# Extra paragraph for the `*_aux` docstrings: unlike `project`, which projects into exactly the given +# indices, these derive and append the flux-carrying leg. +const _project_aux_named_body = """ +`a` may carry the physical rank the indices account for, or one trailing slice axis. `project_aux` +derives an auxiliary domain index to make the result symmetry-allowed (for example a flux-canceling +charge leg for a charge-shifting operator) and returns it as a named dimension with a freshly +generated name the caller can read off the result. """ const _project_named_docstring = """ @@ -851,6 +877,9 @@ throwing an `InexactError` otherwise (keyword arguments are forwarded to the `is check). $(_project_named_body) +`project` projects into exactly the given indices. To append a derived flux-carrying leg for a +charge-shifting operator or non-invariant state, use `TensorAlgebra.project_aux`. + See also `TensorAlgebra.tryproject` and `TensorAlgebra.unchecked_project`. """ @@ -863,7 +892,7 @@ component of `a` would be discarded (keyword arguments are forwarded to the `isa check). $(_project_named_body) -See also `TensorAlgebra.project` and `TensorAlgebra.unchecked_project`. +See also `TensorAlgebra.project`, `TensorAlgebra.unchecked_project`, and `TensorAlgebra.tryproject_aux`. """ const _unchecked_project_named_docstring = """ @@ -874,13 +903,54 @@ Like `TensorAlgebra.project`, but skip the verification: components of `a` outsi symmetry-allowed structure are dropped without inspection. $(_project_named_body) -See also `TensorAlgebra.project` and `TensorAlgebra.tryproject`. +See also `TensorAlgebra.project`, `TensorAlgebra.tryproject`, and `TensorAlgebra.unchecked_project_aux`. +""" + +const _project_aux_named_docstring = """ + TensorAlgebra.project_aux(a::AbstractArray, codomain_inds, domain_inds; kwargs...) -> t + TensorAlgebra.project_aux(a::AbstractArray, inds; kwargs...) -> t + +Build a named tensor by projecting `a` and appending a derived auxiliary domain index carrying its +flux, verifying that only a negligible component of `a` is discarded and throwing an `InexactError` +otherwise (keyword arguments are forwarded to the `isapprox` tolerance check). + +$(_project_named_body) +$(_project_aux_named_body) +See also `TensorAlgebra.tryproject_aux` and `TensorAlgebra.unchecked_project_aux`. +""" + +const _tryproject_aux_named_docstring = """ + TensorAlgebra.tryproject_aux(a::AbstractArray, codomain_inds, domain_inds; kwargs...) -> Union{t, Nothing} + TensorAlgebra.tryproject_aux(a::AbstractArray, inds; kwargs...) -> Union{t, Nothing} + +Like `TensorAlgebra.project_aux`, but return `nothing` instead of throwing when a non-negligible +component of `a` would be discarded (keyword arguments are forwarded to the `isapprox` tolerance +check). + +$(_project_named_body) +$(_project_aux_named_body) +See also `TensorAlgebra.project_aux` and `TensorAlgebra.unchecked_project_aux`. +""" + +const _unchecked_project_aux_named_docstring = """ + TensorAlgebra.unchecked_project_aux(a::AbstractArray, codomain_inds, domain_inds; kwargs...) -> t + TensorAlgebra.unchecked_project_aux(a::AbstractArray, inds; kwargs...) -> t + +Like `TensorAlgebra.project_aux`, but skip the verification: components of `a` outside the +symmetry-allowed structure are dropped without inspection. + +$(_project_named_body) +$(_project_aux_named_body) +See also `TensorAlgebra.project_aux` and `TensorAlgebra.tryproject_aux`. """ # Forward each named-index signature to its worker, attaching the family docstring to the split # form. Two split entries per verb so an empty codomain or an empty domain still selects this # overload instead of the unnamed-axis generic. The flat (state) form forwards with an empty domain. -for f in (:project, :tryproject, :unchecked_project) +for f in ( + :project, :tryproject, :unchecked_project, + :project_aux, :tryproject_aux, :unchecked_project_aux, + ) fnamed = Symbol(f, :_nameddims) doc = Symbol("_", f, "_named_docstring") @eval begin diff --git a/test/Project.toml b/test/Project.toml index 4875603e..f23d922e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -13,6 +13,7 @@ Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SparseArraysBase = "0d5efcca-f356-4864-8770-e1ed8d78f208" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" @@ -24,15 +25,23 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" +[sources.GradedArrays] +rev = "mf/project-aux" +url = "https://github.com/ITensor/GradedArrays.jl" + [sources.ITensorBase] path = ".." +[sources.SparseArraysBase] +rev = "mf/tensoralgebra-0.18-compat" +url = "https://github.com/ITensor/SparseArraysBase.jl" + [compat] AbstractTrees = "0.4.5" Adapt = "4" Aqua = "0.8.9" Combinatorics = "1" -GradedArrays = "0.14.6" +GradedArrays = "0.15" ITensorBase = "0.13" ITensorPkgSkeleton = "0.3.42" JLArrays = "0.2, 0.3" @@ -42,9 +51,10 @@ Mooncake = "0.4, 0.5" OMEinsumContractionOrders = "1.3" Random = "1.10" SafeTestsets = "0.1" +SparseArraysBase = "0.10.9" StableRNGs = "1" Suppressor = "0.2" -TensorAlgebra = "0.17.5" +TensorAlgebra = "0.18" TensorKit = "0.17" TensorKitSectors = "0.3.9" TermInterface = "2" diff --git a/test/test_gradedarraysext.jl b/test/test_gradedarraysext.jl index bc3036da..6e7095e7 100644 --- a/test/test_gradedarraysext.jl +++ b/test/test_gradedarraysext.jl @@ -1,9 +1,10 @@ using GradedArrays: U1, sectors using ITensorBase: ITensorBase, Index, inds, prime, space using StableRNGs: StableRNG -using TensorAlgebra: TensorAlgebra, isdual, project, tryproject, unchecked_project +using TensorAlgebra: TensorAlgebra, isdual, project, project_aux, tryproject, + tryproject_aux, unchecked_project, unchecked_project_aux using TensorKitSectors: FermionNumber -using Test: @test, @testset +using Test: @test, @test_throws, @testset # The flux-canceling constructor mints an auxiliary `Index` carrying the requested charge and # appends it to the domain, so an `ITensor` over graded (block-sparse) indices can be built @@ -75,10 +76,10 @@ using Test: @test, @testset @test length(inds(fill(elt(2), U1(1), (), (j,)))) == 2 end -# `project` and its siblings derive the same kind of auxiliary leg: a trailing surplus axis on the -# dense array becomes a named aux dimension carrying the operator's flux, so a charge-shifting -# operator stays symmetry-allowed instead of being projected away. -@testset "project derives a named auxiliary leg (eltype = $elt)" for elt in +# `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. +@testset "project_aux derives a named auxiliary leg (eltype = $elt)" for elt in ( Float64, ComplexF64, @@ -86,18 +87,24 @@ end s = Index([U1(0) => 1, U1(1) => 1]; tags = "s") cdag = elt[0 0; 1 0] # raising operator, flux +1 - # without a surplus axis the charge-shifting operator has nothing to carry its flux + # without an auxiliary leg the charge-shifting operator has nothing to carry its flux @test iszero(unchecked_project(cdag, (prime(s),), (s,))) - # reshaping to a trailing length-1 axis lets each verb mint the flux-canceling aux leg - @testset "$f" for f in (project, tryproject, unchecked_project) - op = f(reshape(cdag, (2, 2, 1)), (prime(s),), (s,)) - @test length(inds(op)) == 3 - @test !iszero(op) - @test eltype(op) == elt - aux = only(setdiff(collect(inds(op)), [prime(s), s])) - @test length(aux) == 1 - @test isdual(aux) # dualized, in the domain - @test only(sectors(space(aux))) == U1(1) # carries the operator's flux + # strict `project` projects into exactly the given indices, so a surplus axis is an error + @test_throws ArgumentError project(reshape(cdag, (2, 2, 1)), (prime(s),), (s,)) + + # each `*_aux` verb derives the flux-canceling aux leg, whether given the physical rank or a + # trailing length-1 slice axis + @testset "$f" for f in (project_aux, tryproject_aux, unchecked_project_aux) + for a in (cdag, reshape(cdag, (2, 2, 1))) + op = f(a, (prime(s),), (s,)) + @test length(inds(op)) == 3 + @test !iszero(op) + @test eltype(op) == elt + aux = only(setdiff(collect(inds(op)), [prime(s), s])) + @test length(aux) == 1 + @test isdual(aux) # dualized, in the domain + @test only(sectors(space(aux))) == U1(1) # carries the operator's flux + end end end From 7a6f526e408d5585230461788e98764045c445d4 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 3 Aug 2026 15:03:11 -0400 Subject: [PATCH 2/3] Drop `[sources]` pins and the temporary test `SparseArraysBase` dep now that the upstreams are registered Co-Authored-By: Claude Opus 4.8 --- Project.toml | 4 ---- test/Project.toml | 10 ---------- 2 files changed, 14 deletions(-) diff --git a/Project.toml b/Project.toml index b42692bc..b4f3ec1c 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.TensorAlgebra] -rev = "mf/project-aux-interface" -url = "https://github.com/ITensor/TensorAlgebra.jl" - [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseGradedArraysExt = ["GradedArrays", "TensorKitSectors"] diff --git a/test/Project.toml b/test/Project.toml index f23d922e..4f0da763 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -13,7 +13,6 @@ Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -SparseArraysBase = "0d5efcca-f356-4864-8770-e1ed8d78f208" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" @@ -25,17 +24,9 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" -[sources.GradedArrays] -rev = "mf/project-aux" -url = "https://github.com/ITensor/GradedArrays.jl" - [sources.ITensorBase] path = ".." -[sources.SparseArraysBase] -rev = "mf/tensoralgebra-0.18-compat" -url = "https://github.com/ITensor/SparseArraysBase.jl" - [compat] AbstractTrees = "0.4.5" Adapt = "4" @@ -51,7 +42,6 @@ Mooncake = "0.4, 0.5" OMEinsumContractionOrders = "1.3" Random = "1.10" SafeTestsets = "0.1" -SparseArraysBase = "0.10.9" StableRNGs = "1" Suppressor = "0.2" TensorAlgebra = "0.18" From c4629a7cd2de5979b1b7f3f7e94684c420f55f7f Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 3 Aug 2026 15:32:57 -0400 Subject: [PATCH 3/3] Make uniquename autodiff tests interface-only for Mooncake 0.5.44 Mooncake 0.5.44 checks that a rule's primal is reproducible across reruns. uniquename mints a fresh name on every call, so only its AD interface is meaningful to test. --- test/test_mooncakeext.jl | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/test_mooncakeext.jl b/test/test_mooncakeext.jl index 4faf1bee..befd7e73 100644 --- a/test/test_mooncakeext.jl +++ b/test/test_mooncakeext.jl @@ -36,8 +36,20 @@ using Test: @test, @testset is_primitive ) Mooncake.TestUtils.test_rule(rng, name, i; mode, is_primitive) - Mooncake.TestUtils.test_rule(rng, uniquename, i; mode, is_primitive) - Mooncake.TestUtils.test_rule(rng, uniquename, rng, i; mode, is_primitive) + # `uniquename` mints a fresh name on every call, so its primal is not + # reproducible across the reruns Mooncake's value checks rely on. Only its + # AD interface (zero derivatives) is meaningful here. + Mooncake.TestUtils.test_rule( + rng, + uniquename, + i; + mode, + is_primitive, + interface_only = true + ) + Mooncake.TestUtils.test_rule( + rng, uniquename, rng, i; mode, is_primitive, interface_only = true + ) Mooncake.TestUtils.test_rule(rng, to_inds, a1, (i, j); mode, is_primitive) end @testset "contract" begin