From c0971803ed9144b23fabb2d4c96bbabdf9939119 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 24 Jun 2026 21:48:21 -0400 Subject: [PATCH 1/3] Add name-aware VectorInterface methods for ITensors Adds zerovector, scale, scale!!, add!! and inner for AbstractITensor, alongside the existing scalartype, so an ITensor can be used directly as a vector in iterative solvers like KrylovKit.eigsolve that drive their Krylov vectors through VectorInterface. The generic AbstractArray fallbacks are not name-aware. --- Project.toml | 2 +- src/abstractitensor.jl | 17 +++++++++++++++++ test/test_vectorinterface.jl | 30 ++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/test_vectorinterface.jl diff --git a/Project.toml b/Project.toml index 4c84d8a..58dd56e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.8.1" +version = "0.8.2" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/abstractitensor.jl b/src/abstractitensor.jl index ddcc882..1085c19 100644 --- a/src/abstractitensor.jl +++ b/src/abstractitensor.jl @@ -230,6 +230,23 @@ using VectorInterface: VectorInterface, scalartype # Circumvent issue when eltype isn't known at compile time. VectorInterface.scalartype(a::AbstractITensor) = scalartype(unnamed(a)) +# Name-aware `VectorInterface` methods so that ITensors can be used directly as the +# vectors in iterative solvers such as `KrylovKit.eigsolve`, which drive their Krylov +# vectors through `VectorInterface`. The generic `AbstractArray` fallbacks are not +# name-aware and broadcast in ways that fail on an ITensor. +function VectorInterface.zerovector(a::AbstractITensor, ::Type{S}) where {S <: Number} + return fill!(similar(a, S), zero(S)) +end +VectorInterface.scale(a::AbstractITensor, α::Number) = a * α +VectorInterface.scale!!(a::AbstractITensor, α::Number) = a * α +VectorInterface.scale!!(::AbstractITensor, a::AbstractITensor, α::Number) = a * α +function VectorInterface.add!!( + y::AbstractITensor, x::AbstractITensor, α::Number, β::Number + ) + return x * α + y * β +end +VectorInterface.inner(x::AbstractITensor, y::AbstractITensor) = (conj(x) * y)[] + Base.axes(a::AbstractITensor, dimname::Name) = axes(a, dim(a, dimname)) Base.size(a::AbstractITensor, dimname::Name) = size(a, dim(a, dimname)) diff --git a/test/test_vectorinterface.jl b/test/test_vectorinterface.jl new file mode 100644 index 0000000..63e0b67 --- /dev/null +++ b/test/test_vectorinterface.jl @@ -0,0 +1,30 @@ +import VectorInterface as VI +using ITensorBase: dimnames, named, unnamed +using Test: @test, @testset + +# These name-aware methods are what let an ITensor be used directly as a vector in +# iterative solvers such as `KrylovKit.eigsolve`, which drive their Krylov vectors +# through `VectorInterface`. +@testset "VectorInterface (eltype=$(elt))" for elt in + (Float32, Float64, Complex{Float32}) + i, j = named.(2, (:i, :j)) + a = randn(elt, i, j) + b = randn(elt, j, i) + ua = unnamed(a) + ub = unnamed(b, dimnames(a)) + + @test VI.scalartype(a) === elt + + z = VI.zerovector(a, ComplexF64) + @test VI.scalartype(z) === ComplexF64 + @test iszero(unnamed(z)) + @test dimnames(z) == dimnames(a) + + @test unnamed(VI.scale(a, 2)) ≈ 2 * ua + @test unnamed(VI.scale!!(a, 2)) ≈ 2 * ua + @test unnamed(VI.scale!!(copy(a), a, 2)) ≈ 2 * ua + + @test unnamed(VI.add!!(copy(b), a, 2, 3), dimnames(a)) ≈ 2 * ua + 3 * ub + + @test VI.inner(a, b) ≈ VI.inner(ua, ub) +end From 2c40c1c907519d12f6dbcee491d9d3be91ff1a12 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 24 Jun 2026 22:52:35 -0400 Subject: [PATCH 2/3] Implement the full VectorInterface for ITensors Cover the complete interface (add/scale/zerovector and their in-place ! and maybe-in-place !! variants, inner, scalartype) rather than only the methods KrylovKit.eigsolve happens to call. The in-place methods use broadcasting. Each !! method stays in place when the result fits the destination element type and otherwise allocates. zerovector goes through a new in-place TensorAlgebra.zero! method for AbstractITensor. --- src/abstractitensor.jl | 62 ++++++++++++++++++++++++++++-------- test/test_vectorinterface.jl | 27 ++++++++++++++-- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/abstractitensor.jl b/src/abstractitensor.jl index 1085c19..0a0ba80 100644 --- a/src/abstractitensor.jl +++ b/src/abstractitensor.jl @@ -1,6 +1,6 @@ using LinearAlgebra: LinearAlgebra using Random: Random -using TensorAlgebra: permuteddims +using TensorAlgebra: TensorAlgebra, permuteddims, zero! # Some of the interface is inspired by: # https://github.com/ITensor/ITensors.jl @@ -226,25 +226,59 @@ Base.ndims(a::AbstractITensor) = ndims(unnamed(a)) # Circumvent issue when eltype isn't known at compile time. Base.eltype(a::AbstractITensor) = eltype(unnamed(a)) -using VectorInterface: VectorInterface, scalartype -# Circumvent issue when eltype isn't known at compile time. +# In-place zero of an ITensor, delegating to its unnamed parent array. +TensorAlgebra.zero!(a::AbstractITensor) = (zero!(unnamed(a)); a) + +# Name-aware `VectorInterface` methods so that ITensors can be used directly as the vectors +# in iterative solvers such as `KrylovKit.eigsolve`, which drive their Krylov vectors through +# `VectorInterface`; the generic `AbstractArray` fallbacks are not name-aware. The `!` methods +# operate in place via broadcasting; each `!!` method does so too when the result fits the +# destination's element type, and otherwise allocates. `scalartype` is computed in the value +# domain because an ITensor's element type is not encoded in its type. +using VectorInterface: VectorInterface, add, add!, scalartype, scale, scale!, zerovector! VectorInterface.scalartype(a::AbstractITensor) = scalartype(unnamed(a)) +function VectorInterface.scalartype(a::AbstractArray{<:AbstractITensor}) + return mapreduce(scalartype, promote_type, a; init = Bool) +end -# Name-aware `VectorInterface` methods so that ITensors can be used directly as the -# vectors in iterative solvers such as `KrylovKit.eigsolve`, which drive their Krylov -# vectors through `VectorInterface`. The generic `AbstractArray` fallbacks are not -# name-aware and broadcast in ways that fail on an ITensor. function VectorInterface.zerovector(a::AbstractITensor, ::Type{S}) where {S <: Number} - return fill!(similar(a, S), zero(S)) + return zerovector!(similar(a, S)) end +VectorInterface.zerovector!(a::AbstractITensor) = zero!(a) +VectorInterface.zerovector!!(a::AbstractITensor) = zerovector!(a) + VectorInterface.scale(a::AbstractITensor, α::Number) = a * α -VectorInterface.scale!!(a::AbstractITensor, α::Number) = a * α -VectorInterface.scale!!(::AbstractITensor, a::AbstractITensor, α::Number) = a * α -function VectorInterface.add!!( - y::AbstractITensor, x::AbstractITensor, α::Number, β::Number - ) - return x * α + y * β +function VectorInterface.scale!(a::AbstractITensor, α::Number) + a .= a .* α + return a +end +function VectorInterface.scale!(b::AbstractITensor, a::AbstractITensor, α::Number) + b .= a .* α + return b end +function VectorInterface.scale!!(a::AbstractITensor, α::Number) + promote_type(scalartype(a), typeof(α)) <: scalartype(a) || return scale(a, α) + return scale!(a, α) +end +function VectorInterface.scale!!(b::AbstractITensor, a::AbstractITensor, α::Number) + promote_type(scalartype(b), scalartype(a), typeof(α)) <: scalartype(b) || + return scale(a, α) + return scale!(b, a, α) +end + +function VectorInterface.add(y::AbstractITensor, x::AbstractITensor, α::Number, β::Number) + return y * β + x * α +end +function VectorInterface.add!(y::AbstractITensor, x::AbstractITensor, α::Number, β::Number) + y .= y .* β .+ x .* α + return y +end +function VectorInterface.add!!(y::AbstractITensor, x::AbstractITensor, α::Number, β::Number) + promote_type(scalartype(y), scalartype(x), typeof(α), typeof(β)) <: scalartype(y) || + return add(y, x, α, β) + return add!(y, x, α, β) +end + VectorInterface.inner(x::AbstractITensor, y::AbstractITensor) = (conj(x) * y)[] Base.axes(a::AbstractITensor, dimname::Name) = axes(a, dim(a, dimname)) diff --git a/test/test_vectorinterface.jl b/test/test_vectorinterface.jl index 63e0b67..9e5603c 100644 --- a/test/test_vectorinterface.jl +++ b/test/test_vectorinterface.jl @@ -14,17 +14,38 @@ using Test: @test, @testset ub = unnamed(b, dimnames(a)) @test VI.scalartype(a) === elt + @test VI.scalartype([a, b]) === elt + @test VI.scalartype([a, randn(ComplexF64, i, j)]) === ComplexF64 + # zerovector / zerovector! / zerovector!! z = VI.zerovector(a, ComplexF64) @test VI.scalartype(z) === ComplexF64 @test iszero(unnamed(z)) @test dimnames(z) == dimnames(a) + z = VI.zerovector!(copy(a)) + @test VI.scalartype(z) === elt + @test iszero(unnamed(z)) + @test VI.zerovector!!(a) ≈ VI.zerovector(a) + # scale / scale! / scale!! @test unnamed(VI.scale(a, 2)) ≈ 2 * ua - @test unnamed(VI.scale!!(a, 2)) ≈ 2 * ua - @test unnamed(VI.scale!!(copy(a), a, 2)) ≈ 2 * ua + @test unnamed(VI.scale!(copy(a), 2)) ≈ 2 * ua + @test unnamed(VI.scale!(similar(a), a, 2)) ≈ 2 * ua + @test unnamed(VI.scale!!(copy(a), 2)) ≈ 2 * ua + @test unnamed(VI.scale!!(similar(a), a, 2)) ≈ 2 * ua + # `!!` allocates a new array when the scalar promotes beyond the element type. + s = VI.scale!!(copy(a), 2im) + @test VI.scalartype(s) === complex(elt) + @test unnamed(s) ≈ 2im * ua - @test unnamed(VI.add!!(copy(b), a, 2, 3), dimnames(a)) ≈ 2 * ua + 3 * ub + # add / add! / add!! + @test unnamed(VI.add(b, a), dimnames(a)) ≈ ub + ua + @test unnamed(VI.add(b, a, 2, 3), dimnames(a)) ≈ 3 * ub + 2 * ua + @test unnamed(VI.add!(copy(b), a, 2, 3), dimnames(a)) ≈ 3 * ub + 2 * ua + @test unnamed(VI.add!!(copy(b), a, 2, 3), dimnames(a)) ≈ 3 * ub + 2 * ua + r = VI.add!!(copy(b), a, 2im, 3) + @test VI.scalartype(r) === complex(elt) + @test unnamed(r, dimnames(a)) ≈ 3 * ub + 2im * ua @test VI.inner(a, b) ≈ VI.inner(ua, ub) end From 9c4e250ad37b859f1c4653d8d302bf5afe29deac Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 25 Jun 2026 09:31:16 -0400 Subject: [PATCH 3/3] Use @. broadcasting in the VectorInterface scale/add methods --- src/abstractitensor.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abstractitensor.jl b/src/abstractitensor.jl index 0a0ba80..d3e5f1a 100644 --- a/src/abstractitensor.jl +++ b/src/abstractitensor.jl @@ -249,11 +249,11 @@ VectorInterface.zerovector!!(a::AbstractITensor) = zerovector!(a) VectorInterface.scale(a::AbstractITensor, α::Number) = a * α function VectorInterface.scale!(a::AbstractITensor, α::Number) - a .= a .* α + @. a = a * α return a end function VectorInterface.scale!(b::AbstractITensor, a::AbstractITensor, α::Number) - b .= a .* α + @. b = a * α return b end function VectorInterface.scale!!(a::AbstractITensor, α::Number) @@ -267,10 +267,10 @@ function VectorInterface.scale!!(b::AbstractITensor, a::AbstractITensor, α::Num end function VectorInterface.add(y::AbstractITensor, x::AbstractITensor, α::Number, β::Number) - return y * β + x * α + return @. y * β + x * α end function VectorInterface.add!(y::AbstractITensor, x::AbstractITensor, α::Number, β::Number) - y .= y .* β .+ x .* α + @. y = y * β + x * α return y end function VectorInterface.add!!(y::AbstractITensor, x::AbstractITensor, α::Number, β::Number)