From e2105c08d68ebaeb5a6c38220ae36942189dc504 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 27 Jul 2026 23:03:44 -0400 Subject: [PATCH 1/8] Dispatch belief-propagation message updates on NormNetwork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatches belief-propagation message updates on the factor-graph type so operator-valued messages are preserved end to end. Belief propagation on a `NormNetwork` (the doubled ⟨ψ|ψ⟩ network) produces bond operators pairing a ket leg with a bra leg. The previous `message_update!` recovered that structure by inspecting each message's type inline. Now the generic method contracts into a plain bond vector normalized by its entrywise sum, and a `NormNetwork` method re-wraps the contraction as an operator carrying the network's own ket/bra link names and normalizes by the trace, which is sign-correct on fermionic bonds where the entrywise sum can flip the odd-parity block's sign. `vertex_scalar` and `edge_scalar` unwrap operator messages before contracting, so `bethe_free_energy` works on them too. Adds a belief-propagation test on a norm network over U1 and fermion-parity sites, checking that the converged messages keep their operator structure and that belief propagation stays exact on a tree. --- Project.toml | 2 +- src/beliefpropagation/beliefpropagation.jl | 55 ++++++++++++---------- src/beliefpropagation/messagecache.jl | 10 ++-- test/test_beliefpropagation.jl | 47 ++++++++++++++++-- 4 files changed, 79 insertions(+), 35 deletions(-) diff --git a/Project.toml b/Project.toml index f9e3661..1dad704 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorNetworksNext" uuid = "302f2e75-49f0-4526-aef7-d8ba550cb06c" -version = "0.9.10" +version = "0.9.11" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index 6255df1..e4ae1da 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -3,8 +3,7 @@ using .AlgorithmsInterfaceExtensions: using AlgorithmsInterface: AlgorithmsInterface as AI using DataGraphs: edge_data using Graphs: AbstractEdge, edges, edgetype, has_edge, vertices -using ITensorBase: - AbstractITensor, NamedTensorOperator, inputnames, operator, outputnames, state +using ITensorBase: AbstractITensor, operator, state using LinearAlgebra: norm, normalize, tr using NamedGraphs.GraphsExtensions: add_edges!, boundary_edges, forest_cover_edge_sequence, subgraph @@ -240,37 +239,41 @@ end contraction_alg::ContractionAlg = Exact() end -function message_update!(algorithm::SimpleMessageUpdate, cache, factors, edge) +# Contract the incoming messages into the source factor to form the (unnormalized) new message on +# `edge`. `contract_network` works on plain named arrays, so operator-valued messages are unwrapped +# with `state` (idempotent on plain messages) before contracting; fermionic signs ride on the graded +# arrays, so nothing is lost. +function contracted_message(algorithm::SimpleMessageUpdate, cache, factors, edge) messages = collect(incoming_messages(cache, edge)) factor = factors[src(edge)] + return contract_network([state.(messages); [factor]]; alg = algorithm.contraction_alg) +end - # `contract_network` works on plain named arrays, so unwrap any operator messages to - # their underlying tensors before contracting (fermionic signs ride on the graded - # arrays, so nothing is lost). - message_tensors = state.(messages) - new_message = contract_network( - [message_tensors; [factor]]; alg = algorithm.contraction_alg - ) - - # `contract_network` drops the bra/ket operator structure, so restore it from the - # existing message. A doubled (ket/bra) message is then a bond operator: normalize by - # its trace, which is sign-correct for fermionic bonds (the entrywise `sum` can flip - # the odd-parity block's sign). A single-layer message stays a vector with no bra/ket - # pairing, so fall back to the entrywise sum there. - old_message = cache[edge] - if old_message isa NamedTensorOperator - new_message = - operator(new_message, outputnames(old_message), inputnames(old_message)) +# Single-layer network: the message is a plain bond vector, normalized by its entrywise sum. +function message_update!(algorithm::SimpleMessageUpdate, cache, factors, edge) + new_message = contracted_message(algorithm, cache, factors, edge) + if algorithm.normalize + message_norm = sum(new_message) + iszero(message_norm) || (new_message /= message_norm) end + cache[edge] = new_message + return cache +end +# `NormNetwork`: the message is a doubled (ket/bra) bond operator. `contract_network` drops the +# operator structure, so re-wrap the result with the ket/bra names the norm network assigns to this +# edge (the same convention as `similar_message_environment`) rather than reconstructing them from +# the old message. Normalize by the trace, which is sign-correct on fermionic bonds where the +# entrywise `sum` can flip the odd-parity block's sign. +function message_update!(algorithm::SimpleMessageUpdate, cache, factors::NormNetwork, edge) + new_tensor = contracted_message(algorithm, cache, factors, edge) + new_message = operator( + new_tensor, linknames(KetView(factors), edge), linknames(BraView(factors), edge) + ) if algorithm.normalize - message_norm = - new_message isa NamedTensorOperator ? tr(new_message) : sum(new_message) - if !iszero(message_norm) - new_message /= message_norm - end + message_norm = tr(new_message) + iszero(message_norm) || (new_message /= message_norm) end - cache[edge] = new_message return cache end diff --git a/src/beliefpropagation/messagecache.jl b/src/beliefpropagation/messagecache.jl index 22d3aa4..38f50c7 100644 --- a/src/beliefpropagation/messagecache.jl +++ b/src/beliefpropagation/messagecache.jl @@ -3,7 +3,7 @@ using DataGraphs: DataGraphs, AbstractDataGraph, AbstractEdgeDataGraph, edge_dat vertex_data_type using Dictionaries: Dictionary, delete!, getindices, set! using Graphs: AbstractGraph, connected_components, is_directed, is_tree -using ITensorBase: unnamed +using ITensorBase: state, unnamed using NamedGraphs.GraphsExtensions: IsDirected, boundary_edges, default_root_vertex, directed_graph, forest_cover, in_incident_edges, post_order_dfs_edges, undirected_graph, vertextype @@ -136,9 +136,11 @@ function incoming_edge_data(cache::AbstractGraph, vertices) return getindices(cache, dimnames) end +# `contract_network` works on plain named arrays, so operator-valued (doubled ket/bra) messages are +# unwrapped with `state` (idempotent on plain messages) before contracting into a scalar. function vertex_scalar(factors, messages, vertex; kwargs...) in_messages = incoming_edge_data(messages, [vertex]) - tensors = vcat([factors[vertex]], collect(in_messages)) + tensors = vcat([factors[vertex]], state.(collect(in_messages))) return contract_network(tensors; kwargs...)[] end @@ -151,8 +153,8 @@ function vertex_scalars(factors, messages, vertices) end function edge_scalar(cache, edge; kwargs...) - m1 = cache[edge] - m2 = cache[reverse(edge)] + m1 = state(cache[edge]) + m2 = state(cache[reverse(edge)]) return contract_network([m1, m2]; kwargs...)[] end diff --git a/test/test_beliefpropagation.jl b/test/test_beliefpropagation.jl index 3ecf8ee..3ee7a32 100644 --- a/test/test_beliefpropagation.jl +++ b/test/test_beliefpropagation.jl @@ -1,16 +1,19 @@ import AlgorithmsInterface as AI using DataGraphs: DataGraphs, DataGraph, edge_data, edge_data_type using Dictionaries: Dictionary, dictionary, set! +using GradedArrays: U1, gradedrange using Graphs: AbstractGraph, dst, edges, has_edge, src, vertices -using ITensorBase: ITensor, Index, inds, name, noprime, prime -using ITensorNetworksNext: ITensorNetworksNext, ITensorNetwork, MessageCache, - StopWhenConverged, bethe_free_energy, edge_scalar, incoming_messages, linkinds, - messagecache, region_scalar, subgraph, tensornetwork, vertex_scalar, vertex_scalars +using ITensorBase: ITensor, Index, inds, name, noprime, outputnames, prime +using ITensorNetworksNext: ITensorNetworksNext, ITensorNetwork, MessageCache, NormNetwork, + StopWhenConverged, bethe_free_energy, edge_scalar, incoming_messages, insertlink!, + linkinds, message_environment, messagecache, region_scalar, subgraph, tensornetwork, + vertex_scalar, vertex_scalars using LinearAlgebra: LinearAlgebra using NamedGraphs.GraphsExtensions: all_edges, arranged_edges, incident_edges, vertextype using NamedGraphs.NamedGraphGenerators: named_comb_tree, named_grid, named_path_graph using NamedGraphs: NamedEdge using StableRNGs: StableRNG +using TensorKitSectors: FermionParity using Test: @test, @testset function spin_ice_tensornetwork(g) @@ -225,4 +228,40 @@ end end end end + + @testset "NormNetwork (operator-valued messages)" begin + site_ranges = ( + "U1" => gradedrange([U1(0) => 1, U1(1) => 1]), + "FermionParity" => + gradedrange([FermionParity(0) => 1, FermionParity(1) => 1]), + ) + @testset "$label, T=$T" for (label, site_range) in site_ranges, + T in (Float64, ComplexF64) + + rng = StableRNG(123) + g = named_path_graph(4) + site_axes = Dict(v => Index(site_range) for v in vertices(g)) + network = tensornetwork(vertices(g)) do v + return randn(rng, T, (site_axes[v],)) + end + for edge in edges(g) + insertlink!(network, edge) + end + nn = NormNetwork(network) + + cache = ITensorNetworksNext.beliefpropagation( + nn, message_environment(one, nn); + stopping_criterion = (; maxiter = 20, tol = 1.0e-10) + ) + + # Messages stay operator-valued end to end (a plain message has no output names). + @test all(msg -> !isempty(outputnames(msg)), edge_data(cache)) + + # Belief propagation is exact on a tree, including on the fermionic norm network. + ket = prod(network) + z_exact = (ket * conj(ket))[] + z_bp = exp(bethe_free_energy(nn, cache)) + @test z_bp ≈ z_exact rtol = eps(real(T))^(1 / 3) + end + end end From 9a2d58a5441452bc07a99d03e338f0baa809eeab Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 27 Jul 2026 23:33:59 -0400 Subject: [PATCH 2/8] Contract the edge scalar with `*` to preserve operator messages `edge_scalar` is a single binary contraction, so it uses `*` directly instead of `contract_network`. Binary `*` dispatches through the operator product, so operator-valued messages contract to a scalar without unwrapping, preserving operators. `vertex_scalar` is N-ary and still routes through `contract_network`, whose lazy path only accepts plain operands, so it keeps the `state` unwrap for now. --- src/beliefpropagation/messagecache.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/beliefpropagation/messagecache.jl b/src/beliefpropagation/messagecache.jl index 38f50c7..7b531f5 100644 --- a/src/beliefpropagation/messagecache.jl +++ b/src/beliefpropagation/messagecache.jl @@ -136,8 +136,10 @@ function incoming_edge_data(cache::AbstractGraph, vertices) return getindices(cache, dimnames) end -# `contract_network` works on plain named arrays, so operator-valued (doubled ket/bra) messages are -# unwrapped with `state` (idempotent on plain messages) before contracting into a scalar. +# `contract_network` routes operands through the lazy `Mul` path, which is invariant in the operand +# type and currently only accepts plain arrays, so operator-valued (doubled ket/bra) messages are +# unwrapped with `state` (idempotent on plain messages) before contracting. Drop the unwrap once +# `contract_network` preserves operator operands. function vertex_scalar(factors, messages, vertex; kwargs...) in_messages = incoming_edge_data(messages, [vertex]) tensors = vcat([factors[vertex]], state.(collect(in_messages))) @@ -152,10 +154,8 @@ function vertex_scalars(factors, messages, vertices) return map(v -> vertex_scalar(factors, messages, v), vertices) end -function edge_scalar(cache, edge; kwargs...) - m1 = state(cache[edge]) - m2 = state(cache[reverse(edge)]) - return contract_network([m1, m2]; kwargs...)[] +function edge_scalar(cache, edge) + return (cache[edge] * cache[reverse(edge)])[] end edge_scalars(cache) = edge_scalars(cache, keys(cache)) From a45b5fabef19a6a5e37a961801cd678079f136f9 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 10:26:43 -0400 Subject: [PATCH 3/8] Note the operator-unwrap TODO on `contracted_message` --- src/beliefpropagation/beliefpropagation.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index e4ae1da..1dafa61 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -240,12 +240,13 @@ end end # Contract the incoming messages into the source factor to form the (unnormalized) new message on -# `edge`. `contract_network` works on plain named arrays, so operator-valued messages are unwrapped -# with `state` (idempotent on plain messages) before contracting; fermionic signs ride on the graded -# arrays, so nothing is lost. +# `edge`. function contracted_message(algorithm::SimpleMessageUpdate, cache, factors, edge) messages = collect(incoming_messages(cache, edge)) factor = factors[src(edge)] + # TODO: `contract_network` can't contract a mix of operator and plain operands right now (its + # lazy `Mul` path is invariant in the operand type), so unwrap operator-valued messages with + # `state` first. Remove the `state.` once `contract_network` handles operator operands. return contract_network([state.(messages); [factor]]; alg = algorithm.contraction_alg) end From 591d928ce34926d6f0c5cc9d9d036f12dcd749fb Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 10:28:44 -0400 Subject: [PATCH 4/8] Simplify the operator-unwrap TODO on `contracted_message` --- src/beliefpropagation/beliefpropagation.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index 1dafa61..cb9fc4d 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -244,9 +244,9 @@ end function contracted_message(algorithm::SimpleMessageUpdate, cache, factors, edge) messages = collect(incoming_messages(cache, edge)) factor = factors[src(edge)] - # TODO: `contract_network` can't contract a mix of operator and plain operands right now (its - # lazy `Mul` path is invariant in the operand type), so unwrap operator-valued messages with - # `state` first. Remove the `state.` once `contract_network` handles operator operands. + # TODO: `contract_network` can't currently contract a mix of operator and plain operands, so + # unwrap operator-valued messages with `state` first. Remove the `state.` once `contract_network` + # handles operator operands. return contract_network([state.(messages); [factor]]; alg = algorithm.contraction_alg) end From f64ed8efd6bf61ad46f80ffb373bb78a3a4e1c42 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 10:33:37 -0400 Subject: [PATCH 5/8] Rename `contracted_message` to `updated_message` --- src/beliefpropagation/beliefpropagation.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index cb9fc4d..5ded889 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -241,7 +241,7 @@ end # Contract the incoming messages into the source factor to form the (unnormalized) new message on # `edge`. -function contracted_message(algorithm::SimpleMessageUpdate, cache, factors, edge) +function updated_message(algorithm::SimpleMessageUpdate, cache, factors, edge) messages = collect(incoming_messages(cache, edge)) factor = factors[src(edge)] # TODO: `contract_network` can't currently contract a mix of operator and plain operands, so @@ -252,7 +252,7 @@ end # Single-layer network: the message is a plain bond vector, normalized by its entrywise sum. function message_update!(algorithm::SimpleMessageUpdate, cache, factors, edge) - new_message = contracted_message(algorithm, cache, factors, edge) + new_message = updated_message(algorithm, cache, factors, edge) if algorithm.normalize message_norm = sum(new_message) iszero(message_norm) || (new_message /= message_norm) @@ -267,7 +267,7 @@ end # the old message. Normalize by the trace, which is sign-correct on fermionic bonds where the # entrywise `sum` can flip the odd-parity block's sign. function message_update!(algorithm::SimpleMessageUpdate, cache, factors::NormNetwork, edge) - new_tensor = contracted_message(algorithm, cache, factors, edge) + new_tensor = updated_message(algorithm, cache, factors, edge) new_message = operator( new_tensor, linknames(KetView(factors), edge), linknames(BraView(factors), edge) ) From e2bac9fb77caa6383a9d9c05285a5043f544ab27 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 10:35:21 -0400 Subject: [PATCH 6/8] Simplify and move the operator-unwrap TODO on `vertex_scalar` --- src/beliefpropagation/messagecache.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/beliefpropagation/messagecache.jl b/src/beliefpropagation/messagecache.jl index 7b531f5..11e1942 100644 --- a/src/beliefpropagation/messagecache.jl +++ b/src/beliefpropagation/messagecache.jl @@ -136,12 +136,11 @@ function incoming_edge_data(cache::AbstractGraph, vertices) return getindices(cache, dimnames) end -# `contract_network` routes operands through the lazy `Mul` path, which is invariant in the operand -# type and currently only accepts plain arrays, so operator-valued (doubled ket/bra) messages are -# unwrapped with `state` (idempotent on plain messages) before contracting. Drop the unwrap once -# `contract_network` preserves operator operands. function vertex_scalar(factors, messages, vertex; kwargs...) in_messages = incoming_edge_data(messages, [vertex]) + # TODO: `contract_network` can't currently contract a mix of operator and plain operands, so + # unwrap operator-valued messages with `state` first. Remove the `state.` once `contract_network` + # handles operator operands. tensors = vcat([factors[vertex]], state.(collect(in_messages))) return contract_network(tensors; kwargs...)[] end From 49623ce7cd707ead9bda6bc796fa0884cb2499ac Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 10:38:47 -0400 Subject: [PATCH 7/8] Use bracket concatenation instead of `vcat` in `vertex_scalar` --- src/beliefpropagation/messagecache.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/beliefpropagation/messagecache.jl b/src/beliefpropagation/messagecache.jl index 11e1942..0cc9a25 100644 --- a/src/beliefpropagation/messagecache.jl +++ b/src/beliefpropagation/messagecache.jl @@ -141,7 +141,7 @@ function vertex_scalar(factors, messages, vertex; kwargs...) # TODO: `contract_network` can't currently contract a mix of operator and plain operands, so # unwrap operator-valued messages with `state` first. Remove the `state.` once `contract_network` # handles operator operands. - tensors = vcat([factors[vertex]], state.(collect(in_messages))) + tensors = [[factors[vertex]]; state.(collect(in_messages))] return contract_network(tensors; kwargs...)[] end From 3ecad66e94e444eb98b642f5f3ab450608db85bc Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 10:38:52 -0400 Subject: [PATCH 8/8] Import `beliefpropagation` by name in the belief-propagation tests --- test/test_beliefpropagation.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_beliefpropagation.jl b/test/test_beliefpropagation.jl index 3ee7a32..9c0d999 100644 --- a/test/test_beliefpropagation.jl +++ b/test/test_beliefpropagation.jl @@ -5,9 +5,9 @@ using GradedArrays: U1, gradedrange using Graphs: AbstractGraph, dst, edges, has_edge, src, vertices using ITensorBase: ITensor, Index, inds, name, noprime, outputnames, prime using ITensorNetworksNext: ITensorNetworksNext, ITensorNetwork, MessageCache, NormNetwork, - StopWhenConverged, bethe_free_energy, edge_scalar, incoming_messages, insertlink!, - linkinds, message_environment, messagecache, region_scalar, subgraph, tensornetwork, - vertex_scalar, vertex_scalars + StopWhenConverged, beliefpropagation, bethe_free_energy, edge_scalar, incoming_messages, + insertlink!, linkinds, message_environment, messagecache, region_scalar, subgraph, + tensornetwork, vertex_scalar, vertex_scalars using LinearAlgebra: LinearAlgebra using NamedGraphs.GraphsExtensions: all_edges, arranged_edges, incident_edges, vertextype using NamedGraphs.NamedGraphGenerators: named_comb_tree, named_grid, named_path_graph @@ -174,7 +174,7 @@ end edge => ones(T, Tuple(linkinds(tn, edge))) for edge in all_edges(g) ) - cache = ITensorNetworksNext.beliefpropagation( + cache = beliefpropagation( tn, messages; stopping_criterion = (; maxiter = 1) ) z_bp = exp(bethe_free_energy(tn, cache)) @@ -197,7 +197,7 @@ end edge => ones(T, Tuple(linkinds(tn, edge))) for edge in all_edges(g) ) - cache = ITensorNetworksNext.beliefpropagation( + cache = beliefpropagation( tn, messages; stopping_criterion = (; maxiter = 1) ) z_bp = exp(bethe_free_energy(tn, cache)) @@ -216,7 +216,7 @@ end for edge in all_edges(g) ) - cache = ITensorNetworksNext.beliefpropagation( + cache = beliefpropagation( tn, messages; stopping_criterion = (; maxiter = 10, tol = 1.0e-10) ) @@ -249,7 +249,7 @@ end end nn = NormNetwork(network) - cache = ITensorNetworksNext.beliefpropagation( + cache = beliefpropagation( nn, message_environment(one, nn); stopping_criterion = (; maxiter = 20, tol = 1.0e-10) )