diff --git a/Project.toml b/Project.toml index 34bf4aee..b4f3ec1c 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] @@ -45,14 +45,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..4f0da763 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.14.6" +GradedArrays = "0.15" 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.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 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