From 34796d32703e5ef55dac992001ec5effcd13c751 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 9 Sep 2026 11:02:33 -0400 Subject: [PATCH 01/28] Add `QuadraticFormNetwork` and a shared bilinear-form interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `QuadraticFormNetwork` represents `⟨tn|op|tn⟩` lazily, with an operator layer between the bra and ket layers. A ket index name is renamed in the bra layer when it is a link name or appears in the operator's `inputnames`, so a ket index the operator does not act on still contracts straight onto the bra. `operatortensor` renames each operator output name to the bra name of the input name it is paired with, leaving the input names to meet the ket. The constructor throws unless `vertices(operator)` and `vertices(ket)` are set-equal. `AbstractBilinearFormNetwork` is the new supertype of `NormNetwork` and `QuadraticFormNetwork`. A subtype implements `braname`, `kettensor` and, where it has an operator layer, `operatortensor`; `bratensor`, `conj_bratensor` and `indmap` are defined once in terms of those, so the two concrete files now share them instead of each carrying a copy. `AbstractBilinearFormNetworkView` does the same for `KetView`, `BraView` and the new `OperatorView`: each implements `Base.parent` and `get_vertex_data`, and takes its graph structure, assignment and mutability from the parent network. `test/test_quadraticformnetwork.jl` covers the name map, the three views, and three contractions: an identity operator layer reproduces `contract_network(NormNetwork(tn))`, a random on-site product operator matches a dense `⟨ψ|O|ψ⟩` reference, and a `2I` layer on three vertices scales the norm by 8. Co-Authored-By: Claude Opus 5 --- src/ITensorNetworksNext.jl | 3 + src/abstractbilinearformnetwork.jl | 57 +++++++ src/abstractbilinearformnetworkview.jl | 43 ++++++ src/normnetwork.jl | 25 +--- src/normnetworkview.jl | 55 ++++--- src/quadraticformnetwork.jl | 110 ++++++++++++++ test/test_quadraticformnetwork.jl | 199 +++++++++++++++++++++++++ 7 files changed, 444 insertions(+), 48 deletions(-) create mode 100644 src/abstractbilinearformnetwork.jl create mode 100644 src/abstractbilinearformnetworkview.jl create mode 100644 src/quadraticformnetwork.jl create mode 100644 test/test_quadraticformnetwork.jl diff --git a/src/ITensorNetworksNext.jl b/src/ITensorNetworksNext.jl index 0298a85f..ccfe33b4 100644 --- a/src/ITensorNetworksNext.jl +++ b/src/ITensorNetworksNext.jl @@ -13,7 +13,10 @@ include("select_algorithm.jl") include("AlgorithmsInterfaceExtensions/AlgorithmsInterfaceExtensions.jl") include("abstracttensornetwork.jl") include("tensornetwork.jl") +include("abstractbilinearformnetwork.jl") +include("abstractbilinearformnetworkview.jl") include("normnetwork.jl") +include("quadraticformnetwork.jl") include("normnetworkview.jl") include("ITensorNetworkGenerators/ITensorNetworkGenerators.jl") include("contraction_tree.jl") diff --git a/src/abstractbilinearformnetwork.jl b/src/abstractbilinearformnetwork.jl new file mode 100644 index 00000000..6d1e894e --- /dev/null +++ b/src/abstractbilinearformnetwork.jl @@ -0,0 +1,57 @@ +using Dictionaries: Dictionaries +using ITensorBase: conj, name, rename, setname + +""" + abstract type AbstractBilinearFormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} + +Supertype of the lazy multi-layer networks built from a ket layer of type +`ITensorNetwork{T, V, I}` and a ket→bra index name mapping. + +A subtype supplies its own graph structure and implements [`braname`](@ref) together with one +accessor per layer: [`kettensor`](@ref), [`bratensor`](@ref) and, where the subtype has an +operator layer, [`operatortensor`](@ref). `bratensor` has a default built from `kettensor` +and `braname`. +""" +abstract type AbstractBilinearFormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} end + +# =================================== Dictionaries.jl ==================================== # + +Dictionaries.issettable(::AbstractBilinearFormNetwork) = false +Dictionaries.isinsertable(::AbstractBilinearFormNetwork) = false + +# ====================================== interface ======================================= # + +""" + braname(bn::AbstractBilinearFormNetwork, name) + +The bra-layer index name corresponding to the ket-layer index name `name`. +""" +function braname end + +""" + kettensor(bn::AbstractBilinearFormNetwork, vertex) + +The ket-layer tensor at `vertex`. +""" +function kettensor end + +""" + operatortensor(bn::AbstractBilinearFormNetwork, vertex) + +The operator-layer tensor at `vertex`, with its index names renamed so that its input legs +meet the ket layer and its output legs meet the bra layer. +""" +function operatortensor end + +function conj_bratensor(bn::AbstractBilinearFormNetwork, vertex) + return rename(n -> braname(bn, n), kettensor(bn, vertex)) +end + +""" + bratensor(bn::AbstractBilinearFormNetwork, vertex) + +The bra-layer tensor at `vertex`. +""" +bratensor(bn::AbstractBilinearFormNetwork, vertex) = conj(conj_bratensor(bn, vertex)) + +indmap(bn::AbstractBilinearFormNetwork, ind) = setname(conj(ind), braname(bn, name(ind))) diff --git a/src/abstractbilinearformnetworkview.jl b/src/abstractbilinearformnetworkview.jl new file mode 100644 index 00000000..828cbce1 --- /dev/null +++ b/src/abstractbilinearformnetworkview.jl @@ -0,0 +1,43 @@ +using DataGraphs: DataGraphs, is_vertex_assigned +using Dictionaries: Dictionaries, isinsertable, issettable +using Graphs: Graphs, edges, vertices +using NamedGraphs: NamedGraphs, ordered_vertices, position_graph, vertex_positions + +""" + abstract type AbstractBilinearFormNetworkView{T, V, I} <: AbstractITensorNetwork{T, V} + +Supertype of the single-layer views of an `AbstractBilinearFormNetwork{T, V, I}`. + +A subtype implements `Base.parent`, returning the network it views, and +`DataGraphs.get_vertex_data`, returning that layer's tensor at a vertex. Its graph structure +and mutability are those of the parent network. +""" +abstract type AbstractBilinearFormNetworkView{T, V, I} <: AbstractITensorNetwork{T, V} end + +# ====================================== Graphs.jl ======================================= # + +Graphs.edges(nnv::AbstractBilinearFormNetworkView) = edges(parent(nnv)) +Graphs.vertices(nnv::AbstractBilinearFormNetworkView) = vertices(parent(nnv)) + +# ==================================== NamedGraphs.jl ==================================== # + +function NamedGraphs.vertex_positions(nnv::AbstractBilinearFormNetworkView) + return vertex_positions(parent(nnv)) +end +function NamedGraphs.ordered_vertices(nnv::AbstractBilinearFormNetworkView) + return ordered_vertices(parent(nnv)) +end +function NamedGraphs.position_graph(nnv::AbstractBilinearFormNetworkView) + return position_graph(parent(nnv)) +end + +# ==================================== DataGraphs.jl ===================================== # + +function DataGraphs.is_vertex_assigned(nnv::AbstractBilinearFormNetworkView, vertex) + return is_vertex_assigned(parent(nnv), vertex) +end + +# =================================== Dictionaries.jl ==================================== # + +Dictionaries.issettable(nnv::AbstractBilinearFormNetworkView) = issettable(parent(nnv)) +Dictionaries.isinsertable(nnv::AbstractBilinearFormNetworkView) = isinsertable(parent(nnv)) diff --git a/src/normnetwork.jl b/src/normnetwork.jl index d735b22f..4b136b3a 100644 --- a/src/normnetwork.jl +++ b/src/normnetwork.jl @@ -1,15 +1,15 @@ using Dictionaries: Dictionary, dictionary -using ITensorBase: rename, setname, similar_operator, uniquename +using ITensorBase: similar_operator, uniquename using ITensorNetworksNext """ - struct NormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} + struct NormNetwork{T, V, I} <: AbstractBilinearFormNetwork{T, V, I} Lazy wrapper representing the norm `⟨tn|tn⟩` of `tn::ITensorNetwork{T, V, I}`, together with a per-edge ket→bra name mapping that, for each index in the ket layer, defines the name of the corresponding index in the bra layer. """ -struct NormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} +struct NormNetwork{T, V, I} <: AbstractBilinearFormNetwork{T, V, I} ket::ITensorNetwork{T, V, I} braname::Dictionary{I, I} function NormNetwork( @@ -49,20 +49,16 @@ NamedGraphs.encoded_graph(nn::NormNetwork) = encoded_graph(nn.ket) # ==================================== DataGraphs.jl ===================================== # +function DataGraphs.is_vertex_assigned(nn::NormNetwork, vertex) + return isassigned(nn.ket, vertex) +end + function DataGraphs.get_vertex_data(nn::NormNetwork, vertex) return error( "Indexing a `NormNetwork` is not defined, since the double-layer tensor at a vertex has no representation of its own. Use `kettensor` and `bratensor` to reach the individual layers." ) end -function DataGraphs.is_vertex_assigned(nn::NormNetwork, vertex) - return isassigned(nn.ket, vertex) -end -# =================================== Dictionaries.jl ==================================== # - -Dictionaries.issettable(::NormNetwork) = false -Dictionaries.isinsertable(::NormNetwork) = false - # ====================================== interface ======================================= # function braname(nn::NormNetwork, name) @@ -74,14 +70,7 @@ function braname(nn::NormNetwork, name) return get(nn.braname, name, name) end -indmap(nn::NormNetwork, ind) = setname(conj(ind), braname(nn, name(ind))) - kettensor(nn::NormNetwork, vertex) = nn.ket[vertex] -function conj_bratensor(nn::NormNetwork, vertex) - return rename(n -> braname(nn, n), kettensor(nn, vertex)) -end - -bratensor(nn::NormNetwork, vertex) = conj(conj_bratensor(nn, vertex)) """ flatten_network(nn::NormNetwork) -> ITensorNetwork diff --git a/src/normnetworkview.jl b/src/normnetworkview.jl index 75c57926..3b197aeb 100644 --- a/src/normnetworkview.jl +++ b/src/normnetworkview.jl @@ -3,20 +3,11 @@ using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex -struct KetView{T, V, I} <: AbstractITensorNetwork{T, V} - parent::NormNetwork{T, V, I} -end - -struct BraView{T, V, I} <: AbstractITensorNetwork{T, V} - parent::NormNetwork{T, V, I} -end - -# ====================================== Graphs.jl ======================================= # - -for View in (:KetView, :BraView) - @eval begin - Graphs.edges(nnv::$View) = edges(nnv.parent) - Graphs.vertices(nnv::$View) = vertices(nnv.parent) +struct KetView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: + AbstractBilinearFormNetworkView{T, V, I} + parent::P + function KetView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} + return new{T, V, I, typeof(parent)}(parent) end end @@ -35,24 +26,28 @@ for View in (:KetView, :BraView) end end -# ==================================== DataGraphs.jl ===================================== # - -DataGraphs.get_vertex_data(nn::KetView, vertex) = kettensor(nn.parent, vertex) -DataGraphs.get_vertex_data(nn::BraView, vertex) = bratensor(nn.parent, vertex) - -for View in (:KetView, :BraView) - @eval begin - function DataGraphs.is_vertex_assigned(nnv::$View, vertex) - return isassigned(nnv.parent.ket, vertex) - end +struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: + AbstractBilinearFormNetworkView{T, V, I} + parent::P + function BraView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} + return new{T, V, I, typeof(parent)}(parent) end end -# =================================== Dictionaries.jl ==================================== # - -for View in (:KetView, :BraView) - @eval begin - Dictionaries.issettable(nnv::$View) = issettable(nnv.parent) - Dictionaries.isinsertable(nnv::$View) = isinsertable(nnv.parent) +struct OperatorView{T, V, I, P <: QuadraticFormNetwork{T, V, I}} <: + AbstractBilinearFormNetworkView{T, V, I} + parent::P + function OperatorView(parent::QuadraticFormNetwork{T, V, I}) where {T, V, I} + return new{T, V, I, typeof(parent)}(parent) end end + +Base.parent(nnv::KetView) = nnv.parent +Base.parent(nnv::BraView) = nnv.parent +Base.parent(nnv::OperatorView) = nnv.parent + +# ==================================== DataGraphs.jl ===================================== # + +DataGraphs.get_vertex_data(nnv::KetView, vertex) = kettensor(parent(nnv), vertex) +DataGraphs.get_vertex_data(nnv::BraView, vertex) = bratensor(parent(nnv), vertex) +DataGraphs.get_vertex_data(nnv::OperatorView, vertex) = operatortensor(parent(nnv), vertex) diff --git a/src/quadraticformnetwork.jl b/src/quadraticformnetwork.jl new file mode 100644 index 00000000..1b803476 --- /dev/null +++ b/src/quadraticformnetwork.jl @@ -0,0 +1,110 @@ +using Dictionaries: Dictionary +using ITensorBase: + LazyNamedTensor, inputnames, lazy, outputnames, rename, state, uniquename +using ITensorNetworksNext + +""" + struct QuadraticFormNetwork{T, V, I, O} <: AbstractBilinearFormNetwork{T, V, I} + +Lazy wrapper representing the quadratic form `⟨tn|op|tn⟩` of `tn::ITensorNetwork{T, V, I}` +sandwiched around the operator layer `op::ITensorNetwork{O, V, I}`, together with a per-index +ket→bra name mapping that, for each index in the ket layer, defines the name of the +corresponding index in the bra layer. + +The operator layer must be defined on every vertex of `tn`. At each vertex the operator's +input names are ket index names, and each paired output name is renamed to the bra layer, so +the operator's remaining index names must be distinct from every index name of `tn`. +""" +struct QuadraticFormNetwork{T, V, I, O} <: AbstractBilinearFormNetwork{T, V, I} + ket::ITensorNetwork{T, V, I} + operator::ITensorNetwork{O, V, I} + braname::Dictionary{I, I} + function QuadraticFormNetwork( + ket::ITensorNetwork{T, V, I}, + operator::ITensorNetwork{O, V, I}, + map::Dictionary{I, I} + ) where {T, V, I, O} + if !issetequal(vertices(operator), vertices(ket)) + error("the operator layer must be defined on every vertex of the ket layer.") + end + acted = Set{I}() + for vertex in vertices(operator) + union!(acted, inputnames(operator[vertex])) + end + braname = Dictionary{I, I}() + for (name, vertices) in pairs(ket.dimname_vertices) + if length(vertices) == 2 || name in acted + insert!(braname, name, map[name]) + end + end + return new{T, V, I, O}(ket, operator, braname) + end +end + +Base.eltype(::Type{<:QuadraticFormNetwork{T, V, I}}) where {T, V, I} = LazyNamedTensor{I, T} + +function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetwork) + return QuadraticFormNetwork(ket, operator, map(uniquename, keys(ket.dimname_vertices))) +end + +# ====================================== Graphs.jl ======================================= # + +Graphs.edges(qf::QuadraticFormNetwork) = edges(qf.ket) +Graphs.vertices(qf::QuadraticFormNetwork) = vertices(qf.ket) + +# ==================================== NamedGraphs.jl ==================================== # + +NamedGraphs.vertex_positions(qf::QuadraticFormNetwork) = vertex_positions(qf.ket) +NamedGraphs.ordered_vertices(qf::QuadraticFormNetwork) = ordered_vertices(qf.ket) +NamedGraphs.position_graph(qf::QuadraticFormNetwork) = position_graph(qf.ket) + +# ==================================== DataGraphs.jl ===================================== # + +function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) + A = kettensor(qf, vertex) + O = operatortensor(qf, vertex) + B = conj_bratensor(qf, vertex) + # TODO: implement and use a lazy `conj` via `LazyNamedDimsArrays` here? + return lazy(A) * lazy(state(O)) * lazy(conj(B)) +end + +function DataGraphs.is_vertex_assigned(qf::QuadraticFormNetwork, vertex) + return isassigned(qf.ket, vertex) && isassigned(qf.operator, vertex) +end + +# ====================================== interface ======================================= # + +function braname(qf::QuadraticFormNetwork, name) + if !has_dimname(qf.ket, name) + error("index name $name not found underlying tensor network.") + end + # The indices not stored in `qf.braname` are the dangling ket indices the operator does + # not act on, which get mapped to themselves. + return get(qf.braname, name, name) +end + +kettensor(qf::QuadraticFormNetwork, vertex) = qf.ket[vertex] + +# Each output name is renamed to the bra name of the input name it is paired with, so the +# operator's output legs meet the bra layer and its input legs meet the ket layer. +function operatortensor(qf::QuadraticFormNetwork, vertex) + tensor = qf.operator[vertex] + names = map(inputnames(tensor)) do n + return braname(qf, n) + end + return rename(tensor, (outputnames(tensor) .=> names)...) +end + +""" + quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetwork, [braname]) + +Build the triple-layer network `⟨tn|op|tn⟩`, represented lazily as a +`QuadraticFormNetwork` object. The optional third argument `braname` should implement +`braname[ketdimname] = bradimname` for every link dimension name `ketdimname` in `tn` and +every dimension name of `tn` the operator acts on. If this is not specified, then a name is +generated via the `ITensorBase.uniquename` function. +""" +quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetwork) = QuadraticFormNetwork(tn, op) +function quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetwork, braname) + return QuadraticFormNetwork(tn, op, braname) +end diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl new file mode 100644 index 00000000..f303b259 --- /dev/null +++ b/test/test_quadraticformnetwork.jl @@ -0,0 +1,199 @@ +using DataGraphs: is_vertex_assigned +using Dictionaries: isinsertable, issettable +using Graphs: edges, vertices +using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, operator, + outputnames, rename, setname, uniquename +using ITensorNetworksNext: BraView, ITensorNetwork, KetView, NormNetwork, OperatorView, + QuadraticFormNetwork, braname, bratensor, conj_bratensor, contract_network, indmap, + kettensor, operatortensor, quadraticformnetwork, tensornetwork +using LinearAlgebra: I, norm +using NamedGraphs.GraphsExtensions: incident_edges +using NamedGraphs.NamedGraphGenerators: named_grid, named_path_graph +using NamedGraphs: NamedEdge +using Test: @test, @test_throws, @testset + +# Build a random `ITensorNetwork` state on the graph `g` with site dimension `d` and +# bond dimension `χ`. +function random_state(::Type{T}, g; d = 2, χ = 2) where {T} + l = Dict(e => Index(χ) for e in edges(g)) + l = merge(l, Dict(reverse(e) => l[e] for e in edges(g))) + s = Dict(v => Index(d) for v in vertices(g)) + tn = tensornetwork(vertices(g)) do v + is = map(e -> l[e], incident_edges(g, v)) + return randn(T, (s[v], is...)) + end + return tn, l, s +end + +# Build a bondless operator layer on the vertices of `g`, with `f(v)` supplying the matrix +# acting on the site index `s[v]`. +function product_operator(f, g, s; d = 2) + op = tensornetwork(vertices(g)) do v + out = Index(d) + return operator(ITensor(f(v), (out, s[v])), (name(out),), (name(s[v]),)) + end + return op +end + +identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; d) + +@testset "`QuadraticFormNetwork`" begin + @testset "Basics" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + op = identity_operator(g, s) + qf = QuadraticFormNetwork(tn, op) + + # `quadraticformnetwork` is the public constructor and agrees with the type. + @test quadraticformnetwork(tn, op) isa QuadraticFormNetwork + @test qf isa QuadraticFormNetwork + + # The quadratic form shares the graph structure of the ket layer. + @test issetequal(vertices(qf), vertices(tn)) + @test issetequal(edges(qf), edges(tn)) + + # `eltype` is the type of the (lazy triple-layer) vertex data. + @test eltype(qf) === typeof(qf[1]) + + # Vertex data is assigned wherever both layers are. + @test is_vertex_assigned(qf, 1) + + # The quadratic form is neither settable nor insertable (it is a lazy view). + @test !issettable(qf) + @test !isinsertable(qf) + + # The operator layer must cover every vertex of the ket layer. + g4 = named_path_graph(4) + _, _, s4 = random_state(Float64, g4) + @test_throws ErrorException QuadraticFormNetwork(tn, identity_operator(g4, s4)) + end + + @testset "layer tensors and the name map" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + op = identity_operator(g, s) + qf = QuadraticFormNetwork(tn, op) + + # `kettensor` returns the underlying tensor untouched. + @test kettensor(qf, 2) === tn[2] + + # Unlike the norm network, the site indices *are* renamed in the bra layer: the + # operator sits between the two layers, so they no longer contract directly. + sname = name(s[2]) + @test braname(qf, sname) != sname + @test sname in name.(inds(kettensor(qf, 2))) + @test !(sname in name.(inds(conj_bratensor(qf, 2)))) + @test braname(qf, sname) in name.(inds(conj_bratensor(qf, 2))) + + # Link indices are shared by two tensors, so they are renamed in the bra layer to + # keep the two layers' bonds distinct. + lname = name(l[NamedEdge(1 => 2)]) + @test braname(qf, lname) != lname + @test lname in name.(inds(kettensor(qf, 2))) + @test !(lname in name.(inds(conj_bratensor(qf, 2)))) + @test braname(qf, lname) in name.(inds(conj_bratensor(qf, 2))) + + # The operator's input name meets the ket and its output name is renamed to meet + # the bra. + o = operatortensor(qf, 2) + @test inputnames(o) == [sname] + @test outputnames(o) == [braname(qf, sname)] + + # `bratensor` is the elementwise conjugate of `conj_bratensor` and carries the same + # indices. + @test inds(bratensor(qf, 2)) == inds(conj_bratensor(qf, 2)) + + # `indmap` conjugates an index and renames it according to the name map. + ind = only(i for i in inds(kettensor(qf, 2)) if name(i) == lname) + @test name(indmap(qf, ind)) == braname(qf, name(ind)) + @test indmap(qf, ind) == setname(conj(ind), braname(qf, name(ind))) + + # Querying the name map with an index name absent from the ket layer errors. + @test_throws ErrorException braname(qf, name(Index(2))) + end + + @testset "custom name map" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + op = identity_operator(g, s) + + # A user-supplied map dictates the bra-layer name for each renamed index. + custom = map(uniquename, keys(tn.dimname_vertices)) + qf = quadraticformnetwork(tn, op, custom) + + lname = name(l[NamedEdge(1 => 2)]) + @test braname(qf, lname) == custom[lname] + @test braname(qf, lname) in name.(inds(conj_bratensor(qf, 2))) + + sname = name(s[2]) + @test braname(qf, sname) == custom[sname] + end + + @testset "`KetView` / `OperatorView` / `BraView`" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + op = identity_operator(g, s) + qf = QuadraticFormNetwork(tn, op) + + kv = KetView(qf) + ov = OperatorView(qf) + bv = BraView(qf) + + # Views share the graph structure of the ket layer. + for view in (kv, ov, bv) + @test issetequal(vertices(view), vertices(tn)) + @test issetequal(edges(view), edges(tn)) + @test !issettable(view) + @test !isinsertable(view) + @test is_vertex_assigned(view, 1) + end + + for v in vertices(tn) + @test kv[v] === kettensor(qf, v) + @test inds(ov[v]) == inds(operatortensor(qf, v)) + @test inds(bv[v]) == inds(bratensor(qf, v)) + end + end + + @testset "contraction / physics" begin + @testset "identity operator layer reproduces the norm network" begin + g = named_grid((2, 2)) + tn, l, s = random_state(Float64, g) + op = identity_operator(g, s) + + @test contract_network(QuadraticFormNetwork(tn, op))[] ≈ + contract_network(NormNetwork(tn))[] + end + + @testset "$T" for T in (Float64, ComplexF64) + g = named_grid((2, 2)) + tn, l, s = random_state(T, g) + + # A product of on-site matrices, contracted densely for the reference value. + mats = Dict(v => randn(T, 2, 2) for v in vertices(g)) + op = product_operator(v -> mats[v], g, s) + qf = QuadraticFormNetwork(tn, op) + + # ⟨ψ|O|ψ⟩ built by applying each on-site matrix to the dense ket and + # overlapping with the dense bra. + psi = prod(tn) + ket = psi + for v in vertices(g) + out = Index(2) + gate = ITensor(mats[v], (out, s[v])) + ket = rename(gate * ket, name(out) => name(s[v])) + end + @test contract_network(qf)[] ≈ (conj(psi) * ket)[] + end + + @testset "scaled identity" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + op = product_operator(v -> 2.0 * Matrix(1.0I, 2, 2), g, s) + + # A factor of 2 at each of the three vertices scales the norm by 2³. + @test contract_network(QuadraticFormNetwork(tn, op))[] ≈ + 8 * contract_network(NormNetwork(tn))[] + end + end +end From 6828d4b882b63d85ae4df6b3b7a8e9c96d9db507 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 9 Sep 2026 11:02:54 -0400 Subject: [PATCH 02/28] Move the bilinear-form networks and views to `src/bilinearforms/` The five files are byte-identical at their new paths; only the `include` paths in `src/ITensorNetworksNext.jl` change. `src/normnetworkview.jl` becomes `bilinearformnetworkview.jl`, since it now holds the views of both `NormNetwork` and `QuadraticFormNetwork`. Co-Authored-By: Claude Opus 5 --- src/ITensorNetworksNext.jl | 10 +++++----- src/{ => bilinearforms}/abstractbilinearformnetwork.jl | 0 .../abstractbilinearformnetworkview.jl | 0 .../bilinearformnetworkview.jl} | 0 src/{ => bilinearforms}/normnetwork.jl | 0 src/{ => bilinearforms}/quadraticformnetwork.jl | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename src/{ => bilinearforms}/abstractbilinearformnetwork.jl (100%) rename src/{ => bilinearforms}/abstractbilinearformnetworkview.jl (100%) rename src/{normnetworkview.jl => bilinearforms/bilinearformnetworkview.jl} (100%) rename src/{ => bilinearforms}/normnetwork.jl (100%) rename src/{ => bilinearforms}/quadraticformnetwork.jl (100%) diff --git a/src/ITensorNetworksNext.jl b/src/ITensorNetworksNext.jl index ccfe33b4..d995b541 100644 --- a/src/ITensorNetworksNext.jl +++ b/src/ITensorNetworksNext.jl @@ -13,11 +13,11 @@ include("select_algorithm.jl") include("AlgorithmsInterfaceExtensions/AlgorithmsInterfaceExtensions.jl") include("abstracttensornetwork.jl") include("tensornetwork.jl") -include("abstractbilinearformnetwork.jl") -include("abstractbilinearformnetworkview.jl") -include("normnetwork.jl") -include("quadraticformnetwork.jl") -include("normnetworkview.jl") +include("bilinearforms/abstractbilinearformnetwork.jl") +include("bilinearforms/abstractbilinearformnetworkview.jl") +include("bilinearforms/normnetwork.jl") +include("bilinearforms/quadraticformnetwork.jl") +include("bilinearforms/bilinearformnetworkview.jl") include("ITensorNetworkGenerators/ITensorNetworkGenerators.jl") include("contraction_tree.jl") include("prod_tensors.jl") diff --git a/src/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl similarity index 100% rename from src/abstractbilinearformnetwork.jl rename to src/bilinearforms/abstractbilinearformnetwork.jl diff --git a/src/abstractbilinearformnetworkview.jl b/src/bilinearforms/abstractbilinearformnetworkview.jl similarity index 100% rename from src/abstractbilinearformnetworkview.jl rename to src/bilinearforms/abstractbilinearformnetworkview.jl diff --git a/src/normnetworkview.jl b/src/bilinearforms/bilinearformnetworkview.jl similarity index 100% rename from src/normnetworkview.jl rename to src/bilinearforms/bilinearformnetworkview.jl diff --git a/src/normnetwork.jl b/src/bilinearforms/normnetwork.jl similarity index 100% rename from src/normnetwork.jl rename to src/bilinearforms/normnetwork.jl diff --git a/src/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl similarity index 100% rename from src/quadraticformnetwork.jl rename to src/bilinearforms/quadraticformnetwork.jl From d88f133a56fa8ecce30df1ff69d224320d1a9e97 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 9 Sep 2026 13:12:18 -0400 Subject: [PATCH 03/28] Reject `NormNetwork` in `OperatorView` with an `ArgumentError` `OperatorView` now accepts any `AbstractBilinearFormNetwork` rather than only a `QuadraticFormNetwork`, and a second constructor throws an `ArgumentError` for a `NormNetwork`, which reports the reason rather than surfacing a `MethodError`. A `NormNetwork` maps every site index name to itself in its bra layer, so an identity operator between the layers would need its output name to equal its input name, and `ITensor` rejects that: "Dimension names must be distinct". Co-Authored-By: Claude Opus 5 --- src/bilinearforms/bilinearformnetworkview.jl | 10 ++++++++-- test/test_quadraticformnetwork.jl | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/bilinearforms/bilinearformnetworkview.jl b/src/bilinearforms/bilinearformnetworkview.jl index 3b197aeb..4c940778 100644 --- a/src/bilinearforms/bilinearformnetworkview.jl +++ b/src/bilinearforms/bilinearformnetworkview.jl @@ -34,14 +34,20 @@ struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: end end -struct OperatorView{T, V, I, P <: QuadraticFormNetwork{T, V, I}} <: +struct OperatorView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractBilinearFormNetworkView{T, V, I} parent::P - function OperatorView(parent::QuadraticFormNetwork{T, V, I}) where {T, V, I} + function OperatorView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} return new{T, V, I, typeof(parent)}(parent) end end +# A `NormNetwork`'s bra and ket layers share their site index names, so there is no distinct +# bra site index for an identity operator to carry as its output name. +function OperatorView(::NormNetwork) + throw(ArgumentError("a `NormNetwork` has no operator layer to view.")) +end + Base.parent(nnv::KetView) = nnv.parent Base.parent(nnv::BraView) = nnv.parent Base.parent(nnv::OperatorView) = nnv.parent diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index f303b259..cd90bad1 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -148,6 +148,9 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; @test is_vertex_assigned(view, 1) end + # A `NormNetwork` has no operator layer, so `OperatorView` rejects one. + @test_throws ArgumentError OperatorView(NormNetwork(tn)) + for v in vertices(tn) @test kv[v] === kettensor(qf, v) @test inds(ov[v]) == inds(operatortensor(qf, v)) From 54780f562187f5231bc7e637638237b401864f0a Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 9 Sep 2026 13:57:58 -0400 Subject: [PATCH 04/28] Define the NamedGraphs vertex encoding once on the abstract view type `encoded_vertex`, `decoded_vertex` and `encoded_graph` move from a `for View in (:KetView, :BraView)` loop in `bilinearformnetworkview.jl` to `AbstractBilinearFormNetworkView` in `abstractbilinearformnetworkview.jl`, replacing the `vertex_positions`/`ordered_vertices`/`position_graph` methods that NamedGraphs 0.13 removed. `OperatorView` is now covered as well, as it was by the methods being replaced. The loop also `@eval`ed against `BraView` above that type's own definition, so including the file threw `UndefVarError: BraView`. `QuadraticFormNetwork` gets the same rename; it still defined the three removed names, so the package did not load. `test_quadraticformnetwork.jl` takes `incident_edges`, `named_grid` and `named_path_graph` from `NamedGraphs` itself, since the `GraphsExtensions` and `NamedGraphGenerators` submodules are gone. Co-Authored-By: Claude Opus 5 --- .../abstractbilinearformnetworkview.jl | 14 ++++++-------- src/bilinearforms/bilinearformnetworkview.jl | 16 ---------------- src/bilinearforms/quadraticformnetwork.jl | 10 +++++++--- test/test_quadraticformnetwork.jl | 4 +--- 4 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/bilinearforms/abstractbilinearformnetworkview.jl b/src/bilinearforms/abstractbilinearformnetworkview.jl index 828cbce1..262bc4e7 100644 --- a/src/bilinearforms/abstractbilinearformnetworkview.jl +++ b/src/bilinearforms/abstractbilinearformnetworkview.jl @@ -1,7 +1,7 @@ using DataGraphs: DataGraphs, is_vertex_assigned using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices -using NamedGraphs: NamedGraphs, ordered_vertices, position_graph, vertex_positions +using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex """ abstract type AbstractBilinearFormNetworkView{T, V, I} <: AbstractITensorNetwork{T, V} @@ -21,15 +21,13 @@ Graphs.vertices(nnv::AbstractBilinearFormNetworkView) = vertices(parent(nnv)) # ==================================== NamedGraphs.jl ==================================== # -function NamedGraphs.vertex_positions(nnv::AbstractBilinearFormNetworkView) - return vertex_positions(parent(nnv)) +function NamedGraphs.encoded_vertex(nnv::AbstractBilinearFormNetworkView, vertex) + return encoded_vertex(parent(nnv), vertex) end -function NamedGraphs.ordered_vertices(nnv::AbstractBilinearFormNetworkView) - return ordered_vertices(parent(nnv)) -end -function NamedGraphs.position_graph(nnv::AbstractBilinearFormNetworkView) - return position_graph(parent(nnv)) +function NamedGraphs.decoded_vertex(nnv::AbstractBilinearFormNetworkView, code::Integer) + return decoded_vertex(parent(nnv), code) end +NamedGraphs.encoded_graph(nnv::AbstractBilinearFormNetworkView) = encoded_graph(parent(nnv)) # ==================================== DataGraphs.jl ===================================== # diff --git a/src/bilinearforms/bilinearformnetworkview.jl b/src/bilinearforms/bilinearformnetworkview.jl index 4c940778..dbeb81bf 100644 --- a/src/bilinearforms/bilinearformnetworkview.jl +++ b/src/bilinearforms/bilinearformnetworkview.jl @@ -1,7 +1,6 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices -using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex struct KetView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractBilinearFormNetworkView{T, V, I} @@ -11,21 +10,6 @@ struct KetView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: end end -# ==================================== NamedGraphs.jl ==================================== # - -for View in (:KetView, :BraView) - @eval begin - function NamedGraphs.encoded_vertex(nnv::$View, vertex) - return encoded_vertex(nnv.parent, vertex) - end - function NamedGraphs.decoded_vertex(nnv::$View, code::Integer) - return decoded_vertex(nnv.parent, code) - end - - NamedGraphs.encoded_graph(nnv::$View) = encoded_graph(nnv.parent) - end -end - struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractBilinearFormNetworkView{T, V, I} parent::P diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 1b803476..39c2dd0e 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -54,9 +54,13 @@ Graphs.vertices(qf::QuadraticFormNetwork) = vertices(qf.ket) # ==================================== NamedGraphs.jl ==================================== # -NamedGraphs.vertex_positions(qf::QuadraticFormNetwork) = vertex_positions(qf.ket) -NamedGraphs.ordered_vertices(qf::QuadraticFormNetwork) = ordered_vertices(qf.ket) -NamedGraphs.position_graph(qf::QuadraticFormNetwork) = position_graph(qf.ket) +function NamedGraphs.encoded_vertex(qf::QuadraticFormNetwork, vertex) + return encoded_vertex(qf.ket, vertex) +end +function NamedGraphs.decoded_vertex(qf::QuadraticFormNetwork, code::Integer) + return decoded_vertex(qf.ket, code) +end +NamedGraphs.encoded_graph(qf::QuadraticFormNetwork) = encoded_graph(qf.ket) # ==================================== DataGraphs.jl ===================================== # diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index cd90bad1..7c036bd2 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -7,9 +7,7 @@ using ITensorNetworksNext: BraView, ITensorNetwork, KetView, NormNetwork, Operat QuadraticFormNetwork, braname, bratensor, conj_bratensor, contract_network, indmap, kettensor, operatortensor, quadraticformnetwork, tensornetwork using LinearAlgebra: I, norm -using NamedGraphs.GraphsExtensions: incident_edges -using NamedGraphs.NamedGraphGenerators: named_grid, named_path_graph -using NamedGraphs: NamedEdge +using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset # Build a random `ITensorNetwork` state on the graph `g` with site dimension `d` and From fb44fc92490207c65d6fd8d652e4a332afb7b39d Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 14 Sep 2026 11:56:24 -0400 Subject: [PATCH 05/28] Add `ITensorNetworkOperator` and use it as the operator layer `ITensorNetworkOperator` is the network equivalent of `ITensorBase.ITensorOperator`: a tensor network together with a positional pairing of its dangling index names, `outputnames[i]` with `inputnames[i]`. The constructor, reached through `ITensorBase.operator(tn, out, in)`, throws an `ArgumentError` when the two name lists differ in length and when a named index is not dangling, since `dimnamevertices` must return exactly one vertex for an operator leg. A pair may straddle two vertices, as it does for a swap or a translation, so `getindex` wraps each vertex tensor as an `ITensorOperator` carrying only the pairs whose two halves both sit on that vertex; the remaining legs are dangling on the wrapper, which `NamedTensorOperator` permits for names in neither list. The wrapper is built even when no pair is local, so `eltype` is `NamedTensorOperator{I, T}` for every vertex. `QuadraticFormNetwork` takes one as its operator layer. Its `acted` set is now `Set{I}(inputnames(operator))` rather than a loop over per-vertex input names, and `operatortensor` reads the network-level pairing: reading `qf.operator[vertex]` would skip a crossing pair's output leg, which is dangling on that wrapper, and the bra layer would not meet it. `operatortensor` therefore returns a plain tensor and `get_vertex_data` drops its `state` call. `supportof` gains a method for the new type. Its body moves to `supportof_dimnames(tn, opnames)` in `abstracttensornetwork.jl` so both methods share it; a `Union` signature is not possible there because `ITensorNetworkOperator` is not yet defined at that point in the include order. `replacedimnames` is not implemented for the new type: `ITensorNetwork` has no `replacedimnames` method to delegate to. Co-Authored-By: Claude Opus 5 --- src/ITensorNetworksNext.jl | 1 + src/abstracttensornetwork.jl | 8 +- src/bilinearforms/quadraticformnetwork.jl | 55 ++++++----- src/itensornetworkoperator.jl | 114 +++++++++++++++++++++ test/test_itensornetworkoperator.jl | 115 ++++++++++++++++++++++ test/test_quadraticformnetwork.jl | 16 +-- 6 files changed, 276 insertions(+), 33 deletions(-) create mode 100644 src/itensornetworkoperator.jl create mode 100644 test/test_itensornetworkoperator.jl diff --git a/src/ITensorNetworksNext.jl b/src/ITensorNetworksNext.jl index d995b541..036b895a 100644 --- a/src/ITensorNetworksNext.jl +++ b/src/ITensorNetworksNext.jl @@ -13,6 +13,7 @@ include("select_algorithm.jl") include("AlgorithmsInterfaceExtensions/AlgorithmsInterfaceExtensions.jl") include("abstracttensornetwork.jl") include("tensornetwork.jl") +include("itensornetworkoperator.jl") include("bilinearforms/abstractbilinearformnetwork.jl") include("bilinearforms/abstractbilinearformnetworkview.jl") include("bilinearforms/normnetwork.jl") diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index eabe39ba..b909a1da 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -121,9 +121,15 @@ function insertlink!(tn::AbstractGraph, e) end function operator_support(tn::AbstractGraph, op::ITensorOperator) + return operator_support_names(tn, inputnames(op)) +end + +# Shared with the `ITensorNetworkOperator` method, which is defined alongside that type +# because it is not yet known at this point in the include order. +function operator_support_names(tn::AbstractGraph, opnames) support = Indices{vertextype(tn)}() - for name in inputnames(op) + for name in opnames vertices = dimnamevertices(tn, name) if length(vertices) > 1 diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 39c2dd0e..74808803 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,49 +1,47 @@ using Dictionaries: Dictionary using ITensorBase: - LazyNamedTensor, inputnames, lazy, outputnames, rename, state, uniquename + LazyNamedTensor, inputnames, lazy, names, outputnames, rename, state, uniquename using ITensorNetworksNext """ struct QuadraticFormNetwork{T, V, I, O} <: AbstractBilinearFormNetwork{T, V, I} Lazy wrapper representing the quadratic form `⟨tn|op|tn⟩` of `tn::ITensorNetwork{T, V, I}` -sandwiched around the operator layer `op::ITensorNetwork{O, V, I}`, together with a per-index +sandwiched around the operator layer `op::ITensorNetworkOperator`, together with a per-index ket→bra name mapping that, for each index in the ket layer, defines the name of the corresponding index in the bra layer. -The operator layer must be defined on every vertex of `tn`. At each vertex the operator's -input names are ket index names, and each paired output name is renamed to the bra layer, so -the operator's remaining index names must be distinct from every index name of `tn`. +The operator layer must be defined on every vertex of `tn`. Its input names are ket index +names, and each paired output name is renamed to the bra layer, so the operator's remaining +index names must be distinct from every index name of `tn`. """ -struct QuadraticFormNetwork{T, V, I, O} <: AbstractBilinearFormNetwork{T, V, I} +struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: + AbstractBilinearFormNetwork{T, V, I} ket::ITensorNetwork{T, V, I} - operator::ITensorNetwork{O, V, I} + operator::O braname::Dictionary{I, I} function QuadraticFormNetwork( ket::ITensorNetwork{T, V, I}, - operator::ITensorNetwork{O, V, I}, + operator::ITensorNetworkOperator, map::Dictionary{I, I} - ) where {T, V, I, O} + ) where {T, V, I} if !issetequal(vertices(operator), vertices(ket)) error("the operator layer must be defined on every vertex of the ket layer.") end - acted = Set{I}() - for vertex in vertices(operator) - union!(acted, inputnames(operator[vertex])) - end + acted = Set{I}(inputnames(operator)) braname = Dictionary{I, I}() for (name, vertices) in pairs(ket.dimname_vertices) if length(vertices) == 2 || name in acted insert!(braname, name, map[name]) end end - return new{T, V, I, O}(ket, operator, braname) + return new{T, V, I, typeof(operator)}(ket, operator, braname) end end Base.eltype(::Type{<:QuadraticFormNetwork{T, V, I}}) where {T, V, I} = LazyNamedTensor{I, T} -function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetwork) +function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetworkOperator) return QuadraticFormNetwork(ket, operator, map(uniquename, keys(ket.dimname_vertices))) end @@ -69,7 +67,7 @@ function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) O = operatortensor(qf, vertex) B = conj_bratensor(qf, vertex) # TODO: implement and use a lazy `conj` via `LazyNamedDimsArrays` here? - return lazy(A) * lazy(state(O)) * lazy(conj(B)) + return lazy(A) * lazy(O) * lazy(conj(B)) end function DataGraphs.is_vertex_assigned(qf::QuadraticFormNetwork, vertex) @@ -90,17 +88,22 @@ end kettensor(qf::QuadraticFormNetwork, vertex) = qf.ket[vertex] # Each output name is renamed to the bra name of the input name it is paired with, so the -# operator's output legs meet the bra layer and its input legs meet the ket layer. +# operator's output legs meet the bra layer and its input legs meet the ket layer. The pairing +# is read from the operator network rather than from `qf.operator[vertex]`, whose wrapper drops +# a pair whose input sits on another vertex. function operatortensor(qf::QuadraticFormNetwork, vertex) - tensor = qf.operator[vertex] - names = map(inputnames(tensor)) do n - return braname(qf, n) - end - return rename(tensor, (outputnames(tensor) .=> names)...) + tensor = state(qf.operator)[vertex] + tensor_names = names(tensor) + replacements = [ + output => braname(qf, input) for + (output, input) in zip(outputnames(qf.operator), inputnames(qf.operator)) + if output in tensor_names + ] + return rename(tensor, replacements...) end """ - quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetwork, [braname]) + quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator, [braname]) Build the triple-layer network `⟨tn|op|tn⟩`, represented lazily as a `QuadraticFormNetwork` object. The optional third argument `braname` should implement @@ -108,7 +111,9 @@ Build the triple-layer network `⟨tn|op|tn⟩`, represented lazily as a every dimension name of `tn` the operator acts on. If this is not specified, then a name is generated via the `ITensorBase.uniquename` function. """ -quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetwork) = QuadraticFormNetwork(tn, op) -function quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetwork, braname) +function quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator) + return QuadraticFormNetwork(tn, op) +end +function quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator, braname) return QuadraticFormNetwork(tn, op, braname) end diff --git a/src/itensornetworkoperator.jl b/src/itensornetworkoperator.jl new file mode 100644 index 00000000..c64d1208 --- /dev/null +++ b/src/itensornetworkoperator.jl @@ -0,0 +1,114 @@ +using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned +using Dictionaries: Dictionaries +using Graphs: Graphs, AbstractGraph, edges, vertices +using ITensorBase: ITensorBase, NamedTensorOperator, inputnames, names, nametype, operator, + outputnames, state +using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex + +""" + struct ITensorNetworkOperator{T, V, I, P} <: AbstractITensorNetwork{T, V} + +The network equivalent of `ITensorBase.ITensorOperator`: a tensor network of type +`P <: AbstractITensorNetwork{T, V}` together with a pairing of its dangling index names, +where `outputnames[i]` is paired with `inputnames[i]`. Applying the operator contracts over +the input names and leaves the output names. + +A pair may straddle two vertices, as it does for a swap or a translation. Indexing returns +the vertex tensor wrapped as an `ITensorOperator` carrying only the pairs whose two halves +both sit on that vertex; the remaining legs are dangling on the wrapper. +""" +struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: + AbstractITensorNetwork{T, V} + parent::P + outputnames::Vector{I} + inputnames::Vector{I} + function ITensorNetworkOperator( + parent::AbstractITensorNetwork{T, V}, outputnames, inputnames + ) where {T, V} + I = nametype(T) + outputnames = collect(I, outputnames) + inputnames = collect(I, inputnames) + if length(outputnames) != length(inputnames) + throw( + ArgumentError( + "Operator `outputnames` and `inputnames` must have equal length " * + "(positional pairing), got $(length(outputnames)) and " * + "$(length(inputnames))." + ) + ) + end + for opname in Iterators.flatten((outputnames, inputnames)) + nvertices = length(dimnamevertices(parent, opname)) + if nvertices != 1 + throw( + ArgumentError( + "operator dim name $opname is associated with $nvertices vertices " * + "in the tensor network; an operator leg must be a dangling index." + ) + ) + end + end + return new{T, V, I, typeof(parent)}(parent, outputnames, inputnames) + end +end + +function ITensorBase.operator(tn::AbstractITensorNetwork, outputnames, inputnames) + return ITensorNetworkOperator(tn, outputnames, inputnames) +end + +function Base.eltype(::Type{<:ITensorNetworkOperator{T, V, I}}) where {T, V, I} + return NamedTensorOperator{I, T} +end + +# ====================================== Graphs.jl ======================================= # + +Graphs.edges(op::ITensorNetworkOperator) = edges(state(op)) +Graphs.vertices(op::ITensorNetworkOperator) = vertices(state(op)) + +# ==================================== NamedGraphs.jl ==================================== # + +function NamedGraphs.encoded_vertex(op::ITensorNetworkOperator, vertex) + return encoded_vertex(state(op), vertex) +end +function NamedGraphs.decoded_vertex(op::ITensorNetworkOperator, code::Integer) + return decoded_vertex(state(op), code) +end +NamedGraphs.encoded_graph(op::ITensorNetworkOperator) = encoded_graph(state(op)) + +# ==================================== DataGraphs.jl ===================================== # + +# A pair whose two halves sit on different vertices has no bijection to give this vertex, so +# only the pairs local to `vertex` become the wrapper's pairing and the rest stay dangling. +function DataGraphs.get_vertex_data(op::ITensorNetworkOperator, vertex) + tensor = state(op)[vertex] + tensor_names = names(tensor) + outputs = similar(outputnames(op), 0) + inputs = similar(inputnames(op), 0) + for (output, input) in zip(outputnames(op), inputnames(op)) + if output in tensor_names && input in tensor_names + push!(outputs, output) + push!(inputs, input) + end + end + return operator(tensor, outputs, inputs) +end + +function DataGraphs.is_vertex_assigned(op::ITensorNetworkOperator, vertex) + return is_vertex_assigned(state(op), vertex) +end + +# =================================== Dictionaries.jl ==================================== # + +Dictionaries.issettable(::ITensorNetworkOperator) = false +Dictionaries.isinsertable(::ITensorNetworkOperator) = false + +# ====================================== interface ======================================= # + +ITensorBase.state(op::ITensorNetworkOperator) = op.parent +Base.parent(op::ITensorNetworkOperator) = state(op) +ITensorBase.outputnames(op::ITensorNetworkOperator) = op.outputnames +ITensorBase.inputnames(op::ITensorNetworkOperator) = op.inputnames + +function operator_support(tn::AbstractGraph, op::ITensorNetworkOperator) + return operator_support_names(tn, inputnames(op)) +end diff --git a/test/test_itensornetworkoperator.jl b/test/test_itensornetworkoperator.jl new file mode 100644 index 00000000..68b0b6c4 --- /dev/null +++ b/test/test_itensornetworkoperator.jl @@ -0,0 +1,115 @@ +using DataGraphs: is_vertex_assigned +using Dictionaries: isinsertable, issettable +using Graphs: edges, vertices +using ITensorBase: ITensor, Index, inputnames, name, names, operator, outputnames, state +using ITensorNetworksNext: + ITensorNetwork, ITensorNetworkOperator, operator_support, tensornetwork +using NamedGraphs: incident_edges, named_path_graph +using Test: @test, @test_throws, @testset + +# A bondless operator network on `g`, with one input/output pair per vertex. +function local_operator(g; d = 2) + inp = Dict(v => Index(d) for v in vertices(g)) + out = Dict(v => Index(d) for v in vertices(g)) + tn = tensornetwork(vertices(g)) do v + return randn((out[v], inp[v])) + end + vs = collect(vertices(g)) + op = operator(tn, [name(out[v]) for v in vs], [name(inp[v]) for v in vs]) + return op, inp, out +end + +@testset "`ITensorNetworkOperator`" begin + @testset "Basics" begin + g = named_path_graph(3) + op, inp, out = local_operator(g) + + @test op isa ITensorNetworkOperator + + # The operator shares the graph structure of the network it wraps. + @test issetequal(vertices(op), vertices(state(op))) + @test issetequal(edges(op), edges(state(op))) + @test parent(op) === state(op) + + # The pairing is positional: `outputnames[i]` goes with `inputnames[i]`. + vs = collect(vertices(g)) + @test outputnames(op) == [name(out[v]) for v in vs] + @test inputnames(op) == [name(inp[v]) for v in vs] + + @test is_vertex_assigned(op, first(vs)) + + # It is a lazy wrapper, so it is neither settable nor insertable. + @test !issettable(op) + @test !isinsertable(op) + end + + @testset "`getindex` wraps the vertex tensor" begin + g = named_path_graph(3) + op, inp, out = local_operator(g) + + # Both halves of each pair sit on the same vertex here, so the wrapper carries them. + for v in vertices(g) + @test outputnames(op[v]) == [name(out[v])] + @test inputnames(op[v]) == [name(inp[v])] + @test state(op[v]) === state(op)[v] + end + + # `eltype` is the type of the wrapped vertex data. + @test eltype(op) === typeof(op[first(vertices(g))]) + end + + @testset "a pair may straddle two vertices" begin + # A swap: the output at vertex 1 is paired with the input at vertex 2, and vice versa. + a, b = Index(2), Index(2) + a′, b′ = Index(2), Index(2) + tn = ITensorNetwork(Dict(1 => randn((a′, a)), 2 => randn((b′, b)))) + op = operator(tn, [name(a′), name(b′)], [name(b), name(a)]) + + @test outputnames(op) == [name(a′), name(b′)] + @test inputnames(op) == [name(b), name(a)] + + # Neither pair is local to a vertex, so each vertex wrapper has an empty pairing and + # both of its legs stay dangling. + for v in (1, 2) + @test isempty(outputnames(op[v])) + @test isempty(inputnames(op[v])) + @test issetequal(names(op[v]), names(state(op)[v])) + end + end + + @testset "constructor validation" begin + g = named_path_graph(2) + op, inp, out = local_operator(g) + tn = state(op) + vs = collect(vertices(g)) + + # The pairing is a bijection, so the two name lists must have equal length. + @test_throws ArgumentError operator(tn, [name(out[vs[1]])], []) + @test_throws ArgumentError operator( + tn, [name(out[v]) for v in vs], [name(inp[vs[1]])] + ) + + # An operator leg must be dangling: a link name touches two vertices. + l = Index(2) + linked = tensornetwork(vertices(g)) do v + return randn((inp[v], l)) + end + @test_throws ArgumentError operator(linked, [name(l)], [name(inp[vs[1]])]) + end + + @testset "`operator_support`" begin + g = named_path_graph(3) + s = Dict(v => Index(2) for v in vertices(g)) + state_tn = tensornetwork(vertices(g)) do v + is = map(e -> Index(2), incident_edges(g, v)) + return randn((s[v], is...)) + end + + # An operator acting on sites 1 and 3 is supported on exactly those vertices. + out1, out3 = Index(2), Index(2) + optn = ITensorNetwork(Dict(1 => randn((out1, s[1])), 3 => randn((out3, s[3])))) + op = operator(optn, [name(out1), name(out3)], [name(s[1]), name(s[3])]) + + @test issetequal(operator_support(state_tn, op), Set([1, 3])) + end +end diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index 7c036bd2..1866e985 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -1,7 +1,7 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, operator, +using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, operator, outputnames, rename, setname, uniquename using ITensorNetworksNext: BraView, ITensorNetwork, KetView, NormNetwork, OperatorView, QuadraticFormNetwork, braname, bratensor, conj_bratensor, contract_network, indmap, @@ -26,11 +26,12 @@ end # Build a bondless operator layer on the vertices of `g`, with `f(v)` supplying the matrix # acting on the site index `s[v]`. function product_operator(f, g, s; d = 2) - op = tensornetwork(vertices(g)) do v - out = Index(d) - return operator(ITensor(f(v), (out, s[v])), (name(out),), (name(s[v]),)) + out = Dict(v => Index(d) for v in vertices(g)) + tn = tensornetwork(vertices(g)) do v + return ITensor(f(v), (out[v], s[v])) end - return op + vs = collect(vertices(g)) + return operator(tn, [name(out[v]) for v in vs], [name(s[v]) for v in vs]) end identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; d) @@ -94,8 +95,9 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; # The operator's input name meets the ket and its output name is renamed to meet # the bra. o = operatortensor(qf, 2) - @test inputnames(o) == [sname] - @test outputnames(o) == [braname(qf, sname)] + @test sname in names(o) + @test braname(qf, sname) in names(o) + @test inputnames(op) == [name(s[v]) for v in vertices(g)] # `bratensor` is the elementwise conjugate of `conj_bratensor` and carries the same # indices. From fdeda86ec86af4d66785572964f398fe6a6b87e0 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 28 Sep 2026 11:54:10 -0400 Subject: [PATCH 06/28] Replace `KetView` and `OperatorView` with `ketnetwork` and `operatornetwork` `ketnetwork` and `operatornetwork` return the stored ket network and `ITensorNetworkOperator`. `branetwork` defaults to a `BraView`, since a bilinear-form network does not store its bra layer. The belief propagation code for `NormNetwork` now reads its link names through these accessors. Co-Authored-By: Claude Opus 5.5 --- src/beliefpropagation/beliefpropagation.jl | 6 +-- src/beliefpropagation/messagecache.jl | 12 +++--- .../abstractbilinearformnetwork.jl | 25 +++++++++++- src/bilinearforms/bilinearformnetworkview.jl | 26 ------------- src/bilinearforms/normnetwork.jl | 1 + src/bilinearforms/quadraticformnetwork.jl | 2 + test/test_normnetwork.jl | 27 +++++-------- test/test_quadraticformnetwork.jl | 38 ++++++++----------- 8 files changed, 62 insertions(+), 75 deletions(-) diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index fa387253..df39650a 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -272,9 +272,9 @@ end # the message is positive semidefinite and its trace is a positive normalization. function message_update!(algorithm::SimpleMessageUpdate, cache, factors::NormNetwork, edge) new_tensor = updated_message(algorithm, cache, factors, edge) - new_message = operator( - new_tensor, linknames(BraView(factors), edge), linknames(KetView(factors), edge) - ) + branames = linknames(branetwork(factors), edge) + ketnames = linknames(ketnetwork(factors), edge) + new_message = operator(new_tensor, branames, ketnames) if algorithm.normalize message_norm = tr(new_message) iszero(message_norm) || (new_message /= message_norm) diff --git a/src/beliefpropagation/messagecache.jl b/src/beliefpropagation/messagecache.jl index ec91cd58..ebbfd759 100644 --- a/src/beliefpropagation/messagecache.jl +++ b/src/beliefpropagation/messagecache.jl @@ -191,16 +191,16 @@ bethe_free_energy(factors, messages) = -bethe_free_entropy(factors, messages) function similar_message_environment(nn::NormNetwork) messages = mapmany(vertices(nn)) do vertex return map(in_incident_edges(nn, vertex)) do edge - braview = BraView(nn) - ketview = KetView(nn) + bra = branetwork(nn) + ket = ketnetwork(nn) - ketnames = linknames(ketview, edge) - branames = linknames(braview, edge) - braaxis = unnamed.(linkaxes(braview, edge)) + ketnames = linknames(ket, edge) + branames = linknames(bra, edge) + braaxis = unnamed.(linkaxes(bra, edge)) # Bra leg = operator output, ket leg = input, the bipartition in which the message # is positive semidefinite. - message = similar_operator(ketview[vertex], braaxis, branames, ketnames) + message = similar_operator(ket[vertex], braaxis, branames, ketnames) return edge => message end diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index 6d1e894e..6d0dd6c2 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -10,7 +10,8 @@ Supertype of the lazy multi-layer networks built from a ket layer of type A subtype supplies its own graph structure and implements [`braname`](@ref) together with one accessor per layer: [`kettensor`](@ref), [`bratensor`](@ref) and, where the subtype has an operator layer, [`operatortensor`](@ref). `bratensor` has a default built from `kettensor` -and `braname`. +and `braname`. The layers as whole networks are returned by [`ketnetwork`](@ref), +[`branetwork`](@ref) and [`operatornetwork`](@ref). """ abstract type AbstractBilinearFormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} end @@ -54,4 +55,26 @@ The bra-layer tensor at `vertex`. """ bratensor(bn::AbstractBilinearFormNetwork, vertex) = conj(conj_bratensor(bn, vertex)) +""" + ketnetwork(bn::AbstractBilinearFormNetwork) + +The ket-layer network of `bn`. +""" +function ketnetwork end + +""" + operatornetwork(bn::AbstractBilinearFormNetwork) + +The operator-layer network of `bn`, for a subtype that has an operator layer. +""" +function operatornetwork end + +""" + branetwork(bn::AbstractBilinearFormNetwork) + +The bra-layer network of `bn`. Unless a subtype stores its bra layer as a network, this is a +`BraView`, whose tensors are built by [`bratensor`](@ref) when accessed. +""" +branetwork(bn::AbstractBilinearFormNetwork) = BraView(bn) + indmap(bn::AbstractBilinearFormNetwork, ind) = setname(conj(ind), braname(bn, name(ind))) diff --git a/src/bilinearforms/bilinearformnetworkview.jl b/src/bilinearforms/bilinearformnetworkview.jl index dbeb81bf..d9622fee 100644 --- a/src/bilinearforms/bilinearformnetworkview.jl +++ b/src/bilinearforms/bilinearformnetworkview.jl @@ -2,14 +2,6 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices -struct KetView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: - AbstractBilinearFormNetworkView{T, V, I} - parent::P - function KetView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} - return new{T, V, I, typeof(parent)}(parent) - end -end - struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractBilinearFormNetworkView{T, V, I} parent::P @@ -18,26 +10,8 @@ struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: end end -struct OperatorView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: - AbstractBilinearFormNetworkView{T, V, I} - parent::P - function OperatorView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} - return new{T, V, I, typeof(parent)}(parent) - end -end - -# A `NormNetwork`'s bra and ket layers share their site index names, so there is no distinct -# bra site index for an identity operator to carry as its output name. -function OperatorView(::NormNetwork) - throw(ArgumentError("a `NormNetwork` has no operator layer to view.")) -end - -Base.parent(nnv::KetView) = nnv.parent Base.parent(nnv::BraView) = nnv.parent -Base.parent(nnv::OperatorView) = nnv.parent # ==================================== DataGraphs.jl ===================================== # -DataGraphs.get_vertex_data(nnv::KetView, vertex) = kettensor(parent(nnv), vertex) DataGraphs.get_vertex_data(nnv::BraView, vertex) = bratensor(parent(nnv), vertex) -DataGraphs.get_vertex_data(nnv::OperatorView, vertex) = operatortensor(parent(nnv), vertex) diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index 4b136b3a..2e2ae76e 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -71,6 +71,7 @@ function braname(nn::NormNetwork, name) end kettensor(nn::NormNetwork, vertex) = nn.ket[vertex] +ketnetwork(nn::NormNetwork) = nn.ket """ flatten_network(nn::NormNetwork) -> ITensorNetwork diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 74808803..a01b5887 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -86,6 +86,8 @@ function braname(qf::QuadraticFormNetwork, name) end kettensor(qf::QuadraticFormNetwork, vertex) = qf.ket[vertex] +ketnetwork(qf::QuadraticFormNetwork) = qf.ket +operatornetwork(qf::QuadraticFormNetwork) = qf.operator # Each output name is renamed to the bra name of the input name it is paired with, so the # operator's output legs meet the bra layer and its input legs meet the ket layer. The pairing diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 4528b220..56d2eec9 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -2,9 +2,9 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, conj, inds, name, setname, uniquename -using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, KetView, NormNetwork, braname, - bratensor, conj_bratensor, contraction_tree, flatten_network, indmap, kettensor, - normnetwork, prod_tensors, tensornetwork +using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, branetwork, + bratensor, conj_bratensor, contraction_tree, flatten_network, indmap, ketnetwork, + kettensor, normnetwork, prod_tensors, tensornetwork using LinearAlgebra: norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -101,32 +101,25 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test braname(nn, lname) in name.(inds(conj_bratensor(nn, 2))) end - @testset "`KetView` / `BraView`" begin + @testset "`ketnetwork` / `branetwork`" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) nn = NormNetwork(tn) - kv = KetView(nn) - bv = BraView(nn) + # The ket layer is the network the norm network was built from. + @test ketnetwork(nn) === tn - # Views share the graph structure of the underlying network. - @test issetequal(vertices(kv), vertices(tn)) + # The bra layer is not stored, so it is a view sharing the ket layer's graph structure. + bv = branetwork(nn) + @test bv isa BraView @test issetequal(vertices(bv), vertices(tn)) - @test issetequal(edges(kv), edges(tn)) @test issetequal(edges(bv), edges(tn)) - - # The ket view exposes the bare ket tensors; the bra view exposes the bra tensors. for v in vertices(tn) - @test kv[v] === kettensor(nn, v) @test inds(bv[v]) == inds(bratensor(nn, v)) end - - @test is_vertex_assigned(kv, 1) @test is_vertex_assigned(bv, 1) - # Views inherit the (non-)mutability of their parent norm network. - @test !issettable(kv) - @test !isinsertable(kv) + # The view inherits the (non-)mutability of its parent norm network. @test !issettable(bv) @test !isinsertable(bv) end diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index 1866e985..fd18c2e6 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -3,9 +3,9 @@ using Dictionaries: isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, operator, outputnames, rename, setname, uniquename -using ITensorNetworksNext: BraView, ITensorNetwork, KetView, NormNetwork, OperatorView, - QuadraticFormNetwork, braname, bratensor, conj_bratensor, contract_network, indmap, - kettensor, operatortensor, quadraticformnetwork, tensornetwork +using ITensorNetworksNext: BraView, ITensorNetwork, NormNetwork, QuadraticFormNetwork, + braname, branetwork, bratensor, conj_bratensor, contract_network, indmap, ketnetwork, + kettensor, operatornetwork, operatortensor, quadraticformnetwork, tensornetwork using LinearAlgebra: I, norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -129,31 +129,25 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; @test braname(qf, sname) == custom[sname] end - @testset "`KetView` / `OperatorView` / `BraView`" begin + @testset "`ketnetwork` / `operatornetwork` / `branetwork`" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) op = identity_operator(g, s) qf = QuadraticFormNetwork(tn, op) - kv = KetView(qf) - ov = OperatorView(qf) - bv = BraView(qf) - - # Views share the graph structure of the ket layer. - for view in (kv, ov, bv) - @test issetequal(vertices(view), vertices(tn)) - @test issetequal(edges(view), edges(tn)) - @test !issettable(view) - @test !isinsertable(view) - @test is_vertex_assigned(view, 1) - end - - # A `NormNetwork` has no operator layer, so `OperatorView` rejects one. - @test_throws ArgumentError OperatorView(NormNetwork(tn)) - + # The ket and operator layers are the networks the quadratic form was built from. + @test ketnetwork(qf) === tn + @test operatornetwork(qf) === op + + # The bra layer is not stored, so it is a view sharing the ket layer's graph structure. + bv = branetwork(qf) + @test bv isa BraView + @test issetequal(vertices(bv), vertices(tn)) + @test issetequal(edges(bv), edges(tn)) + @test !issettable(bv) + @test !isinsertable(bv) + @test is_vertex_assigned(bv, 1) for v in vertices(tn) - @test kv[v] === kettensor(qf, v) - @test inds(ov[v]) == inds(operatortensor(qf, v)) @test inds(bv[v]) == inds(bratensor(qf, v)) end end From 9891d03f8eadc2a590da7a847fdd72b59a5d4c8c Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 28 Sep 2026 11:57:41 -0400 Subject: [PATCH 07/28] Remove `AbstractBilinearFormNetworkView`; define its forwarding methods on `BraView` `BraView` was the only remaining subtype, so it now subtypes `AbstractITensorNetwork` directly and carries the graph, encoding, assignment and mutability forwarding itself. The include order places `bilinearformnetworkview.jl` first, since the forwarding methods now name `BraView`. Co-Authored-By: Claude Opus 5.5 --- src/ITensorNetworksNext.jl | 2 +- .../abstractbilinearformnetworkview.jl | 27 ++++++------------- src/bilinearforms/bilinearformnetworkview.jl | 8 +++++- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/ITensorNetworksNext.jl b/src/ITensorNetworksNext.jl index 036b895a..1a6ec964 100644 --- a/src/ITensorNetworksNext.jl +++ b/src/ITensorNetworksNext.jl @@ -15,10 +15,10 @@ include("abstracttensornetwork.jl") include("tensornetwork.jl") include("itensornetworkoperator.jl") include("bilinearforms/abstractbilinearformnetwork.jl") -include("bilinearforms/abstractbilinearformnetworkview.jl") include("bilinearforms/normnetwork.jl") include("bilinearforms/quadraticformnetwork.jl") include("bilinearforms/bilinearformnetworkview.jl") +include("bilinearforms/abstractbilinearformnetworkview.jl") include("ITensorNetworkGenerators/ITensorNetworkGenerators.jl") include("contraction_tree.jl") include("prod_tensors.jl") diff --git a/src/bilinearforms/abstractbilinearformnetworkview.jl b/src/bilinearforms/abstractbilinearformnetworkview.jl index 262bc4e7..02bc8607 100644 --- a/src/bilinearforms/abstractbilinearformnetworkview.jl +++ b/src/bilinearforms/abstractbilinearformnetworkview.jl @@ -3,39 +3,28 @@ using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex -""" - abstract type AbstractBilinearFormNetworkView{T, V, I} <: AbstractITensorNetwork{T, V} - -Supertype of the single-layer views of an `AbstractBilinearFormNetwork{T, V, I}`. - -A subtype implements `Base.parent`, returning the network it views, and -`DataGraphs.get_vertex_data`, returning that layer's tensor at a vertex. Its graph structure -and mutability are those of the parent network. -""" -abstract type AbstractBilinearFormNetworkView{T, V, I} <: AbstractITensorNetwork{T, V} end - # ====================================== Graphs.jl ======================================= # -Graphs.edges(nnv::AbstractBilinearFormNetworkView) = edges(parent(nnv)) -Graphs.vertices(nnv::AbstractBilinearFormNetworkView) = vertices(parent(nnv)) +Graphs.edges(nnv::BraView) = edges(parent(nnv)) +Graphs.vertices(nnv::BraView) = vertices(parent(nnv)) # ==================================== NamedGraphs.jl ==================================== # -function NamedGraphs.encoded_vertex(nnv::AbstractBilinearFormNetworkView, vertex) +function NamedGraphs.encoded_vertex(nnv::BraView, vertex) return encoded_vertex(parent(nnv), vertex) end -function NamedGraphs.decoded_vertex(nnv::AbstractBilinearFormNetworkView, code::Integer) +function NamedGraphs.decoded_vertex(nnv::BraView, code::Integer) return decoded_vertex(parent(nnv), code) end -NamedGraphs.encoded_graph(nnv::AbstractBilinearFormNetworkView) = encoded_graph(parent(nnv)) +NamedGraphs.encoded_graph(nnv::BraView) = encoded_graph(parent(nnv)) # ==================================== DataGraphs.jl ===================================== # -function DataGraphs.is_vertex_assigned(nnv::AbstractBilinearFormNetworkView, vertex) +function DataGraphs.is_vertex_assigned(nnv::BraView, vertex) return is_vertex_assigned(parent(nnv), vertex) end # =================================== Dictionaries.jl ==================================== # -Dictionaries.issettable(nnv::AbstractBilinearFormNetworkView) = issettable(parent(nnv)) -Dictionaries.isinsertable(nnv::AbstractBilinearFormNetworkView) = isinsertable(parent(nnv)) +Dictionaries.issettable(nnv::BraView) = issettable(parent(nnv)) +Dictionaries.isinsertable(nnv::BraView) = isinsertable(parent(nnv)) diff --git a/src/bilinearforms/bilinearformnetworkview.jl b/src/bilinearforms/bilinearformnetworkview.jl index d9622fee..277529a5 100644 --- a/src/bilinearforms/bilinearformnetworkview.jl +++ b/src/bilinearforms/bilinearformnetworkview.jl @@ -2,8 +2,14 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices +""" + struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractITensorNetwork{T, V} + +The bra layer of the bilinear-form network `parent(view)`, with each vertex tensor built by +[`bratensor`](@ref) when accessed. Its graph structure and mutability are those of the parent. +""" struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: - AbstractBilinearFormNetworkView{T, V, I} + AbstractITensorNetwork{T, V} parent::P function BraView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} return new{T, V, I, typeof(parent)}(parent) From 90dbabe62c6173c5168265c50475060613402555 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 28 Sep 2026 11:59:17 -0400 Subject: [PATCH 08/28] Move `BraView` into `abstractbilinearformnetwork.jl` Pure move: the contents of `bilinearformnetworkview.jl` and `abstractbilinearformnetworkview.jl` are appended verbatim, and their `using` lines are merged into the existing imports. `src/bilinearforms/` now holds the interface, `NormNetwork` and `QuadraticFormNetwork`. Co-Authored-By: Claude Opus 5.5 --- src/ITensorNetworksNext.jl | 2 - .../abstractbilinearformnetwork.jl | 51 ++++++++++++++++++- .../abstractbilinearformnetworkview.jl | 30 ----------- src/bilinearforms/bilinearformnetworkview.jl | 23 --------- 4 files changed, 50 insertions(+), 56 deletions(-) delete mode 100644 src/bilinearforms/abstractbilinearformnetworkview.jl delete mode 100644 src/bilinearforms/bilinearformnetworkview.jl diff --git a/src/ITensorNetworksNext.jl b/src/ITensorNetworksNext.jl index 1a6ec964..838d7339 100644 --- a/src/ITensorNetworksNext.jl +++ b/src/ITensorNetworksNext.jl @@ -17,8 +17,6 @@ include("itensornetworkoperator.jl") include("bilinearforms/abstractbilinearformnetwork.jl") include("bilinearforms/normnetwork.jl") include("bilinearforms/quadraticformnetwork.jl") -include("bilinearforms/bilinearformnetworkview.jl") -include("bilinearforms/abstractbilinearformnetworkview.jl") include("ITensorNetworkGenerators/ITensorNetworkGenerators.jl") include("contraction_tree.jl") include("prod_tensors.jl") diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index 6d0dd6c2..10f54661 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -1,5 +1,8 @@ -using Dictionaries: Dictionaries +using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned +using Dictionaries: Dictionaries, isinsertable, issettable +using Graphs: Graphs, edges, vertices using ITensorBase: conj, name, rename, setname +using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex """ abstract type AbstractBilinearFormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} @@ -78,3 +81,49 @@ The bra-layer network of `bn`. Unless a subtype stores its bra layer as a networ branetwork(bn::AbstractBilinearFormNetwork) = BraView(bn) indmap(bn::AbstractBilinearFormNetwork, ind) = setname(conj(ind), braname(bn, name(ind))) + +""" + struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractITensorNetwork{T, V} + +The bra layer of the bilinear-form network `parent(view)`, with each vertex tensor built by +[`bratensor`](@ref) when accessed. Its graph structure and mutability are those of the parent. +""" +struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: + AbstractITensorNetwork{T, V} + parent::P + function BraView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} + return new{T, V, I, typeof(parent)}(parent) + end +end + +Base.parent(nnv::BraView) = nnv.parent + +# ==================================== DataGraphs.jl ===================================== # + +DataGraphs.get_vertex_data(nnv::BraView, vertex) = bratensor(parent(nnv), vertex) + +# ====================================== Graphs.jl ======================================= # + +Graphs.edges(nnv::BraView) = edges(parent(nnv)) +Graphs.vertices(nnv::BraView) = vertices(parent(nnv)) + +# ==================================== NamedGraphs.jl ==================================== # + +function NamedGraphs.encoded_vertex(nnv::BraView, vertex) + return encoded_vertex(parent(nnv), vertex) +end +function NamedGraphs.decoded_vertex(nnv::BraView, code::Integer) + return decoded_vertex(parent(nnv), code) +end +NamedGraphs.encoded_graph(nnv::BraView) = encoded_graph(parent(nnv)) + +# ==================================== DataGraphs.jl ===================================== # + +function DataGraphs.is_vertex_assigned(nnv::BraView, vertex) + return is_vertex_assigned(parent(nnv), vertex) +end + +# =================================== Dictionaries.jl ==================================== # + +Dictionaries.issettable(nnv::BraView) = issettable(parent(nnv)) +Dictionaries.isinsertable(nnv::BraView) = isinsertable(parent(nnv)) diff --git a/src/bilinearforms/abstractbilinearformnetworkview.jl b/src/bilinearforms/abstractbilinearformnetworkview.jl deleted file mode 100644 index 02bc8607..00000000 --- a/src/bilinearforms/abstractbilinearformnetworkview.jl +++ /dev/null @@ -1,30 +0,0 @@ -using DataGraphs: DataGraphs, is_vertex_assigned -using Dictionaries: Dictionaries, isinsertable, issettable -using Graphs: Graphs, edges, vertices -using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex - -# ====================================== Graphs.jl ======================================= # - -Graphs.edges(nnv::BraView) = edges(parent(nnv)) -Graphs.vertices(nnv::BraView) = vertices(parent(nnv)) - -# ==================================== NamedGraphs.jl ==================================== # - -function NamedGraphs.encoded_vertex(nnv::BraView, vertex) - return encoded_vertex(parent(nnv), vertex) -end -function NamedGraphs.decoded_vertex(nnv::BraView, code::Integer) - return decoded_vertex(parent(nnv), code) -end -NamedGraphs.encoded_graph(nnv::BraView) = encoded_graph(parent(nnv)) - -# ==================================== DataGraphs.jl ===================================== # - -function DataGraphs.is_vertex_assigned(nnv::BraView, vertex) - return is_vertex_assigned(parent(nnv), vertex) -end - -# =================================== Dictionaries.jl ==================================== # - -Dictionaries.issettable(nnv::BraView) = issettable(parent(nnv)) -Dictionaries.isinsertable(nnv::BraView) = isinsertable(parent(nnv)) diff --git a/src/bilinearforms/bilinearformnetworkview.jl b/src/bilinearforms/bilinearformnetworkview.jl deleted file mode 100644 index 277529a5..00000000 --- a/src/bilinearforms/bilinearformnetworkview.jl +++ /dev/null @@ -1,23 +0,0 @@ -using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned -using Dictionaries: Dictionaries, isinsertable, issettable -using Graphs: Graphs, edges, vertices - -""" - struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractITensorNetwork{T, V} - -The bra layer of the bilinear-form network `parent(view)`, with each vertex tensor built by -[`bratensor`](@ref) when accessed. Its graph structure and mutability are those of the parent. -""" -struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: - AbstractITensorNetwork{T, V} - parent::P - function BraView(parent::AbstractBilinearFormNetwork{T, V, I}) where {T, V, I} - return new{T, V, I, typeof(parent)}(parent) - end -end - -Base.parent(nnv::BraView) = nnv.parent - -# ==================================== DataGraphs.jl ===================================== # - -DataGraphs.get_vertex_data(nnv::BraView, vertex) = bratensor(parent(nnv), vertex) From 68546b00922f05fd6cbeba0eb8f62eb24e7de95f Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 28 Sep 2026 12:56:44 -0400 Subject: [PATCH 09/28] Combine the two `DataGraphs.jl` sections of `BraView` Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/abstractbilinearformnetwork.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index 10f54661..6a8d8c9b 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -101,6 +101,9 @@ Base.parent(nnv::BraView) = nnv.parent # ==================================== DataGraphs.jl ===================================== # DataGraphs.get_vertex_data(nnv::BraView, vertex) = bratensor(parent(nnv), vertex) +function DataGraphs.is_vertex_assigned(nnv::BraView, vertex) + return is_vertex_assigned(parent(nnv), vertex) +end # ====================================== Graphs.jl ======================================= # @@ -117,12 +120,6 @@ function NamedGraphs.decoded_vertex(nnv::BraView, code::Integer) end NamedGraphs.encoded_graph(nnv::BraView) = encoded_graph(parent(nnv)) -# ==================================== DataGraphs.jl ===================================== # - -function DataGraphs.is_vertex_assigned(nnv::BraView, vertex) - return is_vertex_assigned(parent(nnv), vertex) -end - # =================================== Dictionaries.jl ==================================== # Dictionaries.issettable(nnv::BraView) = issettable(parent(nnv)) From 1d570624a8701ec9fbabdab4064b0418d622bce7 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 11:10:10 -0400 Subject: [PATCH 10/28] Reject crossing operator pairs; read the vertex operator's pairing in `operatortensor` `ITensorNetworkOperator` now throws when a pair's output and input sit on different vertices, so each vertex operator carries its full pairing, and `operatortensor` reads it from `qf.operator[vertex]` rather than from the network-wide name vectors. Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/quadraticformnetwork.jl | 16 ++++++---------- src/itensornetworkoperator.jl | 20 ++++++++++++++------ test/test_itensornetworkoperator.jl | 19 ++++--------------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index a01b5887..d78bc214 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,6 +1,6 @@ using Dictionaries: Dictionary using ITensorBase: - LazyNamedTensor, inputnames, lazy, names, outputnames, rename, state, uniquename + LazyNamedTensor, inputnames, lazy, outputnames, rename, state, uniquename using ITensorNetworksNext """ @@ -89,19 +89,15 @@ kettensor(qf::QuadraticFormNetwork, vertex) = qf.ket[vertex] ketnetwork(qf::QuadraticFormNetwork) = qf.ket operatornetwork(qf::QuadraticFormNetwork) = qf.operator -# Each output name is renamed to the bra name of the input name it is paired with, so the -# operator's output legs meet the bra layer and its input legs meet the ket layer. The pairing -# is read from the operator network rather than from `qf.operator[vertex]`, whose wrapper drops -# a pair whose input sits on another vertex. +# Each output name is renamed to the bra name of the input it is paired with, so the output legs +# meet the bra layer and the input legs meet the ket layer. function operatortensor(qf::QuadraticFormNetwork, vertex) - tensor = state(qf.operator)[vertex] - tensor_names = names(tensor) + op = qf.operator[vertex] replacements = [ output => braname(qf, input) for - (output, input) in zip(outputnames(qf.operator), inputnames(qf.operator)) - if output in tensor_names + (output, input) in zip(outputnames(op), inputnames(op)) ] - return rename(tensor, replacements...) + return rename(state(op), replacements...) end """ diff --git a/src/itensornetworkoperator.jl b/src/itensornetworkoperator.jl index c64d1208..48fe077a 100644 --- a/src/itensornetworkoperator.jl +++ b/src/itensornetworkoperator.jl @@ -13,9 +13,8 @@ The network equivalent of `ITensorBase.ITensorOperator`: a tensor network of typ where `outputnames[i]` is paired with `inputnames[i]`. Applying the operator contracts over the input names and leaves the output names. -A pair may straddle two vertices, as it does for a swap or a translation. Indexing returns -the vertex tensor wrapped as an `ITensorOperator` carrying only the pairs whose two halves -both sit on that vertex; the remaining legs are dangling on the wrapper. +The output and input of each pair must sit on the same vertex. Indexing returns the vertex +tensor wrapped as an `ITensorOperator` carrying the pairs at that vertex. """ struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: AbstractITensorNetwork{T, V} @@ -48,6 +47,16 @@ struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: ) end end + for (output, input) in zip(outputnames, inputnames) + if only(dimnamevertices(parent, output)) != only(dimnamevertices(parent, input)) + throw( + ArgumentError( + "operator output $output and its paired input $input must sit on " * + "the same vertex." + ) + ) + end + end return new{T, V, I, typeof(parent)}(parent, outputnames, inputnames) end end @@ -77,15 +86,14 @@ NamedGraphs.encoded_graph(op::ITensorNetworkOperator) = encoded_graph(state(op)) # ==================================== DataGraphs.jl ===================================== # -# A pair whose two halves sit on different vertices has no bijection to give this vertex, so -# only the pairs local to `vertex` become the wrapper's pairing and the rest stay dangling. +# Both names of a pair sit on one vertex, so the pairs whose output is on `vertex` are its pairing. function DataGraphs.get_vertex_data(op::ITensorNetworkOperator, vertex) tensor = state(op)[vertex] tensor_names = names(tensor) outputs = similar(outputnames(op), 0) inputs = similar(inputnames(op), 0) for (output, input) in zip(outputnames(op), inputnames(op)) - if output in tensor_names && input in tensor_names + if output in tensor_names push!(outputs, output) push!(inputs, input) end diff --git a/test/test_itensornetworkoperator.jl b/test/test_itensornetworkoperator.jl index 68b0b6c4..0fb4363c 100644 --- a/test/test_itensornetworkoperator.jl +++ b/test/test_itensornetworkoperator.jl @@ -1,7 +1,7 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, inputnames, name, names, operator, outputnames, state +using ITensorBase: ITensor, Index, inputnames, name, operator, outputnames, state using ITensorNetworksNext: ITensorNetwork, ITensorNetworkOperator, operator_support, tensornetwork using NamedGraphs: incident_edges, named_path_graph @@ -58,23 +58,12 @@ end @test eltype(op) === typeof(op[first(vertices(g))]) end - @testset "a pair may straddle two vertices" begin - # A swap: the output at vertex 1 is paired with the input at vertex 2, and vice versa. + @testset "a pair must sit on one vertex" begin + # A swap pairs the output at vertex 1 with the input at vertex 2, and vice versa. a, b = Index(2), Index(2) a′, b′ = Index(2), Index(2) tn = ITensorNetwork(Dict(1 => randn((a′, a)), 2 => randn((b′, b)))) - op = operator(tn, [name(a′), name(b′)], [name(b), name(a)]) - - @test outputnames(op) == [name(a′), name(b′)] - @test inputnames(op) == [name(b), name(a)] - - # Neither pair is local to a vertex, so each vertex wrapper has an empty pairing and - # both of its legs stay dangling. - for v in (1, 2) - @test isempty(outputnames(op[v])) - @test isempty(inputnames(op[v])) - @test issetequal(names(op[v]), names(state(op)[v])) - end + @test_throws ArgumentError operator(tn, [name(a′), name(b′)], [name(b), name(a)]) end @testset "constructor validation" begin From 809d403191572d8c79d39951fc6b840ecdea8b80 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Tue, 29 Sep 2026 10:54:24 -0400 Subject: [PATCH 11/28] Store the `ITensorNetworkOperator` pairing as a `Dictionary` The output and input name vectors are replaced by one map from each output name to its input name, so `getindex` looks up a vertex's pairs by name instead of scanning every pair. A repeated output name now throws an `ArgumentError`. Co-Authored-By: Claude Opus 5.5 --- src/itensornetworkoperator.jl | 29 ++++++++++++----------------- test/test_itensornetworkoperator.jl | 4 ++++ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/itensornetworkoperator.jl b/src/itensornetworkoperator.jl index 48fe077a..c8b68686 100644 --- a/src/itensornetworkoperator.jl +++ b/src/itensornetworkoperator.jl @@ -1,5 +1,5 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned -using Dictionaries: Dictionaries +using Dictionaries: Dictionaries, Dictionary using Graphs: Graphs, AbstractGraph, edges, vertices using ITensorBase: ITensorBase, NamedTensorOperator, inputnames, names, nametype, operator, outputnames, state @@ -10,7 +10,7 @@ using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex The network equivalent of `ITensorBase.ITensorOperator`: a tensor network of type `P <: AbstractITensorNetwork{T, V}` together with a pairing of its dangling index names, -where `outputnames[i]` is paired with `inputnames[i]`. Applying the operator contracts over +stored as a map from each output name to its input name. Applying the operator contracts over the input names and leaves the output names. The output and input of each pair must sit on the same vertex. Indexing returns the vertex @@ -19,8 +19,7 @@ tensor wrapped as an `ITensorOperator` carrying the pairs at that vertex. struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: AbstractITensorNetwork{T, V} parent::P - outputnames::Vector{I} - inputnames::Vector{I} + pairing::Dictionary{I, I} function ITensorNetworkOperator( parent::AbstractITensorNetwork{T, V}, outputnames, inputnames ) where {T, V} @@ -36,6 +35,9 @@ struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: ) ) end + if !allunique(outputnames) + throw(ArgumentError("each operator output name must appear only once.")) + end for opname in Iterators.flatten((outputnames, inputnames)) nvertices = length(dimnamevertices(parent, opname)) if nvertices != 1 @@ -57,7 +59,8 @@ struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: ) end end - return new{T, V, I, typeof(parent)}(parent, outputnames, inputnames) + pairing = Dictionary(outputnames, inputnames) + return new{T, V, I, typeof(parent)}(parent, pairing) end end @@ -89,16 +92,8 @@ NamedGraphs.encoded_graph(op::ITensorNetworkOperator) = encoded_graph(state(op)) # Both names of a pair sit on one vertex, so the pairs whose output is on `vertex` are its pairing. function DataGraphs.get_vertex_data(op::ITensorNetworkOperator, vertex) tensor = state(op)[vertex] - tensor_names = names(tensor) - outputs = similar(outputnames(op), 0) - inputs = similar(inputnames(op), 0) - for (output, input) in zip(outputnames(op), inputnames(op)) - if output in tensor_names - push!(outputs, output) - push!(inputs, input) - end - end - return operator(tensor, outputs, inputs) + outputs = filter(name -> haskey(op.pairing, name), names(tensor)) + return operator(tensor, outputs, [op.pairing[output] for output in outputs]) end function DataGraphs.is_vertex_assigned(op::ITensorNetworkOperator, vertex) @@ -114,8 +109,8 @@ Dictionaries.isinsertable(::ITensorNetworkOperator) = false ITensorBase.state(op::ITensorNetworkOperator) = op.parent Base.parent(op::ITensorNetworkOperator) = state(op) -ITensorBase.outputnames(op::ITensorNetworkOperator) = op.outputnames -ITensorBase.inputnames(op::ITensorNetworkOperator) = op.inputnames +ITensorBase.outputnames(op::ITensorNetworkOperator) = collect(keys(op.pairing)) +ITensorBase.inputnames(op::ITensorNetworkOperator) = collect(op.pairing) function operator_support(tn::AbstractGraph, op::ITensorNetworkOperator) return operator_support_names(tn, inputnames(op)) diff --git a/test/test_itensornetworkoperator.jl b/test/test_itensornetworkoperator.jl index 0fb4363c..ae6eb51c 100644 --- a/test/test_itensornetworkoperator.jl +++ b/test/test_itensornetworkoperator.jl @@ -77,6 +77,10 @@ end @test_throws ArgumentError operator( tn, [name(out[v]) for v in vs], [name(inp[vs[1]])] ) + # An output name can be paired with only one input. + @test_throws ArgumentError operator( + tn, [name(out[vs[1]]), name(out[vs[1]])], [name(inp[vs[1]]), name(inp[vs[1]])] + ) # An operator leg must be dangling: a link name touches two vertices. l = Index(2) From 2e532ffe3f26b5fa363c160d92181dce240177ce Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 11:11:28 -0400 Subject: [PATCH 12/28] Define shared bilinear-form methods on the abstract type; remove `indmap` Graph forwarding, the ket part of `is_vertex_assigned`, `braname` and `kettensor` are defined once on `AbstractBilinearFormNetwork` through `ketnetwork` and a new `branamemap` accessor, and both constructors build the name map with `select_branames`. The unused `indmap` is removed. Co-Authored-By: Claude Opus 5.5 --- .../abstractbilinearformnetwork.jl | 63 ++++++++++++++++--- src/bilinearforms/normnetwork.jl | 34 +--------- src/bilinearforms/quadraticformnetwork.jl | 39 ++---------- test/test_normnetwork.jl | 11 +--- test/test_quadraticformnetwork.jl | 11 +--- 5 files changed, 65 insertions(+), 93 deletions(-) diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index 6a8d8c9b..4d4f73f0 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -1,7 +1,7 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned -using Dictionaries: Dictionaries, isinsertable, issettable +using Dictionaries: Dictionaries, Dictionary, isinsertable, issettable using Graphs: Graphs, edges, vertices -using ITensorBase: conj, name, rename, setname +using ITensorBase: conj, rename using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex """ @@ -10,14 +10,34 @@ using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex Supertype of the lazy multi-layer networks built from a ket layer of type `ITensorNetwork{T, V, I}` and a ket→bra index name mapping. -A subtype supplies its own graph structure and implements [`braname`](@ref) together with one -accessor per layer: [`kettensor`](@ref), [`bratensor`](@ref) and, where the subtype has an -operator layer, [`operatortensor`](@ref). `bratensor` has a default built from `kettensor` -and `braname`. The layers as whole networks are returned by [`ketnetwork`](@ref), +A subtype implements [`ketnetwork`](@ref) and [`branamemap`](@ref), from which the graph +structure, [`braname`](@ref), [`kettensor`](@ref) and [`bratensor`](@ref) are defined, and, +where it has an operator layer, [`operatornetwork`](@ref) and [`operatortensor`](@ref). The layers as whole networks are returned by [`ketnetwork`](@ref), [`branetwork`](@ref) and [`operatornetwork`](@ref). """ abstract type AbstractBilinearFormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} end +# ====================================== Graphs.jl ======================================= # + +Graphs.edges(bn::AbstractBilinearFormNetwork) = edges(ketnetwork(bn)) +Graphs.vertices(bn::AbstractBilinearFormNetwork) = vertices(ketnetwork(bn)) + +# ==================================== NamedGraphs.jl ==================================== # + +function NamedGraphs.encoded_vertex(bn::AbstractBilinearFormNetwork, vertex) + return encoded_vertex(ketnetwork(bn), vertex) +end +function NamedGraphs.decoded_vertex(bn::AbstractBilinearFormNetwork, code::Integer) + return decoded_vertex(ketnetwork(bn), code) +end +NamedGraphs.encoded_graph(bn::AbstractBilinearFormNetwork) = encoded_graph(ketnetwork(bn)) + +# ==================================== DataGraphs.jl ===================================== # + +function DataGraphs.is_vertex_assigned(bn::AbstractBilinearFormNetwork, vertex) + return isassigned(ketnetwork(bn), vertex) +end + # =================================== Dictionaries.jl ==================================== # Dictionaries.issettable(::AbstractBilinearFormNetwork) = false @@ -30,14 +50,39 @@ Dictionaries.isinsertable(::AbstractBilinearFormNetwork) = false The bra-layer index name corresponding to the ket-layer index name `name`. """ -function braname end +function braname(bn::AbstractBilinearFormNetwork, name) + if !has_dimname(ketnetwork(bn), name) + error("index name $name not found underlying tensor network.") + end + # A name absent from the map has no separate bra copy and maps to itself: a site index of a + # norm network, or a site index a quadratic form's operator does not act on. + return get(branamemap(bn), name, name) +end + +""" + branamemap(bn::AbstractBilinearFormNetwork) + +The ket→bra name map, holding a bra name for each ket index name that has a separate bra copy. +""" +function branamemap end + +# A link name, or a name in `acted`, gets its bra name from `map`; every other name has none. +function select_branames(ket::ITensorNetwork{T, V, I}, map, acted) where {T, V, I} + braname = Dictionary{I, I}() + for (name, vertices) in pairs(ket.dimname_vertices) + if length(vertices) == 2 || name in acted + insert!(braname, name, map[name]) + end + end + return braname +end """ kettensor(bn::AbstractBilinearFormNetwork, vertex) The ket-layer tensor at `vertex`. """ -function kettensor end +kettensor(bn::AbstractBilinearFormNetwork, vertex) = ketnetwork(bn)[vertex] """ operatortensor(bn::AbstractBilinearFormNetwork, vertex) @@ -80,8 +125,6 @@ The bra-layer network of `bn`. Unless a subtype stores its bra layer as a networ """ branetwork(bn::AbstractBilinearFormNetwork) = BraView(bn) -indmap(bn::AbstractBilinearFormNetwork, ind) = setname(conj(ind), braname(bn, name(ind))) - """ struct BraView{T, V, I, P <: AbstractBilinearFormNetwork{T, V, I}} <: AbstractITensorNetwork{T, V} diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index 2e2ae76e..ad70a6f8 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -16,13 +16,7 @@ struct NormNetwork{T, V, I} <: AbstractBilinearFormNetwork{T, V, I} ket::ITensorNetwork{T, V, I}, map::Dictionary{I, I} ) where {T, V, I} - braname = Dictionary{I, I}() - for (name, vertices) in pairs(ket.dimname_vertices) - if length(vertices) == 2 - insert!(braname, name, map[name]) - end - end - return new{T, V, I}(ket, braname) + return new{T, V, I}(ket, select_branames(ket, map, ())) end end @@ -36,23 +30,8 @@ function NormNetwork(tn::ITensorNetwork) return NormNetwork(tn, map(uniquename, keys(tn.dimname_vertices))) end -# ====================================== Graphs.jl ======================================= # - -Graphs.edges(nn::NormNetwork) = edges(nn.ket) -Graphs.vertices(nn::NormNetwork) = vertices(nn.ket) - -# ==================================== NamedGraphs.jl ==================================== # - -NamedGraphs.encoded_vertex(nn::NormNetwork, vertex) = encoded_vertex(nn.ket, vertex) -NamedGraphs.decoded_vertex(nn::NormNetwork, code::Integer) = decoded_vertex(nn.ket, code) -NamedGraphs.encoded_graph(nn::NormNetwork) = encoded_graph(nn.ket) - # ==================================== DataGraphs.jl ===================================== # -function DataGraphs.is_vertex_assigned(nn::NormNetwork, vertex) - return isassigned(nn.ket, vertex) -end - function DataGraphs.get_vertex_data(nn::NormNetwork, vertex) return error( "Indexing a `NormNetwork` is not defined, since the double-layer tensor at a vertex has no representation of its own. Use `kettensor` and `bratensor` to reach the individual layers." @@ -61,17 +40,8 @@ end # ====================================== interface ======================================= # -function braname(nn::NormNetwork, name) - if !has_dimname(nn.ket, name) - error("index name $name not found underlying tensor network.") - end - # The indices not stored in `nn.braname` are precisely the site indices, which - # get mapped to themselves. - return get(nn.braname, name, name) -end - -kettensor(nn::NormNetwork, vertex) = nn.ket[vertex] ketnetwork(nn::NormNetwork) = nn.ket +branamemap(nn::NormNetwork) = nn.braname """ flatten_network(nn::NormNetwork) -> ITensorNetwork diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index d78bc214..c26b5cbe 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,6 +1,5 @@ using Dictionaries: Dictionary -using ITensorBase: - LazyNamedTensor, inputnames, lazy, outputnames, rename, state, uniquename +using ITensorBase: LazyNamedTensor, inputnames, lazy, outputnames, rename, state, uniquename using ITensorNetworksNext """ @@ -28,13 +27,7 @@ struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: if !issetequal(vertices(operator), vertices(ket)) error("the operator layer must be defined on every vertex of the ket layer.") end - acted = Set{I}(inputnames(operator)) - braname = Dictionary{I, I}() - for (name, vertices) in pairs(ket.dimname_vertices) - if length(vertices) == 2 || name in acted - insert!(braname, name, map[name]) - end - end + braname = select_branames(ket, map, Set{I}(inputnames(operator))) return new{T, V, I, typeof(operator)}(ket, operator, braname) end end @@ -45,21 +38,6 @@ function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetworkOpera return QuadraticFormNetwork(ket, operator, map(uniquename, keys(ket.dimname_vertices))) end -# ====================================== Graphs.jl ======================================= # - -Graphs.edges(qf::QuadraticFormNetwork) = edges(qf.ket) -Graphs.vertices(qf::QuadraticFormNetwork) = vertices(qf.ket) - -# ==================================== NamedGraphs.jl ==================================== # - -function NamedGraphs.encoded_vertex(qf::QuadraticFormNetwork, vertex) - return encoded_vertex(qf.ket, vertex) -end -function NamedGraphs.decoded_vertex(qf::QuadraticFormNetwork, code::Integer) - return decoded_vertex(qf.ket, code) -end -NamedGraphs.encoded_graph(qf::QuadraticFormNetwork) = encoded_graph(qf.ket) - # ==================================== DataGraphs.jl ===================================== # function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) @@ -71,22 +49,13 @@ function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) end function DataGraphs.is_vertex_assigned(qf::QuadraticFormNetwork, vertex) - return isassigned(qf.ket, vertex) && isassigned(qf.operator, vertex) + return isassigned(ketnetwork(qf), vertex) && isassigned(operatornetwork(qf), vertex) end # ====================================== interface ======================================= # -function braname(qf::QuadraticFormNetwork, name) - if !has_dimname(qf.ket, name) - error("index name $name not found underlying tensor network.") - end - # The indices not stored in `qf.braname` are the dangling ket indices the operator does - # not act on, which get mapped to themselves. - return get(qf.braname, name, name) -end - -kettensor(qf::QuadraticFormNetwork, vertex) = qf.ket[vertex] ketnetwork(qf::QuadraticFormNetwork) = qf.ket +branamemap(qf::QuadraticFormNetwork) = qf.braname operatornetwork(qf::QuadraticFormNetwork) = qf.operator # Each output name is renamed to the bra name of the input it is paired with, so the output legs diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 56d2eec9..41642940 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,9 +1,9 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, IndexName, conj, inds, name, setname, uniquename -using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, branetwork, - bratensor, conj_bratensor, contraction_tree, flatten_network, indmap, ketnetwork, +using ITensorBase: ITensor, Index, IndexName, inds, name, uniquename +using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, + branetwork, bratensor, conj_bratensor, contraction_tree, flatten_network, ketnetwork, kettensor, normnetwork, prod_tensors, tensornetwork using LinearAlgebra: norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph @@ -79,11 +79,6 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] # `bra` is the elementwise conjugate of `conj_bratensor` and carries the same indices. @test inds(bratensor(nn, 2)) == inds(conj_bratensor(nn, 2)) - # `indmap` conjugates an index and renames it according to the name map. - ind = only(i for i in inds(kettensor(nn, 2)) if name(i) == lname) - @test name(indmap(nn, ind)) == braname(nn, name(ind)) - @test indmap(nn, ind) == setname(conj(ind), braname(nn, name(ind))) - # Querying the name map with an index name absent from the network errors. @test_throws ErrorException braname(nn, name(Index(2))) end diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index fd18c2e6..ba968ed3 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -2,10 +2,10 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, operator, - outputnames, rename, setname, uniquename + outputnames, rename, uniquename using ITensorNetworksNext: BraView, ITensorNetwork, NormNetwork, QuadraticFormNetwork, - braname, branetwork, bratensor, conj_bratensor, contract_network, indmap, ketnetwork, - kettensor, operatornetwork, operatortensor, quadraticformnetwork, tensornetwork + braname, branetwork, bratensor, conj_bratensor, contract_network, ketnetwork, kettensor, + operatornetwork, operatortensor, quadraticformnetwork, tensornetwork using LinearAlgebra: I, norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -103,11 +103,6 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; # indices. @test inds(bratensor(qf, 2)) == inds(conj_bratensor(qf, 2)) - # `indmap` conjugates an index and renames it according to the name map. - ind = only(i for i in inds(kettensor(qf, 2)) if name(i) == lname) - @test name(indmap(qf, ind)) == braname(qf, name(ind)) - @test indmap(qf, ind) == setname(conj(ind), braname(qf, name(ind))) - # Querying the name map with an index name absent from the ket layer errors. @test_throws ErrorException braname(qf, name(Index(2))) end From 27d6a6b6ea49aa7ff7a400e5559c90ecec105b43 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 11:11:40 -0400 Subject: [PATCH 13/28] Add `nametype` to the bilinear-form network interface `AbstractBilinearFormNetwork{T, V, I}` carries the index name type `I`; `nametype` returns it, as it does for `ITensorNetwork`. Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/abstractbilinearformnetwork.jl | 10 +++++++++- test/test_normnetwork.jl | 3 ++- test/test_quadraticformnetwork.jl | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index 4d4f73f0..d42754a3 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -1,7 +1,7 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned using Dictionaries: Dictionaries, Dictionary, isinsertable, issettable using Graphs: Graphs, edges, vertices -using ITensorBase: conj, rename +using ITensorBase: ITensorBase, conj, nametype, rename using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex """ @@ -45,6 +45,14 @@ Dictionaries.isinsertable(::AbstractBilinearFormNetwork) = false # ====================================== interface ======================================= # +# The name type of the indices, the same as the ket network's. +function ITensorBase.nametype( + ::Type{<:AbstractBilinearFormNetwork{T, V, I}} + ) where {T, V, I} + return I +end +ITensorBase.nametype(bn::AbstractBilinearFormNetwork) = nametype(typeof(bn)) + """ braname(bn::AbstractBilinearFormNetwork, name) diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 41642940..062c01af 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,7 +1,7 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, IndexName, inds, name, uniquename +using ITensorBase: ITensor, Index, IndexName, inds, name, nametype, uniquename using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, branetwork, bratensor, conj_bratensor, contraction_tree, flatten_network, ketnetwork, kettensor, normnetwork, prod_tensors, tensornetwork @@ -44,6 +44,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] # `eltype` is defined. @test_throws ErrorException nn[1] @test_throws ErrorException eltype(nn) + @test nametype(nn) === nametype(tn) === IndexName # Vertex data is assigned wherever the underlying network is. @test is_vertex_assigned(nn, 1) diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index ba968ed3..b3e43f08 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -1,8 +1,8 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, operator, - outputnames, rename, uniquename +using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, nametype, + operator, outputnames, rename, uniquename using ITensorNetworksNext: BraView, ITensorNetwork, NormNetwork, QuadraticFormNetwork, braname, branetwork, bratensor, conj_bratensor, contract_network, ketnetwork, kettensor, operatornetwork, operatortensor, quadraticformnetwork, tensornetwork @@ -46,6 +46,7 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; # `quadraticformnetwork` is the public constructor and agrees with the type. @test quadraticformnetwork(tn, op) isa QuadraticFormNetwork @test qf isa QuadraticFormNetwork + @test nametype(qf) === nametype(tn) === IndexName # The quadratic form shares the graph structure of the ket layer. @test issetequal(vertices(qf), vertices(tn)) From 736c0ea5720e57c19df295a66eb64e7bdf187ce8 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 11:12:22 -0400 Subject: [PATCH 14/28] Reject a `QuadraticFormNetwork` operator input name absent from the ket layer Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/quadraticformnetwork.jl | 3 +++ test/test_quadraticformnetwork.jl | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index c26b5cbe..fcb2256f 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -27,6 +27,9 @@ struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: if !issetequal(vertices(operator), vertices(ket)) error("the operator layer must be defined on every vertex of the ket layer.") end + if !issubset(inputnames(operator), keys(ket.dimname_vertices)) + error("every operator input name must be an index name of the ket layer.") + end braname = select_branames(ket, map, Set{I}(inputnames(operator))) return new{T, V, I, typeof(operator)}(ket, operator, braname) end diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index b3e43f08..83637553 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -68,6 +68,16 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; @test_throws ErrorException QuadraticFormNetwork(tn, identity_operator(g4, s4)) end + @testset "operator input outside the ket network" begin + g = named_path_graph(2) + tn, l, s = random_state(Float64, g) + stray = Index(2) + out1, out2 = Index(2), Index(2) + optn = ITensorNetwork(Dict(1 => randn((out1, stray)), 2 => randn((out2, s[2])))) + op = operator(optn, [name(out1), name(out2)], [name(stray), name(s[2])]) + @test_throws ErrorException QuadraticFormNetwork(tn, op) + end + @testset "layer tensors and the name map" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) From ee408abd86f2f0b12dc49d90947e0246daf2d26b Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 11:25:31 -0400 Subject: [PATCH 15/28] Add `factor_tensors` for `QuadraticFormNetwork`; make its indexing an error A `QuadraticFormNetwork`'s factor at a vertex is its ket, operator and bra layers as separate operands. Indexing and `eltype` now throw, as they do for `NormNetwork`, since the lazy triple-layer product they returned cannot be contracted by `prod_tensors`. Co-Authored-By: Claude Opus 5.5 --- src/beliefpropagation/beliefpropagation.jl | 6 +++++ src/bilinearforms/quadraticformnetwork.jl | 16 ++++++----- test/test_quadraticformnetwork.jl | 31 +++++++++++++++------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index df39650a..fbb8f625 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -246,6 +246,12 @@ factor_tensors(factors, vertex) = [factors[vertex]] function factor_tensors(factors::NormNetwork, vertex) return [kettensor(factors, vertex), bratensor(factors, vertex)] end +function factor_tensors(factors::QuadraticFormNetwork, vertex) + return [ + kettensor(factors, vertex), operatortensor(factors, vertex), + bratensor(factors, vertex), + ] +end # Contract the incoming messages into the source factor to form the (unnormalized) new message on # `edge`. diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index fcb2256f..8a933f2f 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,5 +1,5 @@ using Dictionaries: Dictionary -using ITensorBase: LazyNamedTensor, inputnames, lazy, outputnames, rename, state, uniquename +using ITensorBase: inputnames, outputnames, rename, state, uniquename using ITensorNetworksNext """ @@ -35,7 +35,11 @@ struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: end end -Base.eltype(::Type{<:QuadraticFormNetwork{T, V, I}}) where {T, V, I} = LazyNamedTensor{I, T} +function Base.eltype(::Type{<:QuadraticFormNetwork}) + return error( + "`eltype` of a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and `bratensor` to reach the individual layers." + ) +end function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetworkOperator) return QuadraticFormNetwork(ket, operator, map(uniquename, keys(ket.dimname_vertices))) @@ -44,11 +48,9 @@ end # ==================================== DataGraphs.jl ===================================== # function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) - A = kettensor(qf, vertex) - O = operatortensor(qf, vertex) - B = conj_bratensor(qf, vertex) - # TODO: implement and use a lazy `conj` via `LazyNamedDimsArrays` here? - return lazy(A) * lazy(O) * lazy(conj(B)) + return error( + "Indexing a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and `bratensor` to reach the individual layers." + ) end function DataGraphs.is_vertex_assigned(qf::QuadraticFormNetwork, vertex) diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index 83637553..03f07316 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -3,9 +3,10 @@ using Dictionaries: isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, nametype, operator, outputnames, rename, uniquename -using ITensorNetworksNext: BraView, ITensorNetwork, NormNetwork, QuadraticFormNetwork, - braname, branetwork, bratensor, conj_bratensor, contract_network, ketnetwork, kettensor, - operatornetwork, operatortensor, quadraticformnetwork, tensornetwork +using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, + QuadraticFormNetwork, braname, branetwork, bratensor, conj_bratensor, factor_tensors, + ketnetwork, kettensor, operatornetwork, operatortensor, prod_tensors, + quadraticformnetwork, tensornetwork using LinearAlgebra: I, norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -36,6 +37,10 @@ end identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; d) +function contract(bn) + return prod_tensors(reduce(vcat, factor_tensors(bn, v) for v in vertices(bn)), Greedy()) +end + @testset "`QuadraticFormNetwork`" begin @testset "Basics" begin g = named_path_graph(3) @@ -52,8 +57,10 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; @test issetequal(vertices(qf), vertices(tn)) @test issetequal(edges(qf), edges(tn)) - # `eltype` is the type of the (lazy triple-layer) vertex data. - @test eltype(qf) === typeof(qf[1]) + # The triple-layer tensor at a vertex has no representation, so neither indexing nor + # `eltype` is defined. + @test_throws ErrorException qf[1] + @test_throws ErrorException eltype(qf) # Vertex data is assigned wherever both layers are. @test is_vertex_assigned(qf, 1) @@ -116,6 +123,10 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; # Querying the name map with an index name absent from the ket layer errors. @test_throws ErrorException braname(qf, name(Index(2))) + + # `factor_tensors` returns the three layers as separate operands. + @test factor_tensors(qf, 2) == + [kettensor(qf, 2), operatortensor(qf, 2), bratensor(qf, 2)] end @testset "custom name map" begin @@ -164,8 +175,8 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; tn, l, s = random_state(Float64, g) op = identity_operator(g, s) - @test contract_network(QuadraticFormNetwork(tn, op))[] ≈ - contract_network(NormNetwork(tn))[] + @test contract(QuadraticFormNetwork(tn, op))[] ≈ + contract(NormNetwork(tn))[] end @testset "$T" for T in (Float64, ComplexF64) @@ -186,7 +197,7 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; gate = ITensor(mats[v], (out, s[v])) ket = rename(gate * ket, name(out) => name(s[v])) end - @test contract_network(qf)[] ≈ (conj(psi) * ket)[] + @test contract(qf)[] ≈ (conj(psi) * ket)[] end @testset "scaled identity" begin @@ -195,8 +206,8 @@ identity_operator(g, s; d = 2) = product_operator(v -> Matrix(1.0I, d, d), g, s; op = product_operator(v -> 2.0 * Matrix(1.0I, 2, 2), g, s) # A factor of 2 at each of the three vertices scales the norm by 2³. - @test contract_network(QuadraticFormNetwork(tn, op))[] ≈ - 8 * contract_network(NormNetwork(tn))[] + @test contract(QuadraticFormNetwork(tn, op))[] ≈ + 8 * contract(NormNetwork(tn))[] end end end From 6624d4d5c5fff15887a66c4c7873fa7c255f64d5 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Tue, 6 Oct 2026 15:22:33 -0400 Subject: [PATCH 16/28] Read vertex indices through `inds(tn, v)`; `BraView` overloads it `inds(tn, v)` and `names(tn, v)` give the indices and names of the tensor at vertex `v`, defaulting to `inds(tn[v])`. The generic link and site functions, `dimnamevertices`, `has_dimname` and `insertlink!` read vertex metadata through them instead of indexing the vertex. Indexing a `BraView` builds the bra tensor through `bratensor`, whose `conj` broadcast allocates a conjugated copy of the ket tensor; `BraView` now overloads `inds(tn, v)` with the conjugated indices of `conj_bratensor`, which only renames. This removes the tensor-sized copies from `similar_message_environment` and from the bra names read after every bilinear-form message update. Co-Authored-By: Claude Opus 5.5 --- src/ITensorNetworkGenerators/ising_network.jl | 4 +-- src/abstracttensornetwork.jl | 31 ++++++++++--------- .../abstractbilinearformnetwork.jl | 8 ++++- src/tensornetwork.jl | 2 +- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/ITensorNetworkGenerators/ising_network.jl b/src/ITensorNetworkGenerators/ising_network.jl index 702b8787..e2ebe8b3 100644 --- a/src/ITensorNetworkGenerators/ising_network.jl +++ b/src/ITensorNetworkGenerators/ising_network.jl @@ -1,6 +1,6 @@ using ..ITensorNetworksNext using Graphs: degree, dst, edges, src -using ITensorBase: NamedTensor, name, uniquename +using ITensorBase: NamedTensor, inds, name, uniquename using LinearAlgebra: Diagonal, eigen using NamedGraphs: vertextype @@ -33,7 +33,7 @@ function ising_network( fp(e) = get(() -> l̃[reverse(e)], l̃, e) tn = delta_network(fp, elt, g) for v in sz_vertices - tn[v] = diagonaltensor(elt[1, -1], axes(tn[v])) + tn[v] = diagonaltensor(elt[1, -1], Tuple(inds(tn, v))) end for e in edges(g) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index b909a1da..8d0ce9a3 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -3,8 +3,8 @@ using DataGraphs: DataGraphs, AbstractDataGraph, AbstractVertexDataGraph, edge_d set_vertex_data!, underlying_graph, underlying_graph_type, vertex_data using Graphs: Graphs, AbstractEdge, AbstractGraph, add_edge!, add_vertex!, dst, edges, edgetype, ne, neighbors, nv, rem_edge!, src, vertices -using ITensorBase: ITensorOperator, NamedUnitRange, inds, inputnames, name, names, nametype, - prime, uniquename, unnamedtype +using ITensorBase: ITensorBase, ITensorOperator, NamedUnitRange, inds, inputnames, name, names, + nametype, prime, uniquename, unnamedtype using LinearAlgebra: LinearAlgebra using MacroTools: @capture using NamedGraphs: @@ -37,13 +37,16 @@ end # ====================================== interface ======================================= # +ITensorBase.inds(tn::AbstractGraph, v) = inds(tn[v]) +ITensorBase.names(tn::AbstractGraph, v) = name.(inds(tn, v)) + linkinds(tn::AbstractGraph, edge::Pair) = linkinds(tn, edgetype(tn)(edge)) # Pick the link indices from the `src` side, identified by name match with `dst`. # A range-strict intersection (`inds(src) ∩ inds(dst)`) would drop graded links # whose two endpoints carry dual-related ranges. function linkinds(tn::AbstractGraph, edge::AbstractEdge) ln = linknames(tn, edge) - return [i for i in inds(tn[src(edge)]) if name(i) in ln] + return [i for i in inds(tn, src(edge)) if name(i) in ln] end function linkaxes(tn::AbstractGraph, edge::Pair) @@ -51,33 +54,33 @@ function linkaxes(tn::AbstractGraph, edge::Pair) end function linkaxes(tn::AbstractGraph, edge::AbstractEdge) ln = linknames(tn, edge) - return [ax for ax in axes(tn[src(edge)]) if name(ax) in ln] + return [ax for ax in inds(tn, src(edge)) if name(ax) in ln] end function linknames(tn::AbstractGraph, edge::Pair) return linknames(tn, edgetype(tn)(edge)) end function linknames(tn::AbstractGraph, edge::AbstractEdge) - return names(tn[src(edge)]) ∩ names(tn[dst(edge)]) + return names(tn, src(edge)) ∩ names(tn, dst(edge)) end function siteinds(tn::AbstractGraph, v) - s = inds(tn[v]) + s = inds(tn, v) for v′ in neighbors(tn, v) - s = setdiff(s, inds(tn[v′])) + s = setdiff(s, inds(tn, v′)) end return s end function siteaxes(tn::AbstractGraph, v) - s = axes(tn[v]) + s = Tuple(inds(tn, v)) for v′ in neighbors(tn, v) - s = setdiff(s, axes(tn[v′])) + s = setdiff(s, inds(tn, v′)) end return s end function sitenames(tn::AbstractGraph, v) - s = names(tn[v]) + s = names(tn, v) for v′ in neighbors(tn, v) - s = setdiff(s, names(tn[v′])) + s = setdiff(s, names(tn, v′)) end return s end @@ -87,7 +90,7 @@ function dimnamevertices(tn::AbstractGraph, name) sites = vertextype(tn)[] for v in vertices(tn) - if name ∈ names(tn[v]) + if name ∈ names(tn, v) push!(sites, v) end end @@ -97,7 +100,7 @@ end function has_dimname(tn::AbstractGraph, name) for v in vertices(tn) - if name ∈ names(tn[v]) + if name ∈ names(tn, v) return true end end @@ -107,7 +110,7 @@ end has_ind(tn::AbstractGraph, ind) = has_dimname(tn, name(ind)) function insertlink!(tn::AbstractGraph, e) - T = eltype(inds(tn[src(e)])) + T = eltype(inds(tn, src(e))) linkind = NamedUnitRange(trivialrange(unnamedtype(T)), uniquename(nametype(T))) diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index d42754a3..bec25178 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -1,7 +1,7 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned using Dictionaries: Dictionaries, Dictionary, isinsertable, issettable using Graphs: Graphs, edges, vertices -using ITensorBase: ITensorBase, conj, nametype, rename +using ITensorBase: ITensorBase, conj, inds, nametype, rename using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex """ @@ -156,6 +156,12 @@ function DataGraphs.is_vertex_assigned(nnv::BraView, vertex) return is_vertex_assigned(parent(nnv), vertex) end +# ==================================== ITensorBase.jl ==================================== # + +function ITensorBase.inds(nnv::BraView, vertex) + return conj.(inds(conj_bratensor(parent(nnv), vertex))) +end + # ====================================== Graphs.jl ======================================= # Graphs.edges(nnv::BraView) = edges(parent(nnv)) diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 57ad5b2b..3efde05a 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -146,7 +146,7 @@ function DataGraphs.set_vertex_data!(tn::ITensorNetwork, tensor, vertex) end function update_tensornetwork_metadata!(tn, vertex, tensor) - oldnames = isassigned(tn, vertex) ? names(tn[vertex]) : Set{nametype(tn)}() + oldnames = isassigned(tn, vertex) ? names(tn, vertex) : Set{nametype(tn)}() newnames = names(tensor) update_tensornetwork_metadata!(tn, vertex, oldnames, newnames) From aff3d916665b5b62802f18dc635c37b25b0bea86 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 12:08:09 -0400 Subject: [PATCH 17/28] Return a `Set` from both `dimnamevertices` methods The generic method returned a `Vector` in vertex order, while the `ITensorNetwork` method returned the network's stored `Set` itself, so a caller mutating the result changed `tn.dimname_vertices`. Both now return a new `Set{vertextype(tn)}`. Co-Authored-By: Claude Opus 5.5 --- src/abstracttensornetwork.jl | 2 +- src/tensornetwork.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 8d0ce9a3..0c78c571 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -87,7 +87,7 @@ end # Return the non-link vertices associated with an dim name function dimnamevertices(tn::AbstractGraph, name) - sites = vertextype(tn)[] + sites = Set{vertextype(tn)}() for v in vertices(tn) if name ∈ names(tn, v) diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 3efde05a..837cc928 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -197,7 +197,7 @@ Graphs.add_edge!(::ITensorNetwork, _edge) = false # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. function dimnamevertices(tn::ITensorNetwork, name) - return get(tn.dimname_vertices, name, Set{vertextype(tn)}()) + return copy(get(tn.dimname_vertices, name, Set{vertextype(tn)}())) end # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. From b78ea291998ebd692061f0ec0d851742b822e44b Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 13:42:08 -0400 Subject: [PATCH 18/28] Define `inds(tn, v)` and `names(tn, v)` on `AbstractITensorNetwork` Defining them on Graphs' `AbstractGraph` added methods of an ITensorBase function to a type neither package owns, which Aqua's piracy check rejects. Every caller passes an `AbstractITensorNetwork`. Co-Authored-By: Claude Opus 5.5 --- src/abstracttensornetwork.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 0c78c571..2eb1f7e4 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -37,8 +37,8 @@ end # ====================================== interface ======================================= # -ITensorBase.inds(tn::AbstractGraph, v) = inds(tn[v]) -ITensorBase.names(tn::AbstractGraph, v) = name.(inds(tn, v)) +ITensorBase.inds(tn::AbstractITensorNetwork, v) = inds(tn[v]) +ITensorBase.names(tn::AbstractITensorNetwork, v) = name.(inds(tn, v)) linkinds(tn::AbstractGraph, edge::Pair) = linkinds(tn, edgetype(tn)(edge)) # Pick the link indices from the `src` side, identified by name match with `dst`. From 7ce328211db4298ecf58ffe655de8819d140f5e2 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 8 Oct 2026 15:26:05 -0400 Subject: [PATCH 19/28] Replace `select_branames` with `linknames(tn)` and `getindices` `select_branames` both chose the names that get a separate bra name and restricted `map` to them. `linknames(tn)` now gives the link names of a whole network, with a fast method for `ITensorNetwork`, and the constructors restrict `map` with Dictionaries' `getindices`: `NormNetwork` to the link names, and `QuadraticFormNetwork` to the link names and the operator's input names. Co-Authored-By: Claude Opus 5.5 --- src/abstracttensornetwork.jl | 3 +++ src/bilinearforms/abstractbilinearformnetwork.jl | 13 +------------ src/bilinearforms/normnetwork.jl | 4 ++-- src/bilinearforms/quadraticformnetwork.jl | 4 ++-- src/tensornetwork.jl | 5 +++++ test/test_tensornetwork.jl | 6 ++++-- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 2eb1f7e4..33cd9a1f 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -62,6 +62,9 @@ end function linknames(tn::AbstractGraph, edge::AbstractEdge) return names(tn, src(edge)) ∩ names(tn, dst(edge)) end +function linknames(tn::AbstractGraph) + return unique(Iterators.flatten(linknames(tn, e) for e in edges(tn))) +end function siteinds(tn::AbstractGraph, v) s = inds(tn, v) diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index bec25178..43bff26b 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -1,5 +1,5 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned -using Dictionaries: Dictionaries, Dictionary, isinsertable, issettable +using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices using ITensorBase: ITensorBase, conj, inds, nametype, rename using NamedGraphs: NamedGraphs, decoded_vertex, encoded_graph, encoded_vertex @@ -74,17 +74,6 @@ The ket→bra name map, holding a bra name for each ket index name that has a se """ function branamemap end -# A link name, or a name in `acted`, gets its bra name from `map`; every other name has none. -function select_branames(ket::ITensorNetwork{T, V, I}, map, acted) where {T, V, I} - braname = Dictionary{I, I}() - for (name, vertices) in pairs(ket.dimname_vertices) - if length(vertices) == 2 || name in acted - insert!(braname, name, map[name]) - end - end - return braname -end - """ kettensor(bn::AbstractBilinearFormNetwork, vertex) diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index ad70a6f8..553686f0 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -1,4 +1,4 @@ -using Dictionaries: Dictionary, dictionary +using Dictionaries: Dictionary, Indices, dictionary, getindices using ITensorBase: similar_operator, uniquename using ITensorNetworksNext @@ -16,7 +16,7 @@ struct NormNetwork{T, V, I} <: AbstractBilinearFormNetwork{T, V, I} ket::ITensorNetwork{T, V, I}, map::Dictionary{I, I} ) where {T, V, I} - return new{T, V, I}(ket, select_branames(ket, map, ())) + return new{T, V, I}(ket, getindices(map, Indices{I}(linknames(ket)))) end end diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 8a933f2f..f1c909fd 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,4 +1,4 @@ -using Dictionaries: Dictionary +using Dictionaries: Dictionary, Indices, getindices using ITensorBase: inputnames, outputnames, rename, state, uniquename using ITensorNetworksNext @@ -30,7 +30,7 @@ struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: if !issubset(inputnames(operator), keys(ket.dimname_vertices)) error("every operator input name must be an index name of the ket layer.") end - braname = select_branames(ket, map, Set{I}(inputnames(operator))) + braname = getindices(map, Indices{I}(union(linknames(ket), inputnames(operator)))) return new{T, V, I, typeof(operator)}(ket, operator, braname) end end diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 837cc928..2ec03229 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -203,6 +203,11 @@ end # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. has_dimname(tn::ITensorNetwork, name) = haskey(tn.dimname_vertices, name) +# PERF: fast lookup compared to `AbstractITensorNetwork` fallback. +function linknames(tn::ITensorNetwork) + return [name for (name, vertices) in pairs(tn.dimname_vertices) if length(vertices) == 2] +end + function NamedGraphs.similar_graph( T::Type{<:ITensorNetwork}, vertices = vertextype(T)[] diff --git a/test/test_tensornetwork.jl b/test/test_tensornetwork.jl index 0beaad01..7a2bee94 100644 --- a/test/test_tensornetwork.jl +++ b/test/test_tensornetwork.jl @@ -1,7 +1,7 @@ using DataGraphs: DataGraph, assigned_edge_data, assigned_vertex_data, underlying_graph, vertex_data -using Graphs: add_edge!, add_vertex!, dst, edges, edgetype, has_edge, has_vertex, - is_directed, ne, nv, rem_edge!, rem_vertex!, src, vertices +using Graphs: AbstractGraph, add_edge!, add_vertex!, dst, edges, edgetype, has_edge, + has_vertex, is_directed, ne, nv, rem_edge!, rem_vertex!, src, vertices using ITensorBase: Index, inds, operator using ITensorNetworksNext: ITensorNetwork, has_ind, linkaxes, linkinds, linknames, operator_support, siteaxes, siteinds, sitenames, tensornetwork @@ -119,6 +119,8 @@ using Test: @test, @test_throws, @testset @test linknames(tn, 1 => 2) == [l[E(1 => 2)].name] @test linknames(tn, E(1 => 2)) == [l[E(1 => 2)].name] + @test issetequal(linknames(tn), [l[e].name for e in edges(g)]) + @test issetequal(invoke(linknames, Tuple{AbstractGraph}, tn), linknames(tn)) @test siteinds(tn, 1) == [s[1]] @test siteaxes(tn, 2) == [s[2]] From b77215efd8740c18aa0aec55ebee3a721fc004f9 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 12 Oct 2026 13:31:00 -0400 Subject: [PATCH 20/28] Give a separate bra name to every mapped index of the ket layer `NormNetwork` and `QuadraticFormNetwork` kept bra names only for the links, so on a network that is part of a larger one, a bond to a vertex outside it kept its ket name in the bra layer. Message updates then left that bond's legs uncontracted. Any index name of the ket layer in the map now gets its own bra name; the one-argument constructors map only the links (and operator inputs). Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/normnetwork.jl | 5 +++-- src/bilinearforms/quadraticformnetwork.jl | 9 +++++++-- test/test_normnetwork.jl | 24 +++++++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index 553686f0..344adbb4 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -16,7 +16,8 @@ struct NormNetwork{T, V, I} <: AbstractBilinearFormNetwork{T, V, I} ket::ITensorNetwork{T, V, I}, map::Dictionary{I, I} ) where {T, V, I} - return new{T, V, I}(ket, getindices(map, Indices{I}(linknames(ket)))) + names_bra = union(linknames(ket), filter(in(keys(map)), keys(ket.dimname_vertices))) + return new{T, V, I}(ket, getindices(map, Indices{I}(names_bra))) end end @@ -27,7 +28,7 @@ function Base.eltype(::Type{<:NormNetwork}) end function NormNetwork(tn::ITensorNetwork) - return NormNetwork(tn, map(uniquename, keys(tn.dimname_vertices))) + return NormNetwork(tn, map(uniquename, Indices(linknames(tn)))) end # ==================================== DataGraphs.jl ===================================== # diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index f1c909fd..5835fe1a 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -30,7 +30,11 @@ struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: if !issubset(inputnames(operator), keys(ket.dimname_vertices)) error("every operator input name must be an index name of the ket layer.") end - braname = getindices(map, Indices{I}(union(linknames(ket), inputnames(operator)))) + names_bra = union( + linknames(ket), inputnames(operator), + filter(in(keys(map)), keys(ket.dimname_vertices)) + ) + braname = getindices(map, Indices{I}(names_bra)) return new{T, V, I, typeof(operator)}(ket, operator, braname) end end @@ -42,7 +46,8 @@ function Base.eltype(::Type{<:QuadraticFormNetwork}) end function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetworkOperator) - return QuadraticFormNetwork(ket, operator, map(uniquename, keys(ket.dimname_vertices))) + names_bra = Indices(union(linknames(ket), inputnames(operator))) + return QuadraticFormNetwork(ket, operator, map(uniquename, names_bra)) end # ==================================== DataGraphs.jl ===================================== # diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 062c01af..da61e420 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,10 +1,10 @@ using DataGraphs: is_vertex_assigned -using Dictionaries: isinsertable, issettable +using Dictionaries: Indices, isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, inds, name, nametype, uniquename using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, branetwork, bratensor, conj_bratensor, contraction_tree, flatten_network, ketnetwork, - kettensor, normnetwork, prod_tensors, tensornetwork + kettensor, linknames, normnetwork, prod_tensors, tensornetwork using LinearAlgebra: norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -89,7 +89,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] tn, l, s = random_state(Float64, g) # A user-supplied map dictates the bra-layer name for each link. - custom = map(uniquename, keys(tn.dimname_vertices)) + custom = map(uniquename, Indices(linknames(tn))) nn = normnetwork(tn, custom) lname = name(l[NamedEdge(1 => 2)]) @@ -97,6 +97,22 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test braname(nn, lname) in name.(inds(conj_bratensor(nn, 2))) end + @testset "bond to a vertex outside the network" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + + # The part of `tn` on vertices 1 and 2, as one rank of a distributed network holds it. + part = ITensorNetwork(Dict(v => tn[v] for v in (1, 2))) + nn = normnetwork(part, map(uniquename, Indices(linknames(tn)))) + + # The bond to vertex 3 is not a link of `part`, but it is in the map, so it gets its + # own bra name and the two layers at vertex 2 share only the site index. + lname = name(l[NamedEdge(2 => 3)]) + @test braname(nn, lname) != lname + shared = intersect(name.(inds(kettensor(nn, 2))), name.(inds(conj_bratensor(nn, 2)))) + @test issetequal(shared, [name(s[2])]) + end + @testset "`ketnetwork` / `branetwork`" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) @@ -170,7 +186,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test contract_norm(NormNetwork(tn)) ≈ 1 # The contracted norm does not depend on the chosen bra-layer name map. - custom = map(uniquename, keys(tn.dimname_vertices)) + custom = map(uniquename, Indices(linknames(tn))) @test contract_norm(normnetwork(tn, custom)) ≈ contract_norm(NormNetwork(tn)) end end From efe6d1d497f74cb06d476e8f341dfb2ff698f328 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 15 Oct 2026 14:03:55 -0400 Subject: [PATCH 21/28] Derive default bra names with `derivedname` and validate the name map Add `derivedname(name, key)`, which replaces the UUID of an `IndexName` with `uuid5(uuid(name), string(key))`, so every process derives the same bra name from the same ket name. `NormNetwork([f], tn, [linknames])` and `QuadraticFormNetwork([f], ket, op, [linknames])` map each link name (and, for the quadratic form, each operator input name) to `f(name)`, with `f` defaulting to `name -> derivedname(name, "bra")`. The inner constructors store the given `Dictionary` as is and `check_input` throws if two bra names coincide or a bra name is already an index name of the ket or operator layer. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 + src/bilinearforms/normnetwork.jl | 48 +++++++++---- src/bilinearforms/quadraticformnetwork.jl | 84 ++++++++++++++--------- src/utils.jl | 14 ++++ test/test_normnetwork.jl | 35 +++++++++- test/test_quadraticformnetwork.jl | 19 ++++- 6 files changed, 149 insertions(+), 53 deletions(-) diff --git a/Project.toml b/Project.toml index c614f208..77469d97 100644 --- a/Project.toml +++ b/Project.toml @@ -23,6 +23,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [weakdeps] OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" @@ -48,4 +49,5 @@ Random = "1.10" SimpleTraits = "0.9.5" SplitApplyCombine = "1.2.3" TensorAlgebra = "0.23.1" +UUIDs = "1.10" julia = "1.10" diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index 344adbb4..bfa35cc1 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -1,5 +1,5 @@ -using Dictionaries: Dictionary, Indices, dictionary, getindices -using ITensorBase: similar_operator, uniquename +using Dictionaries: Dictionary, dictionary +using ITensorBase: similar_operator using ITensorNetworksNext """ @@ -16,21 +16,38 @@ struct NormNetwork{T, V, I} <: AbstractBilinearFormNetwork{T, V, I} ket::ITensorNetwork{T, V, I}, map::Dictionary{I, I} ) where {T, V, I} - names_bra = union(linknames(ket), filter(in(keys(map)), keys(ket.dimname_vertices))) - return new{T, V, I}(ket, getindices(map, Indices{I}(names_bra))) + check_input(NormNetwork, ket, map) + return new{T, V, I}(ket, map) end end +function NormNetwork( + f::Function, tn::ITensorNetwork, linknames = ITensorNetworksNext.linknames(tn) + ) + return NormNetwork(tn, Dictionary(linknames, map(f, linknames))) +end +function NormNetwork(tn::ITensorNetwork, linknames = ITensorNetworksNext.linknames(tn)) + return NormNetwork(name -> derivedname(name, "bra"), tn, linknames) +end + +function check_input(::Type{<:NormNetwork}, ket, braname) + if !allunique(braname) + error("the bra names of a bilinear-form network must be distinct.") + end + for name in braname + if has_dimname(ket, name) + error("bra name $name is already an index name of another layer.") + end + end + return nothing +end + function Base.eltype(::Type{<:NormNetwork}) return error( "`eltype` of a `NormNetwork` is not defined, since the double-layer tensor at a vertex has no representation of its own. Use `kettensor` and `bratensor` to reach the individual layers." ) end -function NormNetwork(tn::ITensorNetwork) - return NormNetwork(tn, map(uniquename, Indices(linknames(tn)))) -end - # ==================================== DataGraphs.jl ===================================== # function DataGraphs.get_vertex_data(nn::NormNetwork, vertex) @@ -63,12 +80,13 @@ function flatten_network(nn::NormNetwork) end """ - normnetwork(tn::ITensorNetwork, [braname]) -> NormNetwork + normnetwork([f::Function], tn::ITensorNetwork, [linknames]) -> NormNetwork + normnetwork(tn::ITensorNetwork, braname::Dictionary) -> NormNetwork -Build the double-layer norm network `⟨tn|tn⟩`, represented lazily as a `NomnNetwork` object. -The optional second argument `braname` should implement `braname[ketdimname] = bradimname` for -every link dimension name `ketdimname` in `tn`. If this is not specified, then a name is -generated via the `ITensorBase.uniquename` function. +Build the double-layer norm network `⟨tn|tn⟩`, represented lazily as a `NormNetwork` object. +Each ket index name in `linknames` (by default `linknames(tn)`) gets the bra name `f(name)`, +where `f` defaults to `name -> derivedname(name, "bra")`. Alternatively pass the ket→bra name +map `braname` directly. Either way an error is thrown if two bra names coincide or a bra name +is already an index name of `tn`. """ -normnetwork(tn::ITensorNetwork) = NormNetwork(tn) -normnetwork(tn::ITensorNetwork, braname) = NormNetwork(tn, braname) +normnetwork(args...) = NormNetwork(args...) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 5835fe1a..8ffe82c6 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,5 +1,5 @@ -using Dictionaries: Dictionary, Indices, getindices -using ITensorBase: inputnames, outputnames, rename, state, uniquename +using Dictionaries: Dictionary +using ITensorBase: inputnames, outputnames, rename, state using ITensorNetworksNext """ @@ -24,37 +24,61 @@ struct QuadraticFormNetwork{T, V, I, O <: ITensorNetworkOperator} <: operator::ITensorNetworkOperator, map::Dictionary{I, I} ) where {T, V, I} - if !issetequal(vertices(operator), vertices(ket)) - error("the operator layer must be defined on every vertex of the ket layer.") - end - if !issubset(inputnames(operator), keys(ket.dimname_vertices)) - error("every operator input name must be an index name of the ket layer.") + check_input(QuadraticFormNetwork, ket, operator, map) + return new{T, V, I, typeof(operator)}(ket, operator, map) + end +end + +function QuadraticFormNetwork( + f::Function, + ket::ITensorNetwork, + operator::ITensorNetworkOperator, + linknames = ITensorNetworksNext.linknames(ket) + ) + names = union(linknames, inputnames(operator)) + return QuadraticFormNetwork(ket, operator, Dictionary(names, map(f, names))) +end +function QuadraticFormNetwork( + ket::ITensorNetwork, + operator::ITensorNetworkOperator, + linknames = ITensorNetworksNext.linknames(ket) + ) + return QuadraticFormNetwork(name -> derivedname(name, "bra"), ket, operator, linknames) +end + +function check_input(::Type{<:QuadraticFormNetwork}, ket, operator, braname) + if !issetequal(vertices(operator), vertices(ket)) + error("the operator layer must be defined on every vertex of the ket layer.") + end + if !issubset(inputnames(operator), keys(ket.dimname_vertices)) + error("every operator input name must be an index name of the ket layer.") + end + if !allunique(braname) + error("the bra names of a bilinear-form network must be distinct.") + end + for name in braname + if has_dimname(ket, name) || has_dimname(state(operator), name) + error("bra name $name is already an index name of another layer.") end - names_bra = union( - linknames(ket), inputnames(operator), - filter(in(keys(map)), keys(ket.dimname_vertices)) - ) - braname = getindices(map, Indices{I}(names_bra)) - return new{T, V, I, typeof(operator)}(ket, operator, braname) end + return nothing end function Base.eltype(::Type{<:QuadraticFormNetwork}) return error( - "`eltype` of a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and `bratensor` to reach the individual layers." + "`eltype` of a `QuadraticFormNetwork` is not defined, since the triple-layer tensor \\ + at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and \\ + `bratensor` to reach the individual layers." ) end -function QuadraticFormNetwork(ket::ITensorNetwork, operator::ITensorNetworkOperator) - names_bra = Indices(union(linknames(ket), inputnames(operator))) - return QuadraticFormNetwork(ket, operator, map(uniquename, names_bra)) -end - # ==================================== DataGraphs.jl ===================================== # function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) return error( - "Indexing a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and `bratensor` to reach the individual layers." + "Indexing a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at \\ + a vertex has no representation of its own. Use `kettensor`, `operatortensor` and \\ + `bratensor` to reach the individual layers." ) end @@ -80,17 +104,13 @@ function operatortensor(qf::QuadraticFormNetwork, vertex) end """ - quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator, [braname]) + quadraticformnetwork([f::Function], tn::ITensorNetwork, op::ITensorNetworkOperator, [linknames]) + quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator, braname::Dictionary) -Build the triple-layer network `⟨tn|op|tn⟩`, represented lazily as a -`QuadraticFormNetwork` object. The optional third argument `braname` should implement -`braname[ketdimname] = bradimname` for every link dimension name `ketdimname` in `tn` and -every dimension name of `tn` the operator acts on. If this is not specified, then a name is -generated via the `ITensorBase.uniquename` function. +Build the triple-layer network `⟨tn|op|tn⟩`, represented lazily as a `QuadraticFormNetwork` +object. Each ket index name in `linknames` (by default `linknames(tn)`) and each input name of +`op` gets the bra name `f(name)`, where `f` defaults to `name -> derivedname(name, "bra")`. +Alternatively pass the ket→bra name map `braname` directly. Either way an error is thrown if +two bra names coincide or a bra name is already an index name of `tn` or `op`. """ -function quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator) - return QuadraticFormNetwork(tn, op) -end -function quadraticformnetwork(tn::ITensorNetwork, op::ITensorNetworkOperator, braname) - return QuadraticFormNetwork(tn, op, braname) -end +quadraticformnetwork(args...) = QuadraticFormNetwork(args...) diff --git a/src/utils.jl b/src/utils.jl index abf666e4..0d5dc01f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,6 +1,20 @@ using Dictionaries: AbstractIndices, Dictionary +using ITensorBase: ITensorBase, IndexName +using UUIDs: uuid5 # `map` does something clever to figure out the element type of the output when not # inferable, but the `map` overload on `Dictionary` does not, so we fix this here. narrow_map(f, v) = map(f, v) narrow_map(f, v::AbstractIndices) = Dictionary(v, [f(x) for x in v]) + +""" + derivedname(name::IndexName, key) -> IndexName + +Return a name derived deterministically from `name` and `key`: the identifier is replaced by +the name-based UUID `uuid5(uuid(name), string(key))`, and the tags and prime level are kept. +Every process derives the same name from the same `name` and `key`, and the result differs +from `name` and from any independently minted name. +""" +function derivedname(name::IndexName, key) + return ITensorBase.setuuid(name, uuid5(ITensorBase.uuid(name), string(key))) +end diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index da61e420..ad80dc04 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,10 +1,11 @@ using DataGraphs: is_vertex_assigned using Dictionaries: Indices, isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, IndexName, inds, name, nametype, uniquename +using ITensorBase: ITensor, Index, IndexName, inds, name, nametype, plev, prime, setname, + tags, uniquename using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, - branetwork, bratensor, conj_bratensor, contraction_tree, flatten_network, ketnetwork, - kettensor, linknames, normnetwork, prod_tensors, tensornetwork + branetwork, bratensor, conj_bratensor, contraction_tree, derivedname, flatten_network, + ketnetwork, kettensor, linknames, normnetwork, prod_tensors, tensornetwork using LinearAlgebra: norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -97,6 +98,34 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test braname(nn, lname) in name.(inds(conj_bratensor(nn, 2))) end + @testset "`derivedname`" begin + n = prime(name(Index(2; tags = "Link"))) + + # The derived name depends only on the name and key, and keeps the decoration. + @test derivedname(n, :bra) == derivedname(n, :bra) + @test derivedname(n, :bra) != n + @test derivedname(n, :bra) != derivedname(n, :other) + @test derivedname(n, :bra) != prime(n) + @test tags(derivedname(n, :bra)) == tags(n) + @test plev(derivedname(n, :bra)) == plev(n) + end + + @testset "default and function name maps" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + lname = name(l[NamedEdge(1 => 2)]) + + # Two independently constructed norm networks agree on every bra name. + @test braname(NormNetwork(tn), lname) == derivedname(lname, :bra) + @test braname(normnetwork(tn), lname) == braname(NormNetwork(tn), lname) + @test braname(normnetwork(prime, tn), lname) == prime(lname) + + # A bra name that is already an index name of the network is rejected. + taken = setname(Index(2), derivedname(lname, :bra)) + tn[1] = tn[1] * ITensor(ones(2), (taken,)) + @test_throws ErrorException NormNetwork(tn) + end + @testset "bond to a vertex outside the network" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index 03f07316..87deedd2 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -2,10 +2,10 @@ using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, conj, inds, inputnames, name, names, nametype, - operator, outputnames, rename, uniquename + operator, outputnames, rename, setname, uniquename using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, - QuadraticFormNetwork, braname, branetwork, bratensor, conj_bratensor, factor_tensors, - ketnetwork, kettensor, operatornetwork, operatortensor, prod_tensors, + QuadraticFormNetwork, braname, branetwork, bratensor, conj_bratensor, derivedname, + factor_tensors, ketnetwork, kettensor, operatornetwork, operatortensor, prod_tensors, quadraticformnetwork, tensornetwork using LinearAlgebra: I, norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph @@ -85,6 +85,19 @@ end @test_throws ErrorException QuadraticFormNetwork(tn, op) end + @testset "a default bra name equal to an operator name" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + + # Each operator output takes the name the default map would derive for its input. + out = Dict(v => setname(Index(2), derivedname(name(s[v]), :bra)) for v in vertices(g)) + optn = tensornetwork(v -> ITensor(Matrix(1.0I, 2, 2), (out[v], s[v])), vertices(g)) + vs = collect(vertices(g)) + op = operator(optn, [name(out[v]) for v in vs], [name(s[v]) for v in vs]) + + @test_throws ErrorException QuadraticFormNetwork(tn, op) + end + @testset "layer tensors and the name map" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) From b100b45f15fc45b60a132e20714453c5438165ff Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 15 Oct 2026 15:14:03 -0400 Subject: [PATCH 22/28] Tag a name from `derivedname` with its key `derivedname(name, key)` now adds `key` as a bare-label tag, so a default bra name carries a `bra` tag. Co-Authored-By: Claude Opus 5.5 --- src/utils.jl | 11 ++++++----- test/test_normnetwork.jl | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 0d5dc01f..5113f317 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,5 +1,5 @@ using Dictionaries: AbstractIndices, Dictionary -using ITensorBase: ITensorBase, IndexName +using ITensorBase: ITensorBase, IndexName, settags using UUIDs: uuid5 # `map` does something clever to figure out the element type of the output when not @@ -11,10 +11,11 @@ narrow_map(f, v::AbstractIndices) = Dictionary(v, [f(x) for x in v]) derivedname(name::IndexName, key) -> IndexName Return a name derived deterministically from `name` and `key`: the identifier is replaced by -the name-based UUID `uuid5(uuid(name), string(key))`, and the tags and prime level are kept. -Every process derives the same name from the same `name` and `key`, and the result differs -from `name` and from any independently minted name. +the name-based UUID `uuid5(uuid(name), string(key))`, `key` is added as a bare-label tag, and +the other tags and the prime level are kept. Every process derives the same name from the same +`name` and `key`, and the result differs from `name` and from any independently minted name. """ function derivedname(name::IndexName, key) - return ITensorBase.setuuid(name, uuid5(ITensorBase.uuid(name), string(key))) + derived = ITensorBase.setuuid(name, uuid5(ITensorBase.uuid(name), string(key))) + return settags(derived, string(key)) end diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index ad80dc04..6b446840 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -101,12 +101,13 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "`derivedname`" begin n = prime(name(Index(2; tags = "Link"))) - # The derived name depends only on the name and key, and keeps the decoration. + # The derived name depends only on the name and key, keeps the decoration and is tagged + # with the key. @test derivedname(n, :bra) == derivedname(n, :bra) @test derivedname(n, :bra) != n @test derivedname(n, :bra) != derivedname(n, :other) @test derivedname(n, :bra) != prime(n) - @test tags(derivedname(n, :bra)) == tags(n) + @test tags(derivedname(n, :bra)) == merge(tags(n), Dict("bra" => "")) @test plev(derivedname(n, :bra)) == plev(n) end From dbf3ed3368a231841af42d8f2f1030727592da57 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 15 Oct 2026 15:33:48 -0400 Subject: [PATCH 23/28] Continue the `QuadraticFormNetwork` error messages with `\` instead of a literal `\\` Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/quadraticformnetwork.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 8ffe82c6..f9f3f439 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -66,8 +66,8 @@ end function Base.eltype(::Type{<:QuadraticFormNetwork}) return error( - "`eltype` of a `QuadraticFormNetwork` is not defined, since the triple-layer tensor \\ - at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and \\ + "`eltype` of a `QuadraticFormNetwork` is not defined, since the triple-layer tensor \ + at a vertex has no representation of its own. Use `kettensor`, `operatortensor` and \ `bratensor` to reach the individual layers." ) end @@ -76,8 +76,8 @@ end function DataGraphs.get_vertex_data(qf::QuadraticFormNetwork, vertex) return error( - "Indexing a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at \\ - a vertex has no representation of its own. Use `kettensor`, `operatortensor` and \\ + "Indexing a `QuadraticFormNetwork` is not defined, since the triple-layer tensor at \ + a vertex has no representation of its own. Use `kettensor`, `operatortensor` and \ `bratensor` to reach the individual layers." ) end From 4149ef56ab333530e4fe07b9a352b225b0cd9a53 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 15 Oct 2026 15:33:48 -0400 Subject: [PATCH 24/28] Require a bra name for every link in `NormNetwork` and `QuadraticFormNetwork` `efe6d1d` stored the name map as given, so a map without a link was accepted and `braname` returned the ket name for it. `check_input` now throws if a link name, or for a quadratic form an operator input name, is not a key of the map; other keys are allowed. The checks shared by the two constructors move into `check_branamemap`. Co-Authored-By: Claude Opus 5.5 --- .../abstractbilinearformnetwork.jl | 17 +++++++++++++++++ src/bilinearforms/normnetwork.jl | 15 ++++----------- src/bilinearforms/quadraticformnetwork.jl | 17 +++++++---------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/bilinearforms/abstractbilinearformnetwork.jl b/src/bilinearforms/abstractbilinearformnetwork.jl index 43bff26b..0d4441b0 100644 --- a/src/bilinearforms/abstractbilinearformnetwork.jl +++ b/src/bilinearforms/abstractbilinearformnetwork.jl @@ -17,6 +17,23 @@ where it has an operator layer, [`operatornetwork`](@ref) and [`operatortensor`] """ abstract type AbstractBilinearFormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} end +# The checks on a ket→bra name map shared by the bilinear-form constructors; `layers` are the +# networks other than `ket` whose index names a bra name must not reuse. +function check_branamemap(ket, braname, layers...) + if !issubset(linknames(ket), keys(braname)) + error("every link name of the ket layer must have a bra name.") + end + if !allunique(braname) + error("the bra names of a bilinear-form network must be distinct.") + end + for name in braname + if has_dimname(ket, name) || any(layer -> has_dimname(layer, name), layers) + error("bra name $name is already an index name of another layer.") + end + end + return nothing +end + # ====================================== Graphs.jl ======================================= # Graphs.edges(bn::AbstractBilinearFormNetwork) = edges(ketnetwork(bn)) diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index bfa35cc1..391fe859 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -31,15 +31,7 @@ function NormNetwork(tn::ITensorNetwork, linknames = ITensorNetworksNext.linknam end function check_input(::Type{<:NormNetwork}, ket, braname) - if !allunique(braname) - error("the bra names of a bilinear-form network must be distinct.") - end - for name in braname - if has_dimname(ket, name) - error("bra name $name is already an index name of another layer.") - end - end - return nothing + return check_branamemap(ket, braname) end function Base.eltype(::Type{<:NormNetwork}) @@ -86,7 +78,8 @@ end Build the double-layer norm network `⟨tn|tn⟩`, represented lazily as a `NormNetwork` object. Each ket index name in `linknames` (by default `linknames(tn)`) gets the bra name `f(name)`, where `f` defaults to `name -> derivedname(name, "bra")`. Alternatively pass the ket→bra name -map `braname` directly. Either way an error is thrown if two bra names coincide or a bra name -is already an index name of `tn`. +map `braname` directly. Either way the names mapped must include every link name of `tn` and +may include others, and an error is thrown if a link name is missing, two bra names coincide +or a bra name is already an index name of `tn`. """ normnetwork(args...) = NormNetwork(args...) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index f9f3f439..a99793be 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -53,15 +53,10 @@ function check_input(::Type{<:QuadraticFormNetwork}, ket, operator, braname) if !issubset(inputnames(operator), keys(ket.dimname_vertices)) error("every operator input name must be an index name of the ket layer.") end - if !allunique(braname) - error("the bra names of a bilinear-form network must be distinct.") + if !issubset(inputnames(operator), keys(braname)) + error("every operator input name must have a bra name.") end - for name in braname - if has_dimname(ket, name) || has_dimname(state(operator), name) - error("bra name $name is already an index name of another layer.") - end - end - return nothing + return check_branamemap(ket, braname, state(operator)) end function Base.eltype(::Type{<:QuadraticFormNetwork}) @@ -110,7 +105,9 @@ end Build the triple-layer network `⟨tn|op|tn⟩`, represented lazily as a `QuadraticFormNetwork` object. Each ket index name in `linknames` (by default `linknames(tn)`) and each input name of `op` gets the bra name `f(name)`, where `f` defaults to `name -> derivedname(name, "bra")`. -Alternatively pass the ket→bra name map `braname` directly. Either way an error is thrown if -two bra names coincide or a bra name is already an index name of `tn` or `op`. +Alternatively pass the ket→bra name map `braname` directly. Either way the names mapped must +include every link name of `tn` and every input name of `op` and may include others, and an +error is thrown if one is missing, two bra names coincide or a bra name is already an index +name of `tn` or `op`. """ quadraticformnetwork(args...) = QuadraticFormNetwork(args...) From bf38a2628fe76afbf548e99063f6fe1ad0247d11 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 15 Oct 2026 15:35:50 -0400 Subject: [PATCH 25/28] Build the default bra-name maps with `narrow_map` `map(f, ::Indices)` keeps the inferred return type of `f`, so a non-inferable `f` gave a `Dictionary{I, Any}` and no inner constructor method matched. `narrow_map` collects the values with a comprehension, which takes the type of the values returned. Co-Authored-By: Claude Opus 5.5 --- src/bilinearforms/normnetwork.jl | 4 ++-- src/bilinearforms/quadraticformnetwork.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bilinearforms/normnetwork.jl b/src/bilinearforms/normnetwork.jl index 391fe859..8410ae49 100644 --- a/src/bilinearforms/normnetwork.jl +++ b/src/bilinearforms/normnetwork.jl @@ -1,4 +1,4 @@ -using Dictionaries: Dictionary, dictionary +using Dictionaries: Dictionary, Indices, dictionary using ITensorBase: similar_operator using ITensorNetworksNext @@ -24,7 +24,7 @@ end function NormNetwork( f::Function, tn::ITensorNetwork, linknames = ITensorNetworksNext.linknames(tn) ) - return NormNetwork(tn, Dictionary(linknames, map(f, linknames))) + return NormNetwork(tn, narrow_map(f, Indices(linknames))) end function NormNetwork(tn::ITensorNetwork, linknames = ITensorNetworksNext.linknames(tn)) return NormNetwork(name -> derivedname(name, "bra"), tn, linknames) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index a99793be..1cf3a39c 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -1,4 +1,4 @@ -using Dictionaries: Dictionary +using Dictionaries: Dictionary, Indices using ITensorBase: inputnames, outputnames, rename, state using ITensorNetworksNext @@ -36,7 +36,7 @@ function QuadraticFormNetwork( linknames = ITensorNetworksNext.linknames(ket) ) names = union(linknames, inputnames(operator)) - return QuadraticFormNetwork(ket, operator, Dictionary(names, map(f, names))) + return QuadraticFormNetwork(ket, operator, narrow_map(f, Indices(names))) end function QuadraticFormNetwork( ket::ITensorNetwork, From 5eb87cffc2c9f2a7edaf446179282b2b194d6659 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 19 Oct 2026 10:25:59 -0400 Subject: [PATCH 26/28] Formatting. --- src/abstracttensornetwork.jl | 4 ++-- src/tensornetwork.jl | 4 +++- test/test_normnetwork.jl | 7 ++++--- test/test_quadraticformnetwork.jl | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 33cd9a1f..197e0c87 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -3,8 +3,8 @@ using DataGraphs: DataGraphs, AbstractDataGraph, AbstractVertexDataGraph, edge_d set_vertex_data!, underlying_graph, underlying_graph_type, vertex_data using Graphs: Graphs, AbstractEdge, AbstractGraph, add_edge!, add_vertex!, dst, edges, edgetype, ne, neighbors, nv, rem_edge!, src, vertices -using ITensorBase: ITensorBase, ITensorOperator, NamedUnitRange, inds, inputnames, name, names, - nametype, prime, uniquename, unnamedtype +using ITensorBase: ITensorBase, ITensorOperator, NamedUnitRange, inds, inputnames, name, + names, nametype, prime, uniquename, unnamedtype using LinearAlgebra: LinearAlgebra using MacroTools: @capture using NamedGraphs: diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 2ec03229..1c972cb2 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -205,7 +205,9 @@ has_dimname(tn::ITensorNetwork, name) = haskey(tn.dimname_vertices, name) # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. function linknames(tn::ITensorNetwork) - return [name for (name, vertices) in pairs(tn.dimname_vertices) if length(vertices) == 2] + return [ + name for (name, vertices) in pairs(tn.dimname_vertices) if length(vertices) == 2 + ] end function NamedGraphs.similar_graph( diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 6b446840..b5109561 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,8 +1,8 @@ using DataGraphs: is_vertex_assigned using Dictionaries: Indices, isinsertable, issettable using Graphs: edges, vertices -using ITensorBase: ITensor, Index, IndexName, inds, name, nametype, plev, prime, setname, - tags, uniquename +using ITensorBase: + ITensor, Index, IndexName, inds, name, nametype, plev, prime, setname, tags, uniquename using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, branetwork, bratensor, conj_bratensor, contraction_tree, derivedname, flatten_network, ketnetwork, kettensor, linknames, normnetwork, prod_tensors, tensornetwork @@ -139,7 +139,8 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] # own bra name and the two layers at vertex 2 share only the site index. lname = name(l[NamedEdge(2 => 3)]) @test braname(nn, lname) != lname - shared = intersect(name.(inds(kettensor(nn, 2))), name.(inds(conj_bratensor(nn, 2)))) + shared = + intersect(name.(inds(kettensor(nn, 2))), name.(inds(conj_bratensor(nn, 2)))) @test issetequal(shared, [name(s[2])]) end diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index 87deedd2..8f1eb25c 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -90,7 +90,8 @@ end tn, l, s = random_state(Float64, g) # Each operator output takes the name the default map would derive for its input. - out = Dict(v => setname(Index(2), derivedname(name(s[v]), :bra)) for v in vertices(g)) + out = + Dict(v => setname(Index(2), derivedname(name(s[v]), :bra)) for v in vertices(g)) optn = tensornetwork(v -> ITensor(Matrix(1.0I, 2, 2), (out[v], s[v])), vertices(g)) vs = collect(vertices(g)) op = operator(optn, [name(out[v]) for v in vs], [name(s[v]) for v in vs]) From f9b505f103f412f62313f8ab8d872573275535b3 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 19 Oct 2026 10:29:36 -0400 Subject: [PATCH 27/28] Bump version to 0.11.2 Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 77469d97..df9a94f6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorNetworksNext" uuid = "302f2e75-49f0-4526-aef7-d8ba550cb06c" -version = "0.11.1" +version = "0.11.2" authors = ["ITensor developers and contributors"] [workspace] From 608bde6c3bbff198be7ecb6a8ed663192256daa2 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 19 Oct 2026 10:38:52 -0400 Subject: [PATCH 28/28] Minor fixes/improvements. --- src/bilinearforms/quadraticformnetwork.jl | 17 ++++++++++--- src/itensornetworkoperator.jl | 6 +++-- test/test_itensornetworkoperator.jl | 7 +++++ test/test_quadraticformnetwork.jl | 31 +++++++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/bilinearforms/quadraticformnetwork.jl b/src/bilinearforms/quadraticformnetwork.jl index 1cf3a39c..169b0b18 100644 --- a/src/bilinearforms/quadraticformnetwork.jl +++ b/src/bilinearforms/quadraticformnetwork.jl @@ -50,10 +50,21 @@ function check_input(::Type{<:QuadraticFormNetwork}, ket, operator, braname) if !issetequal(vertices(operator), vertices(ket)) error("the operator layer must be defined on every vertex of the ket layer.") end - if !issubset(inputnames(operator), keys(ket.dimname_vertices)) - error("every operator input name must be an index name of the ket layer.") + inputs = inputnames(operator) + for input in inputs + if !has_dimname(ket, input) + error("operator input name $input is not an index name of the ket layer.") + end + if dimnamevertices(ket, input) != dimnamevertices(state(operator), input) + error("operator input name $input must be a ket site index on the same vertex.") + end end - if !issubset(inputnames(operator), keys(braname)) + for vertex in vertices(operator), name in names(state(operator), vertex) + if name ∉ inputs && has_dimname(ket, name) + error("operator index name $name is not an input but is a ket index name.") + end + end + if !issubset(inputs, keys(braname)) error("every operator input name must have a bra name.") end return check_branamemap(ket, braname, state(operator)) diff --git a/src/itensornetworkoperator.jl b/src/itensornetworkoperator.jl index c8b68686..58e3e844 100644 --- a/src/itensornetworkoperator.jl +++ b/src/itensornetworkoperator.jl @@ -35,8 +35,10 @@ struct ITensorNetworkOperator{T, V, I, P <: AbstractITensorNetwork{T, V}} <: ) ) end - if !allunique(outputnames) - throw(ArgumentError("each operator output name must appear only once.")) + if !allunique(vcat(outputnames, inputnames)) + throw( + ArgumentError("each operator output and input name must appear only once.") + ) end for opname in Iterators.flatten((outputnames, inputnames)) nvertices = length(dimnamevertices(parent, opname)) diff --git a/test/test_itensornetworkoperator.jl b/test/test_itensornetworkoperator.jl index ae6eb51c..a2918297 100644 --- a/test/test_itensornetworkoperator.jl +++ b/test/test_itensornetworkoperator.jl @@ -81,6 +81,13 @@ end @test_throws ArgumentError operator( tn, [name(out[vs[1]]), name(out[vs[1]])], [name(inp[vs[1]]), name(inp[vs[1]])] ) + # An input name can be paired with only one output, and no name is both. + @test_throws ArgumentError operator( + tn, [name(out[vs[1]]), name(out[vs[2]])], [name(inp[vs[1]]), name(inp[vs[1]])] + ) + @test_throws ArgumentError operator( + tn, [name(out[vs[1]]), name(inp[vs[1]])], [name(inp[vs[1]]), name(out[vs[2]])] + ) # An operator leg must be dangling: a link name touches two vertices. l = Index(2) diff --git a/test/test_quadraticformnetwork.jl b/test/test_quadraticformnetwork.jl index 8f1eb25c..cfcbdc5d 100644 --- a/test/test_quadraticformnetwork.jl +++ b/test/test_quadraticformnetwork.jl @@ -85,6 +85,37 @@ end @test_throws ErrorException QuadraticFormNetwork(tn, op) end + @testset "operator input on a different vertex of the ket network" begin + g = named_path_graph(2) + tn, l, s = random_state(Float64, g) + out1, out2 = Index(2), Index(2) + optn = ITensorNetwork(Dict(1 => randn((out1, s[2])), 2 => randn((out2, s[1])))) + op = operator(optn, [name(out1), name(out2)], [name(s[2]), name(s[1])]) + @test_throws ErrorException QuadraticFormNetwork(tn, op) + end + + @testset "operator input equal to a ket link name" begin + g = named_path_graph(2) + tn, l, s = random_state(Float64, g) + e = only(edges(g)) + out1, out2 = Index(2), Index(2) + optn = ITensorNetwork(Dict(1 => randn((out1, l[e])), 2 => randn((out2, s[2])))) + op = operator(optn, [name(out1), name(out2)], [name(l[e]), name(s[2])]) + @test_throws ErrorException QuadraticFormNetwork(tn, op) + end + + @testset "operator link name equal to a ket link name" begin + g = named_path_graph(2) + tn, l, s = random_state(Float64, g) + e = only(edges(g)) + out1, out2 = Index(2), Index(2) + optn = ITensorNetwork( + Dict(1 => randn((out1, s[1], l[e])), 2 => randn((out2, s[2], l[e]))) + ) + op = operator(optn, [name(out1), name(out2)], [name(s[1]), name(s[2])]) + @test_throws ErrorException QuadraticFormNetwork(tn, op) + end + @testset "a default bra name equal to an operator name" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g)