From f938246e1d490c004ad80d40fce6570a39ec9e48 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Fri, 30 Oct 2026 13:51:53 -0400 Subject: [PATCH 01/10] Give `NormNetwork` checked bra names and layer accessors Default bra names are derived with `derivedname`, and every link name must have one. `KetView` and `indmap` are removed; `ketnetwork` and `branetwork` return the layers. Adds `inds`, `axes` and `names` of a vertex and `linknames(tn)` to the network interface. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 + src/ITensorNetworkGenerators/ising_network.jl | 4 +- src/abstracttensornetwork.jl | 37 +++--- src/beliefpropagation/beliefpropagation.jl | 6 +- src/beliefpropagation/messagecache.jl | 12 +- src/normnetwork.jl | 125 +++++++++++++----- src/normnetworkview.jl | 64 ++++----- src/tensornetwork.jl | 11 +- src/utils.jl | 15 +++ test/test_basics.jl | 3 + test/test_normnetwork.jl | 88 ++++++++---- test/test_tensornetwork.jl | 6 +- 12 files changed, 249 insertions(+), 124 deletions(-) diff --git a/Project.toml b/Project.toml index 94e3b7e2..df9a94f6 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/ITensorNetworkGenerators/ising_network.jl b/src/ITensorNetworkGenerators/ising_network.jl index 702b8787..c1d4b769 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], axes(tn, v)) end for e in edges(g) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index eabe39ba..7a6dcd82 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,17 @@ end # ====================================== interface ======================================= # +ITensorBase.inds(tn::AbstractITensorNetwork, v) = inds(tn[v]) +ITensorBase.axes(tn::AbstractITensorNetwork, v) = axes(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`. # 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,43 +55,46 @@ 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 axes(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 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]) + 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 = axes(tn, v) for v′ in neighbors(tn, v) - s = setdiff(s, axes(tn[v′])) + s = setdiff(s, axes(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 # 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]) + if name ∈ names(tn, v) push!(sites, v) end end @@ -97,7 +104,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 +114,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/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/normnetwork.jl b/src/normnetwork.jl index d735b22f..6736adcf 100644 --- a/src/normnetwork.jl +++ b/src/normnetwork.jl @@ -1,5 +1,5 @@ -using Dictionaries: Dictionary, dictionary -using ITensorBase: rename, setname, similar_operator, uniquename +using Dictionaries: Dictionary, Indices, dictionary +using ITensorBase: ITensorBase, conj, nametype, rename, similar_operator using ITensorNetworksNext """ @@ -16,14 +16,33 @@ struct NormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} 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 + 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, narrow_map(f, Indices(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 !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 norm network must be distinct.") + end + for name in braname + if has_dimname(ket, name) + error("bra name $name is already an index name of the ket layer.") end - return new{T, V, I}(ket, braname) end + return nothing end function Base.eltype(::Type{<:NormNetwork}) @@ -32,20 +51,20 @@ function Base.eltype(::Type{<:NormNetwork}) ) end -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) +Graphs.edges(nn::NormNetwork) = edges(ketnetwork(nn)) +Graphs.vertices(nn::NormNetwork) = vertices(ketnetwork(nn)) # ==================================== 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) +function NamedGraphs.encoded_vertex(nn::NormNetwork, vertex) + return encoded_vertex(ketnetwork(nn), vertex) +end +function NamedGraphs.decoded_vertex(nn::NormNetwork, code::Integer) + return decoded_vertex(ketnetwork(nn), code) +end +NamedGraphs.encoded_graph(nn::NormNetwork) = encoded_graph(ketnetwork(nn)) # ==================================== DataGraphs.jl ===================================== # @@ -56,8 +75,9 @@ function DataGraphs.get_vertex_data(nn::NormNetwork, vertex) end function DataGraphs.is_vertex_assigned(nn::NormNetwork, vertex) - return isassigned(nn.ket, vertex) + return isassigned(ketnetwork(nn), vertex) end + # =================================== Dictionaries.jl ==================================== # Dictionaries.issettable(::NormNetwork) = false @@ -65,22 +85,61 @@ Dictionaries.isinsertable(::NormNetwork) = false # ====================================== interface ======================================= # +# The name type of the indices, the same as the ket network's. +ITensorBase.nametype(::Type{<:NormNetwork{T, V, I}}) where {T, V, I} = I +ITensorBase.nametype(nn::NormNetwork) = nametype(typeof(nn)) + +""" + ketnetwork(nn::NormNetwork) + +The ket-layer network of `nn`. +""" +ketnetwork(nn::NormNetwork) = nn.ket + +""" + branetwork(nn::NormNetwork) + +The bra-layer network of `nn`, a `BraView` whose tensors are built by [`bratensor`](@ref) when +accessed. +""" +branetwork(nn::NormNetwork) = BraView(nn) + +""" + branamemap(nn::NormNetwork) + +The ket→bra name map, holding a bra name for each ket index name that has a separate bra copy. +""" +branamemap(nn::NormNetwork) = nn.braname + +""" + braname(nn::NormNetwork, name) + +The bra-layer index name corresponding to the ket-layer index name `name`. +""" function braname(nn::NormNetwork, name) - if !has_dimname(nn.ket, name) + if !has_dimname(ketnetwork(nn), 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) + # The names absent from the map are the site indices, which map to themselves. + return get(branamemap(nn), name, name) end -indmap(nn::NormNetwork, ind) = setname(conj(ind), braname(nn, name(ind))) +""" + kettensor(nn::NormNetwork, vertex) + +The ket-layer tensor at `vertex`. +""" +kettensor(nn::NormNetwork, vertex) = ketnetwork(nn)[vertex] -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) + +The bra-layer tensor at `vertex`. +""" bratensor(nn::NormNetwork, vertex) = conj(conj_bratensor(nn, vertex)) """ @@ -102,12 +161,14 @@ function flatten_network(nn::NormNetwork) end """ - normnetwork(tn::ITensorNetwork, [braname]) -> 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. + 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 `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 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(tn::ITensorNetwork) = NormNetwork(tn) -normnetwork(tn::ITensorNetwork, braname) = NormNetwork(tn, braname) +normnetwork(args...) = NormNetwork(args...) diff --git a/src/normnetworkview.jl b/src/normnetworkview.jl index 75c57926..8b675089 100644 --- a/src/normnetworkview.jl +++ b/src/normnetworkview.jl @@ -1,58 +1,50 @@ using DataGraphs: DataGraphs, get_vertex_data, is_vertex_assigned using Dictionaries: Dictionaries, isinsertable, issettable using Graphs: Graphs, edges, vertices +using ITensorBase: ITensorBase, conj, inds 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} +The bra layer of the norm 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} <: AbstractITensorNetwork{T, V} parent::NormNetwork{T, V, I} end -# ====================================== Graphs.jl ======================================= # +Base.parent(nnv::BraView) = nnv.parent -for View in (:KetView, :BraView) - @eval begin - Graphs.edges(nnv::$View) = edges(nnv.parent) - Graphs.vertices(nnv::$View) = vertices(nnv.parent) - end +# ==================================== 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 -# ==================================== NamedGraphs.jl ==================================== # +# ==================================== ITensorBase.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 +function ITensorBase.inds(nnv::BraView, vertex) + return conj.(inds(conj_bratensor(parent(nnv), vertex))) end -# ==================================== DataGraphs.jl ===================================== # +# ====================================== Graphs.jl ======================================= # + +Graphs.edges(nnv::BraView) = edges(parent(nnv)) +Graphs.vertices(nnv::BraView) = vertices(parent(nnv)) -DataGraphs.get_vertex_data(nn::KetView, vertex) = kettensor(nn.parent, vertex) -DataGraphs.get_vertex_data(nn::BraView, vertex) = bratensor(nn.parent, vertex) +# ==================================== NamedGraphs.jl ==================================== # -for View in (:KetView, :BraView) - @eval begin - function DataGraphs.is_vertex_assigned(nnv::$View, vertex) - return isassigned(nnv.parent.ket, vertex) - end - end +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)) # =================================== Dictionaries.jl ==================================== # -for View in (:KetView, :BraView) - @eval begin - Dictionaries.issettable(nnv::$View) = issettable(nnv.parent) - Dictionaries.isinsertable(nnv::$View) = isinsertable(nnv.parent) - end -end +Dictionaries.issettable(nnv::BraView) = issettable(parent(nnv)) +Dictionaries.isinsertable(nnv::BraView) = isinsertable(parent(nnv)) diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 57ad5b2b..1c972cb2 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) @@ -197,12 +197,19 @@ 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. 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/src/utils.jl b/src/utils.jl index abf666e4..5113f317 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,6 +1,21 @@ using Dictionaries: AbstractIndices, Dictionary +using ITensorBase: ITensorBase, IndexName, settags +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))`, `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) + derived = ITensorBase.setuuid(name, uuid5(ITensorBase.uuid(name), string(key))) + return settags(derived, string(key)) +end diff --git a/test/test_basics.jl b/test/test_basics.jl index a975170c..2c9318bc 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -18,6 +18,7 @@ using Test: @test, @testset @test issetequal(vertices(tn), vertices(g)) for v in vertices(tn) @test issetequal(siteinds(tn, v), [s[v]]) + @test axes(tn, v) == (s[v],) end end @testset "Construct ITensorNetwork partition function" begin @@ -35,6 +36,8 @@ using Test: @test, @testset @test issetequal(arranged_edges(tn), arranged_edges(g)) for v in vertices(tn) @test isempty(siteinds(tn, v)) + @test axes(tn, v) == axes(tn[v]) + @test issetequal(axes(tn, v), [l[e] for e in incident_edges(g, v)]) end for v1 in vertices(tn) for v2 in vertices(tn) diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 4528b220..b5109561 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,10 +1,11 @@ using DataGraphs: is_vertex_assigned -using Dictionaries: isinsertable, issettable +using Dictionaries: Indices, 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 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 using LinearAlgebra: norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph using Test: @test, @test_throws, @testset @@ -44,6 +45,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) @@ -79,11 +81,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 @@ -93,7 +90,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)]) @@ -101,32 +98,71 @@ 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 "`derivedname`" begin + n = prime(name(Index(2; tags = "Link"))) + + # 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)) == merge(tags(n), Dict("bra" => "")) + @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) + + # 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) 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 @@ -181,7 +217,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 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 cc83668761ee5951d7b09b9f78404c5c819ee8b7 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Fri, 30 Oct 2026 13:39:16 -0400 Subject: [PATCH 02/10] Bump version to 0.12.0 Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- docs/Project.toml | 2 +- examples/Project.toml | 2 +- test/Project.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index df9a94f6..71015667 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorNetworksNext" uuid = "302f2e75-49f0-4526-aef7-d8ba550cb06c" -version = "0.11.2" +version = "0.12.0" authors = ["ITensor developers and contributors"] [workspace] diff --git a/docs/Project.toml b/docs/Project.toml index eee8bc4e..c8846666 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -10,5 +10,5 @@ path = ".." [compat] Documenter = "1" ITensorFormatter = "0.2.27" -ITensorNetworksNext = "0.11" +ITensorNetworksNext = "0.12" Literate = "2" diff --git a/examples/Project.toml b/examples/Project.toml index 0fa22c56..52c95335 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -5,4 +5,4 @@ ITensorNetworksNext = "302f2e75-49f0-4526-aef7-d8ba550cb06c" path = ".." [compat] -ITensorNetworksNext = "0.11" +ITensorNetworksNext = "0.12" diff --git a/test/Project.toml b/test/Project.toml index 9abc495b..8845e97c 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -32,7 +32,7 @@ Dictionaries = "0.4.5" GradedArrays = "0.17" Graphs = "1.13.1" ITensorBase = "0.16.1" -ITensorNetworksNext = "0.11" +ITensorNetworksNext = "0.12" ITensorPkgSkeleton = "0.3.42" MatrixAlgebraKit = "0.6" NamedGraphs = "0.14" From 9966c90ea084872e72d572eb80040d7e832338d4 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Fri, 30 Oct 2026 14:24:19 -0400 Subject: [PATCH 03/10] Test rejected name maps, `BraView` lookups, vertex encoding and the Ising correlator Co-Authored-By: Claude Opus 5.5 --- test/test_itensornetworkgenerators.jl | 18 ++++++++ test/test_normnetwork.jl | 66 +++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/test/test_itensornetworkgenerators.jl b/test/test_itensornetworkgenerators.jl index 0dc9f4b7..1c47b326 100644 --- a/test/test_itensornetworkgenerators.jl +++ b/test/test_itensornetworkgenerators.jl @@ -51,6 +51,24 @@ using Test: @test, @testset f_analytic = TestUtils.f_1d_ising(β, 4; periodic) @test f ≈ f_analytic end + @testset "1D Ising spin correlator" begin + N = 6 + β = 0.4 + g = named_grid((N,); periodic = true) + ldict = DataGraph(g) + for e in edges(g) + ldict[e] = Index(2) + end + l = e -> ldict[e] + z = prod_tensors(ising_network(l, β, g), Greedy())[] + + # On a periodic chain ⟨σᵢσᵢ₊d⟩ = (tᵈ + tᴺ⁻ᵈ) / (1 + tᴺ) with t = tanh(β). + t = tanh(β) + for d in 1:3 + tn = ising_network(l, β, g; sz_vertices = [(1,), (1 + d,)]) + @test prod_tensors(tn, Greedy())[] / z ≈ (t^d + t^(N - d)) / (1 + t^N) + end + end @testset "2D Ising" begin dims = (4, 4) β = TestUtils.βc_2d_ising() diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index b5109561..d390af7b 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,13 +1,15 @@ using DataGraphs: is_vertex_assigned -using Dictionaries: Indices, isinsertable, issettable -using Graphs: edges, vertices +using Dictionaries: Dictionary, Indices, isinsertable, issettable +using Graphs: edges, neighbors, vertices 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 + branetwork, bratensor, conj_bratensor, contraction_tree, derivedname, dimnamevertices, + flatten_network, has_dimname, ketnetwork, kettensor, linknames, normnetwork, + prod_tensors, sitenames, tensornetwork using LinearAlgebra: norm -using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph +using NamedGraphs: NamedEdge, decoded_vertex, encoded_graph, encoded_vertex, 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 @@ -127,6 +129,23 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test_throws ErrorException NormNetwork(tn) end + @testset "rejected name maps" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + l12 = name(l[NamedEdge(1 => 2)]) + l23 = name(l[NamedEdge(2 => 3)]) + + # A map without a bra name for the link 2 => 3 is rejected. + @test_throws ErrorException normnetwork(tn, Dictionary([l12], [uniquename(l12)])) + + # Two links sharing a bra name would join two separate bonds in the bra layer. + shared = uniquename(l12) + @test_throws ErrorException normnetwork( + tn, + Dictionary([l12, l23], [shared, shared]) + ) + end + @testset "bond to a vertex outside the network" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) @@ -167,6 +186,43 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test !isinsertable(bv) end + @testset "`BraView` index lookups" begin + g = named_path_graph(3) + tn, l, s = random_state(Float64, g) + nn = NormNetwork(tn) + bv = branetwork(nn) + lname = name(l[NamedEdge(1 => 2)]) + bname = braname(nn, lname) + + # The generic lookups read the bra-layer names through `inds(bv, v)`, so the bra link + # name sits on both ends of the bond and the ket link name is absent. + @test inds(bv, 2) == inds(bv[2]) + @test dimnamevertices(bv, bname) == Set([1, 2]) + @test dimnamevertices(bv, name(s[2])) == Set([2]) + @test isempty(dimnamevertices(bv, lname)) + @test has_dimname(bv, bname) + @test !has_dimname(bv, lname) + @test issetequal(linknames(bv), [braname(nn, name(l[e])) for e in edges(g)]) + @test sitenames(bv, 2) == [name(s[2])] + end + + @testset "vertex encoding" begin + g = named_grid((2, 3)) + tn, l, s = random_state(Float64, g) + nn = NormNetwork(tn) + bv = branetwork(nn) + + # The norm network and its bra view number the grid vertices as the ket layer does. + @test encoded_graph(nn) == encoded_graph(tn) + @test encoded_graph(bv) == encoded_graph(tn) + for net in (nn, bv), v in vertices(g) + code = encoded_vertex(net, v) + @test code == encoded_vertex(tn, v) + @test decoded_vertex(net, code) == v + @test issetequal(neighbors(net, v), neighbors(g, v)) + end + end + @testset "`flatten_network`" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) From 29029b954cc10338123238c80a9dd01c488fbb03 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 2 Nov 2026 11:05:15 -0500 Subject: [PATCH 04/10] Simplify the network interface and norm-network tests Co-Authored-By: Claude Opus 5.5 --- test/test_normnetwork.jl | 29 +++++++++++++---------------- test/test_tensornetwork.jl | 15 +++++++-------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index d390af7b..812f7e90 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,6 +1,6 @@ using DataGraphs: is_vertex_assigned using Dictionaries: Dictionary, Indices, isinsertable, issettable -using Graphs: edges, neighbors, vertices +using Graphs: edges, has_edge, neighbors, vertices using ITensorBase: ITensor, Index, IndexName, inds, name, nametype, plev, prime, setname, tags, uniquename using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname, @@ -8,8 +8,7 @@ using ITensorNetworksNext: BraView, Greedy, ITensorNetwork, NormNetwork, braname flatten_network, has_dimname, ketnetwork, kettensor, linknames, normnetwork, prod_tensors, sitenames, tensornetwork using LinearAlgebra: norm -using NamedGraphs: NamedEdge, decoded_vertex, encoded_graph, encoded_vertex, incident_edges, - named_grid, named_path_graph +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 @@ -135,10 +134,10 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] l12 = name(l[NamedEdge(1 => 2)]) l23 = name(l[NamedEdge(2 => 3)]) - # A map without a bra name for the link 2 => 3 is rejected. + # A map missing the bra name of the link 2 => 3 is rejected. @test_throws ErrorException normnetwork(tn, Dictionary([l12], [uniquename(l12)])) - # Two links sharing a bra name would join two separate bonds in the bra layer. + # A map giving two links the same bra name is rejected. shared = uniquename(l12) @test_throws ErrorException normnetwork( tn, @@ -194,8 +193,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] lname = name(l[NamedEdge(1 => 2)]) bname = braname(nn, lname) - # The generic lookups read the bra-layer names through `inds(bv, v)`, so the bra link - # name sits on both ends of the bond and the ket link name is absent. + # The bra layer has the bra link names and the same site names as the ket layer. @test inds(bv, 2) == inds(bv[2]) @test dimnamevertices(bv, bname) == Set([1, 2]) @test dimnamevertices(bv, name(s[2])) == Set([2]) @@ -206,21 +204,20 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test sitenames(bv, 2) == [name(s[2])] end - @testset "vertex encoding" begin + @testset "graph structure on a grid" begin g = named_grid((2, 3)) tn, l, s = random_state(Float64, g) nn = NormNetwork(tn) bv = branetwork(nn) - # The norm network and its bra view number the grid vertices as the ket layer does. - @test encoded_graph(nn) == encoded_graph(tn) - @test encoded_graph(bv) == encoded_graph(tn) - for net in (nn, bv), v in vertices(g) - code = encoded_vertex(net, v) - @test code == encoded_vertex(tn, v) - @test decoded_vertex(net, code) == v - @test issetequal(neighbors(net, v), neighbors(g, v)) + # The norm network and its bra layer have the same neighbors and edges as the grid. + for v in vertices(g) + @test issetequal(neighbors(nn, v), neighbors(g, v)) + @test issetequal(neighbors(bv, v), neighbors(g, v)) end + @test has_edge(nn, (1, 1) => (1, 2)) + @test has_edge(bv, (1, 1) => (1, 2)) + @test !has_edge(nn, (1, 1) => (2, 2)) end @testset "`flatten_network`" begin diff --git a/test/test_tensornetwork.jl b/test/test_tensornetwork.jl index 7a2bee94..87aa0e7d 100644 --- a/test/test_tensornetwork.jl +++ b/test/test_tensornetwork.jl @@ -1,8 +1,8 @@ using DataGraphs: DataGraph, assigned_edge_data, assigned_vertex_data, underlying_graph, vertex_data -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 Graphs: 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, name, operator using ITensorNetworksNext: ITensorNetwork, has_ind, linkaxes, linkinds, linknames, operator_support, siteaxes, siteinds, sitenames, tensornetwork using NamedGraphs: convert_vertextype, incident_edges, named_grid, named_path_graph, @@ -117,14 +117,13 @@ using Test: @test, @test_throws, @testset @test linkaxes(tn, 1 => 2) == [l[E(1 => 2)]] @test linkaxes(tn, E(1 => 2)) == [l[E(1 => 2)]] - @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 linknames(tn, 1 => 2) == [name(l[E(1 => 2)])] + @test linknames(tn, E(1 => 2)) == [name(l[E(1 => 2)])] + @test issetequal(linknames(tn), [name(l[e]) for e in edges(g)]) @test siteinds(tn, 1) == [s[1]] @test siteaxes(tn, 2) == [s[2]] - @test sitenames(tn, 3) == [s[3].name] + @test sitenames(tn, 3) == [name(s[3])] end @testset "`operator_support`" begin From f5928b42bb368bea69bf6b06dfaf89c42ab630ca Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 2 Nov 2026 13:01:28 -0500 Subject: [PATCH 05/10] `normnetwork` map now includes link indices automatically, with dangling as optional arg Drop some `NormNetwork` constructors. --- src/normnetwork.jl | 31 ++++++++++++++----------------- test/test_apply_operator.jl | 6 +++--- test/test_beliefpropagation.jl | 12 ++++++------ test/test_free_fermions.jl | 6 +++--- test/test_normnetwork.jl | 29 +++++++++++++---------------- 5 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/normnetwork.jl b/src/normnetwork.jl index 6736adcf..8d24e235 100644 --- a/src/normnetwork.jl +++ b/src/normnetwork.jl @@ -21,15 +21,6 @@ struct NormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} end end -function NormNetwork( - f::Function, tn::ITensorNetwork, linknames = ITensorNetworksNext.linknames(tn) - ) - 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) -end - function check_input(::Type{<:NormNetwork}, ket, braname) if !issubset(linknames(ket), keys(braname)) error("every link name of the ket layer must have a bra name.") @@ -161,14 +152,20 @@ function flatten_network(nn::NormNetwork) end """ - normnetwork([f::Function], tn::ITensorNetwork, [linknames]) -> NormNetwork + normnetwork([f::Function], tn::ITensorNetwork, [dangling]) -> NormNetwork normnetwork(tn::ITensorNetwork, braname::Dictionary) -> NormNetwork 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 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...) +Each ket index name in `union(dangling, linknames(tn))` gets the bra name `f(name)`, +where `f` defaults to `name -> derivedname(name, "bra")`, and `dangling` is empty by default. +Alternatively pass the ket→bra name map `braname` directly, which must include every link +name of `tn` and may include others. Either way 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(tn::ITensorNetwork, braname::Dictionary) = NormNetwork(tn, braname) +function normnetwork(f::Function, tn::ITensorNetwork, dangling = nametype(tn)[]) + return normnetwork(tn, narrow_map(f, Indices(union(linknames(tn), dangling)))) +end +function normnetwork(tn::ITensorNetwork, dangling = nametype(tn)[]) + return normnetwork(name -> derivedname(name, "bra"), tn, dangling) +end diff --git a/test/test_apply_operator.jl b/test/test_apply_operator.jl index 8c2048a9..00e4e855 100644 --- a/test/test_apply_operator.jl +++ b/test/test_apply_operator.jl @@ -1,8 +1,8 @@ using GradedArrays: U1, gradedrange using Graphs: dst, edges, src, vertices using ITensorBase: Index, apply, name, operator, setname, uniquename -using ITensorNetworksNext: NormNetwork, apply_operator, apply_operators, insertlink!, - message_environment, tensornetwork +using ITensorNetworksNext: apply_operator, apply_operators, insertlink!, + message_environment, normnetwork, tensornetwork using MatrixAlgebraKit: svd_trunc, truncrank using NamedGraphs: named_cycle_graph, named_path_graph using Random: AbstractRNG @@ -35,7 +35,7 @@ function random_state(rng::AbstractRNG, elt::Type, g, site_axes; nlayers, trunc) insertlink!(network, edge) end - env = message_environment(one, NormNetwork(network)) + env = message_environment(one, normnetwork(network)) for _ in 1:nlayers, e in edges(g) gate = randn_operator(rng, elt, (site_axes[src(e)], site_axes[dst(e)])) network, env = apply_operator(gate, network, env; trunc) diff --git a/test/test_beliefpropagation.jl b/test/test_beliefpropagation.jl index 1591e26f..67ac8555 100644 --- a/test/test_beliefpropagation.jl +++ b/test/test_beliefpropagation.jl @@ -6,10 +6,10 @@ using Graphs: AbstractGraph, add_vertex!, dst, edges, has_edge, has_vertex, ne, rem_edge!, src, vertices using ITensorBase: ITensor, Index, apply, inds, name, noprime, outputnames, prime, state using ITensorNetworksNext: ITensorNetworksNext, ContractionTreeAlgorithm, Greedy, - ITensorNetwork, MessageCache, NormNetwork, SimpleMessageUpdate, StopWhenConverged, - beliefpropagation, bethe_free_energy, bethe_free_entropy, bratensor, contraction_tree, - edge_scalar, edge_scalars, factor_tensors, incoming_messages, insertlink!, kettensor, - linkaxes, linkinds, message_environment, messagecache, prod_tensors, region_scalar, + ITensorNetwork, MessageCache, SimpleMessageUpdate, StopWhenConverged, beliefpropagation, + bethe_free_energy, bethe_free_entropy, bratensor, contraction_tree, edge_scalar, + edge_scalars, factor_tensors, incoming_messages, insertlink!, kettensor, linkaxes, + linkinds, message_environment, messagecache, normnetwork, prod_tensors, region_scalar, subgraph, tensornetwork, updated_message, vertex_scalar, vertex_scalars using LinearAlgebra: LinearAlgebra, norm, tr using NamedGraphs: NamedEdge, all_edges, incident_edges, named_comb_tree, named_grid, @@ -312,7 +312,7 @@ end for edge in edges(g) insertlink!(network, edge) end - nn = NormNetwork(network) + nn = normnetwork(network) cache = beliefpropagation( nn, message_environment(one, nn); @@ -364,7 +364,7 @@ end for edge in edges(g) insertlink!(network, edge) end - nn = NormNetwork(network) + nn = normnetwork(network) v = (2, 2) # A doubled vertex splits into its two layers. diff --git a/test/test_free_fermions.jl b/test/test_free_fermions.jl index c4450b67..686f8060 100644 --- a/test/test_free_fermions.jl +++ b/test/test_free_fermions.jl @@ -1,8 +1,8 @@ using GradedArrays: U1 using Graphs: dst, edges, src, vertices using ITensorBase: Index, apply, inds, operator, prime -using ITensorNetworksNext: NormNetwork, apply_operator, apply_operators, insertlink!, - message_environment, tensornetwork +using ITensorNetworksNext: apply_operator, apply_operators, insertlink!, + message_environment, normnetwork, tensornetwork using NamedGraphs: named_comb_tree, named_cycle_graph using TensorAlgebra: project, project_aux using TensorKitSectors: FermionNumber @@ -71,7 +71,7 @@ function bp_evolve(sector, g, occupied, bonds) for e in edges(g) insertlink!(ψ, e) end - env = message_environment(one, NormNetwork(ψ)) + env = message_environment(one, normnetwork(ψ)) for v in occupied ψ, env = apply_operator(cdag(sites[v]), ψ, env) end diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 812f7e90..e3fdec1a 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -32,10 +32,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "Basics" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) - nn = NormNetwork(tn) - - # `normnetwork` is the public constructor and agrees with `NormNetwork`. - @test normnetwork(tn) isa NormNetwork + nn = normnetwork(tn) @test nn isa NormNetwork # The norm network shares the graph structure of the underlying network. @@ -59,7 +56,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "kettensor / bratensor / conj_bratensor and the name map" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) - nn = NormNetwork(tn) + nn = normnetwork(tn) # `kettensor` returns the underlying tensor untouched. @test kettensor(nn, 2) === tn[2] @@ -118,14 +115,14 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] 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(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) + @test_throws ErrorException normnetwork(tn) end @testset "rejected name maps" begin @@ -165,7 +162,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "`ketnetwork` / `branetwork`" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) - nn = NormNetwork(tn) + nn = normnetwork(tn) # The ket layer is the network the norm network was built from. @test ketnetwork(nn) === tn @@ -188,7 +185,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "`BraView` index lookups" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) - nn = NormNetwork(tn) + nn = normnetwork(tn) bv = branetwork(nn) lname = name(l[NamedEdge(1 => 2)]) bname = braname(nn, lname) @@ -207,7 +204,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "graph structure on a grid" begin g = named_grid((2, 3)) tn, l, s = random_state(Float64, g) - nn = NormNetwork(tn) + nn = normnetwork(tn) bv = branetwork(nn) # The norm network and its bra layer have the same neighbors and edges as the grid. @@ -223,7 +220,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @testset "`flatten_network`" begin g = named_path_graph(3) tn, l, s = random_state(Float64, g) - nn = NormNetwork(tn) + nn = normnetwork(tn) flat = flatten_network(nn) # One vertex per layer, so the vertex count doubles and each carries its layer's tensor. @@ -249,7 +246,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] v = randn(s) v = v / norm(v) tn = ITensorNetwork(Dict(1 => v)) - nn = NormNetwork(tn) + nn = normnetwork(tn) # ⟨ψ|ψ⟩ for a single normalized site tensor is 1. @test contract_norm(nn) ≈ 1 @@ -260,18 +257,18 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] tn, l, s = random_state(T, g) # The norm network contracts to ⟨tn|tn⟩ = ‖prod(tn)‖², a real nonnegative number. - z = contract_norm(NormNetwork(tn)) + z = contract_norm(normnetwork(tn)) @test z ≈ norm(prod(tn))^2 @test imag(z) ≈ 0 atol = 1.0e-12 * abs(z) @test real(z) > 0 # Rescaling a single tensor by 1/√z normalizes the state, so ⟨tn|tn⟩ = 1. tn[first(vertices(tn))] = tn[first(vertices(tn))] / sqrt(real(z)) - @test contract_norm(NormNetwork(tn)) ≈ 1 + @test contract_norm(normnetwork(tn)) ≈ 1 # The contracted norm does not depend on the chosen bra-layer name map. custom = map(uniquename, Indices(linknames(tn))) - @test contract_norm(normnetwork(tn, custom)) ≈ contract_norm(NormNetwork(tn)) + @test contract_norm(normnetwork(tn, custom)) ≈ contract_norm(normnetwork(tn)) end end end From 2723695ac1cba9d51e3d1fc0a4b0c7e32013630e Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Mon, 2 Nov 2026 16:21:22 -0500 Subject: [PATCH 06/10] Return `Indices` from `dimnamevertices` and tuples from `linkaxes` and `siteaxes` `dimnamevertices` and the `dimname_vertices` table of `ITensorNetwork` now hold `Indices` instead of `Set`, so the vertex order is insertion order. `linkaxes` and `siteaxes` return tuples to match `axes`. Co-Authored-By: Claude Opus 5.5 --- src/abstracttensornetwork.jl | 14 +++----------- src/tensornetwork.jl | 12 ++++++------ test/test_normnetwork.jl | 4 ++-- test/test_tensornetwork.jl | 6 +++--- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 7a6dcd82..1b3766c2 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -55,7 +55,7 @@ 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 Tuple(ax for ax in axes(tn, src(edge)) if name(ax) in ln) end function linknames(tn::AbstractGraph, edge::Pair) return linknames(tn, edgetype(tn)(edge)) @@ -79,7 +79,7 @@ function siteaxes(tn::AbstractGraph, v) for v′ in neighbors(tn, v) s = setdiff(s, axes(tn, v′)) end - return s + return Tuple(s) end function sitenames(tn::AbstractGraph, v) s = names(tn, v) @@ -91,15 +91,7 @@ end # Return the non-link vertices associated with an dim name function dimnamevertices(tn::AbstractGraph, name) - sites = Set{vertextype(tn)}() - - for v in vertices(tn) - if name ∈ names(tn, v) - push!(sites, v) - end - end - - return sites + return Indices(filter(v -> name ∈ names(tn, v), vertices(tn))) end function has_dimname(tn::AbstractGraph, name) diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 1c972cb2..cc6da6fa 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -11,11 +11,11 @@ using SplitApplyCombine: mapview struct ITensorNetwork{T, V, I} <: AbstractITensorNetwork{T, V} tensors::Dictionary{V, T} - dimname_vertices::Dictionary{I, Set{V}} + dimname_vertices::Dictionary{I, Indices{V}} underlying_graph::NamedGraph{V} function ITensorNetwork{T, V, I}(::UndefInitializer, vertices) where {T, V, I} tensors = Dictionary{V, T}() - dimname_vertices = Dictionary{I, Set{V}}() + dimname_vertices = Dictionary{I, Indices{V}}() underlying_graph = NamedGraph(vertices) return new{T, V, I}(tensors, dimname_vertices, underlying_graph) end @@ -118,7 +118,7 @@ DataGraphs.get_vertex_data(tn::ITensorNetwork, v) = tn.tensors[v] function check_input(::typeof(set_vertex_data!), tn, tensor, vertex) for name in names(tensor) - vertices = get(tn.dimname_vertices, name, Set()) + vertices = get(tn.dimname_vertices, name, Indices{vertextype(tn)}()) if length(setdiff(vertices, Set([vertex]))) > 1 throw( ArgumentError( @@ -165,7 +165,7 @@ function update_tensornetwork_metadata!(tn, vertex, oldnames, newnames) # Now `name` must be a new index that's not in `oldinds` - vertex_list = get!(tn.dimname_vertices, name, Set()) + vertex_list = get!(tn.dimname_vertices, name, Indices{vertextype(tn)}()) if length(vertex_list) > 1 throw( ArgumentError( @@ -173,7 +173,7 @@ function update_tensornetwork_metadata!(tn, vertex, oldnames, newnames) ) ) end - push!(vertex_list, vertex) + insert!(vertex_list, vertex) # Add an edge if the index is now shared between two vertices. if length(vertex_list) == 2 @@ -197,7 +197,7 @@ Graphs.add_edge!(::ITensorNetwork, _edge) = false # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. function dimnamevertices(tn::ITensorNetwork, name) - return copy(get(tn.dimname_vertices, name, Set{vertextype(tn)}())) + return copy(get(tn.dimname_vertices, name, Indices{vertextype(tn)}())) end # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index e3fdec1a..bb916d61 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -192,8 +192,8 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] # The bra layer has the bra link names and the same site names as the ket layer. @test inds(bv, 2) == inds(bv[2]) - @test dimnamevertices(bv, bname) == Set([1, 2]) - @test dimnamevertices(bv, name(s[2])) == Set([2]) + @test issetequal(dimnamevertices(bv, bname), [1, 2]) + @test issetequal(dimnamevertices(bv, name(s[2])), [2]) @test isempty(dimnamevertices(bv, lname)) @test has_dimname(bv, bname) @test !has_dimname(bv, lname) diff --git a/test/test_tensornetwork.jl b/test/test_tensornetwork.jl index 87aa0e7d..405a5b6e 100644 --- a/test/test_tensornetwork.jl +++ b/test/test_tensornetwork.jl @@ -114,15 +114,15 @@ using Test: @test, @test_throws, @testset @test linkinds(tn, 1 => 2) == [l[E(1 => 2)]] @test linkinds(tn, E(1 => 2)) == [l[E(1 => 2)]] - @test linkaxes(tn, 1 => 2) == [l[E(1 => 2)]] - @test linkaxes(tn, E(1 => 2)) == [l[E(1 => 2)]] + @test linkaxes(tn, 1 => 2) == (l[E(1 => 2)],) + @test linkaxes(tn, E(1 => 2)) == (l[E(1 => 2)],) @test linknames(tn, 1 => 2) == [name(l[E(1 => 2)])] @test linknames(tn, E(1 => 2)) == [name(l[E(1 => 2)])] @test issetequal(linknames(tn), [name(l[e]) for e in edges(g)]) @test siteinds(tn, 1) == [s[1]] - @test siteaxes(tn, 2) == [s[2]] + @test siteaxes(tn, 2) == (s[2],) @test sitenames(tn, 3) == [name(s[3])] end From 1d5cf747651308a5b22a31fa3b0519415d990f19 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Tue, 3 Nov 2026 11:00:07 -0500 Subject: [PATCH 07/10] Use inferred version of `names` directly in `AbstractTensorNetwork` method. --- src/abstracttensornetwork.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 1b3766c2..74ebf770 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -39,7 +39,7 @@ end ITensorBase.inds(tn::AbstractITensorNetwork, v) = inds(tn[v]) ITensorBase.axes(tn::AbstractITensorNetwork, v) = axes(tn[v]) -ITensorBase.names(tn::AbstractITensorNetwork, v) = name.(inds(tn, v)) +ITensorBase.names(tn::AbstractITensorNetwork, v) = names(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 5df04b60cad94e30d95fb11f4683bbf1dc9b1aa4 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Tue, 3 Nov 2026 12:22:54 -0500 Subject: [PATCH 08/10] Rename `linknames(tn)` to new function `eachlinkname`; make it lazy --- src/abstracttensornetwork.jl | 4 +--- src/normnetwork.jl | 7 ++++--- src/tensornetwork.jl | 8 ++++---- test/test_normnetwork.jl | 10 +++++----- test/test_tensornetwork.jl | 6 +++--- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 74ebf770..06f78ef6 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -63,9 +63,7 @@ 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 +eachlinkname(tn::AbstractGraph) = Iterators.flatten(linknames(tn, e) for e in edges(tn)) function siteinds(tn::AbstractGraph, v) s = inds(tn, v) diff --git a/src/normnetwork.jl b/src/normnetwork.jl index 8d24e235..7fc6304b 100644 --- a/src/normnetwork.jl +++ b/src/normnetwork.jl @@ -22,7 +22,7 @@ struct NormNetwork{T, V, I} <: AbstractITensorNetwork{T, V} end function check_input(::Type{<:NormNetwork}, ket, braname) - if !issubset(linknames(ket), keys(braname)) + if !issubset(eachlinkname(ket), keys(braname)) error("every link name of the ket layer must have a bra name.") end if !allunique(braname) @@ -156,7 +156,7 @@ end normnetwork(tn::ITensorNetwork, braname::Dictionary) -> NormNetwork Build the double-layer norm network `⟨tn|tn⟩`, represented lazily as a `NormNetwork` object. -Each ket index name in `union(dangling, linknames(tn))` gets the bra name `f(name)`, +Each ket index name in `union(dangling, eachlinkname(tn))` gets the bra name `f(name)`, where `f` defaults to `name -> derivedname(name, "bra")`, and `dangling` is empty by default. Alternatively pass the ket→bra name map `braname` directly, which must include every link name of `tn` and may include others. Either way an error is thrown if a link name is missing, @@ -164,7 +164,8 @@ two bra names coincide or a bra name is already an index name of `tn`. """ normnetwork(tn::ITensorNetwork, braname::Dictionary) = NormNetwork(tn, braname) function normnetwork(f::Function, tn::ITensorNetwork, dangling = nametype(tn)[]) - return normnetwork(tn, narrow_map(f, Indices(union(linknames(tn), dangling)))) + names = Indices{nametype(tn)}(union(eachlinkname(tn), dangling)) + return normnetwork(tn, narrow_map(f, names)) end function normnetwork(tn::ITensorNetwork, dangling = nametype(tn)[]) return normnetwork(name -> derivedname(name, "bra"), tn, dangling) diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index cc6da6fa..7f97d97d 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -203,11 +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 [ +# PERF: still linear compared to generic method, but in practice order of magnitudes faster. +function eachlinkname(tn::ITensorNetwork) + 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 bb916d61..caa28736 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -5,7 +5,7 @@ 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, dimnamevertices, - flatten_network, has_dimname, ketnetwork, kettensor, linknames, normnetwork, + eachlinkname, flatten_network, has_dimname, ketnetwork, kettensor, normnetwork, prod_tensors, sitenames, tensornetwork using LinearAlgebra: norm using NamedGraphs: NamedEdge, incident_edges, named_grid, named_path_graph @@ -88,7 +88,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, Indices(linknames(tn))) + custom = map(uniquename, Indices(eachlinkname(tn))) nn = normnetwork(tn, custom) lname = name(l[NamedEdge(1 => 2)]) @@ -148,7 +148,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] # 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)))) + nn = normnetwork(part, map(uniquename, Indices(eachlinkname(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. @@ -197,7 +197,7 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] @test isempty(dimnamevertices(bv, lname)) @test has_dimname(bv, bname) @test !has_dimname(bv, lname) - @test issetequal(linknames(bv), [braname(nn, name(l[e])) for e in edges(g)]) + @test issetequal(eachlinkname(bv), [braname(nn, name(l[e])) for e in edges(g)]) @test sitenames(bv, 2) == [name(s[2])] end @@ -267,7 +267,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, Indices(linknames(tn))) + custom = map(uniquename, Indices(eachlinkname(tn))) @test contract_norm(normnetwork(tn, custom)) ≈ contract_norm(normnetwork(tn)) end end diff --git a/test/test_tensornetwork.jl b/test/test_tensornetwork.jl index 405a5b6e..77384c9d 100644 --- a/test/test_tensornetwork.jl +++ b/test/test_tensornetwork.jl @@ -3,8 +3,8 @@ using DataGraphs: using Graphs: 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, name, operator -using ITensorNetworksNext: ITensorNetwork, has_ind, linkaxes, linkinds, linknames, - operator_support, siteaxes, siteinds, sitenames, tensornetwork +using ITensorNetworksNext: ITensorNetwork, eachlinkname, has_ind, linkaxes, linkinds, + linknames, operator_support, siteaxes, siteinds, sitenames, tensornetwork using NamedGraphs: convert_vertextype, incident_edges, named_grid, named_path_graph, similar_graph, subgraph, vertextype using Test: @test, @test_throws, @testset @@ -119,7 +119,7 @@ using Test: @test, @test_throws, @testset @test linknames(tn, 1 => 2) == [name(l[E(1 => 2)])] @test linknames(tn, E(1 => 2)) == [name(l[E(1 => 2)])] - @test issetequal(linknames(tn), [name(l[e]) for e in edges(g)]) + @test issetequal(eachlinkname(tn), [name(l[e]) for e in edges(g)]) @test siteinds(tn, 1) == [s[1]] @test siteaxes(tn, 2) == (s[2],) From 82aa65d2e9af3db985614dc0194f38e9956697be Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Tue, 3 Nov 2026 12:27:24 -0500 Subject: [PATCH 09/10] Improve performance of `linknames(tn, edge)` using comprehension. --- src/abstracttensornetwork.jl | 3 ++- src/tensornetwork.jl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/abstracttensornetwork.jl b/src/abstracttensornetwork.jl index 06f78ef6..0fe4148b 100644 --- a/src/abstracttensornetwork.jl +++ b/src/abstracttensornetwork.jl @@ -61,7 +61,8 @@ 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)) + dstnames = names(tn, dst(edge)) + return [n for n in names(tn, src(edge)) if n in dstnames] end eachlinkname(tn::AbstractGraph) = Iterators.flatten(linknames(tn, e) for e in edges(tn)) diff --git a/src/tensornetwork.jl b/src/tensornetwork.jl index 7f97d97d..5742d749 100644 --- a/src/tensornetwork.jl +++ b/src/tensornetwork.jl @@ -203,7 +203,7 @@ end # PERF: fast lookup compared to `AbstractITensorNetwork` fallback. has_dimname(tn::ITensorNetwork, name) = haskey(tn.dimname_vertices, name) -# PERF: still linear compared to generic method, but in practice order of magnitudes faster. +# PERF: still linear compared to generic method, but in practice 5x faster function eachlinkname(tn::ITensorNetwork) return ( name for (name, vertices) in pairs(tn.dimname_vertices) if length(vertices) == 2 From 9cb685ed00777b016900942cf77c52dee873b7d1 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 4 Nov 2026 17:54:56 -0500 Subject: [PATCH 10/10] Use `prime` as default dervived name for bra layer in NormNetwork Calls this via a function `derive_braname`. --- src/normnetwork.jl | 8 ++++++-- test/test_normnetwork.jl | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/normnetwork.jl b/src/normnetwork.jl index 7fc6304b..46308a8f 100644 --- a/src/normnetwork.jl +++ b/src/normnetwork.jl @@ -115,6 +115,9 @@ function braname(nn::NormNetwork, name) return get(branamemap(nn), name, name) end +# The default ket→bra name map +derive_braname(name) = prime(name) + """ kettensor(nn::NormNetwork, vertex) @@ -157,7 +160,8 @@ end Build the double-layer norm network `⟨tn|tn⟩`, represented lazily as a `NormNetwork` object. Each ket index name in `union(dangling, eachlinkname(tn))` gets the bra name `f(name)`, -where `f` defaults to `name -> derivedname(name, "bra")`, and `dangling` is empty by default. +where `f` defaults to `derive_braname`, which primes the name, and `dangling` is empty by +default. Alternatively pass the ket→bra name map `braname` directly, which must include every link name of `tn` and may include others. Either way 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`. @@ -168,5 +172,5 @@ function normnetwork(f::Function, tn::ITensorNetwork, dangling = nametype(tn)[]) return normnetwork(tn, narrow_map(f, names)) end function normnetwork(tn::ITensorNetwork, dangling = nametype(tn)[]) - return normnetwork(name -> derivedname(name, "bra"), tn, dangling) + return normnetwork(derive_braname, tn, dangling) end diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index caa28736..7861aec3 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -115,12 +115,13 @@ contract_norm(nn) = prod_tensors(flatten_network(nn), Greedy())[] 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) == prime(lname) @test braname(normnetwork(tn), lname) == braname(normnetwork(tn), lname) - @test braname(normnetwork(prime, tn), lname) == prime(lname) + f = n -> derivedname(n, :bra) + @test braname(normnetwork(f, tn), lname) == derivedname(lname, :bra) # A bra name that is already an index name of the network is rejected. - taken = setname(Index(2), derivedname(lname, :bra)) + taken = setname(Index(2), prime(lname)) tn[1] = tn[1] * ITensor(ones(2), (taken,)) @test_throws ErrorException normnetwork(tn) end