Skip to content

elementwise_product / partial_contract: in-product truncation does not reach canonical rank (extra truncate(...) shrinks bonds further) #112

Description

@sdirnboeck

Description

Tensor4all.TensorNetworks.elementwise_product(a, b; threshold, maxdim) (which forwards to partial_contract with the default method=:zipup) honors the threshold / maxdim kwargs at the bond-dimension cap, but does not shrink bonds whose canonical rank is below the cap. Calling truncate(...; threshold, maxdim) on the result of an unconstrained elementwise_product(a, b) produces a strictly smaller, canonical-rank tensor train at the same accuracy. Users currently need a manual second pass to obtain a fully compressed answer.

Steps to reproduce

This is the 04_operations_on_qtts tutorial scenario: f(x) = x², g(x) = sin(10x) on [0, 1), R = 7, tolerance = 1e-12, maxbonddim = 32.

using Tensor4all
import Tensor4all.QuanticsGrids as QG
import Tensor4all.QuanticsTCI as QTCI
import Tensor4all.TensorNetworks as TN
import Tensor4all.SimpleTT as STT

R = 7; tolerance = 1e-12; maxbonddim = 32; maxiter = 200
f(x) = x^2; g(x) = sin(10x)
grid = QG.DiscretizedGrid{1}(R, 0.0, 1.0; includeendpoint=false)

qtt_f, _, _ = QTCI.quanticscrossinterpolate(Float64, f, grid;
    tolerance=tolerance, maxbonddim=maxbonddim, maxiter=maxiter)
qtt_g, _, _ = QTCI.quanticscrossinterpolate(Float64, g, grid;
    tolerance=tolerance, maxbonddim=maxbonddim, maxiter=maxiter)

function qtt_to_indexed_tt(qtt; tag)
    simple_tt = STT.TensorTrain(qtt.tci)
    sites = [Tensor4all.Index(2; tags=[tag, "bit=$i"]) for i in 1:length(simple_tt)]
    return TN.TensorTrain(simple_tt, sites), sites
end

tt_f, sites_f = qtt_to_indexed_tt(qtt_f; tag="f")
tt_g, sites_g = qtt_to_indexed_tt(qtt_g; tag="g")

tt_h_raw  = TN.elementwise_product(tt_f, tt_g; threshold=tolerance, maxdim=maxbonddim)
tt_h_post = TN.truncate(tt_h_raw; threshold=tolerance, maxdim=maxbonddim)

@show TN.linkdims(tt_f)         # [2, 3, 3, 3, 3, 2]
@show TN.linkdims(tt_g)         # [2, 2, 2, 2, 2, 2]
@show TN.linkdims(tt_h_raw)     # [4, 6, 6, 6, 4, 2]
@show TN.linkdims(tt_h_post)    # [2, 4, 6, 6, 4, 2]

Actual behavior

In the example above:

variant linkdims L2 relative error vs analytic
elementwise_product(tt_f, tt_g; threshold, maxdim) [4, 6, 6, 6, 4, 2] 2.8e-15
follow-up truncate(...; threshold, maxdim) [2, 4, 6, 6, 4, 2] 4.2e-15

Accuracy is at machine precision in both cases — the issue is not numerical correctness, it is that tt_h_raw keeps a leading bond at 4 even though the canonical rank under the requested tolerance is 2. The in-product truncation step appears not to be reaching that canonical rank.

The same pattern shows up more strongly on flatter input spectra. With two random linkdim=32 TensorTrains over 12 sites of physical dim 2 (bonds saturated, no rapidly decaying singular values), the in-product call returns leading linkdims [4, 16, 32, 32, …] whereas a follow-up truncate reaches [2, 4, 8, 16, 32, …]. Accuracy degrades visibly on that flatter spectrum too — the relative error vs the exact product is several times larger than the follow-up truncate path — but the QTT-style smooth-function case in the tutorial is dominated by the compression symptom, not the accuracy one.

method=:fit with nfullsweeps > 0 reaches the same canonical linkdims as the follow-up truncate path, which suggests the suboptimal step is local to the :zipup truncation in the underlying TreeTN kernel.

Expected behavior

elementwise_product(a, b; threshold, maxdim) should produce a result with linkdims matching:

truncate(elementwise_product(a, b); threshold, maxdim)

so that no separate truncate pass is required to reach canonical rank.

Tensor4all.jl version

main (verified at commit 834d0be).

Julia version

1.11

OS

macOS 15


Proposed approach (for spec discussion)

The truncation kwargs are correctly plumbed through Julia (elementwise_productpartial_contract → FFI t4a_treetn_partial_contract); the suboptimal compression appears to come from the TreeTN :zipup truncation kernel in tensor4all-rs. A canonical-rank result requires an SVD truncation against an orthogonalized environment on both sides, which a single-direction zip-up sweep does not provide.

Possible directions:

  1. Fix the :zipup truncation in tensor4all-rs so the result matches the linkdims of a stand-alone truncate(...) pass. Most aligned with the user expectation, almost certainly a tensor4all-rs change.
  2. Change the default of partial_contract / elementwise_product (e.g. :zipup followed by an automatic final canonicalize+truncate sweep, or :fit with at least one full sweep). Pure Tensor4all.jl change.
  3. Document the limitation and recommend method=:fit, nfullsweeps≥1 (or a follow-up truncate) when truncation is requested. Lowest effort, leaves the suboptimality in place.

Option 1 is the proper fix and likely needs a tensor4all-rs companion issue (see CONTRIBUTING.md "Cross-Repository Changes"). Options 2 or 3 can be done purely in Tensor4all.jl as a stop-gap.

Acceptance criteria

  • On the tutorial reproducer above, elementwise_product(tt_f, tt_g; threshold, maxdim) returns linkdims equal to those of truncate(elementwise_product(tt_f, tt_g); threshold, maxdim).
  • Same for partial_contract(a, b, spec; threshold, maxdim) with the default method.
  • A regression test under test/tensornetworks/partial_contract.jl asserts that the truncating path reaches canonical-rank linkdims (not just the bond-dim cap).
  • If the fix lands in tensor4all-rs, deps/build.jl is repinned and the new pin is recorded.
  • The docstring for elementwise_product / partial_contract either no longer needs the workaround, or — if :zipup is intentionally single-sweep — documents the compression caveat and points to :fit or a follow-up truncate.

This issue was drafted by Claude (Anthropic, model claude-opus-4-7[1m]) in collaboration with @sdirnboeck and filed via gh on her authenticated session. Reproducer was generated and run locally by the assistant against main at commit 834d0be.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproposedIssue opened, awaiting maintainer review

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions