From 200ce7c8eab39f52866837ec32cc116eae453af9 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Wed, 12 Aug 2026 17:11:09 +0800 Subject: [PATCH 01/20] Approximate contraction of an iPEPO window --- src/PEPSKit.jl | 20 +- .../contractions/mpo_path/pepo_1layer.jl | 202 +++++++++++++++ .../contractions/window/pepo_1layer.jl | 178 +++++++++++++ src/algorithms/contractions/window/tools.jl | 20 ++ .../contractions/window/twosite/caching.jl | 110 ++++++++ .../window/twosite/pepo_1layer.jl | 245 ++++++++++++++++++ src/algorithms/correlator_approx.jl | 57 ++++ src/algorithms/expval_approx.jl | 61 +++++ src/operators/localoperator.jl | 20 +- src/operators/mpo_observable.jl | 185 +++++++++++++ test/toolbox/correlator_approx.jl | 98 +++++++ test/toolbox/correlator_approx_phys.jl | 39 +++ test/toolbox/expval_approx.jl | 32 +++ test/toolbox/mpo_routing.jl | 36 +++ test/types/mpo_observable.jl | 98 +++++++ 15 files changed, 1392 insertions(+), 9 deletions(-) create mode 100644 src/algorithms/contractions/mpo_path/pepo_1layer.jl create mode 100644 src/algorithms/contractions/window/pepo_1layer.jl create mode 100644 src/algorithms/contractions/window/tools.jl create mode 100644 src/algorithms/contractions/window/twosite/caching.jl create mode 100644 src/algorithms/contractions/window/twosite/pepo_1layer.jl create mode 100644 src/algorithms/correlator_approx.jl create mode 100644 src/algorithms/expval_approx.jl create mode 100644 src/operators/mpo_observable.jl create mode 100644 test/toolbox/correlator_approx.jl create mode 100644 test/toolbox/correlator_approx_phys.jl create mode 100644 test/toolbox/expval_approx.jl create mode 100644 test/toolbox/mpo_routing.jl create mode 100644 test/types/mpo_observable.jl diff --git a/src/PEPSKit.jl b/src/PEPSKit.jl index 0139d60d0..e04679871 100644 --- a/src/PEPSKit.jl +++ b/src/PEPSKit.jl @@ -28,10 +28,13 @@ using LoggingExtras import TupleTools using MPSKit -using MPSKit: MPSTensor, MPOTensor, GenericMPSTensor, MPSBondTensor, ProductTransferMatrix +using MPSKit: + MPSTensor, MPOTensor, GenericMPSTensor, MPSBondTensor, + ProductTransferMatrix, TransferMatrix using MPSKit: InfiniteEnvironments import MPSKit: tensorexpr, leading_boundary, loginit!, logiter!, logfinish!, logcancel!, physicalspace import MPSKit: infinite_temperature_density_matrix +import MPSKit: fuser import TensorKitTensors.SpinOperators as SO import TensorKitTensors.FermionOperators as FO @@ -73,6 +76,8 @@ include("operators/infinitepepo.jl") include("operators/transfermatrix.jl") include("operators/localoperator.jl") include("operators/localcircuit.jl") +include("operators/mpo_observable.jl") + include("operators/lattices/squarelattice.jl") include("operators/models.jl") @@ -107,6 +112,12 @@ include("algorithms/contractions/correlator/peps.jl") include("algorithms/contractions/correlator/pepo_purified.jl") include("algorithms/contractions/correlator/pepo_1layer.jl") +include("algorithms/contractions/mpo_path/pepo_1layer.jl") +include("algorithms/contractions/window/tools.jl") +include("algorithms/contractions/window/pepo_1layer.jl") +include("algorithms/contractions/window/twosite/caching.jl") +include("algorithms/contractions/window/twosite/pepo_1layer.jl") + include("algorithms/ctmrg/sparse_environments.jl") include("algorithms/ctmrg/ctmrg.jl") include("algorithms/ctmrg/projectors.jl") @@ -139,6 +150,8 @@ include("algorithms/transfermatrix.jl") include("algorithms/toolbox.jl") include("algorithms/correlator_adapters.jl") include("algorithms/correlators.jl") +include("algorithms/expval_approx.jl") +include("algorithms/correlator_approx.jl") include("algorithms/optimization/fixed_point_differentiation.jl") include("algorithms/optimization/peps_optimization.jl") @@ -156,9 +169,10 @@ export FixedSpaceTruncation, SiteDependentTruncation export HalfInfiniteProjector, FullInfiniteProjector export C4vCTMRG, C4vEighProjector, C4vQRProjector export initialize_random_c4v_env, initialize_singlet_c4v_env -export LocalOperator, physicalspace +export LocalOperator, MPOObservable, physicalspace export product_peps -export reduced_densitymatrix, expectation_value, network_value, cost_function +export reduced_densitymatrix, expectation_value_approx, correlator_approx +export expectation_value, network_value, cost_function export correlator, correlation_length export leading_boundary export PEPSOptimize, FixedPointGradient, GeomSum, ManualIter diff --git a/src/algorithms/contractions/mpo_path/pepo_1layer.jl b/src/algorithms/contractions/mpo_path/pepo_1layer.jl new file mode 100644 index 000000000..f45cb281c --- /dev/null +++ b/src/algorithms/contractions/mpo_path/pepo_1layer.jl @@ -0,0 +1,202 @@ +""" +Check that the physical legs of a first OBC-MPO tensor match the PEPO site's physical space. +""" +function _check_pepo_first_physicalspace(A, op) + physicalspace(A) == space(op, 1) == space(op, 2)' || + throw(SpaceMismatch("first MPO tensor physical space does not match PEPO site")) + return nothing +end + +""" +Check that the physical legs of a last OBC-MPO tensor match the PEPO site's physical space. +""" +function _check_pepo_last_physicalspace(A, op) + physicalspace(A) == space(op, 2) == space(op, 3)' || + throw(SpaceMismatch("last MPO tensor physical space does not match PEPO site")) + return nothing +end + +""" +Check that the physical legs of a middle MPO tensor match the PEPO site's physical space. +""" +function _check_pepo_middle_physicalspace(A, op) + physicalspace(A) == space(op, 2) == space(op, 3)' || + throw(SpaceMismatch("middle MPO tensor physical space does not match PEPO site")) + return nothing +end + +""" +Convert a symbolic cardinal path direction to the corresponding PEPO virtual-leg index. +""" +function _mpo_path_direction(direction::Symbol) + direction === :north && return NORTH + direction === :east && return EAST + direction === :south && return SOUTH + direction === :west && return WEST + throw(ArgumentError("invalid MPO path direction: $direction")) +end + +""" +Return the tensor-expression label for the PEPO virtual leg in a cardinal direction. +""" +function _mpo_path_virtual_label(direction::Symbol) + return (:N, :E, :S, :W)[_mpo_path_direction(direction)] +end + +""" +Build the `@tensor` labels from `(direction, suffix)` pairs +used to fuse MPO virtual strings. + +- Each direction is `:north`, `:east`, `:south`, or `:west`. +- Suffix `:l` marks an incoming MPO bond and `:r` an outgoing one. + +Examples: + +- `((:east, :r),)` produces `[W S; N Er]`. +- `((:west, :l), (:north, :r))` produces `[Wl S; Nr E]`. +""" +function _mpo_path_result_expr(directions) + labels = [:N, :E, :S, :W] + for (direction, suffix) in directions + index = _mpo_path_direction(direction) + labels[index] = Symbol(labels[index], suffix) + end + return tensorexpr(:t, (labels[WEST], labels[SOUTH]), (labels[NORTH], labels[EAST])) +end + +""" +Act the first tensor `op` of an OBC-MPO on PEPO tensor `A` and fuse the +outgoing MPO string with the virtual space of `A` along `direction`. +""" +@generated function mpo_path_first(A::PEPOTensor, op, ::Val{direction}) where {direction} + direction_index = _mpo_path_direction(direction) + virtual_label = _mpo_path_virtual_label(direction) + fused_label = Symbol(virtual_label, :r) + + result_e = _mpo_path_result_expr(((direction, :r),)) + op_e = tensorexpr(:op, :dout, (:din, :r)) + A_e = tensorexpr(:A′, (:din, :dout), (:N, :E, :S, :W)) + F_e = tensorexpr(:F, fused_label, (virtual_label, :r)) + rhs = Expr(:call, :*, op_e, A_e, F_e) + contraction = macroexpand( + @__MODULE__, :(return @tensoropt $result_e := $rhs) + ) + + return quote + _check_pepo_first_physicalspace(A, op) + A′ = twistdual(A, 2) + F = fuser(storagetype(A), domain(A, $direction_index)', space(op, 3)) + $contraction + end +end + +""" +Act the last tensor `op` of an OBC-MPO on PEPO tensor `A` and fuse the +incoming MPO string with the virtual space of `A` along `direction`. +""" +@generated function mpo_path_last(A::PEPOTensor, op, ::Val{direction}) where {direction} + direction_index = _mpo_path_direction(direction) + virtual_label = _mpo_path_virtual_label(direction) + fused_label = Symbol(virtual_label, :l) + + result_e = _mpo_path_result_expr(((direction, :l),)) + F_e = Expr(:call, :conj, tensorexpr(:F, fused_label, (virtual_label, :l))) + op_e = tensorexpr(:op, (:l, :dout), :din) + A_e = tensorexpr(:A′, (:din, :dout), (:N, :E, :S, :W)) + rhs = Expr(:call, :*, F_e, op_e, A_e) + contraction = macroexpand( + @__MODULE__, :(return @tensoropt $result_e := $rhs) + ) + + return quote + _check_pepo_last_physicalspace(A, op) + A′ = twistdual(A, 2) + F = fuser(storagetype(A), domain(A, $direction_index), space(op, 1)') + $contraction + end +end + +""" +Act the middle tensor `op` of an MPO on PEPO tensor `A` and fuse the +incoming and the outgoing MPO string with the virtual space of `A` along +`directions = (incoming, outgoing)`. +""" +@generated function mpo_path_middle( + A::PEPOTensor, op, ::Val{directions} + ) where {directions} + incoming, outgoing = directions + incoming == outgoing && + throw(ArgumentError("MPO path should enter and exit in different directions")) + + incoming_index = _mpo_path_direction(incoming) + outgoing_index = _mpo_path_direction(outgoing) + incoming_label = _mpo_path_virtual_label(incoming) + outgoing_label = _mpo_path_virtual_label(outgoing) + fused_incoming_label = Symbol(incoming_label, :l) + fused_outgoing_label = Symbol(outgoing_label, :r) + + result_e = _mpo_path_result_expr(((incoming, :l), (outgoing, :r))) + Fin_e = Expr( + :call, :conj, + tensorexpr(:Fin, fused_incoming_label, (incoming_label, :l)), + ) + op_e = tensorexpr(:op, (:l, :dout), (:din, :r)) + A_e = tensorexpr(:A′, (:din, :dout), (:N, :E, :S, :W)) + Fout_e = tensorexpr( + :Fout, fused_outgoing_label, (outgoing_label, :r) + ) + rhs = Expr(:call, :*, Fin_e, op_e, A_e, Fout_e) + contraction = macroexpand( + @__MODULE__, :(return @tensoropt $result_e := $rhs) + ) + + return quote + _check_pepo_middle_physicalspace(A, op) + A′ = twistdual(A, 2) + Fin = fuser(storagetype(A), domain(A, $incoming_index), space(op, 1)') + Fout = fuser(storagetype(A), domain(A, $outgoing_index)', space(op, 4)) + $contraction + end +end + +""" +Route an MPO virtual string with `stringspace` through a PEPO tensor `A` +along `directions = (incoming, outgoing)`. +""" +@generated function mpo_path_string( + A::PEPOTensor, stringspace::ElementarySpace, ::Val{directions} + ) where {directions} + incoming, outgoing = directions + incoming == outgoing && + throw(ArgumentError("MPO path should enter and exit in different directions")) + + incoming_index = _mpo_path_direction(incoming) + outgoing_index = _mpo_path_direction(outgoing) + incoming_label = _mpo_path_virtual_label(incoming) + outgoing_label = _mpo_path_virtual_label(outgoing) + fused_incoming_label = Symbol(incoming_label, :l) + fused_outgoing_label = Symbol(outgoing_label, :r) + + result_e = _mpo_path_result_expr(((incoming, :l), (outgoing, :r))) + Fin_e = Expr( + :call, :conj, + tensorexpr(:Fin, fused_incoming_label, (incoming_label, :l)), + ) + O_e = tensorexpr(:O, (:W, :S), (:N, :E)) + I_e = tensorexpr(:I, :l, :r) + Fout_e = tensorexpr( + :Fout, fused_outgoing_label, (outgoing_label, :r) + ) + rhs = Expr(:call, :*, Fin_e, O_e, I_e, Fout_e) + contraction = macroexpand( + @__MODULE__, :(return @tensoropt $result_e := $rhs) + ) + + return quote + O = trace_physicalspaces(A) + I = id(storagetype(A), stringspace) + Fin = fuser(storagetype(A), domain(A, $incoming_index), stringspace') + Fout = fuser(storagetype(A), domain(A, $outgoing_index)', stringspace') + $contraction + end +end diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl new file mode 100644 index 000000000..7ab2349bd --- /dev/null +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -0,0 +1,178 @@ +# Approximate finite-window contractions for single-layer PEPO networks. + +""" +Validate that the network is a single-layer PEPO and that the sweep direction is supported. +""" +function _check_window_inputs(ρ::InfinitePEPO, direction::Symbol) + size(ρ, 3) == 1 || throw(DimensionMismatch("only single-layer PEPO contractions are supported")) + direction in (:auto, :rows, :columns) || + throw(ArgumentError("invalid sweep direction: $direction")) + return nothing +end + +""" +Contract an MPO observable in its enclosing window, rotating column sweeps into row sweeps. +""" +function _expectation_value_approx( + ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv, + alg::WindowApprox, direction::Symbol, + ) + _check_window_inputs(ρ, direction) + rowrange, colrange = _window_ranges(observable) + sweep = direction === :auto ? (length(colrange) > length(rowrange) ? :rows : :columns) : direction + if sweep === :rows + return _expectation_value_approx_rows( + ρ, observable, env, rowrange, colrange, alg + ) + else + unitcell = size(ρ)[1:2] + sites = siterotl90.(observable.sites, Ref(unitcell)) + path = siterotl90.(observable.path, Ref(unitcell)) + rotated_observable = MPOObservable(sites, path, observable.mpo) + rotated_rowrange, rotated_colrange = _window_ranges(rotated_observable) + return _expectation_value_approx_rows( + rotl90(ρ), rotated_observable, rotl90(env), + rotated_rowrange, rotated_colrange, alg + ) + end +end + +""" +Build an ordinary local row-MPO tensor by tracing the PEPO physical legs. +""" +function _window_site_tensor( + ρ::InfinitePEPO, ::Nothing, row::Int, col::Int, + ) + return trace_physicalspaces(ρ[row, col, 1]) +end + +""" +Build the local row-MPO tensor at one window site, inserting the observable tensor or routed +string when the site lies on the MPO path and tracing the PEPO physical legs otherwise. +""" +function _window_site_tensor( + ρ::InfinitePEPO, observable::MPOObservable, row::Int, col::Int, + ) + A = ρ[row, col, 1] + site = CartesianIndex(row, col) + path_index = findfirst(==(site), observable.path) + isnothing(path_index) && return trace_physicalspaces(A) + + mpo_index = findfirst(==(site), observable.sites) + # sites with string passing by (cannot be first/last site) + if isnothing(mpo_index) + incoming = _step_direction(observable.path[path_index], observable.path[path_index - 1]) + outgoing = _step_direction(observable.path[path_index], observable.path[path_index + 1]) + next_mpo_index = count(in(observable.sites), @view observable.path[1:path_index]) + 1 + stringspace = space(observable.mpo[next_mpo_index], 1) + return mpo_path_string(A, stringspace, Val((incoming, outgoing))) + end + + # sites acted on by the MPO + op = observable.mpo[mpo_index] + if mpo_index == 1 + direction = _step_direction(observable.path[1], observable.path[2]) + return mpo_path_first(A, op, Val(direction)) + elseif mpo_index == length(observable.mpo) + direction = _step_direction(observable.path[end], observable.path[end - 1]) + return mpo_path_last(A, op, Val(direction)) + else + incoming = _step_direction(observable.path[path_index], observable.path[path_index - 1]) + outgoing = _step_direction(observable.path[path_index], observable.path[path_index + 1]) + return mpo_path_middle(A, op, Val((incoming, outgoing))) + end +end + +""" +Contract and normalize an MPO observable using row-oriented window boundary contractions. +""" +function _expectation_value_approx_rows( + ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv, + rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, + ) + numerator = _contract_window_rows(ρ, observable, env, rowrange, colrange, alg) + norm = _contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) + return numerator / norm +end + +""" +Apply a finite MPO to a finite MPS with zip-up truncation and optional DMRG refinement. +""" +function _approximate_window_step(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) + ψ′, = approximate((W, ψ), alg.zipup) + isnothing(alg.dmrg) && return ψ′ + ψ′, = approximate(ψ′, (W, ψ), alg.dmrg) + return ψ′ +end + +""" +Contract a complete PEPO window row by row from north to south, +optionally inserting an MPO observable. +""" +function _contract_window_rows( + ρ::InfinitePEPO, observable::Union{Nothing, MPOObservable}, + env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, + alg::WindowApprox, + ) + ψ = _north_boundary_mps(env, first(rowrange), colrange) + for row in rowrange + W = _row_mpo(ρ, observable, env, row, colrange) + ψ = _approximate_window_step(W, ψ, alg) + end + south = _south_boundary_mps(env, last(rowrange), colrange) + return dot(south, ψ) +end + +""" +Build the finite MPS representing the north CTMRG boundary of a window. +""" +function _north_boundary_mps( + env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, + ) + r = row - 1 + cmin, cmax = first(colrange), last(colrange) + first_tensor = insertleftunit(corner(env, NORTHWEST, r, cmin - 1), 1) + tensors = [first_tensor] + append!(tensors, (edge(env, NORTH, r, col) for col in colrange)) + C = permute(corner(env, NORTHEAST, r, cmax + 1), ((1, 2), ())) + push!(tensors, insertleftunit(C, 3)) + return FiniteMPS(tensors) +end + +""" +Build the finite MPS representing the adjointed south CTMRG boundary of a window. +""" +function _south_boundary_mps( + env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, + ) + r = row + 1 + cmin, cmax = first(colrange), last(colrange) + Cwest = permute(corner(env, SOUTHWEST, r, cmin - 1), ((2,), (1,))) + tensors = [insertleftunit(Cwest, 1; dual = true)] + append!(tensors, (permute(edge(env, SOUTH, r, col), ((3, 2), (1,))) for col in colrange)) + Ceast = permute(corner(env, SOUTHEAST, r, cmax + 1), ((2, 1), ())) + push!(tensors, insertleftunit(Ceast, 3; dual = true)) + return FiniteMPS(_bra_mps_tensor.(tensors)) +end + +""" +Build one finite row MPO from west/east CTMRG edges and the PEPO tensors inside the window. +""" +function _row_mpo( + ρ::InfinitePEPO, observable::Union{Nothing, MPOObservable}, + env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, + ) + cmin, cmax = first(colrange), last(colrange) + W = permute(edge(env, WEST, row, cmin - 1), ((1,), (3, 2))) + tensors = [insertleftunit(W, 1)] + append!( + tensors, + ( + _window_site_tensor(ρ, observable, row, col) + for col in colrange + ), + ) + E = permute(edge(env, EAST, row, cmax + 1), ((2, 3), (1,))) + push!(tensors, insertrightunit(E, 3)) + return FiniteMPO(tensors) +end diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl new file mode 100644 index 000000000..15e3416b4 --- /dev/null +++ b/src/algorithms/contractions/window/tools.jl @@ -0,0 +1,20 @@ +""" +Bundle the zip-up contraction and optional DMRG refinement +algorithms used after each finite window MPO-MPS contraction. +""" +struct WindowApprox{Z, D} + zipup::Z + dmrg::D +end + +""" +Convert a south-boundary MPS tensor into the stored bra representation expected by `dot`. +""" +_bra_mps_tensor(A::GenericMPSTensor) = copy(permute(A', ((2, 3), (1,)))) + +""" +Construct the adjoint of a finite MPO while restoring MPSKit's local MPO leg partition. +""" +function _adjoint_mpo(W::FiniteMPO) + return FiniteMPO(map(A -> permute(A', ((3, 1), (4, 2))), parent(W))) +end diff --git a/src/algorithms/contractions/window/twosite/caching.jl b/src/algorithms/contractions/window/twosite/caching.jl new file mode 100644 index 000000000..27ea864e9 --- /dev/null +++ b/src/algorithms/contractions/window/twosite/caching.jl @@ -0,0 +1,110 @@ +""" +Put 2-site bonds in groups to reuse partial contractions in `correlator_approx`. + +For every bond `(first_site, second_site)` in `bonds`: + +- After ordering the two sites, `source`/`target` is the first/second site. +- `swapped` is `false` when `source == first_site`, and `true` otherwise. +- Bonds with the same `source` and `swapped` are grouped together. +- In each group, the inner dict records each bond's position in `bonds`. + +For example, the ordered bonds + +```julia +CI = CartesianIndex +bonds = [ + (CI(1, 1), CI(1, 3)), + (CI(1, 1), CI(2, 2)), + (CI(1, 2), CI(2, 2)), # another source site + (CI(2, 2), CI(1, 1)), # reversed site order +] +``` + +are grouped as + +```julia +(CI(1, 1), false) => Dict( + CI(1, 3) => 1, + CI(2, 2) => 2, +) +(CI(1, 1), true) => Dict(CI(2, 2) => 4) +(CI(1, 2), false) => Dict(CI(2, 2) => 3) +``` +""" +function _twosite_source_groups( + bonds::Vector{NTuple{2, CartesianIndex{2}}}, + ) + groups = Dict{ + Tuple{CartesianIndex{2}, Bool}, + Dict{CartesianIndex{2}, Int}, + }() + for (i, (first_site, second_site)) in enumerate(bonds) + swapped = !issorted((first_site, second_site); by = Tuple) + source, target = swapped ? (second_site, first_site) : (first_site, second_site) + targets = get!(Dict{CartesianIndex{2}, Int}, groups, (source, swapped)) + targets[target] = i + end + return groups +end + +""" +Group the targets associated with one source by their row coordinate. The returned outer +dictionary maps each target row to a dictionary whose entries retain the original +`target => result_position` mapping. + +This lookup lets the source contraction close all targets in the current row together while +propagating a single open MPO string between rows. +""" +function _twosite_targets_by_row(targets::Dict{CartesianIndex{2}, Int}) + targets_by_row = Dict{Int, Dict{CartesianIndex{2}, Int}}() + for (target, position) in targets + row_targets = get!(Dict{CartesianIndex{2}, Int}, targets_by_row, target[1]) + row_targets[target] = position + end + return targets_by_row +end + +""" +Precompute the row MPOs (without observables) and +north/south boundary contractions reused when measuring on +many two-site bonds in one window. + +The returned named tuple contains: + +- `rowrange` and `colrange`: the coordinate ranges defining the window. +- `row_mpos`: the ordinary finite MPO for each row, including the west and east CTMRG edges. +- `north_prefixes`: `nrows + 1` north boundary MPSs. Entry `k` is above row `k` in the window. +- `south_suffixes`: `nrows + 1` adjointed south boundary MPSs. Entry `k + 1` is below row `k` in the window. +- `norm`: the approximate contraction of the window with no observable inserted. +""" +function _window_row_cache( + ρ::InfinitePEPO, env::CTMRGEnv, + rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, + ) + row_mpos = Dict( + row => _row_mpo(ρ, nothing, env, row, colrange) + for row in rowrange + ) + nrows = length(rowrange) + north = _north_boundary_mps(env, first(rowrange), colrange) + south = _south_boundary_mps(env, last(rowrange), colrange) + + north_prefixes = Vector{typeof(north)}(undef, nrows + 1) + north_prefixes[1] = north + for (k, row) in enumerate(rowrange) + north_prefixes[k + 1] = _approximate_window_step( + row_mpos[row], north_prefixes[k], alg + ) + end + + south_suffixes = Vector{typeof(south)}(undef, nrows + 1) + south_suffixes[end] = south + for (k, row) in Iterators.reverse(enumerate(rowrange)) + W = _adjoint_mpo(row_mpos[row]) + south_suffixes[k] = _approximate_window_step( + W, south_suffixes[k + 1], alg + ) + end + norm = dot(south, north_prefixes[end]) + return (; rowrange, colrange, row_mpos, north_prefixes, south_suffixes, norm) +end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl new file mode 100644 index 000000000..33656f856 --- /dev/null +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -0,0 +1,245 @@ +# Source-cached dense two-site contractions for single-layer PEPO networks. + +""" +Validate a dense two-site measurement, choose its sweep orientation, and dispatch to the +row-oriented source-cached contraction. +""" +function _correlator_approx( + ρ::InfinitePEPO, op::AbstractTensorMap, + bonds::Vector{NTuple{2, CartesianIndex{2}}}, + env::CTMRGEnv, alg::WindowApprox, direction::Symbol, + ) + _check_window_inputs(ρ, direction) + numout(op) == numin(op) == 2 || + throw(ArgumentError("correlator_approx requires a two-site operator")) + for bond in bonds, (i, site) in enumerate(bond) + V = physicalspace(ρ, Tuple(site)...) + V == codomain(op)[i] == domain(op)[i] || + throw(SpaceMismatch("operator physical space does not match PEPO site $site")) + end + + rowrange, colrange = _window_ranges(bonds) + sweep = direction === :auto ? (length(colrange) > length(rowrange) ? :rows : :columns) : direction + if sweep === :rows + return _correlator_approx_rows( + ρ, op, bonds, env, rowrange, colrange, alg + ) + end + # rotate column-wise contraction to reuse row-wise code + unitcell = size(ρ)[1:2] + rotated_bonds = map(bonds) do bond + return (siterotl90(bond[1], unitcell), siterotl90(bond[2], unitcell)) + end + rotated_rowrange, rotated_colrange = _window_ranges(rotated_bonds) + return _correlator_approx_rows( + rotl90(ρ), op, rotated_bonds, rotl90(env), + rotated_rowrange, rotated_colrange, alg + ) +end + +""" +Measure all ordered bonds in one row-oriented window using shared ordinary boundaries and +one exactly decomposed MPO for each operator-leg ordering. +""" +function _correlator_approx_rows( + ρ::InfinitePEPO, op::AbstractTensorMap, + bonds::Vector{NTuple{2, CartesianIndex{2}}}, env::CTMRGEnv, + rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, + ) + cache = _window_row_cache(ρ, env, rowrange, colrange, alg) + groups = _twosite_source_groups(bonds) + mpo = gate_to_mpo(op; trunc = notrunc()) + swapped_mpo = if any(key[2] for key in keys(groups)) + swapped_op = permute(op, ((2, 1), (4, 3))) + gate_to_mpo(swapped_op; trunc = notrunc()) + end + + T = promote_type(scalartype(op), typeof(cache.norm)) + numerators = zeros(T, length(bonds)) + for ((source, swapped), targets) in groups + source_mpo = swapped ? something(swapped_mpo) : mpo + _contract_twosite_source!( + numerators, ρ, source_mpo, source, targets, env, cache, alg, + ) + end + return numerators ./ cache.norm +end + +""" +Contract the correlator numerator for all targets associated with the same source +and one ordering of the dense operator, writing each result into `numerators`. +""" +function _contract_twosite_source!( + numerators::Vector{<:Number}, ρ::InfinitePEPO, mpo, + source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, + env::CTMRGEnv, cache, alg::WindowApprox, + ) + # grouping targets by which row they are in + targets_by_row = _twosite_targets_by_row(targets) + source_idx = source[1] - first(cache.rowrange) + 1 + north = cache.north_prefixes[source_idx] + + # Close targets in the same row as the source + if haskey(targets_by_row, source[1]) + _contract_twosite_target_row!( + numerators, ρ, mpo, source, targets_by_row[source[1]], north, cache + ) + end + last_target_row = maximum(keys(targets_by_row)) + last_target_row == source[1] && return numerators + + # Open the MPO string toward the south for targets in later rows. + A = ρ[source[1], source[2], 1] + source_tensor = mpo_path_first(A, mpo[1], Val(:south)) + W = _row_mpo_with_site(ρ, source_tensor, env, source[1], source[2], cache.colrange) + north = _approximate_window_step(W, north, alg) + + stringspace = space(mpo[2], 1) + for row in (source[1] + 1):last_target_row + # Close every target in this row + if haskey(targets_by_row, row) + _contract_twosite_target_row!( + numerators, ρ, mpo, source, targets_by_row[row], north, cache + ) + end + row == last_target_row && break + # Carry the open string down to the next row + A = ρ[row, source[2], 1] + string_tensor = mpo_path_string(A, stringspace, Val((:north, :south))) + W = _row_mpo_with_site(ρ, string_tensor, env, row, source[2], cache.colrange) + north = _approximate_window_step(W, north, alg) + end + return numerators +end + +""" +Contract the correlator numerator for all targets in one row with a +shared open-string north state, writing the results into `numerators`. +""" +function _contract_twosite_target_row!( + numerators::Vector{<:Number}, ρ::InfinitePEPO, mpo, + source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, + north, cache + ) + row = first(keys(targets))[1] + row_idx = row - first(cache.rowrange) + 1 + south = cache.south_suffixes[row_idx + 1] + envs = environments(south, cache.row_mpos[row], north) + source_site = _window_mps_site(source[2], cache.colrange) + stringspace = space(mpo[2], 1) + + # close the target right at the column of the incoming string + same_col = get(targets, CartesianIndex(row, source[2]), nothing) + if !isnothing(same_col) + target_tensor = mpo_path_last(ρ[row, source[2], 1], mpo[2], Val(:north)) + value = _contract_window_site(envs, north, south, source_site, target_tensor) + numerators[same_col] = value + end + + # Close targets on the right of the incoming string from left to right + right_targets = [target for target in keys(targets) if target[2] > source[2]] + if !isempty(right_targets) + sort!(right_targets; by = x -> x[2]) + A = ρ[row, source[2], 1] + source_tensor = if row == source[1] + mpo_path_first(A, mpo[1], Val(:east)) + else + mpo_path_string(A, stringspace, Val((:north, :east))) + end + left = leftenv(envs, source_site, south) * + TransferMatrix(north.AC[source_site], source_tensor, south.AC[source_site]) + previous_col = source[2] + for target in right_targets + target_col = target[2] + for col in (previous_col + 1):(target_col - 1) + site = _window_mps_site(col, cache.colrange) + string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:west, :east))) + left = left * TransferMatrix(north.AR[site], string_tensor, south.AR[site]) + end + + target_site = _window_mps_site(target_col, cache.colrange) + target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:west)) + target_left = left * TransferMatrix(north.AR[target_site], target_tensor, south.AR[target_site]) + value = _contract_transfer_boundaries(target_left, rightenv(envs, target_site, south)) + numerators[targets[target]] = value + + string_tensor = mpo_path_string(ρ[row, target_col, 1], stringspace, Val((:west, :east))) + left = left * TransferMatrix(north.AR[target_site], string_tensor, south.AR[target_site]) + previous_col = target_col + end + end + + # Close targets on the left of the incoming string from right to left + left_targets = [target for target in keys(targets) if target[2] < source[2]] + if !isempty(left_targets) + sort!(left_targets; by = x -> x[2], rev = true) + A = ρ[row, source[2], 1] + source_tensor = mpo_path_string(A, stringspace, Val((:north, :west))) + right = TransferMatrix( + north.AC[source_site], source_tensor, south.AC[source_site] + ) * rightenv(envs, source_site, south) + previous_col = source[2] + for target in left_targets + target_col = target[2] + for col in (previous_col - 1):-1:(target_col + 1) + site = _window_mps_site(col, cache.colrange) + string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:east, :west))) + right = TransferMatrix(north.AL[site], string_tensor, south.AL[site]) * right + end + + target_site = _window_mps_site(target_col, cache.colrange) + target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:east)) + target_right = TransferMatrix(north.AL[target_site], target_tensor, south.AL[target_site]) * right + value = _contract_transfer_boundaries(leftenv(envs, target_site, south), target_right) + numerators[targets[target]] = value + + string_tensor = mpo_path_string(ρ[row, target_col, 1], stringspace, Val((:east, :west))) + right = TransferMatrix(north.AL[target_site], string_tensor, south.AL[target_site]) * right + previous_col = target_col + end + end + return numerators +end + +""" +Map a PEPO column coordinate to its finite-MPS site, +which includes an additional west edge CTM tensor. +""" +_window_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 + +""" +Build an ordinary finite row MPO with one PEPO site tensor replaced by a supplied tensor. +""" +function _row_mpo_with_site( + ρ::InfinitePEPO, tensor, env::CTMRGEnv, + row::Int, col::Int, colrange::UnitRange{Int}, + ) + W = _row_mpo(ρ, nothing, env, row, colrange) + parent(W)[_window_mps_site(col, colrange)] = tensor + return W +end + +""" +Contract one modified row site between precomputed left and right MPS environments. +""" +function _contract_window_site(envs, north, south, site::Int, tensor) + left = leftenv(envs, site, south) * + TransferMatrix(north.AC[site], tensor, south.AC[site]) + return _contract_transfer_boundaries(left, rightenv(envs, site, south)) +end + +""" +Contract the left and right transfer-matrix environments to a scalar. +``` + (north) + ┌---- 3 ----┐ + | | + L---- 2 ----R + | | + └---- 1 ----┘ + (south) +``` +""" +function _contract_transfer_boundaries(left, right) + return @tensoropt left[1 2; 3] * right[3 2; 1] +end diff --git a/src/algorithms/correlator_approx.jl b/src/algorithms/correlator_approx.jl new file mode 100644 index 000000000..8ac9adcaf --- /dev/null +++ b/src/algorithms/correlator_approx.jl @@ -0,0 +1,57 @@ +# Approximate finite-window two-site correlators +# ---------------------------------------------- + +""" +$(SIGNATURES) + +Approximately measure a dense two-site operator on one or more ordered pairs of sites in a +single-layer PEPO. The first and second operator legs act on the first and second sites of +each pair, respectively. Multiple pairs are evaluated in one shared window and reuse +open-string boundary contractions. Ordered pairs in a batched call must be unique. +""" +function correlator_approx( + ρ::InfinitePEPO, op::AbstractTensorMap, bond::Tuple, env::CTMRGEnv; + trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, + ) + return only( + correlator_approx(ρ, op, [bond], env; trunc, maxiter, direction) + ) +end + +function correlator_approx( + ρ::InfinitePEPO, op::AbstractTensorMap, bonds::AbstractVector, + env::CTMRGEnv; + trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, + ) + bonds′ = _approx_twosite_bonds(bonds) + return _correlator_approx( + ρ, op, bonds′, env, + WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction + ) +end + +""" +Validate and regularize a nonempty collection of ordered two-site bonds to Cartesian indices. +""" +function _approx_twosite_bonds(bonds) + isempty(bonds) && throw(ArgumentError("correlator_approx requires at least one bond")) + bonds′ = NTuple{2, CartesianIndex{2}}[] + sizehint!(bonds′, length(bonds)) + for bond in bonds + length(bond) == 2 || throw(ArgumentError("each bond should contain two sites")) + first_site = _mpo_observable_site(bond[1]) + second_site = _mpo_observable_site(bond[2]) + first_site != second_site || + throw(ArgumentError("the sites of a bond should be distinct")) + push!(bonds′, (first_site, second_site)) + end + allunique(bonds′) || throw(ArgumentError("bonds should be unique")) + return bonds′ +end + +""" +Return the row and column ranges enclosing every endpoint in a collection of bonds. +""" +function _window_ranges(bonds::Vector{NTuple{2, CartesianIndex{2}}}) + return _window_ranges(Iterators.flatten(bonds)) +end diff --git a/src/algorithms/expval_approx.jl b/src/algorithms/expval_approx.jl new file mode 100644 index 000000000..5698eec79 --- /dev/null +++ b/src/algorithms/expval_approx.jl @@ -0,0 +1,61 @@ +# Approximate finite-window expectation values +# -------------------------------------------- + +""" +$(SIGNATURES) + +Approximately measure the expectation value of an open-boundary `MPOObservable` in a +single-layer PEPO using finite boundary MPS/MPO zipup sweeps. The zipup truncation is +controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary +dimension. After each zipup step, the result is refined by a single-site DMRG approximation +step with `maxiter` sweeps. Set `maxiter = 0` to disable this refinement. +""" +function expectation_value_approx( + ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv; + trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, + ) + return _expectation_value_approx( + ρ, observable, env, + WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction + ) +end + +""" +Return the row and column ranges enclosing an MPO observable's complete routed path. +""" +function _window_ranges(observable::MPOObservable) + return _window_ranges(observable.path) +end + +""" +Return the smallest row and column ranges containing a collection of lattice sites. +""" +function _window_ranges(sites) + rows = getindex.(sites, 1) + cols = getindex.(sites, 2) + return UnitRange(extrema(rows)...), UnitRange(extrema(cols)...) +end + +""" +Return the largest CTMRG boundary-space dimension appearing in the corner tensors. +""" +function _ctmrg_boundary_chi(env::CTMRGEnv) + χ = 0 + for C in env.corners + χ = max(χ, dim(space(C, 1)), dim(space(C, 2))) + end + return χ +end + +""" +Construct the default rank truncation from the largest CTMRG boundary dimension. +""" +_approx_trunc(env::CTMRGEnv) = truncrank(_ctmrg_boundary_chi(env)) + +""" +Construct the optional one-site DMRG refinement, or disable refinement for zero iterations. +""" +function _approx_dmrg(maxiter::Int) + maxiter >= 0 || throw(ArgumentError("maxiter should be nonnegative")) + return iszero(maxiter) ? nothing : DMRG(; maxiter, verbosity = 0) +end diff --git a/src/operators/localoperator.jl b/src/operators/localoperator.jl index 83c1a8ff1..652f39f1b 100644 --- a/src/operators/localoperator.jl +++ b/src/operators/localoperator.jl @@ -50,6 +50,19 @@ end # Default to Any for eltype: needs to be abstract anyways so not that much to gain LocalOperator(lattice, terms) = LocalOperator{Any}(lattice, terms) LocalOperator(lattice, terms::Pair...) = LocalOperator(lattice, terms) + +""" +Sort operator sites using the default `CartesianIndex` ordering. When the order changes, +permute the corresponding output and input physical legs by the same ordering. +""" +function _sort_op_sites(sites::Vector{CartesianIndex{2}}, op::AbstractTensorMap) + issorted(sites) && return sites, op + order = sortperm(sites) + sites′ = sites[order] + op′ = permute(op, (Tuple(order), Tuple(order) .+ numout(op))) + return sites′, op′ +end + # TODO: add terms beyond AbstractTensorMap # e.g. tensor product of 1-site operators, MPOs add_term!(operator::LocalOperator, inds::Tuple, term::AbstractTensorMap) = add_term!(operator, collect(inds), term) @@ -68,12 +81,7 @@ function add_term!( end norm(term) <= atol && return operator # skip adding negligible terms - # permute input - if !issorted(inds) - I = sortperm(inds) - inds = inds[I] - term = permute(term, (Tuple(I), Tuple(I) .+ numout(term))) - end + inds, term = _sort_op_sites(inds, term) # translate coordinates _shift_into_unitcell!(inds, size(operator)) diff --git a/src/operators/mpo_observable.jl b/src/operators/mpo_observable.jl new file mode 100644 index 000000000..561200f3c --- /dev/null +++ b/src/operators/mpo_observable.jl @@ -0,0 +1,185 @@ +""" +$(TYPEDEF) + +An open-boundary MPO embedded along a non-self-intersecting nearest-neighbor path on the +square lattice. The tensor `mpo[k]` acts on `sites[k]`; `path` also contains intermediate +sites that only carry the MPO string. + +The first and last MPO tensors use the reduced endpoint partitions `(1, 2)` and `(2, 1)`; +all intermediate tensors use the standard `(2, 2)` MPO partition. +""" +struct MPOObservable{M} + sites::Vector{CartesianIndex{2}} + path::Vector{CartesianIndex{2}} + mpo::Vector{M} + + function MPOObservable( + sites::Vector{CartesianIndex{2}}, path::Vector{CartesianIndex{2}}, + mpo::Vector{M}, + ) where {M} + _validate_mpo_observable(sites, path, mpo) + return new{M}(sites, path, mpo) + end +end + +""" +Construct an MPO observable from ordered operator sites and MPO tensors. The tensor `mpo[k]` +acts on `sites[k]`; consecutive sites are connected by horizontal-first shortest paths. +""" +function MPOObservable(sites, mpo) + sites′ = CartesianIndex{2}[_mpo_observable_site(site) for site in sites] + mpo′ = collect(mpo) + path = _route_mpo_observable(sites′) + return MPOObservable(sites′, path, mpo′) +end + +""" +Construct an MPO observable from a dense operator. Operator sites are ordered in the same +way as `LocalOperator` terms, and consecutive sites are connected by horizontal-first +shortest paths. The lattice is a periodically indexed matrix of elementary physical spaces, +as for `LocalOperator`. +""" +function MPOObservable( + sites, op::AbstractTensorMap, lattice::Matrix{<:ElementarySpace}; + trunc = trunctol(; atol = MPSKit.Defaults.tol), + ) + sites′ = CartesianIndex{2}[_mpo_observable_site(site) for site in sites] + length(sites′) >= 2 || throw(ArgumentError("an MPO observable requires at least two sites")) + allunique(sites′) || throw(ArgumentError("operator sites should be unique")) + length(sites′) == numin(op) == numout(op) || + throw(ArgumentError("number of operator legs should match the number of sites")) + + Nr, Nc = size(lattice) + sites′, op′ = _sort_op_sites(sites′, op) + for (i, site) in enumerate(sites′) + V = lattice[mod1(site[1], Nr), mod1(site[2], Nc)] + V == domain(op′)[i] == codomain(op′)[i] || + throw(SpaceMismatch("operator physical space does not match lattice site $site")) + end + + mpo = gate_to_mpo(op′; trunc) + length(mpo) == length(sites′) || + throw(ArgumentError("expected an MPO decomposition matching the number of sites")) + return MPOObservable(sites′, mpo) +end + +""" +Normalize a supported lattice-site representation to a two-dimensional Cartesian index. +""" +_mpo_observable_site(site::CartesianIndex{2}) = site +_mpo_observable_site(site::Tuple{Int, Int}) = CartesianIndex(site) +_mpo_observable_site(site) = + throw(ArgumentError("MPO sites should be CartesianIndex{2} or (row, col) tuples")) + +""" +Validate the topology, tensor partitions, physical placement, and adjacent string spaces of +an already routed open-boundary MPO observable. +""" +function _validate_mpo_observable(sites, path, mpo) + length(sites) >= 2 || throw(ArgumentError("an MPO observable requires at least two sites")) + length(sites) == length(mpo) || + throw(ArgumentError("the MPO should contain one tensor for every operator site")) + allunique(sites) || throw(ArgumentError("operator sites should be unique")) + allunique(path) || throw(ArgumentError("the MPO path should not intersect itself")) + all(op -> op isa AbstractTensorMap, mpo) || + throw(ArgumentError("all MPO entries should be tensor maps")) + + for (from, to) in zip(path, Iterators.drop(path, 1)) + _step_direction(from, to) + end + path_positions = indexin(sites, path) + all(!isnothing, path_positions) && issorted(path_positions) || + throw(ArgumentError("the MPO path should contain operator sites in MPO order")) + first(path_positions) == 1 && last(path_positions) == length(path) || + throw(ArgumentError("the MPO path should start and end at operator sites")) + + numout(first(mpo)) == 1 && numin(first(mpo)) == 2 || + throw(ArgumentError("the first MPO tensor should have partition (1, 2)")) + numout(last(mpo)) == 2 && numin(last(mpo)) == 1 || + throw(ArgumentError("the last MPO tensor should have partition (2, 1)")) + for op in @view mpo[2:(end - 1)] + numout(op) == 2 && numin(op) == 2 || + throw(ArgumentError("middle MPO tensors should have partition (2, 2)")) + end + for k in 1:(length(mpo) - 1) + _mpo_right_stringspace(mpo[k])' == space(mpo[k + 1], 1) || + throw(SpaceMismatch("incompatible MPO string spaces between sites $k and $(k + 1)")) + end + return nothing +end + +function VI.scalartype(observable::MPOObservable) + return promote_type((scalartype(op) for op in observable.mpo)...) +end + +""" +Connect ordered operator sites by horizontal-first shortest paths while rejecting crossings +and routes that pass through later operator sites. +""" +function _route_mpo_observable(sites) + length(sites) >= 2 || throw(ArgumentError("an MPO observable requires at least two sites")) + allunique(sites) || throw(ArgumentError("operator sites should be unique")) + + path = CartesianIndex{2}[first(sites)] + measured = Set(sites) + visited = Set(path) + + for k in 1:(length(sites) - 1) + segment = _l_path(sites[k], sites[k + 1]) + + for site in @view segment[2:(end - 1)] + site in measured && + throw(ArgumentError("MPO path passes through another operator site")) + site in visited && + throw(ArgumentError("MPO path intersects itself at site $site")) + push!(path, site) + push!(visited, site) + end + + site = last(segment) + site in visited && throw(ArgumentError("MPO path intersects itself at site $site")) + push!(path, site) + push!(visited, site) + end + return path +end + +""" +Return the right MPO string space in the local tensor's stored leg orientation. +""" +_mpo_right_stringspace(op) = space(op, numind(op)) + +""" +Build a deterministic shortest nearest-neighbor path between two sites. The path traverses +the horizontal separation first and then the vertical separation. +""" +function _l_path(start::CartesianIndex{2}, stop::CartesianIndex{2}) + start == stop && throw(ArgumentError("MPO path sites should be unique")) + + path = CartesianIndex{2}[start] + row, col = Tuple(start) + stoprow, stopcol = Tuple(stop) + + while col != stopcol + col += sign(stopcol - col) + push!(path, CartesianIndex(row, col)) + end + while row != stoprow + row += sign(stoprow - row) + push!(path, CartesianIndex(row, col)) + end + return path +end + +""" +Return the cardinal direction of a nearest-neighbor step from one site to another. Throw an +error when the sites are not nearest neighbors. +""" +function _step_direction(from::CartesianIndex{2}, to::CartesianIndex{2}) + delta = to - from + delta == CartesianIndex(0, 1) && return :east + delta == CartesianIndex(0, -1) && return :west + delta == CartesianIndex(1, 0) && return :south + delta == CartesianIndex(-1, 0) && return :north + throw(ArgumentError("MPO path should use nearest-neighbor steps")) +end diff --git a/test/toolbox/correlator_approx.jl b/test/toolbox/correlator_approx.jl new file mode 100644 index 000000000..d74753bb6 --- /dev/null +++ b/test/toolbox/correlator_approx.jl @@ -0,0 +1,98 @@ +using TensorKit +using PEPSKit +using MPSKit +using Test +using Random + +const CI = CartesianIndex + +""" +Contract `⟨op⟩` on each bond in `bonds` independently without caching +""" +function _shared_window_reference( + op::AbstractTensorMap, bonds::Vector{NTuple{2, CI{2}}}, ρ, env + ) + lattice = physicalspace(ρ) + observables = [MPOObservable(collect(pair), op, lattice) for pair in bonds] + rowrange, colrange = PEPSKit._window_ranges(bonds) + alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) + norm = PEPSKit._contract_window_rows( + ρ, nothing, env, rowrange, colrange, alg + ) + return map(observables) do observable + numerator = PEPSKit._contract_window_rows( + ρ, observable, env, rowrange, colrange, alg + ) + return numerator / norm + end +end + +bonds = [ + # source at (1, 1), bond sites in order + ## target in the same row as source + (CI(1, 1), CI(1, 2)), + ## target in a later row, on both sides of or just below the source + (CI(1, 1), CI(2, 0)), (CI(1, 1), CI(2, 1)), (CI(1, 1), CI(2, 2)), + # source still at (1, 1), but bond sites need swapping + (CI(2, 2), CI(1, 1)), + # another source at (1, 2), bond sites in order + (CI(1, 2), CI(2, 0)), (CI(1, 2), CI(2, 2)), +] + +@testset "Two-site source grouping" begin + groups = PEPSKit._twosite_source_groups(bonds) + @test length(groups) == 3 && + groups[(CI(1, 1), false)][CI(2, 2)] == 4 && + haskey(groups, (CI(1, 1), true)) +end + +@testset "Single-layer PEPO" begin + Random.seed!(1234) + + d = ℂ^2 + D = ℂ^3 + χ = ℂ^4 + ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) + lattice = physicalspace(ρ) + env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) + trunc = notrunc() + + O² = rand(ComplexF64, d^2, d^2) + id² = isomorphism(d, d) ⊗ isomorphism(d, d) + + # bonds to be measured should be unique + @test_throws ArgumentError correlator_approx(ρ, O², fill(bonds[3], 2), env) + + exact_same_row = expectation_value(ρ, LocalOperator(lattice, bonds[1] => O²), env) + @test correlator_approx( + ρ, O², bonds[1], env; trunc, maxiter = 0, direction = :rows + ) ≈ exact_same_row + + exact_reversed = expectation_value(ρ, LocalOperator(lattice, bonds[5] => O²), env) + @test correlator_approx( + ρ, O², bonds[5], env; trunc, maxiter = 0, direction = :columns + ) ≈ exact_reversed + + vals_ref = _shared_window_reference(O², bonds, ρ, env) + vals_rows = correlator_approx( + ρ, O², bonds, env; trunc, maxiter = 0, direction = :rows + ) + @test vals_rows ≈ vals_ref + vals_columns = correlator_approx( + ρ, O², bonds, env; trunc, maxiter = 0, direction = :columns + ) + @test vals_columns ≈ vals_rows + vals_auto = correlator_approx( + ρ, O², bonds, env; trunc, maxiter = 0, direction = :auto + ) + @test vals_auto ≈ vals_columns + + @test correlator_approx(ρ, id², bonds, env; trunc, direction = :rows) ≈ + ones(length(bonds)) + + alg = PEPSKit.WindowApprox(Zipup(; trunc), nothing) + cache = PEPSKit._window_row_cache(ρ, env, 1:2, 1:2, alg) + @test all(eachindex(cache.north_prefixes)) do k + dot(cache.south_suffixes[k], cache.north_prefixes[k]) ≈ cache.norm + end +end diff --git a/test/toolbox/correlator_approx_phys.jl b/test/toolbox/correlator_approx_phys.jl new file mode 100644 index 000000000..62aad5ea7 --- /dev/null +++ b/test/toolbox/correlator_approx_phys.jl @@ -0,0 +1,39 @@ +using TensorKit +using PEPSKit +using Test +using TensorKitTensors.SpinOperators: S_exchange + +const CI = CartesianIndex + +@testset "correlator_approx for physical state" begin + Nr, Nc = 2, 2 + lattice = InfiniteSquare(Nr, Nc) + sym = Trivial + ham = j1_j2_model(Float64, sym, lattice; J1 = 1.0, J2 = 0.0, sublattice = false) + op = S_exchange(Float64, sym) + lattice = physicalspace(ham) + + ρ = PEPSKit.infinite_temperature_density_matrix(ham) + state_trunc = truncrank(4) & truncerror(; atol = 1.0e-12) + su_alg = SimpleUpdate(; trunc = state_trunc, purified = false) + ρ, = time_evolve(ρ, ham, 1.0e-2, 50, su_alg, SUWeight(ρ)) + + network = InfinitePartitionFunction(ρ) + env = initialize_ctmrg_environment(network, ProductStateInitialization()) + env_trunc = truncrank(8) & truncerror(; atol = 1.0e-12) + env, = leading_boundary(env, network; alg = :SequentialCTMRG, trunc = env_trunc) + + bonds = [ + (CI(1, 1), CI(1, 2)), + (CI(1, 1), CI(2, 0)), (CI(1, 1), CI(2, 1)), (CI(1, 1), CI(2, 2)), + (CI(1, 1), CI(3, 2)), + ] + cor_exact = map(bonds) do bond + O = LocalOperator(lattice, bond => op) + return expectation_value(ρ, O, env) + end + cor_trunc = correlator_approx(ρ, op, bonds, env; trunc = env_trunc, maxiter = 1) + @info "Exact:" cor_exact + @info "Approx:" cor_trunc + @test cor_trunc ≈ cor_exact rtol = 1.0e-3 +end diff --git a/test/toolbox/expval_approx.jl b/test/toolbox/expval_approx.jl new file mode 100644 index 000000000..0854f424f --- /dev/null +++ b/test/toolbox/expval_approx.jl @@ -0,0 +1,32 @@ +using TensorKit +using PEPSKit +using MPSKit +using Test +using Random + +@testset "Approximate single-layer PEPO expectation values" begin + Random.seed!(1234) + + d = ℂ^2 + D = ℂ^2 + χ = ℂ^3 + ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) + env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) + trunc = notrunc() + + # U-shaped path, passing twice between two rows + sites = CartesianIndex.([(2, 1), (1, 1), (1, 2), (2, 2)]) + V = d^length(sites) + op = rand(ComplexF64, V, V) + mpo = PEPSKit.gate_to_mpo(op; trunc) + observable = MPOObservable(sites, mpo) + exact = expectation_value( + ρ, LocalOperator(physicalspace(ρ), sites => op), env + ) + + for direction in (:rows, :columns) + @test expectation_value_approx( + ρ, observable, env; trunc, maxiter = 0, direction + ) ≈ exact + end +end diff --git a/test/toolbox/mpo_routing.jl b/test/toolbox/mpo_routing.jl new file mode 100644 index 000000000..c4adb648f --- /dev/null +++ b/test/toolbox/mpo_routing.jl @@ -0,0 +1,36 @@ +using PEPSKit +using TensorKit +using Test +using Random + +const directions = (:north, :east, :south, :west) + +@testset "MPO path routing on PEPO" begin + Random.seed!(1234) + + d = Rep[U₁](0 => 1, 1 => 1) + D = Rep[U₁](0 => 1, 1 => 1, -1 => 1) + ρ = InfinitePEPO(d, D; unitcell = (1, 1, 1)) + A = ρ[1, 1, 1] + + op = rand(ComplexF64, d^2 → d^2) + mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + for direction in directions + first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(direction)) + last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(direction)) + @test (numout(first_tensor), numin(first_tensor)) == (2, 2) + @test (numout(last_tensor), numin(last_tensor)) == (2, 2) + end + + stringspace = Rep[U₁](1 => 1) + middle = TensorMap(TensorKit.BraidingTensor{ComplexF64}(d, stringspace)) + for incoming in directions, outgoing in directions + incoming == outgoing && continue + tensor = PEPSKit.mpo_path_middle(A, middle, Val((incoming, outgoing))) + string_tensor = PEPSKit.mpo_path_string( + A, stringspace, Val((incoming, outgoing)) + ) + @test (numout(tensor), numin(tensor)) == (2, 2) + @test string_tensor ≈ tensor + end +end diff --git a/test/types/mpo_observable.jl b/test/types/mpo_observable.jl new file mode 100644 index 000000000..f6498a74e --- /dev/null +++ b/test/types/mpo_observable.jl @@ -0,0 +1,98 @@ +using PEPSKit +using TensorKit +using Test +using Random + +const CI = CartesianIndex + +function _check_observable_bonds(observable::MPOObservable) + @test length(observable.sites) == length(observable.mpo) + path_positions = indexin(observable.sites, observable.path) + @test all(!isnothing, path_positions) + @test issorted(path_positions) + @test first(path_positions) == 1 + @test last(path_positions) == length(observable.path) + for k in 1:(length(observable.mpo) - 1) + @test PEPSKit._mpo_right_stringspace(observable.mpo[k])' == + space(observable.mpo[k + 1], 1) + end + return nothing +end + +Random.seed!(1234) +d = ℂ^2 +lattice = fill(d, 2, 2) + +@testset "Manhattan paths and directions" begin + # first move horizontally + @test PEPSKit._l_path(CI(2, 1), CI(2, 4)) == + CI.([(2, 1), (2, 2), (2, 3), (2, 4)]) + @test PEPSKit._l_path(CI(1, 3), CI(4, 3)) == + CI.([(1, 3), (2, 3), (3, 3), (4, 3)]) + @test PEPSKit._l_path(CI(3, 4), CI(1, 1)) == + CI.([(3, 4), (3, 3), (3, 2), (3, 1), (2, 1), (1, 1)]) + # nearest neighbor directions + origin = CI(2, 2) + @test PEPSKit._step_direction(origin, CI(1, 2)) === :north + @test PEPSKit._step_direction(origin, CI(2, 3)) === :east + @test PEPSKit._step_direction(origin, CI(3, 2)) === :south + @test PEPSKit._step_direction(origin, CI(2, 1)) === :west +end + +@testset "Routed MPO tensors" begin + op2 = rand(ComplexF64, d^2, d^2) + observable2 = MPOObservable([CI(1, 1), CI(3, 3)], op2, lattice) + @test observable2.path == + CI.([(1, 1), (1, 2), (1, 3), (2, 3), (3, 3)]) + @test observable2.sites == CI.([(1, 1), (3, 3)]) + _check_observable_bonds(observable2) + + original2 = PEPSKit.gate_to_mpo(op2) + @test first(observable2.mpo) ≈ first(original2) + @test last(observable2.mpo) ≈ last(original2) + @test length(observable2.mpo) == 2 + + sites4 = [CI(4, 2), CI(1, 3), CI(3, 4), CI(1, 1)] + op4 = rand(ComplexF64, d^4, d^4) + observable4 = MPOObservable(sites4, op4, lattice) + @test observable4.path == CI.( + [ + (1, 1), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), + (3, 3), (2, 3), (1, 3), (1, 4), (2, 4), (3, 4), + ] + ) + _check_observable_bonds(observable4) + + plaquette_sites = CI.([(2, 1), (1, 1), (1, 2), (2, 2)]) + plaquette_mpo = PEPSKit.gate_to_mpo(rand(ComplexF64, d^4, d^4)) + plaquette = MPOObservable(plaquette_sites, plaquette_sites, plaquette_mpo) + @test plaquette.sites == plaquette_sites + @test plaquette.path == plaquette_sites + _check_observable_bonds(plaquette) +end + +@testset "Explicit MPOs and validation" begin + op = rand(ComplexF64, d^2, d^2) + mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + + observable = MPOObservable([(2, 2), (1, 1)], mpo) + @test observable.sites == CI.([(2, 2), (1, 1)]) + @test observable.path == CI.([(2, 2), (2, 1), (1, 1)]) + @test observable.mpo !== mpo + _check_observable_bonds(observable) + + # Invalid MPO with virtual space mismatch + rank_one_mpo = PEPSKit.gate_to_mpo(op; trunc = truncrank(1)) + @test_throws SpaceMismatch MPOObservable( + [CI(1, 1), CI(1, 2)], [first(mpo), last(rank_one_mpo)] + ) + + # the observable must act on two diffrent sites + @test_throws ArgumentError MPOObservable([CI(1, 1), CI(1, 1)], mpo) + @test_throws ArgumentError MPOObservable([CI(1, 1), CI(1, 1)], op, lattice) + # Routing between consecutive tensors must not cross a later operator site. + @test_throws ArgumentError MPOObservable( + [CI(1, 1), CI(1, 3), CI(1, 2)], + PEPSKit.gate_to_mpo(rand(ComplexF64, d^3, d^3)), + ) +end From 7aab80f5551bb3c4d2e11aaba16aac35777bee5d Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Mon, 17 Aug 2026 16:46:36 +0800 Subject: [PATCH 02/20] Standardize virtual arrows before contraction --- .../contractions/window/pepo_1layer.jl | 39 ++++++++++++++++ .../window/twosite/pepo_1layer.jl | 1 + src/algorithms/correlator_approx.jl | 8 ++-- src/algorithms/expval_approx.jl | 9 ++-- test/toolbox/expval_approx_fermions.jl | 46 +++++++++++++++++++ test/types/standardize_dualness.jl | 39 ++++++++++++++++ 6 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 test/toolbox/expval_approx_fermions.jl create mode 100644 test/types/standardize_dualness.jl diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 7ab2349bd..941413e72 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -10,6 +10,44 @@ function _check_window_inputs(ρ::InfinitePEPO, direction::Symbol) return nothing end +""" +Return a PEPO and CTMRG environment with standard virtual-space dualness without mutating the inputs. +""" +function standardize_dualness(ρ::InfinitePEPO, env::CTMRGEnv) + isdual_easts, isdual_norths = _check_virtual_dualness(ρ) + all(isdual_easts) && all(isdual_norths) && return ρ, env + + nrows, ncols = size(ρ, 1), size(ρ, 2) + tensors = map(CartesianIndices(unitcell(ρ))) do site + row, col, layer = Tuple(site) + directions = Int[] + !isdual_norths[row, col, layer] && push!(directions, NORTH) + !isdual_easts[row, col, layer] && push!(directions, EAST) + !isdual_norths[_next(row, nrows), col, layer] && push!(directions, SOUTH) + !isdual_easts[row, _prev(col, ncols), layer] && push!(directions, WEST) + A = unitcell(ρ)[site] + return isempty(directions) ? A : flip_virtualspace(A, directions) + end + ρ′ = InfinitePEPO(tensors) + + edges = map(CartesianIndices(env.edges)) do index + direction, row, col = Tuple(index) + should_flip = if direction == NORTH + !isdual_norths[_next(row, nrows), col, 1] + elseif direction == EAST + !isdual_easts[row, _prev(col, ncols), 1] + elseif direction == SOUTH + !isdual_norths[row, col, 1] + else + !isdual_easts[row, col, 1] + end + E = env.edges[index] + return should_flip ? flip(E, 2) : E + end + env′ = CTMRGEnv(copy(env.corners), edges) + return ρ′, env′ +end + """ Contract an MPO observable in its enclosing window, rotating column sweeps into row sweeps. """ @@ -90,6 +128,7 @@ function _expectation_value_approx_rows( ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, ) + ρ, env = standardize_dualness(ρ, env) numerator = _contract_window_rows(ρ, observable, env, rowrange, colrange, alg) norm = _contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) return numerator / norm diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index 33656f856..0fa57fa11 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -46,6 +46,7 @@ function _correlator_approx_rows( bonds::Vector{NTuple{2, CartesianIndex{2}}}, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, ) + ρ, env = standardize_dualness(ρ, env) cache = _window_row_cache(ρ, env, rowrange, colrange, alg) groups = _twosite_source_groups(bonds) mpo = gate_to_mpo(op; trunc = notrunc()) diff --git a/src/algorithms/correlator_approx.jl b/src/algorithms/correlator_approx.jl index 8ac9adcaf..36499ba28 100644 --- a/src/algorithms/correlator_approx.jl +++ b/src/algorithms/correlator_approx.jl @@ -4,10 +4,10 @@ """ $(SIGNATURES) -Approximately measure a dense two-site operator on one or more ordered pairs of sites in a -single-layer PEPO. The first and second operator legs act on the first and second sites of -each pair, respectively. Multiple pairs are evaluated in one shared window and reuse -open-string boundary contractions. Ordered pairs in a batched call must be unique. +Approximately measure a dense two-site operator on one or more ordered pairs of sites in a single-layer PEPO. +The first and second operator legs act on the first and second sites of each pair, respectively. +Multiple pairs are evaluated in one shared window and reuse open-string boundary contractions. +Ordered pairs in a batched call must be unique. """ function correlator_approx( ρ::InfinitePEPO, op::AbstractTensorMap, bond::Tuple, env::CTMRGEnv; diff --git a/src/algorithms/expval_approx.jl b/src/algorithms/expval_approx.jl index 5698eec79..9876365d6 100644 --- a/src/algorithms/expval_approx.jl +++ b/src/algorithms/expval_approx.jl @@ -4,11 +4,10 @@ """ $(SIGNATURES) -Approximately measure the expectation value of an open-boundary `MPOObservable` in a -single-layer PEPO using finite boundary MPS/MPO zipup sweeps. The zipup truncation is -controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary -dimension. After each zipup step, the result is refined by a single-site DMRG approximation -step with `maxiter` sweeps. Set `maxiter = 0` to disable this refinement. +Approximately measure the expectation value of an open-boundary `MPOObservable` in a single-layer PEPO using finite boundary MPS/MPO zipup sweeps. +The zipup truncation is controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary dimension. +After each zipup step, the result is refined by a single-site DMRG approximation step with `maxiter` sweeps. +Set `maxiter = 0` to disable this refinement. """ function expectation_value_approx( ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv; diff --git a/test/toolbox/expval_approx_fermions.jl b/test/toolbox/expval_approx_fermions.jl new file mode 100644 index 000000000..15f03726d --- /dev/null +++ b/test/toolbox/expval_approx_fermions.jl @@ -0,0 +1,46 @@ +using TensorKit +using PEPSKit +using MPSKit +using Test +using Random + +const CI = CartesianIndex + +@testset "Approximate fermionic single-layer PEPO expectation values" begin + Random.seed!(1234) + + S = Vect[FermionParity] + d = S(0 => 1, 1 => 1) + D = S(0 => 1, 1 => 2) + χ = S(0 => 2, 1 => 2) + Nspaces = fill(D, 2, 2) + Espaces = fill(D, 2, 2) + Pspaces = fill(d, size(Nspaces)) + ρ = InfinitePEPO(randn, ComplexF64, Pspaces, Nspaces, Espaces) + env = CTMRGEnv(randn, ComplexF64, InfinitePartitionFunction(ρ), χ) + trunc = notrunc() + + op = randn(ComplexF64, d ⊗ d → d ⊗ d) + mpo = PEPSKit.gate_to_mpo(op; trunc) + geometries = ( + "horizontal" => [CI(1, 1), CI(1, 2)], + "vertical" => [CI(1, 1), CI(2, 1)], + "turned" => [CI(1, 1), CI(2, 2)], + "reversed turned" => [CI(2, 2), CI(1, 1)], + ) + + for (geometry, sites) in geometries + observable = MPOObservable(sites, mpo) + exact = expectation_value( + ρ, LocalOperator(physicalspace(ρ), sites => op), env + ) + + @testset "$geometry path" begin + for direction in (:rows, :columns) + @test expectation_value_approx( + ρ, observable, env; trunc, maxiter = 0, direction + ) ≈ exact + end + end + end +end diff --git a/test/types/standardize_dualness.jl b/test/types/standardize_dualness.jl new file mode 100644 index 000000000..5b394e39f --- /dev/null +++ b/test/types/standardize_dualness.jl @@ -0,0 +1,39 @@ +using TensorKit +using PEPSKit +using Test + +@testset "Standardize virtual-space dualness" begin + d = U1Space(0 => 1) + D = U1Space(0 => 1) + χ = U1Space(0 => 1) + Pspaces = fill(d, 2, 2, 1) + + patterns = ( + (fill(D', 2, 2, 1), fill(D', 2, 2, 1)), + ( + reshape([D, D', D', D], 2, 2, 1), + reshape([D', D, D, D'], 2, 2, 1), + ), + ) + for (Nspaces, Espaces) in patterns + ρ = InfinitePEPO(randn, ComplexF64, Pspaces, Nspaces, Espaces) + env = CTMRGEnv(randn, ComplexF64, InfinitePartitionFunction(ρ), χ) + standardized_ρ, standardized_env = PEPSKit.standardize_dualness(ρ, env) + + for row in axes(ρ, 1), col in axes(ρ, 2) + A = standardized_ρ[row, col, 1] + edge_coordinates = ( + PEPSKit.NORTH => (row - 1, col), + PEPSKit.EAST => (row, col + 1), + PEPSKit.SOUTH => (row + 1, col), + PEPSKit.WEST => (row, col - 1), + ) + for (direction, coordinates) in edge_coordinates + V = PEPSKit.virtualspace(A, direction) + E = PEPSKit.edge(standardized_env, direction, coordinates...) + @test isdual(V) == (direction in (PEPSKit.NORTH, PEPSKit.EAST)) + @test V == space(E, 2)' + end + end + end +end From 4fa77454efa68c3bcebb80285a2e452d1889a3ce Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 12:02:27 +0800 Subject: [PATCH 03/20] Fix fusers --- .../contractions/mpo_path/pepo_1layer.jl | 48 +++++++++++++++--- test/toolbox/expval_approx_fermions.jl | 46 ----------------- test/toolbox/mpo_routing.jl | 50 ++++++++++++++++++- 3 files changed, 90 insertions(+), 54 deletions(-) delete mode 100644 test/toolbox/expval_approx_fermions.jl diff --git a/src/algorithms/contractions/mpo_path/pepo_1layer.jl b/src/algorithms/contractions/mpo_path/pepo_1layer.jl index f45cb281c..7628b0413 100644 --- a/src/algorithms/contractions/mpo_path/pepo_1layer.jl +++ b/src/algorithms/contractions/mpo_path/pepo_1layer.jl @@ -43,6 +43,24 @@ function _mpo_path_virtual_label(direction::Symbol) return (:N, :E, :S, :W)[_mpo_path_direction(direction)] end +""" +Canonicalize an incoming MPO fuser so its fused PEPO leg has standard dualness. +""" +function _mpo_path_incoming_fuser(F, direction::Int) + direction in (NORTH, EAST) && return F + direction in (SOUTH, WEST) && return twist(flip(F, 1), 1) + throw(ArgumentError("invalid MPO path direction index: $direction")) +end + +""" +Canonicalize an outgoing MPO fuser so its fused PEPO leg has standard dualness and braiding. +""" +function _mpo_path_outgoing_fuser(F, direction::Int) + direction in (NORTH, EAST) && return twist(flip(F, 1), 1) + direction in (SOUTH, WEST) && return twist(F, 3) + throw(ArgumentError("invalid MPO path direction index: $direction")) +end + """ Build the `@tensor` labels from `(direction, suffix)` pairs used to fuse MPO virtual strings. @@ -85,7 +103,10 @@ outgoing MPO string with the virtual space of `A` along `direction`. return quote _check_pepo_first_physicalspace(A, op) A′ = twistdual(A, 2) - F = fuser(storagetype(A), domain(A, $direction_index)', space(op, 3)) + F = _mpo_path_outgoing_fuser( + fuser(storagetype(A), domain(A, $direction_index)', space(op, 3)), + $direction_index, + ) $contraction end end @@ -111,7 +132,10 @@ incoming MPO string with the virtual space of `A` along `direction`. return quote _check_pepo_last_physicalspace(A, op) A′ = twistdual(A, 2) - F = fuser(storagetype(A), domain(A, $direction_index), space(op, 1)') + F = _mpo_path_incoming_fuser( + fuser(storagetype(A), domain(A, $direction_index), space(op, 1)'), + $direction_index, + ) $contraction end end @@ -153,8 +177,14 @@ incoming and the outgoing MPO string with the virtual space of `A` along return quote _check_pepo_middle_physicalspace(A, op) A′ = twistdual(A, 2) - Fin = fuser(storagetype(A), domain(A, $incoming_index), space(op, 1)') - Fout = fuser(storagetype(A), domain(A, $outgoing_index)', space(op, 4)) + Fin = _mpo_path_incoming_fuser( + fuser(storagetype(A), domain(A, $incoming_index), space(op, 1)'), + $incoming_index, + ) + Fout = _mpo_path_outgoing_fuser( + fuser(storagetype(A), domain(A, $outgoing_index)', space(op, 4)), + $outgoing_index, + ) $contraction end end @@ -195,8 +225,14 @@ along `directions = (incoming, outgoing)`. return quote O = trace_physicalspaces(A) I = id(storagetype(A), stringspace) - Fin = fuser(storagetype(A), domain(A, $incoming_index), stringspace') - Fout = fuser(storagetype(A), domain(A, $outgoing_index)', stringspace') + Fin = _mpo_path_incoming_fuser( + fuser(storagetype(A), domain(A, $incoming_index), stringspace'), + $incoming_index, + ) + Fout = _mpo_path_outgoing_fuser( + fuser(storagetype(A), domain(A, $outgoing_index)', stringspace'), + $outgoing_index, + ) $contraction end end diff --git a/test/toolbox/expval_approx_fermions.jl b/test/toolbox/expval_approx_fermions.jl deleted file mode 100644 index 15f03726d..000000000 --- a/test/toolbox/expval_approx_fermions.jl +++ /dev/null @@ -1,46 +0,0 @@ -using TensorKit -using PEPSKit -using MPSKit -using Test -using Random - -const CI = CartesianIndex - -@testset "Approximate fermionic single-layer PEPO expectation values" begin - Random.seed!(1234) - - S = Vect[FermionParity] - d = S(0 => 1, 1 => 1) - D = S(0 => 1, 1 => 2) - χ = S(0 => 2, 1 => 2) - Nspaces = fill(D, 2, 2) - Espaces = fill(D, 2, 2) - Pspaces = fill(d, size(Nspaces)) - ρ = InfinitePEPO(randn, ComplexF64, Pspaces, Nspaces, Espaces) - env = CTMRGEnv(randn, ComplexF64, InfinitePartitionFunction(ρ), χ) - trunc = notrunc() - - op = randn(ComplexF64, d ⊗ d → d ⊗ d) - mpo = PEPSKit.gate_to_mpo(op; trunc) - geometries = ( - "horizontal" => [CI(1, 1), CI(1, 2)], - "vertical" => [CI(1, 1), CI(2, 1)], - "turned" => [CI(1, 1), CI(2, 2)], - "reversed turned" => [CI(2, 2), CI(1, 1)], - ) - - for (geometry, sites) in geometries - observable = MPOObservable(sites, mpo) - exact = expectation_value( - ρ, LocalOperator(physicalspace(ρ), sites => op), env - ) - - @testset "$geometry path" begin - for direction in (:rows, :columns) - @test expectation_value_approx( - ρ, observable, env; trunc, maxiter = 0, direction - ) ≈ exact - end - end - end -end diff --git a/test/toolbox/mpo_routing.jl b/test/toolbox/mpo_routing.jl index c4adb648f..84fbeb0df 100644 --- a/test/toolbox/mpo_routing.jl +++ b/test/toolbox/mpo_routing.jl @@ -5,9 +5,55 @@ using Random const directions = (:north, :east, :south, :west) -@testset "MPO path routing on PEPO" begin - Random.seed!(1234) +@testset "Fuser flips and twists" begin + d = Vect[FermionParity](0 => 1, 1 => 1) + D = Vect[FermionParity](0 => 1, 1 => 1) + ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) + A = ρ[1, 1, 1] + B_horizontal = ρ[1, 2, 1] + B_vertical = ρ[2, 1, 1] + A′ = PEPSKit.twistdual(A, 2) + B_horizontal′ = PEPSKit.twistdual(B_horizontal, 2) + B_vertical′ = PEPSKit.twistdual(B_vertical, 2) + + op = randn(ComplexF64, d^2 → d^2) + mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + + first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:east)) + last_tensor = PEPSKit.mpo_path_last(B_horizontal, last(mpo), Val(:west)) + @tensor exact[W1 S1 S2; N1 N2 E2] := op[po1 po2; pi1 pi2] * + A′[pi1 po1; N1 x S1 W1] * B_horizontal′[pi2 po2; N2 E2 S2 x] + @tensor routed[W1 S1 S2; N1 N2 E2] := + first_tensor[W1 S1; N1 x] * last_tensor[x S2; N2 E2] + @test routed ≈ exact + first_tensor = PEPSKit.mpo_path_first(B_horizontal, first(mpo), Val(:west)) + last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:east)) + @tensor exact[W2 S1 S2; N1 E1 N2] := op[po1 po2; pi1 pi2] * + B_horizontal′[pi1 po1; N1 E1 S1 x] * A′[pi2 po2; N2 x S2 W2] + @tensor routed[W2 S1 S2; N1 E1 N2] := + first_tensor[x S1; N1 E1] * last_tensor[W2 S2; N2 x] + @test routed ≈ exact + + first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:south)) + last_tensor = PEPSKit.mpo_path_last(B_vertical, last(mpo), Val(:north)) + @tensor exact[W1 W2 S2; N1 E1 E2] := op[po1 po2; pi1 pi2] * + A′[pi1 po1; N1 E1 x W1] * B_vertical′[pi2 po2; x E2 S2 W2] + @tensor routed[W1 W2 S2; N1 E1 E2] := + first_tensor[W1 x; N1 E1] * last_tensor[W2 S2; x E2] + @test routed ≈ exact + + first_tensor = PEPSKit.mpo_path_first(B_vertical, first(mpo), Val(:north)) + last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:south)) + @tensor exact[W1 S1 W2; E1 N2 E2] := op[po1 po2; pi1 pi2] * + B_vertical′[pi1 po1; x E1 S1 W1] * A′[pi2 po2; N2 E2 x W2] + @tensor routed[W1 S1 W2; E1 N2 E2] := + first_tensor[W1 S1; x E1] * last_tensor[W2 x; N2 E2] + @test routed ≈ exact +end + +@testset "Bosonic routing identities" begin + Random.seed!(1234) d = Rep[U₁](0 => 1, 1 => 1) D = Rep[U₁](0 => 1, 1 => 1, -1 => 1) ρ = InfinitePEPO(d, D; unitcell = (1, 1, 1)) From 84f56e0b9e19f5273bc902e08ca5b697557fcae5 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 12:05:19 +0800 Subject: [PATCH 04/20] Fix fermions with proper planar contractions --- src/algorithms/contractions/window/pepo_1layer.jl | 9 ++++++--- src/algorithms/contractions/window/tools.jl | 4 ++-- .../contractions/window/twosite/pepo_1layer.jl | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 941413e72..195a127d2 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -189,7 +189,8 @@ function _south_boundary_mps( Cwest = permute(corner(env, SOUTHWEST, r, cmin - 1), ((2,), (1,))) tensors = [insertleftunit(Cwest, 1; dual = true)] append!(tensors, (permute(edge(env, SOUTH, r, col), ((3, 2), (1,))) for col in colrange)) - Ceast = permute(corner(env, SOUTHEAST, r, cmax + 1), ((2, 1), ())) + # Closing the right boundary is a planar bend, not a braid. + Ceast = transpose(corner(env, SOUTHEAST, r, cmax + 1), ((2, 1), ())) push!(tensors, insertleftunit(Ceast, 3; dual = true)) return FiniteMPS(_bra_mps_tensor.(tensors)) end @@ -202,7 +203,8 @@ function _row_mpo( env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, ) cmin, cmax = first(colrange), last(colrange) - W = permute(edge(env, WEST, row, cmin - 1), ((1,), (3, 2))) + # Opening the left boundary is a planar bend, not a braid. + W = transpose(edge(env, WEST, row, cmin - 1), ((1,), (3, 2))) tensors = [insertleftunit(W, 1)] append!( tensors, @@ -211,7 +213,8 @@ function _row_mpo( for col in colrange ), ) - E = permute(edge(env, EAST, row, cmax + 1), ((2, 3), (1,))) + # Closing the right boundary is a planar bend, not a braid. + E = transpose(edge(env, EAST, row, cmax + 1), ((2, 3), (1,))) push!(tensors, insertrightunit(E, 3)) return FiniteMPO(tensors) end diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index 15e3416b4..f313c0265 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -13,8 +13,8 @@ Convert a south-boundary MPS tensor into the stored bra representation expected _bra_mps_tensor(A::GenericMPSTensor) = copy(permute(A', ((2, 3), (1,)))) """ -Construct the adjoint of a finite MPO while restoring MPSKit's local MPO leg partition. +Construct the planar adjoint of a finite MPO while restoring MPSKit's local MPO leg partition. """ function _adjoint_mpo(W::FiniteMPO) - return FiniteMPO(map(A -> permute(A', ((3, 1), (4, 2))), parent(W))) + return FiniteMPO(map(A -> transpose(A', ((3, 1), (4, 2)); copy = true), parent(W))) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index 0fa57fa11..5b6a273e9 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -242,5 +242,6 @@ Contract the left and right transfer-matrix environments to a scalar. ``` """ function _contract_transfer_boundaries(left, right) - return @tensoropt left[1 2; 3] * right[3 2; 1] + # The three bonds close around the window without crossing + return @plansor left[1 2; 3] * right[3 2; 1] end From f1582123f9b9f7571264e7373c970d01cd043c1f Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 15:05:17 +0800 Subject: [PATCH 05/20] Promote WindowRowCache to a struct --- .../contractions/window/pepo_1layer.jl | 2 +- .../contractions/window/twosite/caching.jl | 32 +++++++++++++------ .../window/twosite/pepo_1layer.jl | 26 ++++++++------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 195a127d2..d0312dc58 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -76,7 +76,7 @@ function _expectation_value_approx( end """ -Build an ordinary local row-MPO tensor by tracing the PEPO physical legs. +Build a local tensor for row MPOs without observables by tracing the PEPO physical legs. """ function _window_site_tensor( ρ::InfinitePEPO, ::Nothing, row::Int, col::Int, diff --git a/src/algorithms/contractions/window/twosite/caching.jl b/src/algorithms/contractions/window/twosite/caching.jl index 27ea864e9..28f1e40a5 100644 --- a/src/algorithms/contractions/window/twosite/caching.jl +++ b/src/algorithms/contractions/window/twosite/caching.jl @@ -65,22 +65,34 @@ function _twosite_targets_by_row(targets::Dict{CartesianIndex{2}, Int}) end """ -Precompute the row MPOs (without observables) and -north/south boundary contractions reused when measuring on -many two-site bonds in one window. +Cache the row MPOs without observables and boundary contractions shared by measurements in one window. -The returned named tuple contains: +The fields contain: - `rowrange` and `colrange`: the coordinate ranges defining the window. -- `row_mpos`: the ordinary finite MPO for each row, including the west and east CTMRG edges. -- `north_prefixes`: `nrows + 1` north boundary MPSs. Entry `k` is above row `k` in the window. -- `south_suffixes`: `nrows + 1` adjointed south boundary MPSs. Entry `k + 1` is below row `k` in the window. +- `row_mpos`: the row MPOs without observables, one per row and including the west and east CTMRG edges. +- `north_prefixes`: `nrows + 1` north boundary MPSs. + Entry `k` is above row `k` in the window. +- `south_suffixes`: `nrows + 1` adjointed south boundary MPSs. + Entry `k + 1` is below row `k` in the window. - `norm`: the approximate contraction of the window with no observable inserted. """ +struct WindowRowCache{M <: FiniteMPO, N <: FiniteMPS, S <: FiniteMPS, T <: Number} + rowrange::UnitRange{Int} + colrange::UnitRange{Int} + row_mpos::Dict{Int, M} + north_prefixes::Vector{N} + south_suffixes::Vector{S} + norm::T +end + +""" +Precompute the row MPOs without observables and north/south boundary contractions reused when measuring many two-site bonds in one window. +""" function _window_row_cache( ρ::InfinitePEPO, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, - ) + )::WindowRowCache row_mpos = Dict( row => _row_mpo(ρ, nothing, env, row, colrange) for row in rowrange @@ -106,5 +118,7 @@ function _window_row_cache( ) end norm = dot(south, north_prefixes[end]) - return (; rowrange, colrange, row_mpos, north_prefixes, south_suffixes, norm) + return WindowRowCache( + rowrange, colrange, row_mpos, north_prefixes, south_suffixes, norm + ) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index 5b6a273e9..84e239b1a 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -38,8 +38,7 @@ function _correlator_approx( end """ -Measure all ordered bonds in one row-oriented window using shared ordinary boundaries and -one exactly decomposed MPO for each operator-leg ordering. +Measure all ordered bonds in one row-oriented window using shared row MPOs without observables, shared boundaries, and one exactly decomposed MPO for each operator-leg ordering. """ function _correlator_approx_rows( ρ::InfinitePEPO, op::AbstractTensorMap, @@ -71,9 +70,10 @@ Contract the correlator numerator for all targets associated with the same sourc and one ordering of the dense operator, writing each result into `numerators`. """ function _contract_twosite_source!( - numerators::Vector{<:Number}, ρ::InfinitePEPO, mpo, + numerators::Vector{<:Number}, ρ::InfinitePEPO, + mpo::AbstractVector{<:AbstractTensorMap}, source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, - env::CTMRGEnv, cache, alg::WindowApprox, + env::CTMRGEnv, cache::WindowRowCache, alg::WindowApprox, ) # grouping targets by which row they are in targets_by_row = _twosite_targets_by_row(targets) @@ -118,9 +118,10 @@ Contract the correlator numerator for all targets in one row with a shared open-string north state, writing the results into `numerators`. """ function _contract_twosite_target_row!( - numerators::Vector{<:Number}, ρ::InfinitePEPO, mpo, + numerators::Vector{<:Number}, ρ::InfinitePEPO, + mpo::AbstractVector{<:AbstractTensorMap}, source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, - north, cache + north::FiniteMPS, cache::WindowRowCache, ) row = first(keys(targets))[1] row_idx = row - first(cache.rowrange) + 1 @@ -203,16 +204,16 @@ function _contract_twosite_target_row!( end """ -Map a PEPO column coordinate to its finite-MPS site, +Map a PEPO column coordinate to its finite-MPS site number, which includes an additional west edge CTM tensor. """ _window_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 """ -Build an ordinary finite row MPO with one PEPO site tensor replaced by a supplied tensor. +Build a finite row MPO by replacing one site tensor in one of the row MPOs without observables. """ function _row_mpo_with_site( - ρ::InfinitePEPO, tensor, env::CTMRGEnv, + ρ::InfinitePEPO, tensor::MPOTensor, env::CTMRGEnv, row::Int, col::Int, colrange::UnitRange{Int}, ) W = _row_mpo(ρ, nothing, env, row, colrange) @@ -223,7 +224,10 @@ end """ Contract one modified row site between precomputed left and right MPS environments. """ -function _contract_window_site(envs, north, south, site::Int, tensor) +function _contract_window_site( + envs::MPSKit.FiniteEnvironments, north::FiniteMPS, south::FiniteMPS, + site::Int, tensor::MPOTensor, + ) left = leftenv(envs, site, south) * TransferMatrix(north.AC[site], tensor, south.AC[site]) return _contract_transfer_boundaries(left, rightenv(envs, site, south)) @@ -241,7 +245,7 @@ Contract the left and right transfer-matrix environments to a scalar. (south) ``` """ -function _contract_transfer_boundaries(left, right) +function _contract_transfer_boundaries(left::MPSTensor, right::MPSTensor) # The three bonds close around the window without crossing return @plansor left[1 2; 3] * right[3 2; 1] end From 93476a36cf562f882bdb1ab1f4ae79f6a3e1624f Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 15:52:26 +0800 Subject: [PATCH 06/20] Make south boundary construction planar --- .../contractions/window/pepo_1layer.jl | 23 ++++++++++--------- src/algorithms/contractions/window/tools.jl | 6 +++-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index d0312dc58..00b6de80a 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -170,11 +170,12 @@ function _north_boundary_mps( ) r = row - 1 cmin, cmax = first(colrange), last(colrange) - first_tensor = insertleftunit(corner(env, NORTHWEST, r, cmin - 1), 1) - tensors = [first_tensor] + Cwest = insertleftunit(corner(env, NORTHWEST, r, cmin - 1), 1) + tensors = [Cwest] append!(tensors, (edge(env, NORTH, r, col) for col in colrange)) - C = permute(corner(env, NORTHEAST, r, cmax + 1), ((1, 2), ())) - push!(tensors, insertleftunit(C, 3)) + # Closing the right boundary is a planar bend + Ceast = transpose(corner(env, NORTHEAST, r, cmax + 1), ((1, 2), ())) + push!(tensors, insertleftunit(Ceast, 3)) return FiniteMPS(tensors) end @@ -186,13 +187,13 @@ function _south_boundary_mps( ) r = row + 1 cmin, cmax = first(colrange), last(colrange) - Cwest = permute(corner(env, SOUTHWEST, r, cmin - 1), ((2,), (1,))) - tensors = [insertleftunit(Cwest, 1; dual = true)] - append!(tensors, (permute(edge(env, SOUTH, r, col), ((3, 2), (1,))) for col in colrange)) - # Closing the right boundary is a planar bend, not a braid. - Ceast = transpose(corner(env, SOUTHEAST, r, cmax + 1), ((2, 1), ())) - push!(tensors, insertleftunit(Ceast, 3; dual = true)) - return FiniteMPS(_bra_mps_tensor.(tensors)) + Cwest = insertleftunit(corner(env, SOUTHWEST, r, cmin - 1)', 1) + tensors = [Cwest] + append!(tensors, (_bra_mps_tensor(edge(env, SOUTH, r, col)) for col in colrange)) + Ceast = transpose(corner(env, SOUTHEAST, r, cmax + 1)', ((1, 2), ()); copy = true) + # The planar bend of the adjointed southeast corner carries a twist. + push!(tensors, insertleftunit(twist!(Ceast, 1), 3)) + return FiniteMPS(tensors) end """ diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index f313c0265..518506a6c 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -8,9 +8,11 @@ struct WindowApprox{Z, D} end """ -Convert a south-boundary MPS tensor into the stored bra representation expected by `dot`. +Convert a south-boundary MPS tensor into the stored bra representation expected by `dot` using a planar transpose. """ -_bra_mps_tensor(A::GenericMPSTensor) = copy(permute(A', ((2, 3), (1,)))) +function _bra_mps_tensor(A::MPSTensor) + return transpose(A', ((1, 3), (2,)); copy = true) +end """ Construct the planar adjoint of a finite MPO while restoring MPSKit's local MPO leg partition. From 2991f9b2f276160bb9d740237812380b2356e7bb Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 16:43:13 +0800 Subject: [PATCH 07/20] Cover fermions in tests --- test/toolbox/correlator_approx.jl | 24 ++++++++++--- test/toolbox/expval_approx.jl | 56 +++++++++++++++++++++---------- test/toolbox/mpo_routing.jl | 47 ++++++++++++++++---------- 3 files changed, 86 insertions(+), 41 deletions(-) diff --git a/test/toolbox/correlator_approx.jl b/test/toolbox/correlator_approx.jl index d74753bb6..28429f086 100644 --- a/test/toolbox/correlator_approx.jl +++ b/test/toolbox/correlator_approx.jl @@ -39,6 +39,19 @@ bonds = [ (CI(1, 2), CI(2, 0)), (CI(1, 2), CI(2, 2)), ] +spaces = Dict( + U1Irrep => ( + U1Space(1 => 2, -1 => 1), + U1Space(1 => 1, 0 => 1, -1 => 2), + U1Space(1 => 1, 0 => 1, -1 => 2), + ), + FermionParity => ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 2), + Vect[FermionParity](0 => 2, 1 => 2), + ) +) + @testset "Two-site source grouping" begin groups = PEPSKit._twosite_source_groups(bonds) @test length(groups) == 3 && @@ -46,12 +59,9 @@ bonds = [ haskey(groups, (CI(1, 1), true)) end -@testset "Single-layer PEPO" begin +@testset "Single-layer PEPO ($S)" for S in keys(spaces) Random.seed!(1234) - - d = ℂ^2 - D = ℂ^3 - χ = ℂ^4 + d, D, χ = spaces[S] ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) lattice = physicalspace(ρ) env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) @@ -95,4 +105,8 @@ end @test all(eachindex(cache.north_prefixes)) do k dot(cache.south_suffixes[k], cache.north_prefixes[k]) ≈ cache.norm end + + W = cache.row_mpos[first(cache.rowrange)] + W_adjoint = PEPSKit._adjoint_mpo(W) + @test convert(TensorMap, W_adjoint) ≈ convert(TensorMap, W)' end diff --git a/test/toolbox/expval_approx.jl b/test/toolbox/expval_approx.jl index 0854f424f..2f754c74b 100644 --- a/test/toolbox/expval_approx.jl +++ b/test/toolbox/expval_approx.jl @@ -4,29 +4,49 @@ using MPSKit using Test using Random -@testset "Approximate single-layer PEPO expectation values" begin +const CI = CartesianIndex + +spaces = Dict( + U1Irrep => ( + U1Space(1 => 2, -1 => 1), + U1Space(1 => 1, 0 => 1, -1 => 2), + U1Space(1 => 1, 0 => 1, -1 => 2), + ), + FermionParity => ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 2), + Vect[FermionParity](0 => 2, 1 => 2), + ), +) + +sites_list = ( + [CI(1, 1), CI(1, 2)], # horizontal + [CI(1, 1), CI(2, 1)], # vertical + [CI(1, 1), CI(2, 2)], # turned + [CI(2, 2), CI(1, 1)], # reversed turned + [CI(2, 1), CI(1, 1), CI(1, 2), CI(2, 2)], # U-shaped +) + +@testset "Single-layer PEPO ($S)" for S in keys(spaces) Random.seed!(1234) - d = ℂ^2 - D = ℂ^2 - χ = ℂ^3 + d, D, χ = spaces[S] ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) trunc = notrunc() - # U-shaped path, passing twice between two rows - sites = CartesianIndex.([(2, 1), (1, 1), (1, 2), (2, 2)]) - V = d^length(sites) - op = rand(ComplexF64, V, V) - mpo = PEPSKit.gate_to_mpo(op; trunc) - observable = MPOObservable(sites, mpo) - exact = expectation_value( - ρ, LocalOperator(physicalspace(ρ), sites => op), env - ) - - for direction in (:rows, :columns) - @test expectation_value_approx( - ρ, observable, env; trunc, maxiter = 0, direction - ) ≈ exact + for sites in sites_list + n = length(sites) + op = randn(ComplexF64, d^n → d^n) + mpo = PEPSKit.gate_to_mpo(op; trunc) + observable = MPOObservable(sites, mpo) + exact = expectation_value( + ρ, LocalOperator(physicalspace(ρ), sites => op), env + ) + for direction in (:rows, :columns) + @test expectation_value_approx( + ρ, observable, env; trunc, maxiter = 0, direction + ) ≈ exact + end end end diff --git a/test/toolbox/mpo_routing.jl b/test/toolbox/mpo_routing.jl index 84fbeb0df..19a59385b 100644 --- a/test/toolbox/mpo_routing.jl +++ b/test/toolbox/mpo_routing.jl @@ -5,62 +5,74 @@ using Random const directions = (:north, :east, :south, :west) +spaces = Dict( + U1Irrep => ( + Rep[U₁](0 => 1, 1 => 1), + Rep[U₁](0 => 1, 1 => 1, -1 => 1), + Rep[U₁](1 => 1), + ), + FermionParity => ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](1 => 1), + ), +) + @testset "Fuser flips and twists" begin d = Vect[FermionParity](0 => 1, 1 => 1) D = Vect[FermionParity](0 => 1, 1 => 1) ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) A = ρ[1, 1, 1] - B_horizontal = ρ[1, 2, 1] - B_vertical = ρ[2, 1, 1] + Bh = ρ[1, 2, 1] + Bv = ρ[2, 1, 1] A′ = PEPSKit.twistdual(A, 2) - B_horizontal′ = PEPSKit.twistdual(B_horizontal, 2) - B_vertical′ = PEPSKit.twistdual(B_vertical, 2) + Bh′ = PEPSKit.twistdual(Bh, 2) + Bv′ = PEPSKit.twistdual(Bv, 2) op = randn(ComplexF64, d^2 → d^2) mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:east)) - last_tensor = PEPSKit.mpo_path_last(B_horizontal, last(mpo), Val(:west)) + last_tensor = PEPSKit.mpo_path_last(Bh, last(mpo), Val(:west)) @tensor exact[W1 S1 S2; N1 N2 E2] := op[po1 po2; pi1 pi2] * - A′[pi1 po1; N1 x S1 W1] * B_horizontal′[pi2 po2; N2 E2 S2 x] + A′[pi1 po1; N1 x S1 W1] * Bh′[pi2 po2; N2 E2 S2 x] @tensor routed[W1 S1 S2; N1 N2 E2] := first_tensor[W1 S1; N1 x] * last_tensor[x S2; N2 E2] @test routed ≈ exact - first_tensor = PEPSKit.mpo_path_first(B_horizontal, first(mpo), Val(:west)) + first_tensor = PEPSKit.mpo_path_first(Bh, first(mpo), Val(:west)) last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:east)) @tensor exact[W2 S1 S2; N1 E1 N2] := op[po1 po2; pi1 pi2] * - B_horizontal′[pi1 po1; N1 E1 S1 x] * A′[pi2 po2; N2 x S2 W2] + Bh′[pi1 po1; N1 E1 S1 x] * A′[pi2 po2; N2 x S2 W2] @tensor routed[W2 S1 S2; N1 E1 N2] := first_tensor[x S1; N1 E1] * last_tensor[W2 S2; N2 x] @test routed ≈ exact first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:south)) - last_tensor = PEPSKit.mpo_path_last(B_vertical, last(mpo), Val(:north)) + last_tensor = PEPSKit.mpo_path_last(Bv, last(mpo), Val(:north)) @tensor exact[W1 W2 S2; N1 E1 E2] := op[po1 po2; pi1 pi2] * - A′[pi1 po1; N1 E1 x W1] * B_vertical′[pi2 po2; x E2 S2 W2] + A′[pi1 po1; N1 E1 x W1] * Bv′[pi2 po2; x E2 S2 W2] @tensor routed[W1 W2 S2; N1 E1 E2] := first_tensor[W1 x; N1 E1] * last_tensor[W2 S2; x E2] @test routed ≈ exact - first_tensor = PEPSKit.mpo_path_first(B_vertical, first(mpo), Val(:north)) + first_tensor = PEPSKit.mpo_path_first(Bv, first(mpo), Val(:north)) last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:south)) @tensor exact[W1 S1 W2; E1 N2 E2] := op[po1 po2; pi1 pi2] * - B_vertical′[pi1 po1; x E1 S1 W1] * A′[pi2 po2; N2 E2 x W2] + Bv′[pi1 po1; x E1 S1 W1] * A′[pi2 po2; N2 E2 x W2] @tensor routed[W1 S1 W2; E1 N2 E2] := first_tensor[W1 S1; x E1] * last_tensor[W2 x; N2 E2] @test routed ≈ exact end -@testset "Bosonic routing identities" begin +@testset "Routing identities ($S)" for S in keys(spaces) Random.seed!(1234) - d = Rep[U₁](0 => 1, 1 => 1) - D = Rep[U₁](0 => 1, 1 => 1, -1 => 1) + d, D, stringspace = spaces[S] ρ = InfinitePEPO(d, D; unitcell = (1, 1, 1)) - A = ρ[1, 1, 1] - op = rand(ComplexF64, d^2 → d^2) mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + + A = ρ[1, 1, 1] for direction in directions first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(direction)) last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(direction)) @@ -68,7 +80,6 @@ end @test (numout(last_tensor), numin(last_tensor)) == (2, 2) end - stringspace = Rep[U₁](1 => 1) middle = TensorMap(TensorKit.BraidingTensor{ComplexF64}(d, stringspace)) for incoming in directions, outgoing in directions incoming == outgoing && continue From 86c13673beff400c95ff34f66e2516ffe4456732 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 17:01:12 +0800 Subject: [PATCH 08/20] Use repartition where appropriate --- src/algorithms/contractions/window/pepo_1layer.jl | 6 +++--- src/algorithms/contractions/window/tools.jl | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 00b6de80a..e31ff3b0a 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -174,7 +174,7 @@ function _north_boundary_mps( tensors = [Cwest] append!(tensors, (edge(env, NORTH, r, col) for col in colrange)) # Closing the right boundary is a planar bend - Ceast = transpose(corner(env, NORTHEAST, r, cmax + 1), ((1, 2), ())) + Ceast = repartition(corner(env, NORTHEAST, r, cmax + 1), 2, 0) push!(tensors, insertleftunit(Ceast, 3)) return FiniteMPS(tensors) end @@ -190,7 +190,7 @@ function _south_boundary_mps( Cwest = insertleftunit(corner(env, SOUTHWEST, r, cmin - 1)', 1) tensors = [Cwest] append!(tensors, (_bra_mps_tensor(edge(env, SOUTH, r, col)) for col in colrange)) - Ceast = transpose(corner(env, SOUTHEAST, r, cmax + 1)', ((1, 2), ()); copy = true) + Ceast = repartition(corner(env, SOUTHEAST, r, cmax + 1)', 2, 0; copy = true) # The planar bend of the adjointed southeast corner carries a twist. push!(tensors, insertleftunit(twist!(Ceast, 1), 3)) return FiniteMPS(tensors) @@ -205,7 +205,7 @@ function _row_mpo( ) cmin, cmax = first(colrange), last(colrange) # Opening the left boundary is a planar bend, not a braid. - W = transpose(edge(env, WEST, row, cmin - 1), ((1,), (3, 2))) + W = repartition(edge(env, WEST, row, cmin - 1), 1, 2) tensors = [insertleftunit(W, 1)] append!( tensors, diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index 518506a6c..ac9824db2 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -8,10 +8,10 @@ struct WindowApprox{Z, D} end """ -Convert a south-boundary MPS tensor into the stored bra representation expected by `dot` using a planar transpose. +Convert a south-boundary MPS tensor into the stored bra representation expected by `dot` using a planar repartition. """ function _bra_mps_tensor(A::MPSTensor) - return transpose(A', ((1, 3), (2,)); copy = true) + return repartition(A', 2, 1; copy = true) end """ From 631df2c12dae7a5f47b79e28d9a43bcbfef1e9e6 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 18 Aug 2026 17:50:54 +0800 Subject: [PATCH 09/20] Reorganize code --- .../contractions/window/pepo_1layer.jl | 44 ------------ src/algorithms/contractions/window/tools.jl | 69 +++++++++++++++++++ test/toolbox/mpo_routing.jl | 2 +- 3 files changed, 70 insertions(+), 45 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index e31ff3b0a..f460fcc71 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -134,16 +134,6 @@ function _expectation_value_approx_rows( return numerator / norm end -""" -Apply a finite MPO to a finite MPS with zip-up truncation and optional DMRG refinement. -""" -function _approximate_window_step(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) - ψ′, = approximate((W, ψ), alg.zipup) - isnothing(alg.dmrg) && return ψ′ - ψ′, = approximate(ψ′, (W, ψ), alg.dmrg) - return ψ′ -end - """ Contract a complete PEPO window row by row from north to south, optionally inserting an MPO observable. @@ -162,40 +152,6 @@ function _contract_window_rows( return dot(south, ψ) end -""" -Build the finite MPS representing the north CTMRG boundary of a window. -""" -function _north_boundary_mps( - env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, - ) - r = row - 1 - cmin, cmax = first(colrange), last(colrange) - Cwest = insertleftunit(corner(env, NORTHWEST, r, cmin - 1), 1) - tensors = [Cwest] - append!(tensors, (edge(env, NORTH, r, col) for col in colrange)) - # Closing the right boundary is a planar bend - Ceast = repartition(corner(env, NORTHEAST, r, cmax + 1), 2, 0) - push!(tensors, insertleftunit(Ceast, 3)) - return FiniteMPS(tensors) -end - -""" -Build the finite MPS representing the adjointed south CTMRG boundary of a window. -""" -function _south_boundary_mps( - env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, - ) - r = row + 1 - cmin, cmax = first(colrange), last(colrange) - Cwest = insertleftunit(corner(env, SOUTHWEST, r, cmin - 1)', 1) - tensors = [Cwest] - append!(tensors, (_bra_mps_tensor(edge(env, SOUTH, r, col)) for col in colrange)) - Ceast = repartition(corner(env, SOUTHEAST, r, cmax + 1)', 2, 0; copy = true) - # The planar bend of the adjointed southeast corner carries a twist. - push!(tensors, insertleftunit(twist!(Ceast, 1), 3)) - return FiniteMPS(tensors) -end - """ Build one finite row MPO from west/east CTMRG edges and the PEPO tensors inside the window. """ diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index ac9824db2..176f33050 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -7,6 +7,18 @@ struct WindowApprox{Z, D} dmrg::D end +# TODO: generalize the following to multi-layer networks + +""" +Apply a finite MPO to a finite MPS with zip-up truncation and optional DMRG refinement. +""" +function _approximate_window_step(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) + ψ′, = approximate((W, ψ), alg.zipup) + isnothing(alg.dmrg) && return ψ′ + ψ′, = approximate(ψ′, (W, ψ), alg.dmrg) + return ψ′ +end + """ Convert a south-boundary MPS tensor into the stored bra representation expected by `dot` using a planar repartition. """ @@ -20,3 +32,60 @@ Construct the planar adjoint of a finite MPO while restoring MPSKit's local MPO function _adjoint_mpo(W::FiniteMPO) return FiniteMPO(map(A -> transpose(A', ((3, 1), (4, 2)); copy = true), parent(W))) end + +""" +Build the finite MPS representing the north CTMRG boundary of a window. + +Convention of CTM tensors on the north boundary is +``` + [1; 2] [1 2; 3] [1; 2] + C₁-←-2 1-←-E₁-←-3 1-←-C₂ + ↓ ↓ ↑ + 1 2 2 +``` +""" +function _north_boundary_mps( + env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, + ) + r = row - 1 + cmin, cmax = first(colrange), last(colrange) + Cwest = insertleftunit(corner(env, NORTHWEST, r, cmin - 1), 1) + tensors = [Cwest] + append!(tensors, (edge(env, NORTH, r, col) for col in colrange)) + Ceast = repartition(corner(env, NORTHEAST, r, cmax + 1), 2, 0) + push!(tensors, insertleftunit(Ceast, 3)) + return FiniteMPS(tensors) +end + +""" +Build the finite MPS representing the adjointed south CTMRG boundary of a window. + +Convention of CTM tensors on the south boundary is +``` + [1; 2] [1 2; 3] [1; 2] + 2 2 1 + ↓ ↓ ↑ + C₄-→-1 3-→-E₃-→-1 2-→-C₃ +``` +Their adjoints are +``` + [1; 2] [1; 2 3] [1; 2] + C̄₄-←-2 1-←-Ē₃-←-2 1-←-C̄₃ + ↓ ↓ ↑ + 1 3 2 +``` +The edge tensors then need a further repartition of indices. +""" +function _south_boundary_mps( + env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, + ) + r = row + 1 + cmin, cmax = first(colrange), last(colrange) + Cwest = insertleftunit(corner(env, SOUTHWEST, r, cmin - 1)', 1) + tensors = [Cwest] + append!(tensors, (_bra_mps_tensor(edge(env, SOUTH, r, col)) for col in colrange)) + Ceast = repartition(corner(env, SOUTHEAST, r, cmax + 1)', 2, 0; copy = true) + # The planar bend of the adjointed southeast corner carries a twist + push!(tensors, insertleftunit(twist!(Ceast, 1), 3)) + return FiniteMPS(tensors) +end diff --git a/test/toolbox/mpo_routing.jl b/test/toolbox/mpo_routing.jl index 19a59385b..2b9a4a1ca 100644 --- a/test/toolbox/mpo_routing.jl +++ b/test/toolbox/mpo_routing.jl @@ -71,7 +71,7 @@ end ρ = InfinitePEPO(d, D; unitcell = (1, 1, 1)) op = rand(ComplexF64, d^2 → d^2) mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) - + A = ρ[1, 1, 1] for direction in directions first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(direction)) From 6f5f4d3e387a192747abf1dd48f1ca5c4a47ba09 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Wed, 19 Aug 2026 17:53:12 +0800 Subject: [PATCH 10/20] Standardize envspace arrow on east CTM boundary --- .../contractions/window/pepo_1layer.jl | 18 +++++++++++++++--- src/algorithms/contractions/window/tools.jl | 19 ++++++++++++------- .../window/twosite/pepo_1layer.jl | 6 +++--- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index f460fcc71..8531553c7 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -154,13 +154,23 @@ end """ Build one finite row MPO from west/east CTMRG edges and the PEPO tensors inside the window. + +Convention of west, east CTM edges and the PF tensors: +``` + [1 2; 3] [1 2; 3 4] [1 2; 3] + 3 3 1 + ↓ ↓ ↑ + E₄-←-2 1-←-O-←-4 2-←-C₂ + ↓ ↓ ↑ + 1 2 3 +``` +Legs 1, 3 need to be flipped to match standard MPS convention """ function _row_mpo( ρ::InfinitePEPO, observable::Union{Nothing, MPOObservable}, env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, ) cmin, cmax = first(colrange), last(colrange) - # Opening the left boundary is a planar bend, not a braid. W = repartition(edge(env, WEST, row, cmin - 1), 1, 2) tensors = [insertleftunit(W, 1)] append!( @@ -170,8 +180,10 @@ function _row_mpo( for col in colrange ), ) - # Closing the right boundary is a planar bend, not a braid. - E = transpose(edge(env, EAST, row, cmax + 1), ((2, 3), (1,))) + E = permute( + flip(edge(env, EAST, row, cmax + 1), (1, 3)), + ((2, 3), (1,)) + ) push!(tensors, insertrightunit(E, 3)) return FiniteMPO(tensors) end diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index 176f33050..72ab61b8a 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -40,9 +40,10 @@ Convention of CTM tensors on the north boundary is ``` [1; 2] [1 2; 3] [1; 2] C₁-←-2 1-←-E₁-←-3 1-←-C₂ - ↓ ↓ ↑ + ↓ ↓ ↓ 1 2 2 ``` +Leg 2 of C₂ needs to be flipped to match standard MPS convention. """ function _north_boundary_mps( env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, @@ -52,7 +53,9 @@ function _north_boundary_mps( Cwest = insertleftunit(corner(env, NORTHWEST, r, cmin - 1), 1) tensors = [Cwest] append!(tensors, (edge(env, NORTH, r, col) for col in colrange)) - Ceast = repartition(corner(env, NORTHEAST, r, cmax + 1), 2, 0) + Ceast = repartition( + flip(corner(env, NORTHEAST, r, cmax + 1), 2), 2, 0 + ) push!(tensors, insertleftunit(Ceast, 3)) return FiniteMPS(tensors) end @@ -67,11 +70,12 @@ Convention of CTM tensors on the south boundary is ↓ ↓ ↑ C₄-→-1 3-→-E₃-→-1 2-→-C₃ ``` -Their adjoints are +Leg 1 of C₃ needs to be flipped to match standard MPS convention. +Then, their adjoints are ``` [1; 2] [1; 2 3] [1; 2] C̄₄-←-2 1-←-Ē₃-←-2 1-←-C̄₃ - ↓ ↓ ↑ + ↓ ↓ ↓ 1 3 2 ``` The edge tensors then need a further repartition of indices. @@ -84,8 +88,9 @@ function _south_boundary_mps( Cwest = insertleftunit(corner(env, SOUTHWEST, r, cmin - 1)', 1) tensors = [Cwest] append!(tensors, (_bra_mps_tensor(edge(env, SOUTH, r, col)) for col in colrange)) - Ceast = repartition(corner(env, SOUTHEAST, r, cmax + 1)', 2, 0; copy = true) - # The planar bend of the adjointed southeast corner carries a twist - push!(tensors, insertleftunit(twist!(Ceast, 1), 3)) + Ceast = repartition( + flip(corner(env, SOUTHEAST, r, cmax + 1), 1)', 2, 0 + ) + push!(tensors, insertleftunit(Ceast, 3)) return FiniteMPS(tensors) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index 84e239b1a..c380e3090 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -237,11 +237,11 @@ end Contract the left and right transfer-matrix environments to a scalar. ``` (north) - ┌---- 3 ----┐ + ┌-←-- 3 --←-┐ | | - L---- 2 ----R + L-←-- 2 --←-R | | - └---- 1 ----┘ + └-→-- 1 --→-┘ (south) ``` """ From 57820f87bcc81090a807762db70408165a646ae6 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 15 Sep 2026 08:58:51 +0800 Subject: [PATCH 11/20] Rename fields of `WindowRowCache` --- .../contractions/window/twosite/caching.jl | 28 +++++++++---------- .../window/twosite/pepo_1layer.jl | 4 +-- test/toolbox/correlator_approx.jl | 4 +-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/algorithms/contractions/window/twosite/caching.jl b/src/algorithms/contractions/window/twosite/caching.jl index 28f1e40a5..ca38495ef 100644 --- a/src/algorithms/contractions/window/twosite/caching.jl +++ b/src/algorithms/contractions/window/twosite/caching.jl @@ -71,9 +71,9 @@ The fields contain: - `rowrange` and `colrange`: the coordinate ranges defining the window. - `row_mpos`: the row MPOs without observables, one per row and including the west and east CTMRG edges. -- `north_prefixes`: `nrows + 1` north boundary MPSs. +- `north_boundaries`: `nrows + 1` north boundary MPSs. Entry `k` is above row `k` in the window. -- `south_suffixes`: `nrows + 1` adjointed south boundary MPSs. +- `south_boundaries`: `nrows + 1` adjointed south boundary MPSs. Entry `k + 1` is below row `k` in the window. - `norm`: the approximate contraction of the window with no observable inserted. """ @@ -81,8 +81,8 @@ struct WindowRowCache{M <: FiniteMPO, N <: FiniteMPS, S <: FiniteMPS, T <: Numbe rowrange::UnitRange{Int} colrange::UnitRange{Int} row_mpos::Dict{Int, M} - north_prefixes::Vector{N} - south_suffixes::Vector{S} + north_boundaries::Vector{N} + south_boundaries::Vector{S} norm::T end @@ -101,24 +101,24 @@ function _window_row_cache( north = _north_boundary_mps(env, first(rowrange), colrange) south = _south_boundary_mps(env, last(rowrange), colrange) - north_prefixes = Vector{typeof(north)}(undef, nrows + 1) - north_prefixes[1] = north + north_boundaries = Vector{typeof(north)}(undef, nrows + 1) + north_boundaries[1] = north for (k, row) in enumerate(rowrange) - north_prefixes[k + 1] = _approximate_window_step( - row_mpos[row], north_prefixes[k], alg + north_boundaries[k + 1] = _approximate_window_step( + row_mpos[row], north_boundaries[k], alg ) end - south_suffixes = Vector{typeof(south)}(undef, nrows + 1) - south_suffixes[end] = south + south_boundaries = Vector{typeof(south)}(undef, nrows + 1) + south_boundaries[end] = south for (k, row) in Iterators.reverse(enumerate(rowrange)) W = _adjoint_mpo(row_mpos[row]) - south_suffixes[k] = _approximate_window_step( - W, south_suffixes[k + 1], alg + south_boundaries[k] = _approximate_window_step( + W, south_boundaries[k + 1], alg ) end - norm = dot(south, north_prefixes[end]) + norm = dot(south, north_boundaries[end]) return WindowRowCache( - rowrange, colrange, row_mpos, north_prefixes, south_suffixes, norm + rowrange, colrange, row_mpos, north_boundaries, south_boundaries, norm ) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index c380e3090..d7cbf656c 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -78,7 +78,7 @@ function _contract_twosite_source!( # grouping targets by which row they are in targets_by_row = _twosite_targets_by_row(targets) source_idx = source[1] - first(cache.rowrange) + 1 - north = cache.north_prefixes[source_idx] + north = cache.north_boundaries[source_idx] # Close targets in the same row as the source if haskey(targets_by_row, source[1]) @@ -125,7 +125,7 @@ function _contract_twosite_target_row!( ) row = first(keys(targets))[1] row_idx = row - first(cache.rowrange) + 1 - south = cache.south_suffixes[row_idx + 1] + south = cache.south_boundaries[row_idx + 1] envs = environments(south, cache.row_mpos[row], north) source_site = _window_mps_site(source[2], cache.colrange) stringspace = space(mpo[2], 1) diff --git a/test/toolbox/correlator_approx.jl b/test/toolbox/correlator_approx.jl index 28429f086..dfdb11e4c 100644 --- a/test/toolbox/correlator_approx.jl +++ b/test/toolbox/correlator_approx.jl @@ -102,8 +102,8 @@ end alg = PEPSKit.WindowApprox(Zipup(; trunc), nothing) cache = PEPSKit._window_row_cache(ρ, env, 1:2, 1:2, alg) - @test all(eachindex(cache.north_prefixes)) do k - dot(cache.south_suffixes[k], cache.north_prefixes[k]) ≈ cache.norm + @test all(eachindex(cache.north_boundaries)) do k + dot(cache.south_boundaries[k], cache.north_boundaries[k]) ≈ cache.norm end W = cache.row_mpos[first(cache.rowrange)] From 2a67cf7a9878736463fd37856c10e31d3b3459be Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 15 Sep 2026 09:23:00 +0800 Subject: [PATCH 12/20] Separate `mpo_path` functions from their generated contraction part --- .../contractions/mpo_path/pepo_1layer.jl | 121 ++++++++++-------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/src/algorithms/contractions/mpo_path/pepo_1layer.jl b/src/algorithms/contractions/mpo_path/pepo_1layer.jl index 7628b0413..e93e8dbad 100644 --- a/src/algorithms/contractions/mpo_path/pepo_1layer.jl +++ b/src/algorithms/contractions/mpo_path/pepo_1layer.jl @@ -40,7 +40,11 @@ end Return the tensor-expression label for the PEPO virtual leg in a cardinal direction. """ function _mpo_path_virtual_label(direction::Symbol) - return (:N, :E, :S, :W)[_mpo_path_direction(direction)] + direction === :north && return :N + direction === :east && return :E + direction === :south && return :S + direction === :west && return :W + throw(ArgumentError("invalid MPO path direction: $direction")) end """ @@ -83,77 +87,96 @@ function _mpo_path_result_expr(directions) end """ -Act the first tensor `op` of an OBC-MPO on PEPO tensor `A` and fuse the -outgoing MPO string with the virtual space of `A` along `direction`. +Act the first tensor `op` of an OBC-MPO on PEPO tensor `A` and fuse the outgoing MPO string with the virtual space of `A` along `direction`. +""" +function mpo_path_first(A::PEPOTensor, op, direction::Val{D}) where {D} + _check_pepo_first_physicalspace(A, op) + direction_index = _mpo_path_direction(D) + A′ = twistdual(A, 2) + F = _mpo_path_outgoing_fuser( + fuser(storagetype(A), domain(A, direction_index)', space(op, 3)), + direction_index, + ) + return _mpo_path_first(A′, op, F, direction) +end + +""" +Contract a prepared PEPO tensor with the first MPO tensor and its outgoing fuser. """ -@generated function mpo_path_first(A::PEPOTensor, op, ::Val{direction}) where {direction} - direction_index = _mpo_path_direction(direction) +@generated function _mpo_path_first(A, op, F, ::Val{direction}) where {direction} virtual_label = _mpo_path_virtual_label(direction) fused_label = Symbol(virtual_label, :r) result_e = _mpo_path_result_expr(((direction, :r),)) op_e = tensorexpr(:op, :dout, (:din, :r)) - A_e = tensorexpr(:A′, (:din, :dout), (:N, :E, :S, :W)) + A_e = tensorexpr(:A, (:din, :dout), (:N, :E, :S, :W)) F_e = tensorexpr(:F, fused_label, (virtual_label, :r)) rhs = Expr(:call, :*, op_e, A_e, F_e) - contraction = macroexpand( + return macroexpand( @__MODULE__, :(return @tensoropt $result_e := $rhs) ) +end - return quote - _check_pepo_first_physicalspace(A, op) - A′ = twistdual(A, 2) - F = _mpo_path_outgoing_fuser( - fuser(storagetype(A), domain(A, $direction_index)', space(op, 3)), - $direction_index, - ) - $contraction - end +""" +Act the last tensor `op` of an OBC-MPO on PEPO tensor `A` and fuse the incoming MPO string with the virtual space of `A` along `direction`. +""" +function mpo_path_last(A::PEPOTensor, op, direction::Val{D}) where {D} + _check_pepo_last_physicalspace(A, op) + direction_index = _mpo_path_direction(D) + A′ = twistdual(A, 2) + F = _mpo_path_incoming_fuser( + fuser(storagetype(A), domain(A, direction_index), space(op, 1)'), + direction_index, + ) + return _mpo_path_last(A′, op, F, direction) end """ -Act the last tensor `op` of an OBC-MPO on PEPO tensor `A` and fuse the -incoming MPO string with the virtual space of `A` along `direction`. +Contract a prepared PEPO tensor with the last MPO tensor and its incoming fuser. """ -@generated function mpo_path_last(A::PEPOTensor, op, ::Val{direction}) where {direction} - direction_index = _mpo_path_direction(direction) +@generated function _mpo_path_last(A, op, F, ::Val{direction}) where {direction} virtual_label = _mpo_path_virtual_label(direction) fused_label = Symbol(virtual_label, :l) result_e = _mpo_path_result_expr(((direction, :l),)) F_e = Expr(:call, :conj, tensorexpr(:F, fused_label, (virtual_label, :l))) op_e = tensorexpr(:op, (:l, :dout), :din) - A_e = tensorexpr(:A′, (:din, :dout), (:N, :E, :S, :W)) + A_e = tensorexpr(:A, (:din, :dout), (:N, :E, :S, :W)) rhs = Expr(:call, :*, F_e, op_e, A_e) - contraction = macroexpand( + return macroexpand( @__MODULE__, :(return @tensoropt $result_e := $rhs) ) - - return quote - _check_pepo_last_physicalspace(A, op) - A′ = twistdual(A, 2) - F = _mpo_path_incoming_fuser( - fuser(storagetype(A), domain(A, $direction_index), space(op, 1)'), - $direction_index, - ) - $contraction - end end """ -Act the middle tensor `op` of an MPO on PEPO tensor `A` and fuse the -incoming and the outgoing MPO string with the virtual space of `A` along -`directions = (incoming, outgoing)`. +Act the middle tensor `op` of an MPO on PEPO tensor `A` and fuse the incoming and outgoing MPO strings with the virtual space of `A` along `directions = (incoming, outgoing)`. """ -@generated function mpo_path_middle( - A::PEPOTensor, op, ::Val{directions} - ) where {directions} - incoming, outgoing = directions +function mpo_path_middle(A::PEPOTensor, op, directions::Val{D}) where {D} + incoming, outgoing = D incoming == outgoing && throw(ArgumentError("MPO path should enter and exit in different directions")) - + _check_pepo_middle_physicalspace(A, op) incoming_index = _mpo_path_direction(incoming) outgoing_index = _mpo_path_direction(outgoing) + A′ = twistdual(A, 2) + Fin = _mpo_path_incoming_fuser( + fuser(storagetype(A), domain(A, incoming_index), space(op, 1)'), + incoming_index, + ) + Fout = _mpo_path_outgoing_fuser( + fuser(storagetype(A), domain(A, outgoing_index)', space(op, 4)), + outgoing_index, + ) + return _mpo_path_middle(A′, op, Fin, Fout, directions) +end + +""" +Contract a prepared PEPO tensor with a middle MPO tensor and its incoming and outgoing fusers. +""" +@generated function _mpo_path_middle( + A, op, Fin, Fout, ::Val{directions} + ) where {directions} + incoming, outgoing = directions incoming_label = _mpo_path_virtual_label(incoming) outgoing_label = _mpo_path_virtual_label(outgoing) fused_incoming_label = Symbol(incoming_label, :l) @@ -165,28 +188,14 @@ incoming and the outgoing MPO string with the virtual space of `A` along tensorexpr(:Fin, fused_incoming_label, (incoming_label, :l)), ) op_e = tensorexpr(:op, (:l, :dout), (:din, :r)) - A_e = tensorexpr(:A′, (:din, :dout), (:N, :E, :S, :W)) + A_e = tensorexpr(:A, (:din, :dout), (:N, :E, :S, :W)) Fout_e = tensorexpr( :Fout, fused_outgoing_label, (outgoing_label, :r) ) rhs = Expr(:call, :*, Fin_e, op_e, A_e, Fout_e) - contraction = macroexpand( + return macroexpand( @__MODULE__, :(return @tensoropt $result_e := $rhs) ) - - return quote - _check_pepo_middle_physicalspace(A, op) - A′ = twistdual(A, 2) - Fin = _mpo_path_incoming_fuser( - fuser(storagetype(A), domain(A, $incoming_index), space(op, 1)'), - $incoming_index, - ) - Fout = _mpo_path_outgoing_fuser( - fuser(storagetype(A), domain(A, $outgoing_index)', space(op, 4)), - $outgoing_index, - ) - $contraction - end end """ From af6a64cd4a49bf2d56a865b05ec8449758197069 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 15 Sep 2026 10:01:39 +0800 Subject: [PATCH 13/20] Rename `_approximate` --- src/algorithms/contractions/window/pepo_1layer.jl | 2 +- src/algorithms/contractions/window/tools.jl | 2 +- src/algorithms/contractions/window/twosite/caching.jl | 4 ++-- src/algorithms/contractions/window/twosite/pepo_1layer.jl | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 8531553c7..1e6820784 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -146,7 +146,7 @@ function _contract_window_rows( ψ = _north_boundary_mps(env, first(rowrange), colrange) for row in rowrange W = _row_mpo(ρ, observable, env, row, colrange) - ψ = _approximate_window_step(W, ψ, alg) + ψ = _approximate(W, ψ, alg) end south = _south_boundary_mps(env, last(rowrange), colrange) return dot(south, ψ) diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index 72ab61b8a..2cbeb9dc7 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -12,7 +12,7 @@ end """ Apply a finite MPO to a finite MPS with zip-up truncation and optional DMRG refinement. """ -function _approximate_window_step(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) +function _approximate(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) ψ′, = approximate((W, ψ), alg.zipup) isnothing(alg.dmrg) && return ψ′ ψ′, = approximate(ψ′, (W, ψ), alg.dmrg) diff --git a/src/algorithms/contractions/window/twosite/caching.jl b/src/algorithms/contractions/window/twosite/caching.jl index ca38495ef..b605c6fcc 100644 --- a/src/algorithms/contractions/window/twosite/caching.jl +++ b/src/algorithms/contractions/window/twosite/caching.jl @@ -104,7 +104,7 @@ function _window_row_cache( north_boundaries = Vector{typeof(north)}(undef, nrows + 1) north_boundaries[1] = north for (k, row) in enumerate(rowrange) - north_boundaries[k + 1] = _approximate_window_step( + north_boundaries[k + 1] = _approximate( row_mpos[row], north_boundaries[k], alg ) end @@ -113,7 +113,7 @@ function _window_row_cache( south_boundaries[end] = south for (k, row) in Iterators.reverse(enumerate(rowrange)) W = _adjoint_mpo(row_mpos[row]) - south_boundaries[k] = _approximate_window_step( + south_boundaries[k] = _approximate( W, south_boundaries[k + 1], alg ) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index d7cbf656c..91a151377 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -93,7 +93,7 @@ function _contract_twosite_source!( A = ρ[source[1], source[2], 1] source_tensor = mpo_path_first(A, mpo[1], Val(:south)) W = _row_mpo_with_site(ρ, source_tensor, env, source[1], source[2], cache.colrange) - north = _approximate_window_step(W, north, alg) + north = _approximate(W, north, alg) stringspace = space(mpo[2], 1) for row in (source[1] + 1):last_target_row @@ -108,7 +108,7 @@ function _contract_twosite_source!( A = ρ[row, source[2], 1] string_tensor = mpo_path_string(A, stringspace, Val((:north, :south))) W = _row_mpo_with_site(ρ, string_tensor, env, row, source[2], cache.colrange) - north = _approximate_window_step(W, north, alg) + north = _approximate(W, north, alg) end return numerators end From 13c069c40dc952488eab2e242771a686614b4cc3 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 15 Sep 2026 15:18:33 +0800 Subject: [PATCH 14/20] Make `correlator_approx` interface similar to `correlator` and simplify cache --- .../contractions/window/pepo_1layer.jl | 6 +- .../contractions/window/twosite/caching.jl | 111 +++------------ .../window/twosite/pepo_1layer.jl | 131 +++++------------- src/algorithms/correlator_approx.jl | 65 ++++----- src/algorithms/expval_approx.jl | 11 +- test/toolbox/correlator_approx.jl | 126 +++++++++-------- test/toolbox/correlator_approx_phys.jl | 13 +- test/toolbox/expval_approx.jl | 6 + 8 files changed, 169 insertions(+), 300 deletions(-) diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 1e6820784..958da6a56 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -57,7 +57,11 @@ function _expectation_value_approx( ) _check_window_inputs(ρ, direction) rowrange, colrange = _window_ranges(observable) - sweep = direction === :auto ? (length(colrange) > length(rowrange) ? :rows : :columns) : direction + sweep = if direction === :auto + length(colrange) >= length(rowrange) ? :rows : :columns + else + direction + end if sweep === :rows return _expectation_value_approx_rows( ρ, observable, env, rowrange, colrange, alg diff --git a/src/algorithms/contractions/window/twosite/caching.jl b/src/algorithms/contractions/window/twosite/caching.jl index b605c6fcc..445ee223f 100644 --- a/src/algorithms/contractions/window/twosite/caching.jl +++ b/src/algorithms/contractions/window/twosite/caching.jl @@ -1,63 +1,9 @@ """ -Put 2-site bonds in groups to reuse partial contractions in `correlator_approx`. - -For every bond `(first_site, second_site)` in `bonds`: - -- After ordering the two sites, `source`/`target` is the first/second site. -- `swapped` is `false` when `source == first_site`, and `true` otherwise. -- Bonds with the same `source` and `swapped` are grouped together. -- In each group, the inner dict records each bond's position in `bonds`. - -For example, the ordered bonds - -```julia -CI = CartesianIndex -bonds = [ - (CI(1, 1), CI(1, 3)), - (CI(1, 1), CI(2, 2)), - (CI(1, 2), CI(2, 2)), # another source site - (CI(2, 2), CI(1, 1)), # reversed site order -] -``` - -are grouped as - -```julia -(CI(1, 1), false) => Dict( - CI(1, 3) => 1, - CI(2, 2) => 2, -) -(CI(1, 1), true) => Dict(CI(2, 2) => 4) -(CI(1, 2), false) => Dict(CI(2, 2) => 3) -``` +Group targets by row, retaining each target's original result position. """ -function _twosite_source_groups( - bonds::Vector{NTuple{2, CartesianIndex{2}}}, - ) - groups = Dict{ - Tuple{CartesianIndex{2}, Bool}, - Dict{CartesianIndex{2}, Int}, - }() - for (i, (first_site, second_site)) in enumerate(bonds) - swapped = !issorted((first_site, second_site); by = Tuple) - source, target = swapped ? (second_site, first_site) : (first_site, second_site) - targets = get!(Dict{CartesianIndex{2}, Int}, groups, (source, swapped)) - targets[target] = i - end - return groups -end - -""" -Group the targets associated with one source by their row coordinate. The returned outer -dictionary maps each target row to a dictionary whose entries retain the original -`target => result_position` mapping. - -This lookup lets the source contraction close all targets in the current row together while -propagating a single open MPO string between rows. -""" -function _twosite_targets_by_row(targets::Dict{CartesianIndex{2}, Int}) +function _twosite_targets_by_row(targets::Vector{CartesianIndex{2}}) targets_by_row = Dict{Int, Dict{CartesianIndex{2}, Int}}() - for (target, position) in targets + for (position, target) in enumerate(targets) row_targets = get!(Dict{CartesianIndex{2}, Int}, targets_by_row, target[1]) row_targets[target] = position end @@ -65,60 +11,47 @@ function _twosite_targets_by_row(targets::Dict{CartesianIndex{2}, Int}) end """ -Cache the row MPOs without observables and boundary contractions shared by measurements in one window. +Cache observable-free row MPOs, the initial north boundary, south boundaries, and the window normalization. The fields contain: - `rowrange` and `colrange`: the coordinate ranges defining the window. -- `row_mpos`: the row MPOs without observables, one per row and including the west and east CTMRG edges. -- `north_boundaries`: `nrows + 1` north boundary MPSs. - Entry `k` is above row `k` in the window. -- `south_boundaries`: `nrows + 1` adjointed south boundary MPSs. - Entry `k + 1` is below row `k` in the window. -- `norm`: the approximate contraction of the window with no observable inserted. +- `row_mpos`: observable-free row MPOs, including the west and east CTMRG edges, indexed by window-relative row position. +- `north_boundary`: the initial north boundary MPS above the first window row. +- `south_boundaries`: adjointed south boundary MPSs, with entry `k` immediately below window row `k`. +- `norm`: the approximate contraction without observables, calculated by one full north-to-south sweep without retaining intermediate states. """ struct WindowRowCache{M <: FiniteMPO, N <: FiniteMPS, S <: FiniteMPS, T <: Number} rowrange::UnitRange{Int} colrange::UnitRange{Int} - row_mpos::Dict{Int, M} - north_boundaries::Vector{N} + row_mpos::Vector{M} + north_boundary::N south_boundaries::Vector{S} norm::T end """ -Precompute the row MPOs without observables and north/south boundary contractions reused when measuring many two-site bonds in one window. +Precompute shared row MPOs and south boundaries, and normalize with one rolling north state. """ function _window_row_cache( ρ::InfinitePEPO, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, )::WindowRowCache - row_mpos = Dict( - row => _row_mpo(ρ, nothing, env, row, colrange) - for row in rowrange - ) - nrows = length(rowrange) + row_mpos = [_row_mpo(ρ, nothing, env, row, colrange) for row in rowrange] north = _north_boundary_mps(env, first(rowrange), colrange) south = _south_boundary_mps(env, last(rowrange), colrange) - - north_boundaries = Vector{typeof(north)}(undef, nrows + 1) - north_boundaries[1] = north - for (k, row) in enumerate(rowrange) - north_boundaries[k + 1] = _approximate( - row_mpos[row], north_boundaries[k], alg - ) + state = north + for W in row_mpos + state = _approximate(W, state, alg) end + norm = dot(south, state) - south_boundaries = Vector{typeof(south)}(undef, nrows + 1) + south_boundaries = Vector{typeof(south)}(undef, length(rowrange)) south_boundaries[end] = south - for (k, row) in Iterators.reverse(enumerate(rowrange)) - W = _adjoint_mpo(row_mpos[row]) - south_boundaries[k] = _approximate( - W, south_boundaries[k + 1], alg - ) + for k in (length(rowrange) - 1):-1:1 + W = _adjoint_mpo(row_mpos[k + 1]) + ψ = south_boundaries[k + 1] + south_boundaries[k] = _approximate(W, ψ, alg) end - norm = dot(south, north_boundaries[end]) - return WindowRowCache( - rowrange, colrange, row_mpos, north_boundaries, south_boundaries, norm - ) + return WindowRowCache(rowrange, colrange, row_mpos, north, south_boundaries, norm) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index 91a151377..d35f9f695 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -1,121 +1,62 @@ -# Source-cached dense two-site contractions for single-layer PEPO networks. - """ -Validate a dense two-site measurement, choose its sweep orientation, and dispatch to the -row-oriented source-cached contraction. +Validate operator spaces and rotate column sweeps into the row-oriented contraction. """ function _correlator_approx( ρ::InfinitePEPO, op::AbstractTensorMap, - bonds::Vector{NTuple{2, CartesianIndex{2}}}, + source::CartesianIndex{2}, targets::Vector{CartesianIndex{2}}, env::CTMRGEnv, alg::WindowApprox, direction::Symbol, ) _check_window_inputs(ρ, direction) numout(op) == numin(op) == 2 || throw(ArgumentError("correlator_approx requires a two-site operator")) - for bond in bonds, (i, site) in enumerate(bond) + for (leg, sites) in enumerate(((source,), targets)), site in sites V = physicalspace(ρ, Tuple(site)...) - V == codomain(op)[i] == domain(op)[i] || + V == codomain(op)[leg] == domain(op)[leg] || throw(SpaceMismatch("operator physical space does not match PEPO site $site")) end - - rowrange, colrange = _window_ranges(bonds) - sweep = direction === :auto ? (length(colrange) > length(rowrange) ? :rows : :columns) : direction - if sweep === :rows - return _correlator_approx_rows( - ρ, op, bonds, env, rowrange, colrange, alg - ) - end - # rotate column-wise contraction to reuse row-wise code - unitcell = size(ρ)[1:2] - rotated_bonds = map(bonds) do bond - return (siterotl90(bond[1], unitcell), siterotl90(bond[2], unitcell)) + if direction === :columns + unitcell = size(ρ)[1:2] + source = siterotl90(source, unitcell) + targets = siterotl90.(targets, Ref(unitcell)) + ρ, env = rotl90(ρ), rotl90(env) end - rotated_rowrange, rotated_colrange = _window_ranges(rotated_bonds) - return _correlator_approx_rows( - rotl90(ρ), op, rotated_bonds, rotl90(env), - rotated_rowrange, rotated_colrange, alg - ) + return _correlator_approx_rows(ρ, op, source, targets, env, alg) end """ -Measure all ordered bonds in one row-oriented window using shared row MPOs without observables, shared boundaries, and one exactly decomposed MPO for each operator-leg ordering. +Measure all targets with one MPO decomposition and a single open string propagated south from the fixed source. """ function _correlator_approx_rows( ρ::InfinitePEPO, op::AbstractTensorMap, - bonds::Vector{NTuple{2, CartesianIndex{2}}}, env::CTMRGEnv, - rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, + source::CartesianIndex{2}, targets::Vector{CartesianIndex{2}}, + env::CTMRGEnv, alg::WindowApprox, ) ρ, env = standardize_dualness(ρ, env) + rowrange, colrange = _window_ranges([source; targets]) cache = _window_row_cache(ρ, env, rowrange, colrange, alg) - groups = _twosite_source_groups(bonds) - mpo = gate_to_mpo(op; trunc = notrunc()) - swapped_mpo = if any(key[2] for key in keys(groups)) - swapped_op = permute(op, ((2, 1), (4, 3))) - gate_to_mpo(swapped_op; trunc = notrunc()) - end - - T = promote_type(scalartype(op), typeof(cache.norm)) - numerators = zeros(T, length(bonds)) - for ((source, swapped), targets) in groups - source_mpo = swapped ? something(swapped_mpo) : mpo - _contract_twosite_source!( - numerators, ρ, source_mpo, source, targets, env, cache, alg, - ) - end - return numerators ./ cache.norm -end - -""" -Contract the correlator numerator for all targets associated with the same source -and one ordering of the dense operator, writing each result into `numerators`. -""" -function _contract_twosite_source!( - numerators::Vector{<:Number}, ρ::InfinitePEPO, - mpo::AbstractVector{<:AbstractTensorMap}, - source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, - env::CTMRGEnv, cache::WindowRowCache, alg::WindowApprox, - ) - # grouping targets by which row they are in targets_by_row = _twosite_targets_by_row(targets) - source_idx = source[1] - first(cache.rowrange) + 1 - north = cache.north_boundaries[source_idx] - - # Close targets in the same row as the source - if haskey(targets_by_row, source[1]) - _contract_twosite_target_row!( - numerators, ρ, mpo, source, targets_by_row[source[1]], north, cache - ) - end - last_target_row = maximum(keys(targets_by_row)) - last_target_row == source[1] && return numerators - - # Open the MPO string toward the south for targets in later rows. - A = ρ[source[1], source[2], 1] - source_tensor = mpo_path_first(A, mpo[1], Val(:south)) - W = _row_mpo_with_site(ρ, source_tensor, env, source[1], source[2], cache.colrange) - north = _approximate(W, north, alg) - + mpo = gate_to_mpo(op; trunc = notrunc()) stringspace = space(mpo[2], 1) - for row in (source[1] + 1):last_target_row - # Close every target in this row + numerators = zeros(promote_type(scalartype(op), typeof(cache.norm)), length(targets)) + north = cache.north_boundary + for row in rowrange if haskey(targets_by_row, row) _contract_twosite_target_row!( numerators, ρ, mpo, source, targets_by_row[row], north, cache ) end - row == last_target_row && break - # Carry the open string down to the next row + row == last(rowrange) && break A = ρ[row, source[2], 1] - string_tensor = mpo_path_string(A, stringspace, Val((:north, :south))) - W = _row_mpo_with_site(ρ, string_tensor, env, row, source[2], cache.colrange) + tensor = row == source[1] ? mpo_path_first(A, mpo[1], Val(:south)) : + mpo_path_string(A, stringspace, Val((:north, :south))) + W = _row_mpo_with_site(cache, tensor, row, source[2]) north = _approximate(W, north, alg) end - return numerators + return numerators ./ cache.norm end """ -Contract the correlator numerator for all targets in one row with a -shared open-string north state, writing the results into `numerators`. +Contract all targets in one row with a shared north state, writing results into `numerators`. """ function _contract_twosite_target_row!( numerators::Vector{<:Number}, ρ::InfinitePEPO, @@ -125,8 +66,8 @@ function _contract_twosite_target_row!( ) row = first(keys(targets))[1] row_idx = row - first(cache.rowrange) + 1 - south = cache.south_boundaries[row_idx + 1] - envs = environments(south, cache.row_mpos[row], north) + south = cache.south_boundaries[row_idx] + envs = environments(south, cache.row_mpos[row_idx], north) source_site = _window_mps_site(source[2], cache.colrange) stringspace = space(mpo[2], 1) @@ -176,7 +117,11 @@ function _contract_twosite_target_row!( if !isempty(left_targets) sort!(left_targets; by = x -> x[2], rev = true) A = ρ[row, source[2], 1] - source_tensor = mpo_path_string(A, stringspace, Val((:north, :west))) + source_tensor = if row == source[1] + mpo_path_first(A, mpo[1], Val(:west)) + else + mpo_path_string(A, stringspace, Val((:north, :west))) + end right = TransferMatrix( north.AC[source_site], source_tensor, south.AC[source_site] ) * rightenv(envs, source_site, south) @@ -204,21 +149,19 @@ function _contract_twosite_target_row!( end """ -Map a PEPO column coordinate to its finite-MPS site number, -which includes an additional west edge CTM tensor. +Map a PEPO column to its finite-MPS site, accounting for the additional west CTM edge. """ _window_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 """ -Build a finite row MPO by replacing one site tensor in one of the row MPOs without observables. +Replace one site in a copied row-MPO tensor container, leaving the cached row unchanged. """ function _row_mpo_with_site( - ρ::InfinitePEPO, tensor::MPOTensor, env::CTMRGEnv, - row::Int, col::Int, colrange::UnitRange{Int}, + cache::WindowRowCache, tensor::MPOTensor, row::Int, col::Int, ) - W = _row_mpo(ρ, nothing, env, row, colrange) - parent(W)[_window_mps_site(col, colrange)] = tensor - return W + tensors = copy(parent(cache.row_mpos[row - first(cache.rowrange) + 1])) + tensors[_window_mps_site(col, cache.colrange)] = tensor + return FiniteMPO(tensors) end """ diff --git a/src/algorithms/correlator_approx.jl b/src/algorithms/correlator_approx.jl index 36499ba28..bad3ed8cd 100644 --- a/src/algorithms/correlator_approx.jl +++ b/src/algorithms/correlator_approx.jl @@ -4,54 +4,39 @@ """ $(SIGNATURES) -Approximately measure a dense two-site operator on one or more ordered pairs of sites in a single-layer PEPO. -The first and second operator legs act on the first and second sites of each pair, respectively. -Multiple pairs are evaluated in one shared window and reuse open-string boundary contractions. -Ordered pairs in a batched call must be unique. +Approximately measure a dense two-site operator between a fixed first site `i` and second sites `js` in a single-layer PEPO. + +- Operator leg 1 acts at `i`, and leg 2 acts at each second site. +- The sweep direction is selected automatically: north-to-south requires every `j[1] ≥ i[1]`, and east-to-west requires every `j[2] ≤ i[2]`. + If both sweeps are valid, use north-to-south for a square or wide window, and east-to-west for a tall window. + If neither sweep is valid, throw an `ArgumentError`. +- `js` must be nonempty, unique, and distinct from `i`. + A collection returns a vector in `vec(js)` order; a single second site returns a scalar. + All targets share the smallest enclosing rectangular window and its normalization, calculated once by a full sweep without observables. +- Boundary-MPS truncation uses `trunc` (defaulting to the largest CTMRG boundary dimension), with `maxiter` DMRG refinement sweeps after each zipup step (default 1; 0 disables refinement). """ function correlator_approx( - ρ::InfinitePEPO, op::AbstractTensorMap, bond::Tuple, env::CTMRGEnv; - trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, - ) - return only( - correlator_approx(ρ, op, [bond], env; trunc, maxiter, direction) + ρ::InfinitePEPO, op::AbstractTensorMap, + i::CartesianIndex{2}, j::CartesianIndex{2}, env::CTMRGEnv; + trunc = _approx_trunc(env), maxiter::Int = 1, ) + return only(correlator_approx(ρ, op, i, j:j, env; trunc, maxiter)) end function correlator_approx( - ρ::InfinitePEPO, op::AbstractTensorMap, bonds::AbstractVector, - env::CTMRGEnv; - trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, + ρ::InfinitePEPO, op::AbstractTensorMap, + i::CartesianIndex{2}, js::CoordCollection{2}, env::CTMRGEnv; + trunc = _approx_trunc(env), maxiter::Int = 1, ) - bonds′ = _approx_twosite_bonds(bonds) + isempty(js) && throw(ArgumentError("correlator_approx requires at least one second site")) + allunique(js) || throw(ArgumentError("second sites should be unique")) + i in js && throw(ArgumentError("second sites should be distinct from the first site")) + rowrange, colrange = _window_ranges([i; vec(js)]) + rows, columns = first(rowrange) == i[1], last(colrange) == i[2] + rows || columns || throw(ArgumentError("no valid sweep: second sites must all be at or south of the first row, or all at or west of the first column")) + direction = rows && (!columns || length(colrange) >= length(rowrange)) ? :rows : :columns return _correlator_approx( - ρ, op, bonds′, env, + ρ, op, i, collect(vec(js)), env, WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction ) end - -""" -Validate and regularize a nonempty collection of ordered two-site bonds to Cartesian indices. -""" -function _approx_twosite_bonds(bonds) - isempty(bonds) && throw(ArgumentError("correlator_approx requires at least one bond")) - bonds′ = NTuple{2, CartesianIndex{2}}[] - sizehint!(bonds′, length(bonds)) - for bond in bonds - length(bond) == 2 || throw(ArgumentError("each bond should contain two sites")) - first_site = _mpo_observable_site(bond[1]) - second_site = _mpo_observable_site(bond[2]) - first_site != second_site || - throw(ArgumentError("the sites of a bond should be distinct")) - push!(bonds′, (first_site, second_site)) - end - allunique(bonds′) || throw(ArgumentError("bonds should be unique")) - return bonds′ -end - -""" -Return the row and column ranges enclosing every endpoint in a collection of bonds. -""" -function _window_ranges(bonds::Vector{NTuple{2, CartesianIndex{2}}}) - return _window_ranges(Iterators.flatten(bonds)) -end diff --git a/src/algorithms/expval_approx.jl b/src/algorithms/expval_approx.jl index 9876365d6..00b0028d1 100644 --- a/src/algorithms/expval_approx.jl +++ b/src/algorithms/expval_approx.jl @@ -4,10 +4,13 @@ """ $(SIGNATURES) -Approximately measure the expectation value of an open-boundary `MPOObservable` in a single-layer PEPO using finite boundary MPS/MPO zipup sweeps. -The zipup truncation is controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary dimension. -After each zipup step, the result is refined by a single-site DMRG approximation step with `maxiter` sweeps. -Set `maxiter = 0` to disable this refinement. +Approximately measure the expectation value of an open-boundary `MPOObservable` in a single-layer PEPO using finite boundary MPS zipup sweeps. + +- By default, `direction = :auto` selects north-to-south sweep for wide and square windows, and east-to-west for tall windows. + Specify `direction = :rows` or `:columns` to override this choice. +- The zipup truncation is controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary dimension. +- After each zipup step, the result is refined by a single-site DMRG approximation step with `maxiter` sweeps. + Set `maxiter = 0` to disable this refinement. """ function expectation_value_approx( ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv; diff --git a/test/toolbox/correlator_approx.jl b/test/toolbox/correlator_approx.jl index dfdb11e4c..48326efcc 100644 --- a/test/toolbox/correlator_approx.jl +++ b/test/toolbox/correlator_approx.jl @@ -7,14 +7,12 @@ using Random const CI = CartesianIndex """ -Contract `⟨op⟩` on each bond in `bonds` independently without caching +Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using one shared window. """ -function _shared_window_reference( - op::AbstractTensorMap, bonds::Vector{NTuple{2, CI{2}}}, ρ, env - ) +function _shared_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) lattice = physicalspace(ρ) - observables = [MPOObservable(collect(pair), op, lattice) for pair in bonds] - rowrange, colrange = PEPSKit._window_ranges(bonds) + observables = [MPOObservable([i, j], op, lattice) for j in js] + rowrange, colrange = PEPSKit._window_ranges([i; js]) alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) norm = PEPSKit._contract_window_rows( ρ, nothing, env, rowrange, colrange, alg @@ -27,86 +25,86 @@ function _shared_window_reference( end end -bonds = [ - # source at (1, 1), bond sites in order - ## target in the same row as source - (CI(1, 1), CI(1, 2)), - ## target in a later row, on both sides of or just below the source - (CI(1, 1), CI(2, 0)), (CI(1, 1), CI(2, 1)), (CI(1, 1), CI(2, 2)), - # source still at (1, 1), but bond sites need swapping - (CI(2, 2), CI(1, 1)), - # another source at (1, 2), bond sites in order - (CI(1, 2), CI(2, 0)), (CI(1, 2), CI(2, 2)), -] - +# Exercise both U(1) symmetry and fermionic signs with small physical, virtual, and boundary spaces. spaces = Dict( U1Irrep => ( U1Space(1 => 2, -1 => 1), - U1Space(1 => 1, 0 => 1, -1 => 2), - U1Space(1 => 1, 0 => 1, -1 => 2), + U1Space(0 => 1, 1 => 1), + U1Space(0 => 1, 1 => 1), ), FermionParity => ( Vect[FermionParity](0 => 1, 1 => 1), - Vect[FermionParity](0 => 1, 1 => 2), - Vect[FermionParity](0 => 2, 1 => 2), + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 1), ) ) -@testset "Two-site source grouping" begin - groups = PEPSKit._twosite_source_groups(bonds) - @test length(groups) == 3 && - groups[(CI(1, 1), false)][CI(2, 2)] == 4 && - haskey(groups, (CI(1, 1), true)) -end - @testset "Single-layer PEPO ($S)" for S in keys(spaces) + # Use a reproducible random PEPO on a rectangular unit cell and disable boundary truncation. Random.seed!(1234) d, D, χ = spaces[S] - ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) - lattice = physicalspace(ρ) + ρ = InfinitePEPO(d, D; unitcell = (2, 3, 1)) env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) trunc = notrunc() + i = CI(-1, 0) + # Unsorted targets include both same-row neighbors of i and targets to the east, west, and directly south on row 1. + # Row 0 has no measurement targets but is still contracted, testing MPO-string propagation through a row without targets. + js = [CI(1, 1), CI(-1, -1), CI(1, 0), CI(-1, 1), CI(1, -1)] - O² = rand(ComplexF64, d^2, d^2) + # The normalized expectation of the identity must be one at every target. id² = isomorphism(d, d) ⊗ isomorphism(d, d) + @test correlator_approx(ρ, id², i, js, env; trunc, maxiter = 0) ≈ ones(length(js)) - # bonds to be measured should be unique - @test_throws ArgumentError correlator_approx(ρ, O², fill(bonds[3], 2), env) - - exact_same_row = expectation_value(ρ, LocalOperator(lattice, bonds[1] => O²), env) - @test correlator_approx( - ρ, O², bonds[1], env; trunc, maxiter = 0, direction = :rows - ) ≈ exact_same_row + # This target distribution only permits row sweeps. + # Compare against independent contractions in target order. + O² = rand(ComplexF64, d^2, d^2) + vals_ref = _shared_window_reference(O², i, js, ρ, env) + vals_rows = correlator_approx(ρ, O², i, js, env; trunc, maxiter = 0) + @test vals_rows ≈ vals_ref - exact_reversed = expectation_value(ρ, LocalOperator(lattice, bonds[5] => O²), env) - @test correlator_approx( - ρ, O², bonds[5], env; trunc, maxiter = 0, direction = :columns - ) ≈ exact_reversed + # CartesianIndices are flattened exactly as in correlator. + grid = CartesianIndices((1:1, -1:1)) + @test correlator_approx(ρ, O², i, grid, env; trunc, maxiter = 0) ≈ + vals_ref[[5, 3, 1]] - vals_ref = _shared_window_reference(O², bonds, ρ, env) - vals_rows = correlator_approx( - ρ, O², bonds, env; trunc, maxiter = 0, direction = :rows - ) - @test vals_rows ≈ vals_ref - vals_columns = correlator_approx( - ρ, O², bonds, env; trunc, maxiter = 0, direction = :columns - ) - @test vals_columns ≈ vals_rows - vals_auto = correlator_approx( - ρ, O², bonds, env; trunc, maxiter = 0, direction = :auto - ) - @test vals_auto ≈ vals_columns + # The rotated distribution only permits column sweeps. + # Check rotation and automatic selection together. + cell = size(ρ)[1:2] + ic = PEPSKit.siterotr90(i, cell) + jcs = PEPSKit.siterotr90.(js, Ref(cell)) + ρc, envc = rotr90(ρ), rotr90(env) + @test correlator_approx(ρc, O², ic, jcs, envc; trunc, maxiter = 0) ≈ vals_rows - @test correlator_approx(ρ, id², bonds, env; trunc, direction = :rows) ≈ - ones(length(bonds)) + # Test auto sweep direction choice in for southwest targets. + selection_trunc = truncrank(2) + selection_alg = PEPSKit.WindowApprox(Zipup(; trunc = selection_trunc), nothing) + for (offset, direction) in ((CI(1, -2), :rows), (CI(2, -1), :columns), (CI(1, -1), :rows)) + j = i + offset + expected = only(PEPSKit._correlator_approx(ρ, O², i, [j], env, selection_alg, direction)) + @test correlator_approx(ρ, O², i, j, env; trunc = selection_trunc, maxiter = 0) == expected + end + # Check the cached normalization against a full contraction without observables. alg = PEPSKit.WindowApprox(Zipup(; trunc), nothing) - cache = PEPSKit._window_row_cache(ρ, env, 1:2, 1:2, alg) - @test all(eachindex(cache.north_boundaries)) do k - dot(cache.south_boundaries[k], cache.north_boundaries[k]) ≈ cache.norm + rowrange, colrange = PEPSKit._window_ranges([i; js]) + cache = PEPSKit._window_row_cache(ρ, env, rowrange, colrange, alg) + @test cache.norm ≈ PEPSKit._contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) + + # Verify that south_boundaries[k] closes the north state after row k, catching indexing errors. + north = cache.north_boundary + for k in eachindex(cache.row_mpos) + north = PEPSKit._approximate(cache.row_mpos[k], north, alg) + @test dot(cache.south_boundaries[k], north) ≈ cache.norm end - W = cache.row_mpos[first(cache.rowrange)] - W_adjoint = PEPSKit._adjoint_mpo(W) - @test convert(TensorMap, W_adjoint) ≈ convert(TensorMap, W)' + # Replacing a site must modify the returned MPO while leaving the cached row intact. + W = first(cache.row_mpos) + W_before = deepcopy(parent(W)) + replacement = 2 * parent(W)[2] + modified = PEPSKit._row_mpo_with_site(cache, replacement, first(rowrange), first(colrange)) + @test parent(W) == W_before + @test parent(modified)[2] == replacement + + # Check that the planar row-MPO adjoint agrees with the full tensor adjoint. + @test convert(TensorMap, PEPSKit._adjoint_mpo(W)) ≈ convert(TensorMap, W)' end diff --git a/test/toolbox/correlator_approx_phys.jl b/test/toolbox/correlator_approx_phys.jl index 62aad5ea7..f811d0b90 100644 --- a/test/toolbox/correlator_approx_phys.jl +++ b/test/toolbox/correlator_approx_phys.jl @@ -23,16 +23,13 @@ const CI = CartesianIndex env_trunc = truncrank(8) & truncerror(; atol = 1.0e-12) env, = leading_boundary(env, network; alg = :SequentialCTMRG, trunc = env_trunc) - bonds = [ - (CI(1, 1), CI(1, 2)), - (CI(1, 1), CI(2, 0)), (CI(1, 1), CI(2, 1)), (CI(1, 1), CI(2, 2)), - (CI(1, 1), CI(3, 2)), - ] - cor_exact = map(bonds) do bond - O = LocalOperator(lattice, bond => op) + i = CI(1, 1) + js = [CI(1, 2), CI(2, 0), CI(2, 1), CI(2, 2), CI(3, 2)] + cor_exact = map(js) do j + O = LocalOperator(lattice, (i, j) => op) return expectation_value(ρ, O, env) end - cor_trunc = correlator_approx(ρ, op, bonds, env; trunc = env_trunc, maxiter = 1) + cor_trunc = correlator_approx(ρ, op, i, js, env; trunc = env_trunc, maxiter = 1) @info "Exact:" cor_exact @info "Approx:" cor_trunc @test cor_trunc ≈ cor_exact rtol = 1.0e-3 diff --git a/test/toolbox/expval_approx.jl b/test/toolbox/expval_approx.jl index 2f754c74b..d604d5faa 100644 --- a/test/toolbox/expval_approx.jl +++ b/test/toolbox/expval_approx.jl @@ -48,5 +48,11 @@ sites_list = ( ρ, observable, env; trunc, maxiter = 0, direction ) ≈ exact end + # Square windows must default to row sweeps. + if sites == sites_list[3] + auto = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0) + rows = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0, direction = :rows) + @test auto == rows + end end end From 95f1555805ee8faf032f41bc137e6f46748a2304 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Tue, 15 Sep 2026 17:40:21 +0800 Subject: [PATCH 15/20] Adaptive window depth (row number) for `correlator_approx` --- src/PEPSKit.jl | 1 - src/algorithms/contractions/window/tools.jl | 7 --- .../contractions/window/twosite/caching.jl | 57 ------------------- .../window/twosite/pepo_1layer.jl | 51 ++++++++--------- src/algorithms/correlator_approx.jl | 29 ++++++++-- test/toolbox/correlator_approx.jl | 56 +++++++----------- 6 files changed, 68 insertions(+), 133 deletions(-) delete mode 100644 src/algorithms/contractions/window/twosite/caching.jl diff --git a/src/PEPSKit.jl b/src/PEPSKit.jl index d5e4c3bc9..67f4f1997 100644 --- a/src/PEPSKit.jl +++ b/src/PEPSKit.jl @@ -120,7 +120,6 @@ include("algorithms/contractions/correlator/pepo_1layer.jl") include("algorithms/contractions/mpo_path/pepo_1layer.jl") include("algorithms/contractions/window/tools.jl") include("algorithms/contractions/window/pepo_1layer.jl") -include("algorithms/contractions/window/twosite/caching.jl") include("algorithms/contractions/window/twosite/pepo_1layer.jl") include("algorithms/ctmrg/sparse_environments.jl") diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index 2cbeb9dc7..e04899734 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -26,13 +26,6 @@ function _bra_mps_tensor(A::MPSTensor) return repartition(A', 2, 1; copy = true) end -""" -Construct the planar adjoint of a finite MPO while restoring MPSKit's local MPO leg partition. -""" -function _adjoint_mpo(W::FiniteMPO) - return FiniteMPO(map(A -> transpose(A', ((3, 1), (4, 2)); copy = true), parent(W))) -end - """ Build the finite MPS representing the north CTMRG boundary of a window. diff --git a/src/algorithms/contractions/window/twosite/caching.jl b/src/algorithms/contractions/window/twosite/caching.jl deleted file mode 100644 index 445ee223f..000000000 --- a/src/algorithms/contractions/window/twosite/caching.jl +++ /dev/null @@ -1,57 +0,0 @@ -""" -Group targets by row, retaining each target's original result position. -""" -function _twosite_targets_by_row(targets::Vector{CartesianIndex{2}}) - targets_by_row = Dict{Int, Dict{CartesianIndex{2}, Int}}() - for (position, target) in enumerate(targets) - row_targets = get!(Dict{CartesianIndex{2}, Int}, targets_by_row, target[1]) - row_targets[target] = position - end - return targets_by_row -end - -""" -Cache observable-free row MPOs, the initial north boundary, south boundaries, and the window normalization. - -The fields contain: - -- `rowrange` and `colrange`: the coordinate ranges defining the window. -- `row_mpos`: observable-free row MPOs, including the west and east CTMRG edges, indexed by window-relative row position. -- `north_boundary`: the initial north boundary MPS above the first window row. -- `south_boundaries`: adjointed south boundary MPSs, with entry `k` immediately below window row `k`. -- `norm`: the approximate contraction without observables, calculated by one full north-to-south sweep without retaining intermediate states. -""" -struct WindowRowCache{M <: FiniteMPO, N <: FiniteMPS, S <: FiniteMPS, T <: Number} - rowrange::UnitRange{Int} - colrange::UnitRange{Int} - row_mpos::Vector{M} - north_boundary::N - south_boundaries::Vector{S} - norm::T -end - -""" -Precompute shared row MPOs and south boundaries, and normalize with one rolling north state. -""" -function _window_row_cache( - ρ::InfinitePEPO, env::CTMRGEnv, - rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, - )::WindowRowCache - row_mpos = [_row_mpo(ρ, nothing, env, row, colrange) for row in rowrange] - north = _north_boundary_mps(env, first(rowrange), colrange) - south = _south_boundary_mps(env, last(rowrange), colrange) - state = north - for W in row_mpos - state = _approximate(W, state, alg) - end - norm = dot(south, state) - - south_boundaries = Vector{typeof(south)}(undef, length(rowrange)) - south_boundaries[end] = south - for k in (length(rowrange) - 1):-1:1 - W = _adjoint_mpo(row_mpos[k + 1]) - ψ = south_boundaries[k + 1] - south_boundaries[k] = _approximate(W, ψ, alg) - end - return WindowRowCache(rowrange, colrange, row_mpos, north, south_boundaries, norm) -end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index d35f9f695..8ff826a91 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -24,7 +24,7 @@ function _correlator_approx( end """ -Measure all targets with one MPO decomposition and a single open string propagated south from the fixed source. +Measure targets in windows of fixed width ending at each target row, propagating observable-free and open-string north states together. """ function _correlator_approx_rows( ρ::InfinitePEPO, op::AbstractTensorMap, @@ -33,42 +33,48 @@ function _correlator_approx_rows( ) ρ, env = standardize_dualness(ρ, env) rowrange, colrange = _window_ranges([source; targets]) - cache = _window_row_cache(ρ, env, rowrange, colrange, alg) targets_by_row = _twosite_targets_by_row(targets) mpo = gate_to_mpo(op; trunc = notrunc()) stringspace = space(mpo[2], 1) - numerators = zeros(promote_type(scalartype(op), typeof(cache.norm)), length(targets)) - north = cache.north_boundary + values = zeros(promote_type(scalartype(op), scalartype(ρ), scalartype(env)), length(targets)) + north = _north_boundary_mps(env, source[1], colrange) + plain_north = north for row in rowrange + W = _row_mpo(ρ, nothing, env, row, colrange) if haskey(targets_by_row, row) + south = _south_boundary_mps(env, row, colrange) + norm = dot(south, W, plain_north) _contract_twosite_target_row!( - numerators, ρ, mpo, source, targets_by_row[row], north, cache + values, ρ, mpo, source, targets_by_row[row], north, south, W, colrange ) + for k in Base.values(targets_by_row[row]) + values[k] /= norm + end end row == last(rowrange) && break A = ρ[row, source[2], 1] tensor = row == source[1] ? mpo_path_first(A, mpo[1], Val(:south)) : mpo_path_string(A, stringspace, Val((:north, :south))) - W = _row_mpo_with_site(cache, tensor, row, source[2]) + plain_north = _approximate(W, plain_north, alg) + parent(W)[_window_mps_site(source[2], colrange)] = tensor north = _approximate(W, north, alg) end - return numerators ./ cache.norm + return values end """ -Contract all targets in one row with a shared north state, writing results into `numerators`. +Contract all targets in one row `W` with a shared `north` and `south` boundary MPS, writing results into `numerators`. + """ function _contract_twosite_target_row!( numerators::Vector{<:Number}, ρ::InfinitePEPO, mpo::AbstractVector{<:AbstractTensorMap}, source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, - north::FiniteMPS, cache::WindowRowCache, + north::FiniteMPS, south::FiniteMPS, W::FiniteMPO, colrange::UnitRange{Int}, ) row = first(keys(targets))[1] - row_idx = row - first(cache.rowrange) + 1 - south = cache.south_boundaries[row_idx] - envs = environments(south, cache.row_mpos[row_idx], north) - source_site = _window_mps_site(source[2], cache.colrange) + envs = environments(south, W, north) + source_site = _window_mps_site(source[2], colrange) stringspace = space(mpo[2], 1) # close the target right at the column of the incoming string @@ -95,12 +101,12 @@ function _contract_twosite_target_row!( for target in right_targets target_col = target[2] for col in (previous_col + 1):(target_col - 1) - site = _window_mps_site(col, cache.colrange) + site = _window_mps_site(col, colrange) string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:west, :east))) left = left * TransferMatrix(north.AR[site], string_tensor, south.AR[site]) end - target_site = _window_mps_site(target_col, cache.colrange) + target_site = _window_mps_site(target_col, colrange) target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:west)) target_left = left * TransferMatrix(north.AR[target_site], target_tensor, south.AR[target_site]) value = _contract_transfer_boundaries(target_left, rightenv(envs, target_site, south)) @@ -129,12 +135,12 @@ function _contract_twosite_target_row!( for target in left_targets target_col = target[2] for col in (previous_col - 1):-1:(target_col + 1) - site = _window_mps_site(col, cache.colrange) + site = _window_mps_site(col, colrange) string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:east, :west))) right = TransferMatrix(north.AL[site], string_tensor, south.AL[site]) * right end - target_site = _window_mps_site(target_col, cache.colrange) + target_site = _window_mps_site(target_col, colrange) target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:east)) target_right = TransferMatrix(north.AL[target_site], target_tensor, south.AL[target_site]) * right value = _contract_transfer_boundaries(leftenv(envs, target_site, south), target_right) @@ -153,17 +159,6 @@ Map a PEPO column to its finite-MPS site, accounting for the additional west CTM """ _window_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 -""" -Replace one site in a copied row-MPO tensor container, leaving the cached row unchanged. -""" -function _row_mpo_with_site( - cache::WindowRowCache, tensor::MPOTensor, row::Int, col::Int, - ) - tensors = copy(parent(cache.row_mpos[row - first(cache.rowrange) + 1])) - tensors[_window_mps_site(col, cache.colrange)] = tensor - return FiniteMPO(tensors) -end - """ Contract one modified row site between precomputed left and right MPS environments. """ diff --git a/src/algorithms/correlator_approx.jl b/src/algorithms/correlator_approx.jl index bad3ed8cd..7c00a3064 100644 --- a/src/algorithms/correlator_approx.jl +++ b/src/algorithms/correlator_approx.jl @@ -7,13 +7,18 @@ $(SIGNATURES) Approximately measure a dense two-site operator between a fixed first site `i` and second sites `js` in a single-layer PEPO. - Operator leg 1 acts at `i`, and leg 2 acts at each second site. -- The sweep direction is selected automatically: north-to-south requires every `j[1] ≥ i[1]`, and east-to-west requires every `j[2] ≤ i[2]`. - If both sweeps are valid, use north-to-south for a square or wide window, and east-to-west for a tall window. - If neither sweep is valid, throw an `ArgumentError`. -- `js` must be nonempty, unique, and distinct from `i`. +- The contraction direction is chosen automatically. + Row-by-row contraction proceeds southward and requires every `j[1] ≥ i[1]`; column-by-column contraction proceeds westward and requires every `j[2] ≤ i[2]`. + If both are possible, consider the smallest rectangle containing `i` and all sites in `js`: use rows if it is square or wider than tall, and columns otherwise. + If neither direction is possible, throw an `ArgumentError`. +- The collection `js` must be nonempty, contain no duplicates, and exclude `i`. A collection returns a vector in `vec(js)` order; a single second site returns a scalar. - All targets share the smallest enclosing rectangular window and its normalization, calculated once by a full sweep without observables. -- Boundary-MPS truncation uses `trunc` (defaulting to the largest CTMRG boundary dimension), with `maxiter` DMRG refinement sweeps after each zipup step (default 1; 0 disables refinement). +- For row-by-row contraction, all windows have the same width, covering the columns of `i` and all sites in `js`, but each window ends at the row of the measured site `j`. + For column-by-column contraction, all windows cover the same rows, but each window ends at the column of `j`. + Each window has its own normalization, calculated without the operator using the same contraction arrangement. + The last row or column is contracted without further truncation against the CTMRG boundary immediately beyond it. +- `trunc` controls boundary-MPS truncation; by default, it limits the rank to the largest CTMRG boundary dimension. + After each zipup step, `maxiter` DMRG sweeps refine the result (default 1; use 0 to disable refinement). """ function correlator_approx( ρ::InfinitePEPO, op::AbstractTensorMap, @@ -40,3 +45,15 @@ function correlator_approx( WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction ) end + +""" +Group targets by row, retaining each target's original result position. +""" +function _twosite_targets_by_row(targets::Vector{CartesianIndex{2}}) + targets_by_row = Dict{Int, Dict{CartesianIndex{2}, Int}}() + for (position, target) in enumerate(targets) + row_targets = get!(Dict{CartesianIndex{2}, Int}, targets_by_row, target[1]) + row_targets[target] = position + end + return targets_by_row +end diff --git a/test/toolbox/correlator_approx.jl b/test/toolbox/correlator_approx.jl index 48326efcc..34f4cefa3 100644 --- a/test/toolbox/correlator_approx.jl +++ b/test/toolbox/correlator_approx.jl @@ -7,20 +7,17 @@ using Random const CI = CartesianIndex """ -Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using one shared window. +Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using fixed width and a window ending at each target row. """ -function _shared_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) +function _adaptive_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) lattice = physicalspace(ρ) observables = [MPOObservable([i, j], op, lattice) for j in js] - rowrange, colrange = PEPSKit._window_ranges([i; js]) + _, colrange = PEPSKit._window_ranges([i; js]) alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) - norm = PEPSKit._contract_window_rows( - ρ, nothing, env, rowrange, colrange, alg - ) - return map(observables) do observable - numerator = PEPSKit._contract_window_rows( - ρ, observable, env, rowrange, colrange, alg - ) + return map(observables, js) do observable, j + rowrange = i[1]:j[1] + norm = PEPSKit._contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) + numerator = PEPSKit._contract_window_rows(ρ, observable, env, rowrange, colrange, alg) return numerator / norm end end @@ -58,7 +55,7 @@ spaces = Dict( # This target distribution only permits row sweeps. # Compare against independent contractions in target order. O² = rand(ComplexF64, d^2, d^2) - vals_ref = _shared_window_reference(O², i, js, ρ, env) + vals_ref = _adaptive_window_reference(O², i, js, ρ, env) vals_rows = correlator_approx(ρ, O², i, js, env; trunc, maxiter = 0) @test vals_rows ≈ vals_ref @@ -75,6 +72,12 @@ spaces = Dict( ρc, envc = rotr90(ρ), rotr90(env) @test correlator_approx(ρc, O², ic, jcs, envc; trunc, maxiter = 0) ≈ vals_rows + # Identity normalization must survive truncation for every target row and both sweep orientations. + for maxiter in (0, 1) + @test correlator_approx(ρ, id², i, js, env; trunc = truncrank(2), maxiter) ≈ ones(length(js)) + @test correlator_approx(ρc, id², ic, jcs, envc; trunc = truncrank(2), maxiter) ≈ ones(length(js)) + end + # Test auto sweep direction choice in for southwest targets. selection_trunc = truncrank(2) selection_alg = PEPSKit.WindowApprox(Zipup(; trunc = selection_trunc), nothing) @@ -84,27 +87,12 @@ spaces = Dict( @test correlator_approx(ρ, O², i, j, env; trunc = selection_trunc, maxiter = 0) == expected end - # Check the cached normalization against a full contraction without observables. - alg = PEPSKit.WindowApprox(Zipup(; trunc), nothing) - rowrange, colrange = PEPSKit._window_ranges([i; js]) - cache = PEPSKit._window_row_cache(ρ, env, rowrange, colrange, alg) - @test cache.norm ≈ PEPSKit._contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) - - # Verify that south_boundaries[k] closes the north state after row k, catching indexing errors. - north = cache.north_boundary - for k in eachindex(cache.row_mpos) - north = PEPSKit._approximate(cache.row_mpos[k], north, alg) - @test dot(cache.south_boundaries[k], north) ≈ cache.norm - end - - # Replacing a site must modify the returned MPO while leaving the cached row intact. - W = first(cache.row_mpos) - W_before = deepcopy(parent(W)) - replacement = 2 * parent(W)[2] - modified = PEPSKit._row_mpo_with_site(cache, replacement, first(rowrange), first(colrange)) - @test parent(W) == W_before - @test parent(modified)[2] == replacement - - # Check that the planar row-MPO adjoint agrees with the full tensor adjoint. - @test convert(TensorMap, PEPSKit._adjoint_mpo(W)) ≈ convert(TensorMap, W)' + # Extending the depth must preserve earlier values when the width and selected direction stay fixed. + extended_js = [js; CI(2, 0)] + baseline = correlator_approx(ρ, O², i, js, env; trunc = selection_trunc, maxiter = 0) + extended = correlator_approx(ρ, O², i, extended_js, env; trunc = selection_trunc, maxiter = 0) + @test extended[1:length(js)] ≈ baseline + extended_jcs = PEPSKit.siterotr90.(extended_js, Ref(cell)) + extended_columns = correlator_approx(ρc, O², ic, extended_jcs, envc; trunc = selection_trunc, maxiter = 0) + @test extended_columns ≈ extended end From f56fed85561510b2c037191a37f70b31d3cd5b6f Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 17 Sep 2026 18:10:13 +0800 Subject: [PATCH 16/20] No longer adjointing south boundary --- src/algorithms/contractions/transfer.jl | 96 +++++++++++++++++++ .../contractions/window/pepo_1layer.jl | 2 +- src/algorithms/contractions/window/tools.jl | 45 ++++----- .../window/twosite/pepo_1layer.jl | 58 ++++------- src/operators/transfermatrix.jl | 8 +- test/ctmrg/unitcell.jl | 6 +- 6 files changed, 143 insertions(+), 72 deletions(-) diff --git a/src/algorithms/contractions/transfer.jl b/src/algorithms/contractions/transfer.jl index 19ac6db38..645247f74 100644 --- a/src/algorithms/contractions/transfer.jl +++ b/src/algorithms/contractions/transfer.jl @@ -247,3 +247,99 @@ function edge_transfer_right( Ebot[χ_SE D_S; χ_SW] * O[D_W D_S; D_N D_E] end + +""" +Map north-boundary site `k` to the corresponding site in the east-to-west south boundary. +`N` is the finite window length, including the two boundary sites. +""" +south_site(k::Int, N::Int) = N + 1 - k + +""" +Construct the endpoint identities for a finite north-W-south sandwich. +``` + ┌-←-- north --←-┐ + | | | + L-←---- W ----←-R + | | | + └-→-- south --→-┘ +``` +""" +function _window_edge_boundaries(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS) + N = length(north) + length(south) == length(W) == N || throw(DimensionMismatch("row and boundary lengths must match")) + left = isomorphism( + storagetype(north.AL[1]), domain(south.AR[N])[1] ⊗ space(W[1], 1)', space(north.AL[1], 1) + ) + right = isomorphism( + storagetype(north.AR[N]), domain(north.AR[N])[1] ⊗ domain(W[N])[2], space(south.AL[1], 1) + ) + return left, right +end + +""" +Build left and right environments outside `site` in the north-W-south sandwich. +`lefts[k]` contains columns before `k`; `rights[k - site + 1]` contains columns after `k`. +Only valid entries are stored, with lengths `site` and `length(north) - site + 1`, respectively. + +Note that sites in `south` are ordered from right (east) to left (west). +""" +function _window_edge_environments(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS, site::Int) + left, right = _window_edge_boundaries(south, W, north) + N = length(north) + lefts, rights = [left], [right] + for k in 1:(site - 1) + push!(lefts, last(lefts) * edge_transfermatrix(north.AL[k], W[k], south.AR[south_site(k, N)])) + end + for k in N:-1:(site + 1) + push!(rights, edge_transfermatrix(north.AR[k], W[k], south.AL[south_site(k, N)]) * last(rights)) + end + reverse!(rights) + return (; lefts, rights) +end + +""" +Contract opposite-oriented boundary MPSs without conjugation. +""" +function dot_noconj(south::FiniteMPS, north::FiniteMPS) + N = length(north) + length(south) == N || throw(DimensionMismatch("boundary lengths must match")) + right = isomorphism(storagetype(north.AR[N]), domain(north.AR[N]), space(south.AL[1], 1)) + for k in N:-1:1 + top = k == 1 ? north.AC[k] : north.AR[k] + bottom = k == 1 ? south.AC[N] : south.AL[south_site(k, N)] + right = edge_transfermatrix(top, bottom) * right + end + left = isomorphism(storagetype(right), domain(south.AC[N]), space(north.AC[1], 1)) + return tr(left * right) +end + +""" +Contract a row MPO between opposite-oriented boundary MPSs without conjugation. +""" +function dot_noconj(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS) + left, right = _window_edge_boundaries(south, W, north) + N = length(north) + for k in N:-1:1 + top = k == 1 ? north.AC[k] : north.AR[k] + bottom = k == 1 ? south.AC[N] : south.AL[south_site(k, N)] + right = edge_transfermatrix(top, W[k], bottom) * right + end + return _contract_transfer_boundaries(left, right) +end + +""" +Contract the left and right transfer-matrix environments to a scalar. +``` + (north) + ┌-←-- 3 --←-┐ + | | + L-←-- 2 --←-R + | | + └-→-- 1 --→-┘ + (south) +``` +""" +function _contract_transfer_boundaries(left::MPSTensor, right::MPSTensor) + # The three bonds close around the window without crossing + return @tensor left[1 2; 3] * right[3 2; 1] +end diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 958da6a56..719a232c4 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -153,7 +153,7 @@ function _contract_window_rows( ψ = _approximate(W, ψ, alg) end south = _south_boundary_mps(env, last(rowrange), colrange) - return dot(south, ψ) + return dot_noconj(south, ψ) end """ diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/window/tools.jl index e04899734..97d3cafbb 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/window/tools.jl @@ -19,13 +19,6 @@ function _approximate(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) return ψ′ end -""" -Convert a south-boundary MPS tensor into the stored bra representation expected by `dot` using a planar repartition. -""" -function _bra_mps_tensor(A::MPSTensor) - return repartition(A', 2, 1; copy = true) -end - """ Build the finite MPS representing the north CTMRG boundary of a window. @@ -33,10 +26,10 @@ Convention of CTM tensors on the north boundary is ``` [1; 2] [1 2; 3] [1; 2] C₁-←-2 1-←-E₁-←-3 1-←-C₂ - ↓ ↓ ↓ + ↓ ↓ ↑ 1 2 2 ``` -Leg 2 of C₂ needs to be flipped to match standard MPS convention. +Leg 2 of C₂ needs to be flipped to have a non-dual physical space. """ function _north_boundary_mps( env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, @@ -54,7 +47,8 @@ function _north_boundary_mps( end """ -Build the finite MPS representing the adjointed south CTMRG boundary of a window. +Build the finite MPS representing the south CTMRG boundary of a window, +but with dual physical legs, and sites ordered from east to west. Convention of CTM tensors on the south boundary is ``` @@ -63,27 +57,24 @@ Convention of CTM tensors on the south boundary is ↓ ↓ ↑ C₄-→-1 3-→-E₃-→-1 2-→-C₃ ``` -Leg 1 of C₃ needs to be flipped to match standard MPS convention. -Then, their adjoints are +Leg 1 of C₃ needs to be flipped to have a dual physical space. + +North site `k` pairs with south site `N + 1 - k`. ``` - [1; 2] [1; 2 3] [1; 2] - C̄₄-←-2 1-←-Ē₃-←-2 1-←-C̄₃ - ↓ ↓ ↓ - 1 3 2 + west east + north: 1 ← … ← N - 1 ← N + south: N → … → 2 → 1 ``` -The edge tensors then need a further repartition of indices. +Viewed after a 180° rotation of the entire network, this is a north boundary ordered from west to east as usual, only with dual physical legs. +Because of this reversed site order, `AL` tensors lie to the right (east) of the canonical center in the window, while `AR` tensors lie to its left (west). """ -function _south_boundary_mps( - env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, - ) +function _south_boundary_mps(env::CTMRGEnv, row::Int, colrange::UnitRange{Int}) r = row + 1 cmin, cmax = first(colrange), last(colrange) - Cwest = insertleftunit(corner(env, SOUTHWEST, r, cmin - 1)', 1) - tensors = [Cwest] - append!(tensors, (_bra_mps_tensor(edge(env, SOUTH, r, col)) for col in colrange)) - Ceast = repartition( - flip(corner(env, SOUTHEAST, r, cmax + 1), 1)', 2, 0 - ) - push!(tensors, insertleftunit(Ceast, 3)) + Ceast = insertleftunit(flip(corner(env, SOUTHEAST, r, cmax + 1), 1), 1) + tensors = [Ceast] + append!(tensors, (edge(env, SOUTH, r, col) for col in reverse(colrange))) + Cwest = repartition(corner(env, SOUTHWEST, r, cmin - 1), 2, 0) + push!(tensors, insertleftunit(Cwest, 3)) return FiniteMPS(tensors) end diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/window/twosite/pepo_1layer.jl index 8ff826a91..67864b192 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/window/twosite/pepo_1layer.jl @@ -43,7 +43,7 @@ function _correlator_approx_rows( W = _row_mpo(ρ, nothing, env, row, colrange) if haskey(targets_by_row, row) south = _south_boundary_mps(env, row, colrange) - norm = dot(south, W, plain_north) + norm = dot_noconj(south, W, plain_north) _contract_twosite_target_row!( values, ρ, mpo, source, targets_by_row[row], north, south, W, colrange ) @@ -64,7 +64,6 @@ end """ Contract all targets in one row `W` with a shared `north` and `south` boundary MPS, writing results into `numerators`. - """ function _contract_twosite_target_row!( numerators::Vector{<:Number}, ρ::InfinitePEPO, @@ -72,9 +71,10 @@ function _contract_twosite_target_row!( source::CartesianIndex{2}, targets::Dict{CartesianIndex{2}, Int}, north::FiniteMPS, south::FiniteMPS, W::FiniteMPO, colrange::UnitRange{Int}, ) + N = length(south) row = first(keys(targets))[1] - envs = environments(south, W, north) source_site = _window_mps_site(source[2], colrange) + envs = _window_edge_environments(south, W, north, source_site) stringspace = space(mpo[2], 1) # close the target right at the column of the incoming string @@ -95,25 +95,25 @@ function _contract_twosite_target_row!( else mpo_path_string(A, stringspace, Val((:north, :east))) end - left = leftenv(envs, source_site, south) * - TransferMatrix(north.AC[source_site], source_tensor, south.AC[source_site]) + left = envs.lefts[source_site] * + edge_transfermatrix(north.AC[source_site], source_tensor, south.AC[south_site(source_site, N)]) previous_col = source[2] for target in right_targets target_col = target[2] for col in (previous_col + 1):(target_col - 1) site = _window_mps_site(col, colrange) string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:west, :east))) - left = left * TransferMatrix(north.AR[site], string_tensor, south.AR[site]) + left = left * edge_transfermatrix(north.AR[site], string_tensor, south.AL[south_site(site, N)]) end target_site = _window_mps_site(target_col, colrange) target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:west)) - target_left = left * TransferMatrix(north.AR[target_site], target_tensor, south.AR[target_site]) - value = _contract_transfer_boundaries(target_left, rightenv(envs, target_site, south)) + target_left = left * edge_transfermatrix(north.AR[target_site], target_tensor, south.AL[south_site(target_site, N)]) + value = _contract_transfer_boundaries(target_left, envs.rights[target_site - source_site + 1]) numerators[targets[target]] = value string_tensor = mpo_path_string(ρ[row, target_col, 1], stringspace, Val((:west, :east))) - left = left * TransferMatrix(north.AR[target_site], string_tensor, south.AR[target_site]) + left = left * edge_transfermatrix(north.AR[target_site], string_tensor, south.AL[south_site(target_site, N)]) previous_col = target_col end end @@ -128,26 +128,26 @@ function _contract_twosite_target_row!( else mpo_path_string(A, stringspace, Val((:north, :west))) end - right = TransferMatrix( - north.AC[source_site], source_tensor, south.AC[source_site] - ) * rightenv(envs, source_site, south) + right = edge_transfermatrix( + north.AC[source_site], source_tensor, south.AC[south_site(source_site, N)] + ) * first(envs.rights) previous_col = source[2] for target in left_targets target_col = target[2] for col in (previous_col - 1):-1:(target_col + 1) site = _window_mps_site(col, colrange) string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:east, :west))) - right = TransferMatrix(north.AL[site], string_tensor, south.AL[site]) * right + right = edge_transfermatrix(north.AL[site], string_tensor, south.AR[south_site(site, N)]) * right end target_site = _window_mps_site(target_col, colrange) target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:east)) - target_right = TransferMatrix(north.AL[target_site], target_tensor, south.AL[target_site]) * right - value = _contract_transfer_boundaries(leftenv(envs, target_site, south), target_right) + target_right = edge_transfermatrix(north.AL[target_site], target_tensor, south.AR[south_site(target_site, N)]) * right + value = _contract_transfer_boundaries(envs.lefts[target_site], target_right) numerators[targets[target]] = value string_tensor = mpo_path_string(ρ[row, target_col, 1], stringspace, Val((:east, :west))) - right = TransferMatrix(north.AL[target_site], string_tensor, south.AL[target_site]) * right + right = edge_transfermatrix(north.AL[target_site], string_tensor, south.AR[south_site(target_site, N)]) * right previous_col = target_col end end @@ -163,27 +163,11 @@ _window_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 Contract one modified row site between precomputed left and right MPS environments. """ function _contract_window_site( - envs::MPSKit.FiniteEnvironments, north::FiniteMPS, south::FiniteMPS, + envs::NamedTuple, north::FiniteMPS, south::FiniteMPS, site::Int, tensor::MPOTensor, ) - left = leftenv(envs, site, south) * - TransferMatrix(north.AC[site], tensor, south.AC[site]) - return _contract_transfer_boundaries(left, rightenv(envs, site, south)) -end - -""" -Contract the left and right transfer-matrix environments to a scalar. -``` - (north) - ┌-←-- 3 --←-┐ - | | - L-←-- 2 --←-R - | | - └-→-- 1 --→-┘ - (south) -``` -""" -function _contract_transfer_boundaries(left::MPSTensor, right::MPSTensor) - # The three bonds close around the window without crossing - return @plansor left[1 2; 3] * right[3 2; 1] + N = length(south) + left = envs.lefts[site] * + edge_transfermatrix(north.AC[site], tensor, south.AC[south_site(site, N)]) + return _contract_transfer_boundaries(left, envs.rights[site - length(envs.lefts) + 1]) end diff --git a/src/operators/transfermatrix.jl b/src/operators/transfermatrix.jl index a033817a4..76cfc54e3 100644 --- a/src/operators/transfermatrix.jl +++ b/src/operators/transfermatrix.jl @@ -150,10 +150,10 @@ function initialize_mps( return InfiniteMPS( [ f( - T, - virtualspaces[_prev(i, end)] * _elementwise_dual(north_virtualspace(O, i)), - virtualspaces[mod1(i, end)], - ) for i in 1:length(O) + T, + virtualspaces[_prev(i, end)] * _elementwise_dual(north_virtualspace(O, i)), + virtualspaces[mod1(i, end)], + ) for i in 1:length(O) ] ) end diff --git a/test/ctmrg/unitcell.jl b/test/ctmrg/unitcell.jl index f87548469..577d65205 100644 --- a/test/ctmrg/unitcell.jl +++ b/test/ctmrg/unitcell.jl @@ -30,9 +30,9 @@ function test_unitcell( Pspaces, [ (c,) => randn( - scalartype(peps), - Pspaces[c], Pspaces[c], - ) for c in CartesianIndices(unitcell) + scalartype(peps), + Pspaces[c], Pspaces[c], + ) for c in CartesianIndices(unitcell) ]..., ) @test expectation_value(peps, random_op, env) isa Number From 00bd5344bb96596b256058c3f2c17a846f2a0fff Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 1 Oct 2026 10:04:20 +0800 Subject: [PATCH 17/20] Adapt to new test format --- test/testsuite/TestSuite.jl | 24 ++++ test/testsuite/toolbox/correlator_approx.jl | 104 ++++++++++++++++++ .../toolbox/correlator_approx_phys.jl | 42 +++++++ test/testsuite/toolbox/expval_approx.jl | 64 +++++++++++ test/testsuite/toolbox/mpo_routing.jl | 104 ++++++++++++++++++ test/toolbox/correlator_approx.jl | 99 +---------------- test/toolbox/correlator_approx_phys.jl | 37 +------ test/toolbox/expval_approx.jl | 59 +--------- test/toolbox/mpo_routing.jl | 95 ++-------------- 9 files changed, 363 insertions(+), 265 deletions(-) create mode 100644 test/testsuite/toolbox/correlator_approx.jl create mode 100644 test/testsuite/toolbox/correlator_approx_phys.jl create mode 100644 test/testsuite/toolbox/expval_approx.jl create mode 100644 test/testsuite/toolbox/mpo_routing.jl diff --git a/test/testsuite/TestSuite.jl b/test/testsuite/TestSuite.jl index 018a68a82..11e9246c3 100644 --- a/test/testsuite/TestSuite.jl +++ b/test/testsuite/TestSuite.jl @@ -239,6 +239,30 @@ module ToolboxDensityMatrices end using .ToolboxDensityMatrices +module ToolboxExpvalApprox + include("toolbox/expval_approx.jl") + export toolbox_expval_approx +end +using .ToolboxExpvalApprox + +module ToolboxCorrelatorApprox + include("toolbox/correlator_approx.jl") + export toolbox_correlator_approx +end +using .ToolboxCorrelatorApprox + +module ToolboxCorrelatorApproxPhys + include("toolbox/correlator_approx_phys.jl") + export toolbox_correlator_approx_phys +end +using .ToolboxCorrelatorApproxPhys + +module ToolboxMPORouting + include("toolbox/mpo_routing.jl") + export toolbox_mpo_routing_fusers, toolbox_mpo_routing_identities +end +using .ToolboxMPORouting + # Utility # ------- module UtilityCorrelator diff --git a/test/testsuite/toolbox/correlator_approx.jl b/test/testsuite/toolbox/correlator_approx.jl new file mode 100644 index 000000000..92b9f86d2 --- /dev/null +++ b/test/testsuite/toolbox/correlator_approx.jl @@ -0,0 +1,104 @@ +using TensorKit +using PEPSKit +using MPSKit +using Test +using Adapt +using Random + +const CI = CartesianIndex + +""" +Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using fixed width and a window ending at each target row. +""" +function _adaptive_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) + lattice = physicalspace(ρ) + observables = [MPOObservable([i, j], op, lattice) for j in js] + _, colrange = PEPSKit._window_ranges([i; js]) + alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) + return map(observables, js) do observable, j + rowrange = i[1]:j[1] + norm = PEPSKit._contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) + numerator = PEPSKit._contract_window_rows(ρ, observable, env, rowrange, colrange, alg) + return numerator / norm + end +end + +# Exercise both U(1) symmetry and fermionic signs with small physical, virtual, and boundary spaces. +spaces = Dict( + U1Irrep => ( + U1Space(1 => 2, -1 => 1), + U1Space(0 => 1, 1 => 1), + U1Space(0 => 1, 1 => 1), + ), + FermionParity => ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 1), + ) +) + +""" +Check approximate correlators, sweep selection, and adaptive window normalization. +""" +function toolbox_correlator_approx(AT) + return @testset "Single-layer PEPO ($S) ($AT)" for S in keys(spaces) + # Use a reproducible random PEPO on a rectangular unit cell and disable boundary truncation. + Random.seed!(1234) + d, D, χ = spaces[S] + ρ = adapt(AT, InfinitePEPO(d, D; unitcell = (2, 3, 1))) + env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) + trunc = notrunc() + i = CI(-1, 0) + # Unsorted targets include both same-row neighbors of i and targets to the east, west, and directly south on row 1. + # Row 0 has no measurement targets but is still contracted, testing MPO-string propagation through a row without targets. + js = [CI(1, 1), CI(-1, -1), CI(1, 0), CI(-1, 1), CI(1, -1)] + + # The normalized expectation of the identity must be one at every target. + id² = adapt(AT, isomorphism(d, d) ⊗ isomorphism(d, d)) + @test correlator_approx(ρ, id², i, js, env; trunc, maxiter = 0) ≈ ones(length(js)) + + # This target distribution only permits row sweeps. + # Compare against independent contractions in target order. + O² = adapt(AT, rand(ComplexF64, d^2, d^2)) + vals_ref = _adaptive_window_reference(O², i, js, ρ, env) + vals_rows = correlator_approx(ρ, O², i, js, env; trunc, maxiter = 0) + @test vals_rows ≈ vals_ref + + # CartesianIndices are flattened exactly as in correlator. + grid = CartesianIndices((1:1, -1:1)) + @test correlator_approx(ρ, O², i, grid, env; trunc, maxiter = 0) ≈ + vals_ref[[5, 3, 1]] + + # The rotated distribution only permits column sweeps. + # Check rotation and automatic selection together. + cell = size(ρ)[1:2] + ic = PEPSKit.siterotr90(i, cell) + jcs = PEPSKit.siterotr90.(js, Ref(cell)) + ρc, envc = rotr90(ρ), rotr90(env) + @test correlator_approx(ρc, O², ic, jcs, envc; trunc, maxiter = 0) ≈ vals_rows + + # Identity normalization must survive truncation for every target row and both sweep orientations. + for maxiter in (0, 1) + @test correlator_approx(ρ, id², i, js, env; trunc = truncrank(2), maxiter) ≈ ones(length(js)) + @test correlator_approx(ρc, id², ic, jcs, envc; trunc = truncrank(2), maxiter) ≈ ones(length(js)) + end + + # Test auto sweep direction choice in for southwest targets. + selection_trunc = truncrank(2) + selection_alg = PEPSKit.WindowApprox(Zipup(; trunc = selection_trunc), nothing) + for (offset, direction) in ((CI(1, -2), :rows), (CI(2, -1), :columns), (CI(1, -1), :rows)) + j = i + offset + expected = only(PEPSKit._correlator_approx(ρ, O², i, [j], env, selection_alg, direction)) + @test correlator_approx(ρ, O², i, j, env; trunc = selection_trunc, maxiter = 0) == expected + end + + # Extending the depth must preserve earlier values when the width and selected direction stay fixed. + extended_js = [js; CI(2, 0)] + baseline = correlator_approx(ρ, O², i, js, env; trunc = selection_trunc, maxiter = 0) + extended = correlator_approx(ρ, O², i, extended_js, env; trunc = selection_trunc, maxiter = 0) + @test extended[1:length(js)] ≈ baseline + extended_jcs = PEPSKit.siterotr90.(extended_js, Ref(cell)) + extended_columns = correlator_approx(ρc, O², ic, extended_jcs, envc; trunc = selection_trunc, maxiter = 0) + @test extended_columns ≈ extended + end +end diff --git a/test/testsuite/toolbox/correlator_approx_phys.jl b/test/testsuite/toolbox/correlator_approx_phys.jl new file mode 100644 index 000000000..d54a56400 --- /dev/null +++ b/test/testsuite/toolbox/correlator_approx_phys.jl @@ -0,0 +1,42 @@ +using TensorKit +using PEPSKit +using Test +using Adapt +using TensorKitTensors.SpinOperators: S_exchange + +const CI = CartesianIndex + +""" +Compare approximate and exact correlators in an evolved physical state. +""" +function toolbox_correlator_approx_phys(AT) + return @testset "correlator_approx for physical state ($AT)" begin + Nr, Nc = 2, 2 + lattice = InfiniteSquare(Nr, Nc) + sym = Trivial + ham = adapt(AT, j1_j2_model(Float64, sym, lattice; J1 = 1.0, J2 = 0.0, sublattice = false)) + op = adapt(AT, S_exchange(Float64, sym)) + lattice = physicalspace(ham) + + ρ = PEPSKit.infinite_temperature_density_matrix(ham) + state_trunc = truncrank(4) & truncerror(; atol = 1.0e-12) + su_alg = SimpleUpdate(; trunc = state_trunc, purified = false) + ρ, = time_evolve(ρ, ham, 1.0e-2, 50, su_alg, SUWeight(ρ)) + + network = InfinitePartitionFunction(ρ) + env = initialize_ctmrg_environment(network, ProductStateInitialization()) + env_trunc = truncrank(8) & truncerror(; atol = 1.0e-12) + env, = leading_boundary(env, network; alg = :SequentialCTMRG, trunc = env_trunc) + + i = CI(1, 1) + js = [CI(1, 2), CI(2, 0), CI(2, 1), CI(2, 2), CI(3, 2)] + cor_exact = map(js) do j + O = LocalOperator(lattice, (i, j) => op) + return expectation_value(ρ, O, env) + end + cor_trunc = correlator_approx(ρ, op, i, js, env; trunc = env_trunc, maxiter = 1) + @info "Exact:" cor_exact + @info "Approx:" cor_trunc + @test cor_trunc ≈ cor_exact rtol = 1.0e-3 + end +end diff --git a/test/testsuite/toolbox/expval_approx.jl b/test/testsuite/toolbox/expval_approx.jl new file mode 100644 index 000000000..3019ae81b --- /dev/null +++ b/test/testsuite/toolbox/expval_approx.jl @@ -0,0 +1,64 @@ +using TensorKit +using PEPSKit +using MPSKit +using Test +using Adapt +using Random + +const CI = CartesianIndex + +spaces = Dict( + U1Irrep => ( + U1Space(1 => 2, -1 => 1), + U1Space(1 => 1, 0 => 1, -1 => 2), + U1Space(1 => 1, 0 => 1, -1 => 2), + ), + FermionParity => ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 2), + Vect[FermionParity](0 => 2, 1 => 2), + ), +) + +sites_list = ( + [CI(1, 1), CI(1, 2)], # horizontal + [CI(1, 1), CI(2, 1)], # vertical + [CI(1, 1), CI(2, 2)], # turned + [CI(2, 2), CI(1, 1)], # reversed turned + [CI(2, 1), CI(1, 1), CI(1, 2), CI(2, 2)], # U-shaped +) + +""" +Check approximate expectation values against exact single-layer PEPO contractions. +""" +function toolbox_expval_approx(AT) + return @testset "Single-layer PEPO ($S) ($AT)" for S in keys(spaces) + Random.seed!(1234) + + d, D, χ = spaces[S] + ρ = adapt(AT, InfinitePEPO(d, D; unitcell = (2, 2, 1))) + env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) + trunc = notrunc() + + for sites in sites_list + n = length(sites) + op = adapt(AT, randn(ComplexF64, d^n → d^n)) + mpo = PEPSKit.gate_to_mpo(op; trunc) + observable = MPOObservable(sites, mpo) + exact = expectation_value( + ρ, LocalOperator(physicalspace(ρ), sites => op), env + ) + for direction in (:rows, :columns) + @test expectation_value_approx( + ρ, observable, env; trunc, maxiter = 0, direction + ) ≈ exact + end + # Square windows must default to row sweeps. + if sites == sites_list[3] + auto = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0) + rows = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0, direction = :rows) + @test auto == rows + end + end + end +end diff --git a/test/testsuite/toolbox/mpo_routing.jl b/test/testsuite/toolbox/mpo_routing.jl new file mode 100644 index 000000000..28e4d7f4b --- /dev/null +++ b/test/testsuite/toolbox/mpo_routing.jl @@ -0,0 +1,104 @@ +using PEPSKit +using TensorKit +using Test +using Adapt +using Random + +const directions = (:north, :east, :south, :west) + +spaces = Dict( + U1Irrep => ( + Rep[U₁](0 => 1, 1 => 1), + Rep[U₁](0 => 1, 1 => 1, -1 => 1), + Rep[U₁](1 => 1), + ), + FermionParity => ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](1 => 1), + ), +) + +""" +Check fermionic fuser flips and twists for each MPO routing direction. +""" +function toolbox_mpo_routing_fusers(AT) + return @testset "Fuser flips and twists ($AT)" begin + d = Vect[FermionParity](0 => 1, 1 => 1) + D = Vect[FermionParity](0 => 1, 1 => 1) + ρ = adapt(AT, InfinitePEPO(d, D; unitcell = (2, 2, 1))) + A = ρ[1, 1, 1] + Bh = ρ[1, 2, 1] + Bv = ρ[2, 1, 1] + A′ = PEPSKit.twistdual(A, 2) + Bh′ = PEPSKit.twistdual(Bh, 2) + Bv′ = PEPSKit.twistdual(Bv, 2) + + op = adapt(AT, randn(ComplexF64, d^2 → d^2)) + mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + + first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:east)) + last_tensor = PEPSKit.mpo_path_last(Bh, last(mpo), Val(:west)) + @tensor exact[W1 S1 S2; N1 N2 E2] := op[po1 po2; pi1 pi2] * + A′[pi1 po1; N1 x S1 W1] * Bh′[pi2 po2; N2 E2 S2 x] + @tensor routed[W1 S1 S2; N1 N2 E2] := + first_tensor[W1 S1; N1 x] * last_tensor[x S2; N2 E2] + @test routed ≈ exact + + first_tensor = PEPSKit.mpo_path_first(Bh, first(mpo), Val(:west)) + last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:east)) + @tensor exact[W2 S1 S2; N1 E1 N2] := op[po1 po2; pi1 pi2] * + Bh′[pi1 po1; N1 E1 S1 x] * A′[pi2 po2; N2 x S2 W2] + @tensor routed[W2 S1 S2; N1 E1 N2] := + first_tensor[x S1; N1 E1] * last_tensor[W2 S2; N2 x] + @test routed ≈ exact + + first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:south)) + last_tensor = PEPSKit.mpo_path_last(Bv, last(mpo), Val(:north)) + @tensor exact[W1 W2 S2; N1 E1 E2] := op[po1 po2; pi1 pi2] * + A′[pi1 po1; N1 E1 x W1] * Bv′[pi2 po2; x E2 S2 W2] + @tensor routed[W1 W2 S2; N1 E1 E2] := + first_tensor[W1 x; N1 E1] * last_tensor[W2 S2; x E2] + @test routed ≈ exact + + first_tensor = PEPSKit.mpo_path_first(Bv, first(mpo), Val(:north)) + last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:south)) + @tensor exact[W1 S1 W2; E1 N2 E2] := op[po1 po2; pi1 pi2] * + Bv′[pi1 po1; x E1 S1 W1] * A′[pi2 po2; N2 E2 x W2] + @tensor routed[W1 S1 W2; E1 N2 E2] := + first_tensor[W1 S1; x E1] * last_tensor[W2 x; N2 E2] + @test routed ≈ exact + end +end + +""" +Check MPO endpoint shapes and string-routing identities for each symmetry. +""" +function toolbox_mpo_routing_identities(AT) + return @testset "Routing identities ($S) ($AT)" for S in keys(spaces) + Random.seed!(1234) + d, D, stringspace = spaces[S] + ρ = adapt(AT, InfinitePEPO(d, D; unitcell = (1, 1, 1))) + op = adapt(AT, rand(ComplexF64, d^2 → d^2)) + mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + + A = ρ[1, 1, 1] + for direction in directions + first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(direction)) + last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(direction)) + @test (numout(first_tensor), numin(first_tensor)) == (2, 2) + @test (numout(last_tensor), numin(last_tensor)) == (2, 2) + end + + middle = adapt(AT, TensorMap(TensorKit.BraidingTensor{ComplexF64}(d, stringspace))) + for incoming in directions, outgoing in directions + incoming == outgoing && continue + tensor = PEPSKit.mpo_path_middle(A, middle, Val((incoming, outgoing))) + string_tensor = PEPSKit.mpo_path_string( + A, stringspace, Val((incoming, outgoing)) + ) + @test (numout(tensor), numin(tensor)) == (2, 2) + @test string_tensor ≈ tensor + end + end +end diff --git a/test/toolbox/correlator_approx.jl b/test/toolbox/correlator_approx.jl index 34f4cefa3..9ad275d5d 100644 --- a/test/toolbox/correlator_approx.jl +++ b/test/toolbox/correlator_approx.jl @@ -1,98 +1,11 @@ -using TensorKit -using PEPSKit -using MPSKit using Test -using Random - -const CI = CartesianIndex - -""" -Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using fixed width and a window ending at each target row. -""" -function _adaptive_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) - lattice = physicalspace(ρ) - observables = [MPOObservable([i, j], op, lattice) for j in js] - _, colrange = PEPSKit._window_ranges([i; js]) - alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) - return map(observables, js) do observable, j - rowrange = i[1]:j[1] - norm = PEPSKit._contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) - numerator = PEPSKit._contract_window_rows(ρ, observable, env, rowrange, colrange, alg) - return numerator / norm - end -end - -# Exercise both U(1) symmetry and fermionic signs with small physical, virtual, and boundary spaces. -spaces = Dict( - U1Irrep => ( - U1Space(1 => 2, -1 => 1), - U1Space(0 => 1, 1 => 1), - U1Space(0 => 1, 1 => 1), - ), - FermionParity => ( - Vect[FermionParity](0 => 1, 1 => 1), - Vect[FermionParity](0 => 1, 1 => 1), - Vect[FermionParity](0 => 1, 1 => 1), - ) -) - -@testset "Single-layer PEPO ($S)" for S in keys(spaces) - # Use a reproducible random PEPO on a rectangular unit cell and disable boundary truncation. - Random.seed!(1234) - d, D, χ = spaces[S] - ρ = InfinitePEPO(d, D; unitcell = (2, 3, 1)) - env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) - trunc = notrunc() - i = CI(-1, 0) - # Unsorted targets include both same-row neighbors of i and targets to the east, west, and directly south on row 1. - # Row 0 has no measurement targets but is still contracted, testing MPO-string propagation through a row without targets. - js = [CI(1, 1), CI(-1, -1), CI(1, 0), CI(-1, 1), CI(1, -1)] - - # The normalized expectation of the identity must be one at every target. - id² = isomorphism(d, d) ⊗ isomorphism(d, d) - @test correlator_approx(ρ, id², i, js, env; trunc, maxiter = 0) ≈ ones(length(js)) - - # This target distribution only permits row sweeps. - # Compare against independent contractions in target order. - O² = rand(ComplexF64, d^2, d^2) - vals_ref = _adaptive_window_reference(O², i, js, ρ, env) - vals_rows = correlator_approx(ρ, O², i, js, env; trunc, maxiter = 0) - @test vals_rows ≈ vals_ref - - # CartesianIndices are flattened exactly as in correlator. - grid = CartesianIndices((1:1, -1:1)) - @test correlator_approx(ρ, O², i, grid, env; trunc, maxiter = 0) ≈ - vals_ref[[5, 3, 1]] - - # The rotated distribution only permits column sweeps. - # Check rotation and automatic selection together. - cell = size(ρ)[1:2] - ic = PEPSKit.siterotr90(i, cell) - jcs = PEPSKit.siterotr90.(js, Ref(cell)) - ρc, envc = rotr90(ρ), rotr90(env) - @test correlator_approx(ρc, O², ic, jcs, envc; trunc, maxiter = 0) ≈ vals_rows +using PEPSKit - # Identity normalization must survive truncation for every target row and both sweep orientations. - for maxiter in (0, 1) - @test correlator_approx(ρ, id², i, js, env; trunc = truncrank(2), maxiter) ≈ ones(length(js)) - @test correlator_approx(ρc, id², ic, jcs, envc; trunc = truncrank(2), maxiter) ≈ ones(length(js)) - end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite - # Test auto sweep direction choice in for southwest targets. - selection_trunc = truncrank(2) - selection_alg = PEPSKit.WindowApprox(Zipup(; trunc = selection_trunc), nothing) - for (offset, direction) in ((CI(1, -2), :rows), (CI(2, -1), :columns), (CI(1, -1), :rows)) - j = i + offset - expected = only(PEPSKit._correlator_approx(ρ, O², i, [j], env, selection_alg, direction)) - @test correlator_approx(ρ, O², i, j, env; trunc = selection_trunc, maxiter = 0) == expected - end +is_buildkite = get(ENV, "BUILDKITE", "false") == "true" - # Extending the depth must preserve earlier values when the width and selected direction stay fixed. - extended_js = [js; CI(2, 0)] - baseline = correlator_approx(ρ, O², i, js, env; trunc = selection_trunc, maxiter = 0) - extended = correlator_approx(ρ, O², i, extended_js, env; trunc = selection_trunc, maxiter = 0) - @test extended[1:length(js)] ≈ baseline - extended_jcs = PEPSKit.siterotr90.(extended_js, Ref(cell)) - extended_columns = correlator_approx(ρc, O², ic, extended_jcs, envc; trunc = selection_trunc, maxiter = 0) - @test extended_columns ≈ extended +if !is_buildkite + TestSuite.toolbox_correlator_approx(Vector) end diff --git a/test/toolbox/correlator_approx_phys.jl b/test/toolbox/correlator_approx_phys.jl index f811d0b90..135b330b0 100644 --- a/test/toolbox/correlator_approx_phys.jl +++ b/test/toolbox/correlator_approx_phys.jl @@ -1,36 +1,11 @@ -using TensorKit -using PEPSKit using Test -using TensorKitTensors.SpinOperators: S_exchange - -const CI = CartesianIndex - -@testset "correlator_approx for physical state" begin - Nr, Nc = 2, 2 - lattice = InfiniteSquare(Nr, Nc) - sym = Trivial - ham = j1_j2_model(Float64, sym, lattice; J1 = 1.0, J2 = 0.0, sublattice = false) - op = S_exchange(Float64, sym) - lattice = physicalspace(ham) +using PEPSKit - ρ = PEPSKit.infinite_temperature_density_matrix(ham) - state_trunc = truncrank(4) & truncerror(; atol = 1.0e-12) - su_alg = SimpleUpdate(; trunc = state_trunc, purified = false) - ρ, = time_evolve(ρ, ham, 1.0e-2, 50, su_alg, SUWeight(ρ)) +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite - network = InfinitePartitionFunction(ρ) - env = initialize_ctmrg_environment(network, ProductStateInitialization()) - env_trunc = truncrank(8) & truncerror(; atol = 1.0e-12) - env, = leading_boundary(env, network; alg = :SequentialCTMRG, trunc = env_trunc) +is_buildkite = get(ENV, "BUILDKITE", "false") == "true" - i = CI(1, 1) - js = [CI(1, 2), CI(2, 0), CI(2, 1), CI(2, 2), CI(3, 2)] - cor_exact = map(js) do j - O = LocalOperator(lattice, (i, j) => op) - return expectation_value(ρ, O, env) - end - cor_trunc = correlator_approx(ρ, op, i, js, env; trunc = env_trunc, maxiter = 1) - @info "Exact:" cor_exact - @info "Approx:" cor_trunc - @test cor_trunc ≈ cor_exact rtol = 1.0e-3 +if !is_buildkite + TestSuite.toolbox_correlator_approx_phys(Vector) end diff --git a/test/toolbox/expval_approx.jl b/test/toolbox/expval_approx.jl index d604d5faa..281fffaa2 100644 --- a/test/toolbox/expval_approx.jl +++ b/test/toolbox/expval_approx.jl @@ -1,58 +1,11 @@ -using TensorKit -using PEPSKit -using MPSKit using Test -using Random - -const CI = CartesianIndex - -spaces = Dict( - U1Irrep => ( - U1Space(1 => 2, -1 => 1), - U1Space(1 => 1, 0 => 1, -1 => 2), - U1Space(1 => 1, 0 => 1, -1 => 2), - ), - FermionParity => ( - Vect[FermionParity](0 => 1, 1 => 1), - Vect[FermionParity](0 => 1, 1 => 2), - Vect[FermionParity](0 => 2, 1 => 2), - ), -) - -sites_list = ( - [CI(1, 1), CI(1, 2)], # horizontal - [CI(1, 1), CI(2, 1)], # vertical - [CI(1, 1), CI(2, 2)], # turned - [CI(2, 2), CI(1, 1)], # reversed turned - [CI(2, 1), CI(1, 1), CI(1, 2), CI(2, 2)], # U-shaped -) +using PEPSKit -@testset "Single-layer PEPO ($S)" for S in keys(spaces) - Random.seed!(1234) +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite - d, D, χ = spaces[S] - ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) - env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) - trunc = notrunc() +is_buildkite = get(ENV, "BUILDKITE", "false") == "true" - for sites in sites_list - n = length(sites) - op = randn(ComplexF64, d^n → d^n) - mpo = PEPSKit.gate_to_mpo(op; trunc) - observable = MPOObservable(sites, mpo) - exact = expectation_value( - ρ, LocalOperator(physicalspace(ρ), sites => op), env - ) - for direction in (:rows, :columns) - @test expectation_value_approx( - ρ, observable, env; trunc, maxiter = 0, direction - ) ≈ exact - end - # Square windows must default to row sweeps. - if sites == sites_list[3] - auto = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0) - rows = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0, direction = :rows) - @test auto == rows - end - end +if !is_buildkite + TestSuite.toolbox_expval_approx(Vector) end diff --git a/test/toolbox/mpo_routing.jl b/test/toolbox/mpo_routing.jl index 2b9a4a1ca..c5882bbec 100644 --- a/test/toolbox/mpo_routing.jl +++ b/test/toolbox/mpo_routing.jl @@ -1,93 +1,12 @@ -using PEPSKit -using TensorKit using Test -using Random - -const directions = (:north, :east, :south, :west) - -spaces = Dict( - U1Irrep => ( - Rep[U₁](0 => 1, 1 => 1), - Rep[U₁](0 => 1, 1 => 1, -1 => 1), - Rep[U₁](1 => 1), - ), - FermionParity => ( - Vect[FermionParity](0 => 1, 1 => 1), - Vect[FermionParity](0 => 1, 1 => 1), - Vect[FermionParity](1 => 1), - ), -) - -@testset "Fuser flips and twists" begin - d = Vect[FermionParity](0 => 1, 1 => 1) - D = Vect[FermionParity](0 => 1, 1 => 1) - ρ = InfinitePEPO(d, D; unitcell = (2, 2, 1)) - A = ρ[1, 1, 1] - Bh = ρ[1, 2, 1] - Bv = ρ[2, 1, 1] - A′ = PEPSKit.twistdual(A, 2) - Bh′ = PEPSKit.twistdual(Bh, 2) - Bv′ = PEPSKit.twistdual(Bv, 2) - - op = randn(ComplexF64, d^2 → d^2) - mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) - - first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:east)) - last_tensor = PEPSKit.mpo_path_last(Bh, last(mpo), Val(:west)) - @tensor exact[W1 S1 S2; N1 N2 E2] := op[po1 po2; pi1 pi2] * - A′[pi1 po1; N1 x S1 W1] * Bh′[pi2 po2; N2 E2 S2 x] - @tensor routed[W1 S1 S2; N1 N2 E2] := - first_tensor[W1 S1; N1 x] * last_tensor[x S2; N2 E2] - @test routed ≈ exact - - first_tensor = PEPSKit.mpo_path_first(Bh, first(mpo), Val(:west)) - last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:east)) - @tensor exact[W2 S1 S2; N1 E1 N2] := op[po1 po2; pi1 pi2] * - Bh′[pi1 po1; N1 E1 S1 x] * A′[pi2 po2; N2 x S2 W2] - @tensor routed[W2 S1 S2; N1 E1 N2] := - first_tensor[x S1; N1 E1] * last_tensor[W2 S2; N2 x] - @test routed ≈ exact - - first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(:south)) - last_tensor = PEPSKit.mpo_path_last(Bv, last(mpo), Val(:north)) - @tensor exact[W1 W2 S2; N1 E1 E2] := op[po1 po2; pi1 pi2] * - A′[pi1 po1; N1 E1 x W1] * Bv′[pi2 po2; x E2 S2 W2] - @tensor routed[W1 W2 S2; N1 E1 E2] := - first_tensor[W1 x; N1 E1] * last_tensor[W2 S2; x E2] - @test routed ≈ exact - - first_tensor = PEPSKit.mpo_path_first(Bv, first(mpo), Val(:north)) - last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(:south)) - @tensor exact[W1 S1 W2; E1 N2 E2] := op[po1 po2; pi1 pi2] * - Bv′[pi1 po1; x E1 S1 W1] * A′[pi2 po2; N2 E2 x W2] - @tensor routed[W1 S1 W2; E1 N2 E2] := - first_tensor[W1 S1; x E1] * last_tensor[W2 x; N2 E2] - @test routed ≈ exact -end +using PEPSKit -@testset "Routing identities ($S)" for S in keys(spaces) - Random.seed!(1234) - d, D, stringspace = spaces[S] - ρ = InfinitePEPO(d, D; unitcell = (1, 1, 1)) - op = rand(ComplexF64, d^2 → d^2) - mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite - A = ρ[1, 1, 1] - for direction in directions - first_tensor = PEPSKit.mpo_path_first(A, first(mpo), Val(direction)) - last_tensor = PEPSKit.mpo_path_last(A, last(mpo), Val(direction)) - @test (numout(first_tensor), numin(first_tensor)) == (2, 2) - @test (numout(last_tensor), numin(last_tensor)) == (2, 2) - end +is_buildkite = get(ENV, "BUILDKITE", "false") == "true" - middle = TensorMap(TensorKit.BraidingTensor{ComplexF64}(d, stringspace)) - for incoming in directions, outgoing in directions - incoming == outgoing && continue - tensor = PEPSKit.mpo_path_middle(A, middle, Val((incoming, outgoing))) - string_tensor = PEPSKit.mpo_path_string( - A, stringspace, Val((incoming, outgoing)) - ) - @test (numout(tensor), numin(tensor)) == (2, 2) - @test string_tensor ≈ tensor - end +if !is_buildkite + TestSuite.toolbox_mpo_routing_fusers(Vector) + TestSuite.toolbox_mpo_routing_identities(Vector) end From 8b446cce7c699dcd373541cee58a47e75a1efe9b Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 1 Oct 2026 14:11:15 +0800 Subject: [PATCH 18/20] Port MPOTerm support to LocalOperator from PR #425 Adapt the draft MPO representation with ordered factors, physical-space and bond validation, scalar promotion, and safe scaling. Guard unsupported accumulation and real/imaginary operations, and add focused bookkeeping tests. --- src/PEPSKit.jl | 2 +- src/operators/localoperator.jl | 101 +++++++++++++++++++++++++++++---- test/types/localoperator.jl | 83 +++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 11 deletions(-) diff --git a/src/PEPSKit.jl b/src/PEPSKit.jl index 49f70e655..6b7af7b8b 100644 --- a/src/PEPSKit.jl +++ b/src/PEPSKit.jl @@ -190,7 +190,7 @@ export FixedSpaceTruncation, SiteDependentTruncation export HalfInfiniteProjector, FullInfiniteProjector export C4vCTMRG, C4vEighProjector, C4vQRProjector export initialize_random_c4v_env, initialize_singlet_c4v_env -export LocalOperator, MPOObservable, physicalspace +export LocalOperator, MPOTerm, MPOObservable, physicalspace export product_peps export reduced_densitymatrix, expectation_value_approx, correlator_approx export expectation_value, network_value, cost_function diff --git a/src/operators/localoperator.jl b/src/operators/localoperator.jl index a3d3407f3..c881ff376 100644 --- a/src/operators/localoperator.jl +++ b/src/operators/localoperator.jl @@ -1,10 +1,19 @@ # Hamiltonian consisting of local terms # ------------------------------------- +""" +An open-boundary MPO represented by an ordered vector of tensor maps. +The endpoint partitions are `(1, 2)` and `(2, 1)`, with `(2, 2)` tensors in between; a one-site MPO has partition `(1, 1)`. +Dense tensors can be converted with `gate_to_mpo`. +Evaluation of MPO terms and vectors of tensor-product factors is not implemented. +""" +const MPOTerm{T} = AbstractVector{T} where {T <: AbstractTensorMap} + """ $(TYPEDEF) A sum of local operators acting on a lattice. The lattice is stored as a matrix of vector spaces, and the terms are stored as a `Dict` of indices mapping to operators. +Terms can be dense tensor maps or `MPOTerm`s, whose site and factor ordering is preserved. ## Fields @@ -52,8 +61,8 @@ LocalOperator(lattice, terms) = LocalOperator{Any}(lattice, terms) LocalOperator(lattice, terms::Pair...) = LocalOperator(lattice, terms) """ -Sort operator sites using the default `CartesianIndex` ordering. When the order changes, -permute the corresponding output and input physical legs by the same ordering. +Sort operator sites using the default `CartesianIndex` ordering. +When the order changes, permute the corresponding output and input physical legs by the same ordering. """ function _sort_op_sites(sites::Vector{CartesianIndex{2}}, op::AbstractTensorMap) issorted(sites) && return sites, op @@ -63,10 +72,10 @@ function _sort_op_sites(sites::Vector{CartesianIndex{2}}, op::AbstractTensorMap) return sites′, op′ end -# TODO: add terms beyond AbstractTensorMap -# e.g. tensor product of 1-site operators, MPOs -add_term!(operator::LocalOperator, inds::Tuple, term::AbstractTensorMap) = add_term!(operator, collect(inds), term) -add_term!(operator::LocalOperator, inds::Vector, term::AbstractTensorMap) = add_term!(operator, map(CartesianIndex{2}, inds), term) +add_term!(operator::LocalOperator, inds::Tuple, term::Union{AbstractTensorMap, MPOTerm}; kwargs...) = + add_term!(operator, collect(inds), term; kwargs...) +add_term!(operator::LocalOperator, inds::AbstractVector, term::Union{AbstractTensorMap, MPOTerm}; kwargs...) = + add_term!(operator, CartesianIndex{2}[CartesianIndex{2}(ind) for ind in inds], term; kwargs...) function add_term!( operator::LocalOperator, inds::Vector{CartesianIndex{2}}, term::AbstractTensorMap; atol = zero(real(scalartype(term))), @@ -81,12 +90,14 @@ function add_term!( end norm(term) <= atol && return operator # skip adding negligible terms - inds, term = _sort_op_sites(inds, term) + inds, term = _sort_op_sites(copy(inds), term) # translate coordinates _shift_into_unitcell!(inds, size(operator)) if haskey(operator.terms, inds) + operator.terms[inds] isa MPOTerm && + throw(ArgumentError("Accumulating terms with the same sites is not implemented for MPO terms.")) operator.terms[inds] = VI.add!!(operator.terms[inds], term) else operator.terms[inds] = term @@ -95,6 +106,51 @@ function add_term!( return operator end +""" +Validate an ordered MPO's sites, tensor partitions, physical spaces, and adjacent bond spaces. +""" +function _validate_mpo_term(sites, term::MPOTerm, lattice::AbstractMatrix{<:ElementarySpace}) + isempty(term) && throw(ArgumentError("An MPO term should contain at least one tensor.")) + length(sites) == length(term) || + throw(ArgumentError("The MPO should contain one tensor for every operator site.")) + allunique(sites) || throw(ArgumentError("`inds` should not contain repeated coordinates.")) + + N = length(term) + for (k, op) in enumerate(term) + expected = N == 1 ? (1, 1) : k == 1 ? (1, 2) : k == N ? (2, 1) : (2, 2) + (numout(op), numin(op)) == expected || + throw(ArgumentError("MPO tensor $k should have partition $expected.")) + site = sites[k] + physical = lattice[mod1(site[1], size(lattice, 1)), mod1(site[2], size(lattice, 2))] + physical == codomain(op)[numout(op)] == domain(op)[1] || + throw(SpaceMismatch("MPO physical space does not match lattice site $site.")) + end + for k in 1:(N - 1) + domain(term[k])[numin(term[k])] == codomain(term[k + 1])[1] || + throw(SpaceMismatch("Incompatible MPO bond spaces between tensors $k and $(k + 1).")) + end + return nothing +end + +""" +Insert an ordered MPO term, copying its coordinate and factor containers before translation. +""" +function add_term!(operator::LocalOperator, inds::Vector{CartesianIndex{2}}, term::MPOTerm) + _validate_mpo_term(inds, term, physicalspace(operator)) + _local_term_iszero(term) && return operator + inds = _shift_into_unitcell!(copy(inds), size(operator)) + haskey(operator.terms, inds) && + throw(ArgumentError("Accumulating terms with the same sites is not implemented for MPO terms.")) + operator.terms[inds] = collect(term) + return operator +end + +""" +Detect an identically zero dense tensor or an MPO containing a zero factor. +""" +_local_term_iszero(term::AbstractTensorMap) = iszero(norm(term)) +_local_term_iszero(term::MPOTerm) = any(_local_term_iszero, term) + """ checklattice(Bool, args...) @@ -162,16 +218,27 @@ Base.eltype(::Type{LocalOperator{O, S}}) where {O, S} = O # Real and imaginary part # ----------------------- function Base.real(O::LocalOperator) + any(term -> term isa MPOTerm, values(O.terms)) && + throw(ArgumentError("Taking the real part is not implemented for MPO terms.")) return LocalOperator(O.lattice, (sites => real(op) for (sites, op) in O.terms)...) end function Base.imag(O::LocalOperator) + any(term -> term isa MPOTerm, values(O.terms)) && + throw(ArgumentError("Taking the imaginary part is not implemented for MPO terms.")) return LocalOperator(O.lattice, (sites => imag(op) for (sites, op) in O.terms)...) end # Linear Algebra # -------------- +""" +Scale a local term, applying an MPO's scalar to its first factor only. +""" +_scale_local_term(α::Number, term::AbstractTensorMap) = α * term +_scale_local_term(α::Number, term::MPOTerm) = + AbstractTensorMap[k == 1 ? α * tensor : tensor for (k, tensor) in enumerate(term)] + Base.:*(α::Number, O::LocalOperator) = - LocalOperator(physicalspace(O), inds => α * operator for (inds, operator) in O.terms) + LocalOperator(physicalspace(O), inds => _scale_local_term(α, term) for (inds, term) in O.terms) Base.:*(O::LocalOperator, α::Number) = α * O Base.:/(O::LocalOperator, α::Number) = O * inv(α) @@ -179,7 +246,16 @@ Base.:\(α::Number, O::LocalOperator) = inv(α) * O function Base.:+(O1::LocalOperator, O2::LocalOperator) checklattice(O1, O2) - return LocalOperator(physicalspace(O1), mergewith(VI.add, O1.terms, O2.terms)) + return LocalOperator(physicalspace(O1), mergewith(_add_local_terms, O1.terms, O2.terms)) +end + +""" +Accumulate dense terms while rejecting addition involving an MPO at the same sites. +""" +function _add_local_terms(term1, term2) + (term1 isa MPOTerm || term2 isa MPOTerm) && + throw(ArgumentError("Accumulating terms with the same sites is not implemented for MPO terms.")) + return VI.add(term1, term2) end Base.:-(O::LocalOperator) = -1 * O @@ -190,9 +266,14 @@ Base.:-(O1::LocalOperator, O2::LocalOperator) = O1 + (-O2) # Since we allow abstract types in T, value and type domain might not match function VI.scalartype(operator::LocalOperator) - return promote_type((scalartype(term[2]) for term in operator.terms)...) + return promote_type((_local_term_scalartype(term) for term in values(operator.terms))...) end +""" +Return the promoted scalar type of a dense tensor or the factors of an MPO. +""" +_local_term_scalartype(term::AbstractTensorMap) = scalartype(term) +_local_term_scalartype(term::MPOTerm) = promote_type((scalartype(tensor) for tensor in term)...) # Equivalence # ----------- diff --git a/test/types/localoperator.jl b/test/types/localoperator.jl index 412d9b9a2..4a356fbbd 100644 --- a/test/types/localoperator.jl +++ b/test/types/localoperator.jl @@ -52,6 +52,89 @@ if !is_buildkite @test length(op5.terms) == 2 end + @testset "LocalOperator MPO bookkeeping" begin + d = ℂ^2 + lattice = fill(d, 2, 2) + dense = randn(Float64, d ⊗ d ← d ⊗ d) + mpo = PEPSKit.gate_to_mpo(dense; trunc = notrunc()) + sites = CartesianIndex.([(3, 4), (3, 3)]) + original_sites, original_mpo = copy(sites), copy(mpo) + operator = LocalOperator(lattice, sites => mpo) + stored_sites, stored_mpo = only(operator.terms) + + # Construction translates a copy of the coordinates and must retain the caller's MPO order. + # Mutating either input container afterwards must not alter the stored term. + @test sites == original_sites + reverse!(sites) + reverse!(mpo) + @test stored_sites == CartesianIndex.([(1, 2), (1, 1)]) + @test stored_mpo == original_mpo + + # Scaling every factor would multiply the operator by α^N; only one factor may change. + # A complex coefficient must also widen the container without mutating the original tensors. + α = 2 + 3im + snapshot = deepcopy(stored_mpo) + scaled_mpo = last(only((α * operator).terms)) + @test first(scaled_mpo) ≈ α * first(stored_mpo) + @test last(scaled_mpo) === last(stored_mpo) + @test stored_mpo == snapshot + onsite = randn(ComplexF64, d ← d) + mixed = operator + LocalOperator(lattice, ((2, 1),) => onsite) + @test scalartype(mixed) == ComplexF64 + + # Taking real/imaginary parts factorwise does not give the real/imaginary part of an MPO. + @test_throws ArgumentError real(mixed) + @test_throws ArgumentError imag(mixed) + + # Insertion and operator addition use different accumulation paths; both must reject MPO sums. + # Dense terms sharing the same sites must still accumulate normally. + inds = CartesianIndex.([(1, 1), (1, 2)]) + dense_operator = LocalOperator(lattice, inds => dense) + mpo_operator = LocalOperator(lattice, inds => original_mpo) + @test_throws ArgumentError PEPSKit.add_term!(mpo_operator, inds, dense) + @test_throws ArgumentError PEPSKit.add_term!(dense_operator, inds, original_mpo) + @test_throws ArgumentError mpo_operator + mpo_operator + @test_throws ArgumentError mpo_operator + dense_operator + @test_throws ArgumentError dense_operator + mpo_operator + @test last(only((dense_operator + dense_operator).terms)) ≈ 2 * dense + + # Drop zero factors before they reach boundary-MPS normalization, where they could cause 0/0. + @test isempty(LocalOperator(lattice, inds => [zero(first(original_mpo)), last(original_mpo)]).terms) + + # Unequal physical spaces expose accidental factor reordering during coordinate transformations. + lattice = [ℂ^2 ℂ^3 ℂ^4; ℂ^5 ℂ^6 ℂ^7] + dense = randn(Float64, ℂ^6 ⊗ ℂ^2 ← ℂ^6 ⊗ ℂ^2) + mpo = PEPSKit.gate_to_mpo(dense; trunc = notrunc()) + operator = LocalOperator(lattice, ((2, 2), (1, 1)) => mpo) + @test rotr90(rotl90(operator)) == operator + @test last(only(rotr90(operator).terms)) == mpo + sites, term = only(operator.terms) + @test repeat(operator, 2, 1).terms == Dict(sites => term, (sites .+ CartesianIndex(2, 0)) => term) + end + + @testset "MPO validation" begin + d, b1, b2 = ℂ^2, ℂ^3, ℂ^4 + lattice = fill(d, 2, 2) + first_tensor = randn(Float64, d ← d ⊗ b1) + last_tensor = randn(Float64, b1 ⊗ d ← d) + mpo = [first_tensor, last_tensor] + sites = ((1, 1), (1, 2)) + + # Reject malformed chains at construction, before any routing or contraction is attempted. + @test_throws ArgumentError LocalOperator(lattice, CartesianIndex{2}[] => AbstractTensorMap[]) + @test_throws ArgumentError LocalOperator(lattice, ((1, 1),) => mpo) + @test_throws ArgumentError LocalOperator(lattice, ((1, 1), (1, 1)) => mpo) + @test_throws ArgumentError LocalOperator(lattice, sites => reverse(mpo)) + + # Bond matching and the two physical legs are independent constraints. + wrong_bond = randn(Float64, b2 ⊗ d ← d) + wrong_input = randn(Float64, b1 ⊗ d ← ℂ^3) + wrong_output = randn(Float64, b1 ⊗ ℂ^3 ← d) + @test_throws SpaceMismatch LocalOperator(lattice, sites => [first_tensor, wrong_bond]) + @test_throws SpaceMismatch LocalOperator(lattice, sites => [first_tensor, wrong_input]) + @test_throws SpaceMismatch LocalOperator(lattice, sites => [first_tensor, wrong_output]) + end + @testset "Charge shifting" begin lattice = InfiniteSquare(1, 1) elt = ComplexF64 From a886e24222028a38753493d3ebc9f2ddac4e83db Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 1 Oct 2026 14:11:39 +0800 Subject: [PATCH 19/20] Accept LocalOperator in expectation_value_approx Replace MPOObservable with routed MPO terms, using column-snake decomposition for dense terms and simple nearest-neighbor routing for explicit MPOs. Add LatticePath and RoutedMPOTerm aliases, document path construction, and adapt focused contraction tests. --- src/PEPSKit.jl | 4 +- .../contractions/mpo_path/routing.jl | 206 ++++++++++++++++++ .../contractions/window/pepo_1layer.jl | 76 +++---- src/algorithms/expval_approx.jl | 42 +++- src/operators/localoperator.jl | 2 +- src/operators/mpo_observable.jl | 185 ---------------- src/utility/indexing.jl | 6 + test/testsuite/TestSuite.jl | 4 +- test/testsuite/toolbox/correlator_approx.jl | 2 +- test/testsuite/toolbox/expval_approx.jl | 70 +++++- test/testsuite/toolbox/mpo_routing.jl | 49 +++++ test/toolbox/expval_approx.jl | 1 + test/toolbox/mpo_routing.jl | 1 + test/types/mpo_observable.jl | 98 --------- 14 files changed, 395 insertions(+), 351 deletions(-) create mode 100644 src/algorithms/contractions/mpo_path/routing.jl delete mode 100644 src/operators/mpo_observable.jl delete mode 100644 test/types/mpo_observable.jl diff --git a/src/PEPSKit.jl b/src/PEPSKit.jl index 6b7af7b8b..3c5fffdb1 100644 --- a/src/PEPSKit.jl +++ b/src/PEPSKit.jl @@ -82,7 +82,6 @@ include("operators/infinitepepo.jl") include("operators/transfermatrix.jl") include("operators/localoperator.jl") include("operators/localcircuit.jl") -include("operators/mpo_observable.jl") include("operators/lattices/squarelattice.jl") include("operators/models.jl") @@ -125,6 +124,7 @@ include("algorithms/contractions/correlator/peps.jl") include("algorithms/contractions/correlator/pepo_purified.jl") include("algorithms/contractions/correlator/pepo_1layer.jl") +include("algorithms/contractions/mpo_path/routing.jl") include("algorithms/contractions/mpo_path/pepo_1layer.jl") include("algorithms/contractions/window/tools.jl") include("algorithms/contractions/window/pepo_1layer.jl") @@ -190,7 +190,7 @@ export FixedSpaceTruncation, SiteDependentTruncation export HalfInfiniteProjector, FullInfiniteProjector export C4vCTMRG, C4vEighProjector, C4vQRProjector export initialize_random_c4v_env, initialize_singlet_c4v_env -export LocalOperator, MPOTerm, MPOObservable, physicalspace +export LocalOperator, MPOTerm, physicalspace export product_peps export reduced_densitymatrix, expectation_value_approx, correlator_approx export expectation_value, network_value, cost_function diff --git a/src/algorithms/contractions/mpo_path/routing.jl b/src/algorithms/contractions/mpo_path/routing.jl new file mode 100644 index 000000000..81995118a --- /dev/null +++ b/src/algorithms/contractions/mpo_path/routing.jl @@ -0,0 +1,206 @@ +""" +A `path => mpo` pair with one MPO tensor per site of a non-self-intersecting nearest-neighbor lattice path. +Intermediate sites carry inserted braiding tensors that propagate the MPO bond. +These properties are checked by routing validation, not enforced by this alias. +""" +const RoutedMPOTerm = Pair{<:LatticePath, <:MPOTerm} + +""" +Convert a dense term to an exact MPO in column-snake order and expand its nearest-neighbor path. + +Visit occupied columns from left to right, alternating between increasing and decreasing row indices within each column. +Permute the operator's physical legs into this order before decomposing it into an MPO. +For example, the six operator sites below are visited as `A → B → C → D → E → F`, with row indices increasing downward: + +```text + col 1 col 2 col 3 +row 1 A + --→-- E + ↓ ↑ ↓ +row 2 + D + + ↓ ↑ ↓ +row 3 B + F + ↓ ↑ +row 4 + --→-- C +``` + +The path is constructed successively between these ordered sites: + +1. `A → B`: descend from `(1, 1)` through `(2, 1)` to `(3, 1)`. +2. `B → C`: connect the column bottoms at row `max(3, 4) = 4`, passing through `(4, 1)` before reaching `(4, 2)`. +3. `C → D`: ascend through `(3, 2)` to `(2, 2)`. +4. `D → E`: connect the column tops at row `min(2, 1) = 1`, passing through `(1, 2)` before reaching `(1, 3)`. +5. `E → F`: descend through `(2, 3)` to `(3, 3)`. + +The `+` sites carry inserted braiding tensors that propagate the MPO bond without an additional physical operator. +Connecting successive columns at their outermost endpoint row avoids retracing the path: a horizontal-first connection from `B` to `C` would visit `(3, 2)`, which the later `C → D` segment needs. +""" +function _route_mpo_term( + sites::LatticePath, op::AbstractTensorMap, + lattice::Matrix{<:ElementarySpace}, + ) + isempty(sites) && throw(ArgumentError("an operator term requires at least one site")) + allunique(sites) || throw(ArgumentError("operator sites should be unique")) + length(sites) == numout(op) == numin(op) || + throw(ArgumentError("number of operator legs should match the number of sites")) + Nr, Nc = size(lattice) + for (k, site) in enumerate(sites) + V = lattice[mod1(site[1], Nr), mod1(site[2], Nc)] + V == codomain(op)[k] == domain(op)[k] || + throw(SpaceMismatch("operator physical space does not match lattice site $site")) + end + length(sites) == 1 && return copy(sites) => [op] + + columns = sort!(unique(site[2] for site in sites)) + permutation = Int[] + for (k, col) in enumerate(columns) + indices = findall(site -> site[2] == col, sites) + sort!(indices; by = i -> sites[i][1], rev = iseven(k)) + append!(permutation, indices) + end + ordered_sites = sites[permutation] + N = length(sites) + ordered_op = permute(op, (Tuple(permutation), Tuple(permutation .+ N))) + mpo = gate_to_mpo(ordered_op; trunc = notrunc()) + path = _dense_mpo_path(ordered_sites) + return _expand_mpo_path(ordered_sites, mpo, path, lattice) +end + +""" +Preserve an explicit MPO's factor order while connecting its sites with simple nearest-neighbor paths. +""" +function _route_mpo_term( + sites::LatticePath, mpo::MPOTerm, + lattice::Matrix{<:ElementarySpace}, + ) + _validate_mpo_term(sites, mpo, lattice) + path = _ordered_mpo_path(sites) + return _expand_mpo_path(sites, mpo, path, lattice) +end + +""" +Connect snake-ordered columns beyond their bottom or top endpoints to avoid retracing sparse columns. +""" +function _dense_mpo_path(sites::LatticePath) + path = CartesianIndex{2}[first(sites)] + column_index = 1 + for (from, to) in zip(sites, Iterators.drop(sites, 1)) + if from[2] == to[2] + append!(path, Iterators.drop(_l_path(from, to), 1)) + else + row = isodd(column_index) ? max(from[1], to[1]) : min(from[1], to[1]) + corners = (CartesianIndex(row, from[2]), CartesianIndex(row, to[2]), to) + for corner in corners + last(path) == corner && continue + append!(path, Iterators.drop(_l_path(last(path), corner), 1)) + end + column_index += 1 + end + end + return path +end + +""" +Route ordered sites with horizontal-first L paths, falling back to vertical-first paths without backtracking. +""" +function _ordered_mpo_path(sites::LatticePath) + isempty(sites) && throw(ArgumentError("an MPO term requires at least one site")) + allunique(sites) || throw(ArgumentError("operator sites should be unique")) + path = CartesianIndex{2}[first(sites)] + measured = Set(sites) + visited = Set(path) + for (from, to) in zip(sites, Iterators.drop(sites, 1)) + segment = _l_path(from, to) + if !_mpo_segment_is_free(segment, measured, visited) + segment = _l_path(from, to; horizontal_first = false) + _mpo_segment_is_free(segment, measured, visited) || throw( + ArgumentError( + "routing this MPO ordering is not implemented: no nonintersecting L path from $from to $to", + ), + ) + end + append!(path, Iterators.drop(segment, 1)) + union!(visited, segment) + end + return path +end + +""" +Check that a connecting segment neither revisits a site nor crosses another operator site. +""" +function _mpo_segment_is_free(segment, measured::Set, visited::Set) + any(site -> site in visited, @view segment[2:end]) && return false + return all(site -> !(site in measured), @view segment[2:(end - 1)]) +end + +""" +Insert braiding tensors at unmeasured path sites using periodic physical spaces and the neighboring MPO bond. +""" +function _expand_mpo_path( + sites::LatticePath, mpo::MPOTerm, + path::LatticePath, lattice::Matrix{<:ElementarySpace}, + ) + _validate_mpo_term(sites, mpo, lattice) + allunique(path) || throw(ArgumentError("the MPO path should not intersect itself")) + positions = indexin(sites, path) + all(!isnothing, positions) && issorted(positions) || + throw(ArgumentError("the MPO path should contain operator sites in MPO order")) + first(positions) == 1 && last(positions) == length(path) || + throw(ArgumentError("the MPO path should start and end at operator sites")) + for (from, to) in zip(path, Iterators.drop(path, 1)) + _step_direction(from, to) + end + + Nr, Nc = size(lattice) + k = 1 + expanded_mpo = AbstractTensorMap[] + for site in path + if site == sites[k] + op = mpo[k] + k += 1 + push!(expanded_mpo, op) + continue + end + previous = mpo[k - 1] + V = lattice[mod1(site[1], Nr), mod1(site[2], Nc)] + bond = _mpo_right_stringspace(previous)' + braid = TensorKit.BraidingTensor{scalartype(previous)}(V, bond) + push!(expanded_mpo, copy!(similar(previous, scalartype(previous), space(braid)), braid)) + end + return copy(path) => expanded_mpo +end + +""" +Return the right MPO string space in the local tensor's stored leg orientation. +""" +_mpo_right_stringspace(op::AbstractTensorMap) = space(op, numind(op)) + +""" +Build a shortest nearest-neighbor L path with the chosen horizontal or vertical first step. +""" +function _l_path( + start::CartesianIndex{2}, stop::CartesianIndex{2}; horizontal_first::Bool = true + ) + start == stop && throw(ArgumentError("MPO path sites should be unique")) + path = CartesianIndex{2}[start] + axes = horizontal_first ? (2, 1) : (1, 2) + for axis in axes + while last(path)[axis] != stop[axis] + step = sign(stop[axis] - last(path)[axis]) + delta = axis == 1 ? CartesianIndex(step, 0) : CartesianIndex(0, step) + push!(path, last(path) + delta) + end + end + return path +end + +""" +Return the cardinal direction of a nearest-neighbor step, rejecting longer or stationary steps. +""" +function _step_direction(from::CartesianIndex{2}, to::CartesianIndex{2}) + delta = to - from + delta == CartesianIndex(0, 1) && return :east + delta == CartesianIndex(0, -1) && return :west + delta == CartesianIndex(1, 0) && return :south + delta == CartesianIndex(-1, 0) && return :north + throw(ArgumentError("MPO path should use nearest-neighbor steps")) +end diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/window/pepo_1layer.jl index 719a232c4..3554dc6ea 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/window/pepo_1layer.jl @@ -49,14 +49,14 @@ function standardize_dualness(ρ::InfinitePEPO, env::CTMRGEnv) end """ -Contract an MPO observable in its enclosing window, rotating column sweeps into row sweeps. +Contract a routed MPO term in its enclosing window, rotating column sweeps into row sweeps. """ function _expectation_value_approx( - ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv, + ρ::InfinitePEPO, routed::RoutedMPOTerm, env::CTMRGEnv, alg::WindowApprox, direction::Symbol, ) _check_window_inputs(ρ, direction) - rowrange, colrange = _window_ranges(observable) + rowrange, colrange = _window_ranges(first(routed)) sweep = if direction === :auto length(colrange) >= length(rowrange) ? :rows : :columns else @@ -64,16 +64,15 @@ function _expectation_value_approx( end if sweep === :rows return _expectation_value_approx_rows( - ρ, observable, env, rowrange, colrange, alg + ρ, routed, env, rowrange, colrange, alg ) else unitcell = size(ρ)[1:2] - sites = siterotl90.(observable.sites, Ref(unitcell)) - path = siterotl90.(observable.path, Ref(unitcell)) - rotated_observable = MPOObservable(sites, path, observable.mpo) - rotated_rowrange, rotated_colrange = _window_ranges(rotated_observable) + path, mpo = routed + rotated = siterotl90.(path, Ref(unitcell)) => mpo + rotated_rowrange, rotated_colrange = _window_ranges(first(rotated)) return _expectation_value_approx_rows( - rotl90(ρ), rotated_observable, rotl90(env), + rotl90(ρ), rotated, rotl90(env), rotated_rowrange, rotated_colrange, alg ) end @@ -89,67 +88,54 @@ function _window_site_tensor( end """ -Build the local row-MPO tensor at one window site, inserting the observable tensor or routed -string when the site lies on the MPO path and tracing the PEPO physical legs otherwise. +Insert the MPO factor at a path site, or trace the PEPO physical legs at an off-path site. """ function _window_site_tensor( - ρ::InfinitePEPO, observable::MPOObservable, row::Int, col::Int, + ρ::InfinitePEPO, routed::RoutedMPOTerm, + row::Int, col::Int, ) A = ρ[row, col, 1] - site = CartesianIndex(row, col) - path_index = findfirst(==(site), observable.path) - isnothing(path_index) && return trace_physicalspaces(A) + path, mpo = routed + k = findfirst(==(CartesianIndex(row, col)), path) + isnothing(k) && return trace_physicalspaces(A) - mpo_index = findfirst(==(site), observable.sites) - # sites with string passing by (cannot be first/last site) - if isnothing(mpo_index) - incoming = _step_direction(observable.path[path_index], observable.path[path_index - 1]) - outgoing = _step_direction(observable.path[path_index], observable.path[path_index + 1]) - next_mpo_index = count(in(observable.sites), @view observable.path[1:path_index]) + 1 - stringspace = space(observable.mpo[next_mpo_index], 1) - return mpo_path_string(A, stringspace, Val((incoming, outgoing))) - end - - # sites acted on by the MPO - op = observable.mpo[mpo_index] - if mpo_index == 1 - direction = _step_direction(observable.path[1], observable.path[2]) - return mpo_path_first(A, op, Val(direction)) - elseif mpo_index == length(observable.mpo) - direction = _step_direction(observable.path[end], observable.path[end - 1]) - return mpo_path_last(A, op, Val(direction)) + if k == 1 + direction = _step_direction(path[1], path[2]) + return mpo_path_first(A, mpo[k], Val(direction)) + elseif k == length(path) + direction = _step_direction(path[end], path[end - 1]) + return mpo_path_last(A, mpo[k], Val(direction)) else - incoming = _step_direction(observable.path[path_index], observable.path[path_index - 1]) - outgoing = _step_direction(observable.path[path_index], observable.path[path_index + 1]) - return mpo_path_middle(A, op, Val((incoming, outgoing))) + incoming = _step_direction(path[k], path[k - 1]) + outgoing = _step_direction(path[k], path[k + 1]) + return mpo_path_middle(A, mpo[k], Val((incoming, outgoing))) end end """ -Contract and normalize an MPO observable using row-oriented window boundary contractions. +Contract and normalize a routed MPO term using row-oriented window boundary contractions. """ function _expectation_value_approx_rows( - ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv, + ρ::InfinitePEPO, routed::RoutedMPOTerm, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, ) ρ, env = standardize_dualness(ρ, env) - numerator = _contract_window_rows(ρ, observable, env, rowrange, colrange, alg) + numerator = _contract_window_rows(ρ, routed, env, rowrange, colrange, alg) norm = _contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) return numerator / norm end """ -Contract a complete PEPO window row by row from north to south, -optionally inserting an MPO observable. +Contract a complete PEPO window row by row from north to south, optionally inserting a routed MPO term. """ function _contract_window_rows( - ρ::InfinitePEPO, observable::Union{Nothing, MPOObservable}, + ρ::InfinitePEPO, routed::Union{Nothing, RoutedMPOTerm}, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, ) ψ = _north_boundary_mps(env, first(rowrange), colrange) for row in rowrange - W = _row_mpo(ρ, observable, env, row, colrange) + W = _row_mpo(ρ, routed, env, row, colrange) ψ = _approximate(W, ψ, alg) end south = _south_boundary_mps(env, last(rowrange), colrange) @@ -171,7 +157,7 @@ Convention of west, east CTM edges and the PF tensors: Legs 1, 3 need to be flipped to match standard MPS convention """ function _row_mpo( - ρ::InfinitePEPO, observable::Union{Nothing, MPOObservable}, + ρ::InfinitePEPO, routed::Union{Nothing, RoutedMPOTerm}, env::CTMRGEnv, row::Int, colrange::UnitRange{Int}, ) cmin, cmax = first(colrange), last(colrange) @@ -180,7 +166,7 @@ function _row_mpo( append!( tensors, ( - _window_site_tensor(ρ, observable, row, col) + _window_site_tensor(ρ, routed, row, col) for col in colrange ), ) diff --git a/src/algorithms/expval_approx.jl b/src/algorithms/expval_approx.jl index 00b0028d1..f6ff4c2f6 100644 --- a/src/algorithms/expval_approx.jl +++ b/src/algorithms/expval_approx.jl @@ -4,29 +4,51 @@ """ $(SIGNATURES) -Approximately measure the expectation value of an open-boundary `MPOObservable` in a single-layer PEPO using finite boundary MPS zipup sweeps. +Approximately measure a `LocalOperator` in a single-layer PEPO using finite boundary MPS zipup sweeps. +Each term is normalized in its own enclosing window, and the resulting contributions are summed. +Dense terms are reordered into a column-wise snake and decomposed into MPOs without truncation. +Explicit [`MPOTerm`](@ref) factors retain their given order and are connected by non-self-intersecting nearest-neighbor paths. +Routing tries horizontal-first and then vertical-first shortest connections; more complicated explicit MPO routes raise a not-implemented error. +Single-site terms are evaluated exactly, and an empty operator returns zero. -- By default, `direction = :auto` selects north-to-south sweep for wide and square windows, and east-to-west for tall windows. +- By default, `direction = :auto` selects north-to-south sweeps for wide and square windows, and east-to-west for tall windows, separately for each term. Specify `direction = :rows` or `:columns` to override this choice. - The zipup truncation is controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary dimension. + This keyword does not truncate the operator decomposition. - After each zipup step, the result is refined by a single-site DMRG approximation step with `maxiter` sweeps. Set `maxiter = 0` to disable this refinement. """ function expectation_value_approx( - ρ::InfinitePEPO, observable::MPOObservable, env::CTMRGEnv; + ρ::InfinitePEPO, O::LocalOperator, env::CTMRGEnv; trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, ) - return _expectation_value_approx( - ρ, observable, env, - WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction - ) + _check_window_inputs(ρ, direction) + checklattice(ρ, O) + alg = WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)) + isempty(O.terms) && return zero(promote_type(scalartype(ρ), scalartype(env))) + term_vals = map(collect(O.terms)) do (sites, term) + return _local_expectation_value_approx(sites, ρ, term, env, alg, direction) + end + return sum(term_vals) end """ -Return the row and column ranges enclosing an MPO observable's complete routed path. +Evaluate a dense or MPO term, using exact single-site contractions and routing larger terms. """ -function _window_ranges(observable::MPOObservable) - return _window_ranges(observable.path) +function _local_expectation_value_approx( + sites::Vector{CartesianIndex{2}}, ρ::InfinitePEPO, + term::Union{AbstractTensorMap, MPOTerm}, env::CTMRGEnv, + alg::WindowApprox, direction::Symbol, + ) + if _local_term_iszero(term) + return zero(promote_type(scalartype(ρ), scalartype(env), _local_term_scalartype(term))) + end + if length(sites) == 1 + op = term isa AbstractTensorMap ? term : only(term) + return local_expectation_value(sites, ρ, op, env) + end + routed = _route_mpo_term(sites, term, physicalspace(ρ)) + return _expectation_value_approx(ρ, routed, env, alg, direction) end """ diff --git a/src/operators/localoperator.jl b/src/operators/localoperator.jl index c881ff376..35e632637 100644 --- a/src/operators/localoperator.jl +++ b/src/operators/localoperator.jl @@ -4,7 +4,7 @@ An open-boundary MPO represented by an ordered vector of tensor maps. The endpoint partitions are `(1, 2)` and `(2, 1)`, with `(2, 2)` tensors in between; a one-site MPO has partition `(1, 1)`. Dense tensors can be converted with `gate_to_mpo`. -Evaluation of MPO terms and vectors of tensor-product factors is not implemented. +MPO terms are supported by `expectation_value_approx`; exact expectation values and vectors of tensor-product factors are not implemented. """ const MPOTerm{T} = AbstractVector{T} where {T <: AbstractTensorMap} diff --git a/src/operators/mpo_observable.jl b/src/operators/mpo_observable.jl deleted file mode 100644 index 561200f3c..000000000 --- a/src/operators/mpo_observable.jl +++ /dev/null @@ -1,185 +0,0 @@ -""" -$(TYPEDEF) - -An open-boundary MPO embedded along a non-self-intersecting nearest-neighbor path on the -square lattice. The tensor `mpo[k]` acts on `sites[k]`; `path` also contains intermediate -sites that only carry the MPO string. - -The first and last MPO tensors use the reduced endpoint partitions `(1, 2)` and `(2, 1)`; -all intermediate tensors use the standard `(2, 2)` MPO partition. -""" -struct MPOObservable{M} - sites::Vector{CartesianIndex{2}} - path::Vector{CartesianIndex{2}} - mpo::Vector{M} - - function MPOObservable( - sites::Vector{CartesianIndex{2}}, path::Vector{CartesianIndex{2}}, - mpo::Vector{M}, - ) where {M} - _validate_mpo_observable(sites, path, mpo) - return new{M}(sites, path, mpo) - end -end - -""" -Construct an MPO observable from ordered operator sites and MPO tensors. The tensor `mpo[k]` -acts on `sites[k]`; consecutive sites are connected by horizontal-first shortest paths. -""" -function MPOObservable(sites, mpo) - sites′ = CartesianIndex{2}[_mpo_observable_site(site) for site in sites] - mpo′ = collect(mpo) - path = _route_mpo_observable(sites′) - return MPOObservable(sites′, path, mpo′) -end - -""" -Construct an MPO observable from a dense operator. Operator sites are ordered in the same -way as `LocalOperator` terms, and consecutive sites are connected by horizontal-first -shortest paths. The lattice is a periodically indexed matrix of elementary physical spaces, -as for `LocalOperator`. -""" -function MPOObservable( - sites, op::AbstractTensorMap, lattice::Matrix{<:ElementarySpace}; - trunc = trunctol(; atol = MPSKit.Defaults.tol), - ) - sites′ = CartesianIndex{2}[_mpo_observable_site(site) for site in sites] - length(sites′) >= 2 || throw(ArgumentError("an MPO observable requires at least two sites")) - allunique(sites′) || throw(ArgumentError("operator sites should be unique")) - length(sites′) == numin(op) == numout(op) || - throw(ArgumentError("number of operator legs should match the number of sites")) - - Nr, Nc = size(lattice) - sites′, op′ = _sort_op_sites(sites′, op) - for (i, site) in enumerate(sites′) - V = lattice[mod1(site[1], Nr), mod1(site[2], Nc)] - V == domain(op′)[i] == codomain(op′)[i] || - throw(SpaceMismatch("operator physical space does not match lattice site $site")) - end - - mpo = gate_to_mpo(op′; trunc) - length(mpo) == length(sites′) || - throw(ArgumentError("expected an MPO decomposition matching the number of sites")) - return MPOObservable(sites′, mpo) -end - -""" -Normalize a supported lattice-site representation to a two-dimensional Cartesian index. -""" -_mpo_observable_site(site::CartesianIndex{2}) = site -_mpo_observable_site(site::Tuple{Int, Int}) = CartesianIndex(site) -_mpo_observable_site(site) = - throw(ArgumentError("MPO sites should be CartesianIndex{2} or (row, col) tuples")) - -""" -Validate the topology, tensor partitions, physical placement, and adjacent string spaces of -an already routed open-boundary MPO observable. -""" -function _validate_mpo_observable(sites, path, mpo) - length(sites) >= 2 || throw(ArgumentError("an MPO observable requires at least two sites")) - length(sites) == length(mpo) || - throw(ArgumentError("the MPO should contain one tensor for every operator site")) - allunique(sites) || throw(ArgumentError("operator sites should be unique")) - allunique(path) || throw(ArgumentError("the MPO path should not intersect itself")) - all(op -> op isa AbstractTensorMap, mpo) || - throw(ArgumentError("all MPO entries should be tensor maps")) - - for (from, to) in zip(path, Iterators.drop(path, 1)) - _step_direction(from, to) - end - path_positions = indexin(sites, path) - all(!isnothing, path_positions) && issorted(path_positions) || - throw(ArgumentError("the MPO path should contain operator sites in MPO order")) - first(path_positions) == 1 && last(path_positions) == length(path) || - throw(ArgumentError("the MPO path should start and end at operator sites")) - - numout(first(mpo)) == 1 && numin(first(mpo)) == 2 || - throw(ArgumentError("the first MPO tensor should have partition (1, 2)")) - numout(last(mpo)) == 2 && numin(last(mpo)) == 1 || - throw(ArgumentError("the last MPO tensor should have partition (2, 1)")) - for op in @view mpo[2:(end - 1)] - numout(op) == 2 && numin(op) == 2 || - throw(ArgumentError("middle MPO tensors should have partition (2, 2)")) - end - for k in 1:(length(mpo) - 1) - _mpo_right_stringspace(mpo[k])' == space(mpo[k + 1], 1) || - throw(SpaceMismatch("incompatible MPO string spaces between sites $k and $(k + 1)")) - end - return nothing -end - -function VI.scalartype(observable::MPOObservable) - return promote_type((scalartype(op) for op in observable.mpo)...) -end - -""" -Connect ordered operator sites by horizontal-first shortest paths while rejecting crossings -and routes that pass through later operator sites. -""" -function _route_mpo_observable(sites) - length(sites) >= 2 || throw(ArgumentError("an MPO observable requires at least two sites")) - allunique(sites) || throw(ArgumentError("operator sites should be unique")) - - path = CartesianIndex{2}[first(sites)] - measured = Set(sites) - visited = Set(path) - - for k in 1:(length(sites) - 1) - segment = _l_path(sites[k], sites[k + 1]) - - for site in @view segment[2:(end - 1)] - site in measured && - throw(ArgumentError("MPO path passes through another operator site")) - site in visited && - throw(ArgumentError("MPO path intersects itself at site $site")) - push!(path, site) - push!(visited, site) - end - - site = last(segment) - site in visited && throw(ArgumentError("MPO path intersects itself at site $site")) - push!(path, site) - push!(visited, site) - end - return path -end - -""" -Return the right MPO string space in the local tensor's stored leg orientation. -""" -_mpo_right_stringspace(op) = space(op, numind(op)) - -""" -Build a deterministic shortest nearest-neighbor path between two sites. The path traverses -the horizontal separation first and then the vertical separation. -""" -function _l_path(start::CartesianIndex{2}, stop::CartesianIndex{2}) - start == stop && throw(ArgumentError("MPO path sites should be unique")) - - path = CartesianIndex{2}[start] - row, col = Tuple(start) - stoprow, stopcol = Tuple(stop) - - while col != stopcol - col += sign(stopcol - col) - push!(path, CartesianIndex(row, col)) - end - while row != stoprow - row += sign(stoprow - row) - push!(path, CartesianIndex(row, col)) - end - return path -end - -""" -Return the cardinal direction of a nearest-neighbor step from one site to another. Throw an -error when the sites are not nearest neighbors. -""" -function _step_direction(from::CartesianIndex{2}, to::CartesianIndex{2}) - delta = to - from - delta == CartesianIndex(0, 1) && return :east - delta == CartesianIndex(0, -1) && return :west - delta == CartesianIndex(1, 0) && return :south - delta == CartesianIndex(-1, 0) && return :north - throw(ArgumentError("MPO path should use nearest-neighbor steps")) -end diff --git a/src/utility/indexing.jl b/src/utility/indexing.jl index cc5fa1249..de0b166b8 100644 --- a/src/utility/indexing.jl +++ b/src/utility/indexing.jl @@ -1,3 +1,9 @@ +""" +An ordered sequence of sites on a two-dimensional lattice. +Nearest-neighbor connectivity and nonintersection are checked by routing functions, not enforced by this alias. +""" +const LatticePath = AbstractVector{CartesianIndex{2}} + # Get next and previous directional CTMRG environment index, respecting periodicity _next(i, total) = mod1(i + 1, total) _prev(i, total) = mod1(i - 1, total) diff --git a/test/testsuite/TestSuite.jl b/test/testsuite/TestSuite.jl index 11e9246c3..29967b264 100644 --- a/test/testsuite/TestSuite.jl +++ b/test/testsuite/TestSuite.jl @@ -241,7 +241,7 @@ using .ToolboxDensityMatrices module ToolboxExpvalApprox include("toolbox/expval_approx.jl") - export toolbox_expval_approx + export toolbox_expval_approx, toolbox_expval_approx_localoperator end using .ToolboxExpvalApprox @@ -259,7 +259,7 @@ using .ToolboxCorrelatorApproxPhys module ToolboxMPORouting include("toolbox/mpo_routing.jl") - export toolbox_mpo_routing_fusers, toolbox_mpo_routing_identities + export toolbox_mpo_routing_fusers, toolbox_mpo_routing_identities, toolbox_mpo_routing_paths end using .ToolboxMPORouting diff --git a/test/testsuite/toolbox/correlator_approx.jl b/test/testsuite/toolbox/correlator_approx.jl index 92b9f86d2..e87b5867a 100644 --- a/test/testsuite/toolbox/correlator_approx.jl +++ b/test/testsuite/toolbox/correlator_approx.jl @@ -12,7 +12,7 @@ Contract `⟨op⟩` between `i` and each site in `js` independently without cach """ function _adaptive_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) lattice = physicalspace(ρ) - observables = [MPOObservable([i, j], op, lattice) for j in js] + observables = [PEPSKit._route_mpo_term([i, j], op, lattice) for j in js] _, colrange = PEPSKit._window_ranges([i; js]) alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) return map(observables, js) do observable, j diff --git a/test/testsuite/toolbox/expval_approx.jl b/test/testsuite/toolbox/expval_approx.jl index 3019ae81b..8364da96b 100644 --- a/test/testsuite/toolbox/expval_approx.jl +++ b/test/testsuite/toolbox/expval_approx.jl @@ -38,27 +38,83 @@ function toolbox_expval_approx(AT) d, D, χ = spaces[S] ρ = adapt(AT, InfinitePEPO(d, D; unitcell = (2, 2, 1))) env = CTMRGEnv(InfinitePartitionFunction(ρ), χ) + lattice = physicalspace(ρ) trunc = notrunc() + # Dense terms may change MPO order when forming a snake; explicit MPOs must keep theirs. + # Exact contractions expose permutation or braiding errors in both paths and sweep orientations. for sites in sites_list n = length(sites) op = adapt(AT, randn(ComplexF64, d^n → d^n)) mpo = PEPSKit.gate_to_mpo(op; trunc) - observable = MPOObservable(sites, mpo) - exact = expectation_value( - ρ, LocalOperator(physicalspace(ρ), sites => op), env - ) - for direction in (:rows, :columns) + dense = LocalOperator(lattice, sites => op) + factorized = LocalOperator(lattice, sites => mpo) + exact = expectation_value(ρ, dense, env) + for observable in (dense, factorized), direction in (:rows, :columns) @test expectation_value_approx( ρ, observable, env; trunc, maxiter = 0, direction ) ≈ exact end # Square windows must default to row sweeps. if sites == sites_list[3] - auto = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0) - rows = expectation_value_approx(ρ, observable, env; trunc = truncrank(2), maxiter = 0, direction = :rows) + auto = expectation_value_approx(ρ, dense, env; trunc = truncrank(2), maxiter = 0) + rows = expectation_value_approx(ρ, dense, env; trunc = truncrank(2), maxiter = 0, direction = :rows) @test auto == rows end end end end + +""" +Check single-site dispatch, sparse snakes, and sums of independently normalized terms. +""" +function toolbox_expval_approx_localoperator(AT) + return @testset "LocalOperator interface ($AT)" begin + Random.seed!(4321) + d = Vect[FermionParity](0 => 1, 1 => 1) + ρ = adapt(AT, InfinitePEPO(d, d; unitcell = (2, 2, 1))) + env = CTMRGEnv(InfinitePartitionFunction(ρ), d) + lattice = physicalspace(ρ) + trunc = notrunc() + + # Single-site terms bypass MPO decomposition, which requires at least two sites. + op1 = adapt(AT, randn(ComplexF64, d → d)) + one_site = LocalOperator(lattice, [CI(1, 1)] => op1) + one_factor = LocalOperator(lattice, [CI(1, 1)] => [op1]) + for observable in (one_site, one_factor) + @test expectation_value_approx(ρ, observable, env; trunc, maxiter = 0) ≈ + expectation_value(ρ, one_site, env) + end + + # A greedy horizontal-first route revisits a site for this support. + # The dense snake must connect outside the column's endpoints and preserve the operator. + op3 = adapt(AT, randn(ComplexF64, d^3 → d^3)) + snake = LocalOperator(lattice, [CI(1, 0), CI(0, 1), CI(2, 1)] => op3) + @test expectation_value_approx(ρ, snake, env; trunc, maxiter = 0) ≈ + expectation_value(ρ, snake, env) + + # These terms use different normalization windows; the gapped MPO also inserts a string. + # Non-unit-cell coordinates and both sweeps exercise translation and rotation of the expanded path. + op2 = adapt(AT, randn(ComplexF64, d^2 → d^2)) + sites2 = [CI(-1, 0), CI(-1, 2)] + mixed = LocalOperator(lattice, [CI(1, 1)] => op1, sites2 => PEPSKit.gate_to_mpo(op2; trunc)) + dense_sum = LocalOperator(lattice, [CI(1, 1)] => op1, sites2 => op2) + for direction in (:rows, :columns) + @test expectation_value_approx(ρ, mixed, env; trunc, maxiter = 0, direction) ≈ + expectation_value(ρ, dense_sum, env) + end + + # Complex scaling leaves real factors in the MPO; expansion must retain a valid mixed-scalar container. + real_op = real(op2) + real_mpo = LocalOperator(lattice, sites2 => PEPSKit.gate_to_mpo(real_op; trunc)) + coefficient = 2 + 3im + @test expectation_value_approx(ρ, coefficient * real_mpo, env; trunc, maxiter = 0) ≈ + coefficient * expectation_value(ρ, LocalOperator(lattice, sites2 => real_op), env) + + # Constructors discard zero terms, so an empty sum must produce a scalar zero without contracting. + @test iszero(expectation_value_approx(ρ, LocalOperator(lattice), env)) + # Reject a mismatched unit cell even when the individual operator's physical space matches. + wrong_lattice = LocalOperator(fill(d, 1, 1), [CI(1, 1)] => op1) + @test_throws ArgumentError expectation_value_approx(ρ, wrong_lattice, env) + end +end diff --git a/test/testsuite/toolbox/mpo_routing.jl b/test/testsuite/toolbox/mpo_routing.jl index 28e4d7f4b..96cbf851d 100644 --- a/test/testsuite/toolbox/mpo_routing.jl +++ b/test/testsuite/toolbox/mpo_routing.jl @@ -102,3 +102,52 @@ function toolbox_mpo_routing_identities(AT) end end end + +""" +Check routing choices, dense leg permutations, and periodic string insertion. +""" +function toolbox_mpo_routing_paths(AT) + return @testset "MPO paths ($AT)" begin + Random.seed!(1234) + CI = CartesianIndex + + # The first horizontal L crosses a later operator site, so routing must try the vertical L. + # Conversely, a collinear out-of-order MPO cannot be connected by either supported L path. + fallback_sites = CI.([(1, 1), (3, 3), (1, 2)]) + @test PEPSKit._ordered_mpo_path(fallback_sites) == + CI.([(1, 1), (2, 1), (3, 1), (3, 2), (3, 3), (2, 3), (1, 3), (1, 2)]) + @test_throws r"routing this MPO ordering is not implemented" PEPSKit._ordered_mpo_path( + CI.([(1, 1), (1, 3), (1, 2)]) + ) + + # Reconstructing on unequal physical spaces catches mismatches between the snake order and either group of permuted physical legs. + lattice = [ℂ^2 ℂ^3; ℂ^1 ℂ^2] + sites = CI.([(2, 2), (1, 1), (1, 2), (2, 1)]) + physical = foldl(⊗, lattice[sites]) + op = adapt(AT, randn(ComplexF64, physical ← physical)) + path, expanded = PEPSKit._route_mpo_term(sites, op, lattice) + @test path == sites[[2, 4, 1, 3]] + @tensor reconstructed[p1 p2 p3 p4; q1 q2 q3 q4] := + expanded[1][p1; q1 a] * expanded[2][a p2; q2 b] * + expanded[3][b p3; q3 c] * expanded[4][c p4; q4] + @test reconstructed ≈ permute(op, ((2, 4, 1, 3), (6, 8, 5, 7))) + + # Intermediate sites have different physical spaces from the endpoints and lie outside the unit cell. + # Each inserted braid must therefore use periodic lookup and the MPO's storage type. + @testset "Periodic strings ($S)" for S in keys(spaces) + physical, _, _ = spaces[S] + intermediate = physical ⊕ physical + lattice = [physical intermediate; intermediate physical] + op = adapt(AT, randn(ComplexF64, physical^2 ← physical^2)) + mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) + path, expanded = PEPSKit._route_mpo_term(CI.([(0, 0), (2, 2)]), mpo, lattice) + for k in 2:(length(path) - 1) + site = path[k] + V = lattice[mod1(site[1], 2), mod1(site[2], 2)] + braid = adapt(AT, TensorMap(TensorKit.BraidingTensor{ComplexF64}(V, space(mpo[2], 1)))) + @test expanded[k] ≈ braid + end + @test all(t -> storagetype(t) == storagetype(mpo[1]), expanded) + end + end +end diff --git a/test/toolbox/expval_approx.jl b/test/toolbox/expval_approx.jl index 281fffaa2..ca8fb80ec 100644 --- a/test/toolbox/expval_approx.jl +++ b/test/toolbox/expval_approx.jl @@ -8,4 +8,5 @@ is_buildkite = get(ENV, "BUILDKITE", "false") == "true" if !is_buildkite TestSuite.toolbox_expval_approx(Vector) + TestSuite.toolbox_expval_approx_localoperator(Vector) end diff --git a/test/toolbox/mpo_routing.jl b/test/toolbox/mpo_routing.jl index c5882bbec..49c08c02b 100644 --- a/test/toolbox/mpo_routing.jl +++ b/test/toolbox/mpo_routing.jl @@ -9,4 +9,5 @@ is_buildkite = get(ENV, "BUILDKITE", "false") == "true" if !is_buildkite TestSuite.toolbox_mpo_routing_fusers(Vector) TestSuite.toolbox_mpo_routing_identities(Vector) + TestSuite.toolbox_mpo_routing_paths(Vector) end diff --git a/test/types/mpo_observable.jl b/test/types/mpo_observable.jl deleted file mode 100644 index f6498a74e..000000000 --- a/test/types/mpo_observable.jl +++ /dev/null @@ -1,98 +0,0 @@ -using PEPSKit -using TensorKit -using Test -using Random - -const CI = CartesianIndex - -function _check_observable_bonds(observable::MPOObservable) - @test length(observable.sites) == length(observable.mpo) - path_positions = indexin(observable.sites, observable.path) - @test all(!isnothing, path_positions) - @test issorted(path_positions) - @test first(path_positions) == 1 - @test last(path_positions) == length(observable.path) - for k in 1:(length(observable.mpo) - 1) - @test PEPSKit._mpo_right_stringspace(observable.mpo[k])' == - space(observable.mpo[k + 1], 1) - end - return nothing -end - -Random.seed!(1234) -d = ℂ^2 -lattice = fill(d, 2, 2) - -@testset "Manhattan paths and directions" begin - # first move horizontally - @test PEPSKit._l_path(CI(2, 1), CI(2, 4)) == - CI.([(2, 1), (2, 2), (2, 3), (2, 4)]) - @test PEPSKit._l_path(CI(1, 3), CI(4, 3)) == - CI.([(1, 3), (2, 3), (3, 3), (4, 3)]) - @test PEPSKit._l_path(CI(3, 4), CI(1, 1)) == - CI.([(3, 4), (3, 3), (3, 2), (3, 1), (2, 1), (1, 1)]) - # nearest neighbor directions - origin = CI(2, 2) - @test PEPSKit._step_direction(origin, CI(1, 2)) === :north - @test PEPSKit._step_direction(origin, CI(2, 3)) === :east - @test PEPSKit._step_direction(origin, CI(3, 2)) === :south - @test PEPSKit._step_direction(origin, CI(2, 1)) === :west -end - -@testset "Routed MPO tensors" begin - op2 = rand(ComplexF64, d^2, d^2) - observable2 = MPOObservable([CI(1, 1), CI(3, 3)], op2, lattice) - @test observable2.path == - CI.([(1, 1), (1, 2), (1, 3), (2, 3), (3, 3)]) - @test observable2.sites == CI.([(1, 1), (3, 3)]) - _check_observable_bonds(observable2) - - original2 = PEPSKit.gate_to_mpo(op2) - @test first(observable2.mpo) ≈ first(original2) - @test last(observable2.mpo) ≈ last(original2) - @test length(observable2.mpo) == 2 - - sites4 = [CI(4, 2), CI(1, 3), CI(3, 4), CI(1, 1)] - op4 = rand(ComplexF64, d^4, d^4) - observable4 = MPOObservable(sites4, op4, lattice) - @test observable4.path == CI.( - [ - (1, 1), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), - (3, 3), (2, 3), (1, 3), (1, 4), (2, 4), (3, 4), - ] - ) - _check_observable_bonds(observable4) - - plaquette_sites = CI.([(2, 1), (1, 1), (1, 2), (2, 2)]) - plaquette_mpo = PEPSKit.gate_to_mpo(rand(ComplexF64, d^4, d^4)) - plaquette = MPOObservable(plaquette_sites, plaquette_sites, plaquette_mpo) - @test plaquette.sites == plaquette_sites - @test plaquette.path == plaquette_sites - _check_observable_bonds(plaquette) -end - -@testset "Explicit MPOs and validation" begin - op = rand(ComplexF64, d^2, d^2) - mpo = PEPSKit.gate_to_mpo(op; trunc = notrunc()) - - observable = MPOObservable([(2, 2), (1, 1)], mpo) - @test observable.sites == CI.([(2, 2), (1, 1)]) - @test observable.path == CI.([(2, 2), (2, 1), (1, 1)]) - @test observable.mpo !== mpo - _check_observable_bonds(observable) - - # Invalid MPO with virtual space mismatch - rank_one_mpo = PEPSKit.gate_to_mpo(op; trunc = truncrank(1)) - @test_throws SpaceMismatch MPOObservable( - [CI(1, 1), CI(1, 2)], [first(mpo), last(rank_one_mpo)] - ) - - # the observable must act on two diffrent sites - @test_throws ArgumentError MPOObservable([CI(1, 1), CI(1, 1)], mpo) - @test_throws ArgumentError MPOObservable([CI(1, 1), CI(1, 1)], op, lattice) - # Routing between consecutive tensors must not cross a later operator site. - @test_throws ArgumentError MPOObservable( - [CI(1, 1), CI(1, 3), CI(1, 2)], - PEPSKit.gate_to_mpo(rand(ComplexF64, d^3, d^3)), - ) -end From ff855429b4088f3ea877c5bbbb821df2cc2513e6 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Fri, 2 Oct 2026 10:59:59 +0800 Subject: [PATCH 20/20] Rename `window` to `patch` --- src/PEPSKit.jl | 6 ++-- .../{window => patch}/pepo_1layer.jl | 36 +++++++++---------- .../contractions/{window => patch}/tools.jl | 12 +++---- .../{window => patch}/twosite/pepo_1layer.jl | 30 ++++++++-------- src/algorithms/contractions/transfer.jl | 12 +++---- src/algorithms/correlator_approx.jl | 12 +++---- src/algorithms/expval_approx.jl | 20 ++++------- test/testsuite/toolbox/correlator_approx.jl | 18 +++++----- test/testsuite/toolbox/expval_approx.jl | 4 +-- 9 files changed, 71 insertions(+), 79 deletions(-) rename src/algorithms/contractions/{window => patch}/pepo_1layer.jl (83%) rename src/algorithms/contractions/{window => patch}/tools.jl (90%) rename src/algorithms/contractions/{window => patch}/twosite/pepo_1layer.jl (88%) diff --git a/src/PEPSKit.jl b/src/PEPSKit.jl index 3c5fffdb1..c6436fb09 100644 --- a/src/PEPSKit.jl +++ b/src/PEPSKit.jl @@ -126,9 +126,9 @@ include("algorithms/contractions/correlator/pepo_1layer.jl") include("algorithms/contractions/mpo_path/routing.jl") include("algorithms/contractions/mpo_path/pepo_1layer.jl") -include("algorithms/contractions/window/tools.jl") -include("algorithms/contractions/window/pepo_1layer.jl") -include("algorithms/contractions/window/twosite/pepo_1layer.jl") +include("algorithms/contractions/patch/tools.jl") +include("algorithms/contractions/patch/pepo_1layer.jl") +include("algorithms/contractions/patch/twosite/pepo_1layer.jl") include("algorithms/ctmrg/sparse_environments.jl") include("algorithms/ctmrg/ctmrg.jl") diff --git a/src/algorithms/contractions/window/pepo_1layer.jl b/src/algorithms/contractions/patch/pepo_1layer.jl similarity index 83% rename from src/algorithms/contractions/window/pepo_1layer.jl rename to src/algorithms/contractions/patch/pepo_1layer.jl index 3554dc6ea..a366a7775 100644 --- a/src/algorithms/contractions/window/pepo_1layer.jl +++ b/src/algorithms/contractions/patch/pepo_1layer.jl @@ -1,9 +1,9 @@ -# Approximate finite-window contractions for single-layer PEPO networks. +# Approximate finite-patch contractions for single-layer PEPO networks. """ Validate that the network is a single-layer PEPO and that the sweep direction is supported. """ -function _check_window_inputs(ρ::InfinitePEPO, direction::Symbol) +function _check_patch_inputs(ρ::InfinitePEPO, direction::Symbol) size(ρ, 3) == 1 || throw(DimensionMismatch("only single-layer PEPO contractions are supported")) direction in (:auto, :rows, :columns) || throw(ArgumentError("invalid sweep direction: $direction")) @@ -49,14 +49,14 @@ function standardize_dualness(ρ::InfinitePEPO, env::CTMRGEnv) end """ -Contract a routed MPO term in its enclosing window, rotating column sweeps into row sweeps. +Contract a routed MPO term in its enclosing patch, rotating column sweeps into row sweeps. """ function _expectation_value_approx( ρ::InfinitePEPO, routed::RoutedMPOTerm, env::CTMRGEnv, - alg::WindowApprox, direction::Symbol, + alg::PatchApprox, direction::Symbol, ) - _check_window_inputs(ρ, direction) - rowrange, colrange = _window_ranges(first(routed)) + _check_patch_inputs(ρ, direction) + rowrange, colrange = _patch_ranges(first(routed)) sweep = if direction === :auto length(colrange) >= length(rowrange) ? :rows : :columns else @@ -70,7 +70,7 @@ function _expectation_value_approx( unitcell = size(ρ)[1:2] path, mpo = routed rotated = siterotl90.(path, Ref(unitcell)) => mpo - rotated_rowrange, rotated_colrange = _window_ranges(first(rotated)) + rotated_rowrange, rotated_colrange = _patch_ranges(first(rotated)) return _expectation_value_approx_rows( rotl90(ρ), rotated, rotl90(env), rotated_rowrange, rotated_colrange, alg @@ -81,7 +81,7 @@ end """ Build a local tensor for row MPOs without observables by tracing the PEPO physical legs. """ -function _window_site_tensor( +function _patch_site_tensor( ρ::InfinitePEPO, ::Nothing, row::Int, col::Int, ) return trace_physicalspaces(ρ[row, col, 1]) @@ -90,7 +90,7 @@ end """ Insert the MPO factor at a path site, or trace the PEPO physical legs at an off-path site. """ -function _window_site_tensor( +function _patch_site_tensor( ρ::InfinitePEPO, routed::RoutedMPOTerm, row::Int, col::Int, ) @@ -113,25 +113,25 @@ function _window_site_tensor( end """ -Contract and normalize a routed MPO term using row-oriented window boundary contractions. +Contract and normalize a routed MPO term using row-oriented patch boundary contractions. """ function _expectation_value_approx_rows( ρ::InfinitePEPO, routed::RoutedMPOTerm, env::CTMRGEnv, - rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::WindowApprox, + rowrange::UnitRange{Int}, colrange::UnitRange{Int}, alg::PatchApprox, ) ρ, env = standardize_dualness(ρ, env) - numerator = _contract_window_rows(ρ, routed, env, rowrange, colrange, alg) - norm = _contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) + numerator = _contract_patch_rows(ρ, routed, env, rowrange, colrange, alg) + norm = _contract_patch_rows(ρ, nothing, env, rowrange, colrange, alg) return numerator / norm end """ -Contract a complete PEPO window row by row from north to south, optionally inserting a routed MPO term. +Contract a complete PEPO patch row by row from north to south, optionally inserting a routed MPO term. """ -function _contract_window_rows( +function _contract_patch_rows( ρ::InfinitePEPO, routed::Union{Nothing, RoutedMPOTerm}, env::CTMRGEnv, rowrange::UnitRange{Int}, colrange::UnitRange{Int}, - alg::WindowApprox, + alg::PatchApprox, ) ψ = _north_boundary_mps(env, first(rowrange), colrange) for row in rowrange @@ -143,7 +143,7 @@ function _contract_window_rows( end """ -Build one finite row MPO from west/east CTMRG edges and the PEPO tensors inside the window. +Build one finite row MPO from west/east CTMRG edges and the PEPO tensors inside the patch. Convention of west, east CTM edges and the PF tensors: ``` @@ -166,7 +166,7 @@ function _row_mpo( append!( tensors, ( - _window_site_tensor(ρ, routed, row, col) + _patch_site_tensor(ρ, routed, row, col) for col in colrange ), ) diff --git a/src/algorithms/contractions/window/tools.jl b/src/algorithms/contractions/patch/tools.jl similarity index 90% rename from src/algorithms/contractions/window/tools.jl rename to src/algorithms/contractions/patch/tools.jl index 97d3cafbb..1a9b0e586 100644 --- a/src/algorithms/contractions/window/tools.jl +++ b/src/algorithms/contractions/patch/tools.jl @@ -1,8 +1,8 @@ """ Bundle the zip-up contraction and optional DMRG refinement -algorithms used after each finite window MPO-MPS contraction. +algorithms used after each finite patch MPO-MPS contraction. """ -struct WindowApprox{Z, D} +struct PatchApprox{Z, D} zipup::Z dmrg::D end @@ -12,7 +12,7 @@ end """ Apply a finite MPO to a finite MPS with zip-up truncation and optional DMRG refinement. """ -function _approximate(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) +function _approximate(W::FiniteMPO, ψ::FiniteMPS, alg::PatchApprox) ψ′, = approximate((W, ψ), alg.zipup) isnothing(alg.dmrg) && return ψ′ ψ′, = approximate(ψ′, (W, ψ), alg.dmrg) @@ -20,7 +20,7 @@ function _approximate(W::FiniteMPO, ψ::FiniteMPS, alg::WindowApprox) end """ -Build the finite MPS representing the north CTMRG boundary of a window. +Build the finite MPS representing the north CTMRG boundary of a patch. Convention of CTM tensors on the north boundary is ``` @@ -47,7 +47,7 @@ function _north_boundary_mps( end """ -Build the finite MPS representing the south CTMRG boundary of a window, +Build the finite MPS representing the south CTMRG boundary of a patch, but with dual physical legs, and sites ordered from east to west. Convention of CTM tensors on the south boundary is @@ -66,7 +66,7 @@ North site `k` pairs with south site `N + 1 - k`. south: N → … → 2 → 1 ``` Viewed after a 180° rotation of the entire network, this is a north boundary ordered from west to east as usual, only with dual physical legs. -Because of this reversed site order, `AL` tensors lie to the right (east) of the canonical center in the window, while `AR` tensors lie to its left (west). +Because of this reversed site order, `AL` tensors lie to the right (east) of the canonical center in the patch, while `AR` tensors lie to its left (west). """ function _south_boundary_mps(env::CTMRGEnv, row::Int, colrange::UnitRange{Int}) r = row + 1 diff --git a/src/algorithms/contractions/window/twosite/pepo_1layer.jl b/src/algorithms/contractions/patch/twosite/pepo_1layer.jl similarity index 88% rename from src/algorithms/contractions/window/twosite/pepo_1layer.jl rename to src/algorithms/contractions/patch/twosite/pepo_1layer.jl index 67864b192..b747045bd 100644 --- a/src/algorithms/contractions/window/twosite/pepo_1layer.jl +++ b/src/algorithms/contractions/patch/twosite/pepo_1layer.jl @@ -4,9 +4,9 @@ Validate operator spaces and rotate column sweeps into the row-oriented contract function _correlator_approx( ρ::InfinitePEPO, op::AbstractTensorMap, source::CartesianIndex{2}, targets::Vector{CartesianIndex{2}}, - env::CTMRGEnv, alg::WindowApprox, direction::Symbol, + env::CTMRGEnv, alg::PatchApprox, direction::Symbol, ) - _check_window_inputs(ρ, direction) + _check_patch_inputs(ρ, direction) numout(op) == numin(op) == 2 || throw(ArgumentError("correlator_approx requires a two-site operator")) for (leg, sites) in enumerate(((source,), targets)), site in sites @@ -24,15 +24,15 @@ function _correlator_approx( end """ -Measure targets in windows of fixed width ending at each target row, propagating observable-free and open-string north states together. +Measure targets in patches of fixed width ending at each target row, propagating observable-free and open-string north states together. """ function _correlator_approx_rows( ρ::InfinitePEPO, op::AbstractTensorMap, source::CartesianIndex{2}, targets::Vector{CartesianIndex{2}}, - env::CTMRGEnv, alg::WindowApprox, + env::CTMRGEnv, alg::PatchApprox, ) ρ, env = standardize_dualness(ρ, env) - rowrange, colrange = _window_ranges([source; targets]) + rowrange, colrange = _patch_ranges([source; targets]) targets_by_row = _twosite_targets_by_row(targets) mpo = gate_to_mpo(op; trunc = notrunc()) stringspace = space(mpo[2], 1) @@ -56,7 +56,7 @@ function _correlator_approx_rows( tensor = row == source[1] ? mpo_path_first(A, mpo[1], Val(:south)) : mpo_path_string(A, stringspace, Val((:north, :south))) plain_north = _approximate(W, plain_north, alg) - parent(W)[_window_mps_site(source[2], colrange)] = tensor + parent(W)[_patch_mps_site(source[2], colrange)] = tensor north = _approximate(W, north, alg) end return values @@ -73,15 +73,15 @@ function _contract_twosite_target_row!( ) N = length(south) row = first(keys(targets))[1] - source_site = _window_mps_site(source[2], colrange) - envs = _window_edge_environments(south, W, north, source_site) + source_site = _patch_mps_site(source[2], colrange) + envs = _patch_edge_environments(south, W, north, source_site) stringspace = space(mpo[2], 1) # close the target right at the column of the incoming string same_col = get(targets, CartesianIndex(row, source[2]), nothing) if !isnothing(same_col) target_tensor = mpo_path_last(ρ[row, source[2], 1], mpo[2], Val(:north)) - value = _contract_window_site(envs, north, south, source_site, target_tensor) + value = _contract_patch_site(envs, north, south, source_site, target_tensor) numerators[same_col] = value end @@ -101,12 +101,12 @@ function _contract_twosite_target_row!( for target in right_targets target_col = target[2] for col in (previous_col + 1):(target_col - 1) - site = _window_mps_site(col, colrange) + site = _patch_mps_site(col, colrange) string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:west, :east))) left = left * edge_transfermatrix(north.AR[site], string_tensor, south.AL[south_site(site, N)]) end - target_site = _window_mps_site(target_col, colrange) + target_site = _patch_mps_site(target_col, colrange) target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:west)) target_left = left * edge_transfermatrix(north.AR[target_site], target_tensor, south.AL[south_site(target_site, N)]) value = _contract_transfer_boundaries(target_left, envs.rights[target_site - source_site + 1]) @@ -135,12 +135,12 @@ function _contract_twosite_target_row!( for target in left_targets target_col = target[2] for col in (previous_col - 1):-1:(target_col + 1) - site = _window_mps_site(col, colrange) + site = _patch_mps_site(col, colrange) string_tensor = mpo_path_string(ρ[row, col, 1], stringspace, Val((:east, :west))) right = edge_transfermatrix(north.AL[site], string_tensor, south.AR[south_site(site, N)]) * right end - target_site = _window_mps_site(target_col, colrange) + target_site = _patch_mps_site(target_col, colrange) target_tensor = mpo_path_last(ρ[row, target_col, 1], mpo[2], Val(:east)) target_right = edge_transfermatrix(north.AL[target_site], target_tensor, south.AR[south_site(target_site, N)]) * right value = _contract_transfer_boundaries(envs.lefts[target_site], target_right) @@ -157,12 +157,12 @@ end """ Map a PEPO column to its finite-MPS site, accounting for the additional west CTM edge. """ -_window_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 +_patch_mps_site(col::Int, colrange::UnitRange{Int}) = col - first(colrange) + 2 """ Contract one modified row site between precomputed left and right MPS environments. """ -function _contract_window_site( +function _contract_patch_site( envs::NamedTuple, north::FiniteMPS, south::FiniteMPS, site::Int, tensor::MPOTensor, ) diff --git a/src/algorithms/contractions/transfer.jl b/src/algorithms/contractions/transfer.jl index 645247f74..b5440f4c7 100644 --- a/src/algorithms/contractions/transfer.jl +++ b/src/algorithms/contractions/transfer.jl @@ -250,7 +250,7 @@ end """ Map north-boundary site `k` to the corresponding site in the east-to-west south boundary. -`N` is the finite window length, including the two boundary sites. +`N` is the finite patch length, including the two boundary sites. """ south_site(k::Int, N::Int) = N + 1 - k @@ -264,7 +264,7 @@ Construct the endpoint identities for a finite north-W-south sandwich. └-→-- south --→-┘ ``` """ -function _window_edge_boundaries(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS) +function _patch_edge_boundaries(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS) N = length(north) length(south) == length(W) == N || throw(DimensionMismatch("row and boundary lengths must match")) left = isomorphism( @@ -283,8 +283,8 @@ Only valid entries are stored, with lengths `site` and `length(north) - site + 1 Note that sites in `south` are ordered from right (east) to left (west). """ -function _window_edge_environments(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS, site::Int) - left, right = _window_edge_boundaries(south, W, north) +function _patch_edge_environments(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS, site::Int) + left, right = _patch_edge_boundaries(south, W, north) N = length(north) lefts, rights = [left], [right] for k in 1:(site - 1) @@ -317,7 +317,7 @@ end Contract a row MPO between opposite-oriented boundary MPSs without conjugation. """ function dot_noconj(south::FiniteMPS, W::FiniteMPO, north::FiniteMPS) - left, right = _window_edge_boundaries(south, W, north) + left, right = _patch_edge_boundaries(south, W, north) N = length(north) for k in N:-1:1 top = k == 1 ? north.AC[k] : north.AR[k] @@ -340,6 +340,6 @@ Contract the left and right transfer-matrix environments to a scalar. ``` """ function _contract_transfer_boundaries(left::MPSTensor, right::MPSTensor) - # The three bonds close around the window without crossing + # The three bonds close around the patch without crossing return @tensor left[1 2; 3] * right[3 2; 1] end diff --git a/src/algorithms/correlator_approx.jl b/src/algorithms/correlator_approx.jl index 7c00a3064..25b59cb7c 100644 --- a/src/algorithms/correlator_approx.jl +++ b/src/algorithms/correlator_approx.jl @@ -1,4 +1,4 @@ -# Approximate finite-window two-site correlators +# Approximate finite-patch two-site correlators # ---------------------------------------------- """ @@ -13,9 +13,9 @@ Approximately measure a dense two-site operator between a fixed first site `i` a If neither direction is possible, throw an `ArgumentError`. - The collection `js` must be nonempty, contain no duplicates, and exclude `i`. A collection returns a vector in `vec(js)` order; a single second site returns a scalar. -- For row-by-row contraction, all windows have the same width, covering the columns of `i` and all sites in `js`, but each window ends at the row of the measured site `j`. - For column-by-column contraction, all windows cover the same rows, but each window ends at the column of `j`. - Each window has its own normalization, calculated without the operator using the same contraction arrangement. +- For row-by-row contraction, all patches have the same width, covering the columns of `i` and all sites in `js`, but each patch ends at the row of the measured site `j`. + For column-by-column contraction, all patches cover the same rows, but each patch ends at the column of `j`. + Each patch has its own normalization, calculated without the operator using the same contraction arrangement. The last row or column is contracted without further truncation against the CTMRG boundary immediately beyond it. - `trunc` controls boundary-MPS truncation; by default, it limits the rank to the largest CTMRG boundary dimension. After each zipup step, `maxiter` DMRG sweeps refine the result (default 1; use 0 to disable refinement). @@ -36,13 +36,13 @@ function correlator_approx( isempty(js) && throw(ArgumentError("correlator_approx requires at least one second site")) allunique(js) || throw(ArgumentError("second sites should be unique")) i in js && throw(ArgumentError("second sites should be distinct from the first site")) - rowrange, colrange = _window_ranges([i; vec(js)]) + rowrange, colrange = _patch_ranges([i; vec(js)]) rows, columns = first(rowrange) == i[1], last(colrange) == i[2] rows || columns || throw(ArgumentError("no valid sweep: second sites must all be at or south of the first row, or all at or west of the first column")) direction = rows && (!columns || length(colrange) >= length(rowrange)) ? :rows : :columns return _correlator_approx( ρ, op, i, collect(vec(js)), env, - WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction + PatchApprox(Zipup(; trunc), _approx_dmrg(maxiter)), direction ) end diff --git a/src/algorithms/expval_approx.jl b/src/algorithms/expval_approx.jl index f6ff4c2f6..813cc3201 100644 --- a/src/algorithms/expval_approx.jl +++ b/src/algorithms/expval_approx.jl @@ -1,17 +1,17 @@ -# Approximate finite-window expectation values +# Approximate finite-patch expectation values # -------------------------------------------- """ $(SIGNATURES) Approximately measure a `LocalOperator` in a single-layer PEPO using finite boundary MPS zipup sweeps. -Each term is normalized in its own enclosing window, and the resulting contributions are summed. +Each term is normalized in its own enclosing patch, and the resulting contributions are summed. Dense terms are reordered into a column-wise snake and decomposed into MPOs without truncation. Explicit [`MPOTerm`](@ref) factors retain their given order and are connected by non-self-intersecting nearest-neighbor paths. Routing tries horizontal-first and then vertical-first shortest connections; more complicated explicit MPO routes raise a not-implemented error. Single-site terms are evaluated exactly, and an empty operator returns zero. -- By default, `direction = :auto` selects north-to-south sweeps for wide and square windows, and east-to-west for tall windows, separately for each term. +- By default, `direction = :auto` selects north-to-south sweeps for wide and square patches, and east-to-west for tall patches, separately for each term. Specify `direction = :rows` or `:columns` to override this choice. - The zipup truncation is controlled by `trunc`, which defaults to `truncrank(χ)` with `χ` the largest CTMRG boundary dimension. This keyword does not truncate the operator decomposition. @@ -22,9 +22,9 @@ function expectation_value_approx( ρ::InfinitePEPO, O::LocalOperator, env::CTMRGEnv; trunc = _approx_trunc(env), maxiter::Int = 1, direction::Symbol = :auto, ) - _check_window_inputs(ρ, direction) + _check_patch_inputs(ρ, direction) checklattice(ρ, O) - alg = WindowApprox(Zipup(; trunc), _approx_dmrg(maxiter)) + alg = PatchApprox(Zipup(; trunc), _approx_dmrg(maxiter)) isempty(O.terms) && return zero(promote_type(scalartype(ρ), scalartype(env))) term_vals = map(collect(O.terms)) do (sites, term) return _local_expectation_value_approx(sites, ρ, term, env, alg, direction) @@ -38,7 +38,7 @@ Evaluate a dense or MPO term, using exact single-site contractions and routing l function _local_expectation_value_approx( sites::Vector{CartesianIndex{2}}, ρ::InfinitePEPO, term::Union{AbstractTensorMap, MPOTerm}, env::CTMRGEnv, - alg::WindowApprox, direction::Symbol, + alg::PatchApprox, direction::Symbol, ) if _local_term_iszero(term) return zero(promote_type(scalartype(ρ), scalartype(env), _local_term_scalartype(term))) @@ -51,14 +51,6 @@ function _local_expectation_value_approx( return _expectation_value_approx(ρ, routed, env, alg, direction) end -""" -Return the smallest row and column ranges containing a collection of lattice sites. -""" -function _window_ranges(sites) - rows = getindex.(sites, 1) - cols = getindex.(sites, 2) - return UnitRange(extrema(rows)...), UnitRange(extrema(cols)...) -end """ Return the largest CTMRG boundary-space dimension appearing in the corner tensors. diff --git a/test/testsuite/toolbox/correlator_approx.jl b/test/testsuite/toolbox/correlator_approx.jl index e87b5867a..fad3f9c02 100644 --- a/test/testsuite/toolbox/correlator_approx.jl +++ b/test/testsuite/toolbox/correlator_approx.jl @@ -8,17 +8,17 @@ using Random const CI = CartesianIndex """ -Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using fixed width and a window ending at each target row. +Contract `⟨op⟩` between `i` and each site in `js` independently without caching, using fixed width and a patch ending at each target row. """ -function _adaptive_window_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) +function _adaptive_patch_reference(op::AbstractTensorMap, i::CI{2}, js, ρ, env) lattice = physicalspace(ρ) observables = [PEPSKit._route_mpo_term([i, j], op, lattice) for j in js] - _, colrange = PEPSKit._window_ranges([i; js]) - alg = PEPSKit.WindowApprox(Zipup(; trunc = notrunc()), nothing) + _, colrange = PEPSKit._patch_ranges([i; js]) + alg = PEPSKit.PatchApprox(Zipup(; trunc = notrunc()), nothing) return map(observables, js) do observable, j rowrange = i[1]:j[1] - norm = PEPSKit._contract_window_rows(ρ, nothing, env, rowrange, colrange, alg) - numerator = PEPSKit._contract_window_rows(ρ, observable, env, rowrange, colrange, alg) + norm = PEPSKit._contract_patch_rows(ρ, nothing, env, rowrange, colrange, alg) + numerator = PEPSKit._contract_patch_rows(ρ, observable, env, rowrange, colrange, alg) return numerator / norm end end @@ -38,7 +38,7 @@ spaces = Dict( ) """ -Check approximate correlators, sweep selection, and adaptive window normalization. +Check approximate correlators, sweep selection, and adaptive patch normalization. """ function toolbox_correlator_approx(AT) return @testset "Single-layer PEPO ($S) ($AT)" for S in keys(spaces) @@ -60,7 +60,7 @@ function toolbox_correlator_approx(AT) # This target distribution only permits row sweeps. # Compare against independent contractions in target order. O² = adapt(AT, rand(ComplexF64, d^2, d^2)) - vals_ref = _adaptive_window_reference(O², i, js, ρ, env) + vals_ref = _adaptive_patch_reference(O², i, js, ρ, env) vals_rows = correlator_approx(ρ, O², i, js, env; trunc, maxiter = 0) @test vals_rows ≈ vals_ref @@ -85,7 +85,7 @@ function toolbox_correlator_approx(AT) # Test auto sweep direction choice in for southwest targets. selection_trunc = truncrank(2) - selection_alg = PEPSKit.WindowApprox(Zipup(; trunc = selection_trunc), nothing) + selection_alg = PEPSKit.PatchApprox(Zipup(; trunc = selection_trunc), nothing) for (offset, direction) in ((CI(1, -2), :rows), (CI(2, -1), :columns), (CI(1, -1), :rows)) j = i + offset expected = only(PEPSKit._correlator_approx(ρ, O², i, [j], env, selection_alg, direction)) diff --git a/test/testsuite/toolbox/expval_approx.jl b/test/testsuite/toolbox/expval_approx.jl index 8364da96b..699eca1cd 100644 --- a/test/testsuite/toolbox/expval_approx.jl +++ b/test/testsuite/toolbox/expval_approx.jl @@ -55,7 +55,7 @@ function toolbox_expval_approx(AT) ρ, observable, env; trunc, maxiter = 0, direction ) ≈ exact end - # Square windows must default to row sweeps. + # Square patches must default to row sweeps. if sites == sites_list[3] auto = expectation_value_approx(ρ, dense, env; trunc = truncrank(2), maxiter = 0) rows = expectation_value_approx(ρ, dense, env; trunc = truncrank(2), maxiter = 0, direction = :rows) @@ -93,7 +93,7 @@ function toolbox_expval_approx_localoperator(AT) @test expectation_value_approx(ρ, snake, env; trunc, maxiter = 0) ≈ expectation_value(ρ, snake, env) - # These terms use different normalization windows; the gapped MPO also inserts a string. + # These terms use different normalization patches; the gapped MPO also inserts a string. # Non-unit-cell coordinates and both sweeps exercise translation and rotation of the expanded path. op2 = adapt(AT, randn(ComplexF64, d^2 → d^2)) sites2 = [CI(-1, 0), CI(-1, 2)]