From 2bbc3b252bd19c46611fda3766dd9020bc68e022 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 15:17:07 -0400 Subject: [PATCH 1/2] Contract networks mixing operators and plain tensors Promote the operands to their common type before lowering to the lazy expression, so a network mixing operators and plain tensors contracts via operator_product instead of throwing a convert MethodError. The promotion lives in ITensorBase (eager operands in ITensorBase.jl PR 235, lazy operands in PR 236), so this also drops the interim state-unwrap workarounds in belief propagation now that operator messages contract directly against the lazy norm-network factor. --- Project.toml | 6 +++- src/beliefpropagation/beliefpropagation.jl | 15 ++++---- src/beliefpropagation/messagecache.jl | 5 +-- src/contract_network.jl | 12 +++++-- test/test_contract_network.jl | 42 +++++++++++++++++++++- test/test_normnetwork.jl | 9 ++--- 6 files changed, 65 insertions(+), 24 deletions(-) diff --git a/Project.toml b/Project.toml index 1dad704..9329cea 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorNetworksNext" uuid = "302f2e75-49f0-4526-aef7-d8ba550cb06c" -version = "0.9.11" +version = "0.9.12" authors = ["ITensor developers and contributors"] [workspace] @@ -26,6 +26,10 @@ TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" [weakdeps] OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" +[sources.ITensorBase] +rev = "mf/lazy-operator-promotion" +url = "https://github.com/ITensor/ITensorBase.jl" + [extensions] ITensorNetworksNextOMEinsumContractionOrdersExt = "OMEinsumContractionOrders" diff --git a/src/beliefpropagation/beliefpropagation.jl b/src/beliefpropagation/beliefpropagation.jl index 5ded889..c0bfa4d 100644 --- a/src/beliefpropagation/beliefpropagation.jl +++ b/src/beliefpropagation/beliefpropagation.jl @@ -244,10 +244,7 @@ end 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 - # 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) + return contract_network([messages; [factor]]; alg = algorithm.contraction_alg) end # Single-layer network: the message is a plain bond vector, normalized by its entrywise sum. @@ -261,11 +258,11 @@ function message_update!(algorithm::SimpleMessageUpdate, cache, factors, edge) 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. +# `NormNetwork`: the message is a doubled (ket/bra) bond operator. Contracting a plain vertex factor +# with the incoming messages leaves the surviving bond legs dangling, so assign the ket/bra pairing +# the norm network gives this edge (the same convention as `similar_message_environment`). 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 = updated_message(algorithm, cache, factors, edge) new_message = operator( diff --git a/src/beliefpropagation/messagecache.jl b/src/beliefpropagation/messagecache.jl index 0cc9a25..0ab1ef3 100644 --- a/src/beliefpropagation/messagecache.jl +++ b/src/beliefpropagation/messagecache.jl @@ -138,10 +138,7 @@ end 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 = [[factors[vertex]]; state.(collect(in_messages))] + tensors = [[factors[vertex]]; collect(in_messages)] return contract_network(tensors; kwargs...)[] end diff --git a/src/contract_network.jl b/src/contract_network.jl index 92d69e5..a48bdda 100644 --- a/src/contract_network.jl +++ b/src/contract_network.jl @@ -26,12 +26,20 @@ function get_order(alg::Exact, tn) end # Contraction order may or may not have indices attached, canonicalize the format # by attaching indices. - subs = Dict(symnameddims(i) => symnameddims(i, Tuple(axes(tn[i]))) for i in keys(tn)) + subs = Dict(symnameddims(i) => symnameddims(i, Tuple(axes(t))) for (i, t) in pairs(tn)) return substitute(order, subs) end +# Promote the operands to their common type before lowering to the lazy expression, so every lazy +# operand shares one concrete type. Otherwise a network of mixed types (a plain tensor is a trivial +# operator, so mixing operators and plain tensors is the common case) widens the symbolic `Mul` +# container to a `UnionAll` it cannot construct. `promote_type`/`convert` keep an all-plain network +# at the plain type (the promotion is a no-op), so its fast path is unchanged. function contract_network(alg::Exact, tn) order = get_order(alg, tn) - syms_to_ts = Dict(symnameddims(i, Tuple(axes(tn[i]))) => lazy(tn[i]) for i in keys(tn)) + T = mapreduce(typeof, promote_type, tn) + syms_to_ts = Dict( + symnameddims(i, Tuple(axes(t))) => lazy(convert(T, t)) for (i, t) in pairs(tn) + ) tn_expression = substitute(order, syms_to_ts) return materialize(tn_expression) end diff --git a/test/test_contract_network.jl b/test/test_contract_network.jl index 67c65b2..e567a62 100644 --- a/test/test_contract_network.jl +++ b/test/test_contract_network.jl @@ -1,5 +1,6 @@ using Graphs: edges, vertices -using ITensorBase: Greedy, Index +using ITensorBase: + Greedy, Index, NamedTensorOperator, inputnames, operator, outputnames, state using ITensorNetworksNext: Exact, ITensorNetwork, LeftAssociative, contract_network, linkinds, siteinds, tensornetwork using NamedGraphs.GraphsExtensions: arranged_edges, incident_edges @@ -51,4 +52,43 @@ using Test: @test, @testset @test z1 ≈ z4 @test z1 ≈ z5 end + + @testset "Contract network with operators" begin + i, j, k = Index(2), Index(2), Index(2) + o = operator(randn(2, 2), (i,), (j,)) # output i, input j + + # A network mixing an operator with plain tensors previously threw a `convert` + # `MethodError`; it now contracts, stays an operator, and matches the binary product. + t = randn(2, 2)[j, k] + r = contract_network([o, t]) + @test r isa NamedTensorOperator + @test state(r) ≈ state(o * t) + @test outputnames(r) == outputnames(o * t) + @test inputnames(r) == inputnames(o * t) + + # An all-operator network is likewise preserved. + o2 = operator(randn(2, 2), (j,), (k,)) + r2 = contract_network([o, o2]) + @test r2 isa NamedTensorOperator + @test state(r2) ≈ state(o * o2) + + # A fully-contracted operator network reads out as a scalar via `[]`. + f = randn(2, 2)[i, j] + @test contract_network([o, f])[] ≈ (o * f)[] + + # An all-plain network is unaffected: it is not promoted to an operator. + a = randn(2, 2)[i, j] + b = randn(2, 2)[j, k] + @test !(contract_network([a, b]) isa NamedTensorOperator) + + # Pairing is order-independent: a branching network with a surviving output/input pair + # matches the binary product under any fold order (greedy contraction included). + ip, mp, m, x = Index(2), Index(2), Index(2), Index(2) + op = operator(randn(2, 2, 2, 2), (ip, mp), (i, m)) + u = randn(2, 2)[mp, x] + w = randn(2, 2)[m, x] + rb = contract_network([op, u, w]) + @test outputnames(rb) == outputnames((op * u) * w) == outputnames(op * (u * w)) + @test inputnames(rb) == inputnames((op * u) * w) == inputnames(op * (u * w)) + end end diff --git a/test/test_normnetwork.jl b/test/test_normnetwork.jl index 921ef9c..eb096ea 100644 --- a/test/test_normnetwork.jl +++ b/test/test_normnetwork.jl @@ -1,22 +1,17 @@ -using Base.Broadcast: materialize using DataGraphs: is_vertex_assigned using Dictionaries: isinsertable, issettable using Graphs: edges, vertices using ITensorBase: ITensor, Index, IndexName, LazyITensor, conj, inds, name, setname, uniquename using ITensorNetworksNext: BraView, ITensorNetwork, KetView, NormNetwork, braname, - bratensor, conj_bratensor, indmap, kettensor, normnetwork, tensornetwork + bratensor, conj_bratensor, contract_network, indmap, kettensor, normnetwork, + tensornetwork using LinearAlgebra: norm using NamedGraphs.GraphsExtensions: incident_edges using NamedGraphs.NamedGraphGenerators: named_grid, named_path_graph using NamedGraphs: NamedEdge using Test: @test, @test_throws, @testset -# Contract a (possibly double-layer) network into a single tensor by multiplying all -# of its vertex tensors together. For a `NormNetwork` the vertex data are lazy products -# `ket * conj(bra)`, so the result is a lazy expression that we materialize. -contract_network(tn) = materialize(prod(tn)) - # 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} From 257e09a55d651b679a5e3548ca896fe61edd3ce0 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Tue, 28 Jul 2026 21:31:10 -0400 Subject: [PATCH 2/2] Drop [sources] pin on ITensorBase now that 0.13.12 is registered --- Project.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Project.toml b/Project.toml index 9329cea..dce5a90 100644 --- a/Project.toml +++ b/Project.toml @@ -26,10 +26,6 @@ TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" [weakdeps] OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" -[sources.ITensorBase] -rev = "mf/lazy-operator-promotion" -url = "https://github.com/ITensor/ITensorBase.jl" - [extensions] ITensorNetworksNextOMEinsumContractionOrdersExt = "OMEinsumContractionOrders"