diff --git a/Project.toml b/Project.toml index 9cc4f0e6..e874e497 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.13.10" +version = "0.13.11" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/namedtensoroperator.jl b/src/namedtensoroperator.jl index 876b23fe..f97de939 100644 --- a/src/namedtensoroperator.jl +++ b/src/namedtensoroperator.jl @@ -390,6 +390,20 @@ function operator(a::AbstractNamedTensor, output, input) return NamedTensorOperator(a, name.(output), name.(input)) end +# A plain tensor is a trivial (empty-pairing) operator, so an operator is the promotion of a +# non-operator tensor. `convert` wraps a plain tensor as a trivial operator and `promote_rule` +# makes the operator the common type, so `promote(state, operator)` returns two operators and a +# mixed collection such as `[state, operator]` builds a homogeneous `Vector{<:NamedTensorOperator}` +# instead of falling back to the abstract `typejoin`. +function Base.convert(::Type{O}, a::NamedTensor) where {O <: NamedTensorOperator} + return operator(a, (), ())::O +end +function Base.promote_rule( + ::Type{<:NamedTensor{D}}, ::Type{O} + ) where {D, O <: NamedTensorOperator{D}} + return O +end + # Operator-preserving contraction. Contracting two named arrays sums over their # shared names, so the result keeps each operand's surviving output/input # structure. A non-operator tensor contributes no pairs (all its names are diff --git a/test/test_operator.jl b/test/test_operator.jl index 9611040c..87f24e8f 100644 --- a/test/test_operator.jl +++ b/test/test_operator.jl @@ -473,3 +473,28 @@ end @test Pmat * Pmat' ≈ A @test Pmat * unnamed(state(Pinv)) ≈ I(n) end + +@testset "operator/state promotion" begin + t = NamedTensor(randn(2, 2), ("i", "j")) + o = operator(randn(2, 2), ("i",), ("j",)) + O = typeof(o) + + # A plain tensor promotes to the operator type (either argument order); the operator is + # the common type, so `promote` returns two operators. + @test promote_type(typeof(t), O) == O + @test promote_type(O, typeof(t)) == O + @test typeof.(promote(t, o)) == (O, O) + + # `convert` wraps a plain tensor as a trivial empty-pairing operator, losslessly, and is a + # no-op on an operator. + to = convert(O, t) + @test to isa NamedTensorOperator + @test isempty(outputnames(to)) && isempty(inputnames(to)) + @test state(to) === t + @test convert(O, o) === o + + # A heterogeneous collection therefore homogenizes to the operator type. + v = [t, o] + @test eltype(v) == O + @test all(x -> x isa NamedTensorOperator, v) +end