T3-SVD ranks: why t3svd may return non-minimal ranks, and how to minimize#
A short note for anyone wondering about the ranks that come back from TuckerTensorTrain.t3svd. The
short version: t3svd is the basic algorithm (Algorithm 10 / Oseledets’ TT-SVD); under truncation it
does not guarantee minimal ranks. It always returns a left-orthogonal result, and to reduce to
minimal ranks you call the separate rank_adjustment_sweep. This note explains what “minimal” means,
why the truncating sweep can leave non-minimal ranks, and why one rank_adjustment_sweep fixes it only
in the matching direction.
What “minimal ranks” means#
A T3 has minimal ranks when every core satisfies the no-redundancy inequalities
n_i <= N_i (Tucker rank <= mode dimension)
n_i <= rL_i * rR_i (Tucker rank <= product of its two TT bonds)
rL_i <= n_i * rR_i (each TT bond <= product of the other two legs of its core)
rR_i <= n_i * rL_i
with rL_i = r_i, rR_i = r_{i+1}. These are exactly the matricization ranks of a generic tensor
with that structure: cutting a single edge of the T3 tensor network splits the physical indices into two
groups, and the minimal dimension of that edge is the rank of the corresponding matricization. (The T3
network is a tree — a TT backbone with a pendant Tucker matrix on each node — so every single-edge cut
is a clean bipartition with no hidden multi-cut degeneracy.) compute_minimal_ranks computes these by
propagating the structural bottlenecks; check with has_minimal_ranks. See
test_compute_minimal_ranks_matches_matricization.
Why the truncating sweep can leave non-minimal ranks#
t3svd sweeps left to right. At mode i it first truncates the Tucker rank n_i (against the
current right bond rR_i), then truncates the TT bond rR_i. With no truncation — or with a
tolerance (rtol/atol) — each SVD keeps exactly min(rows, cols), the structural rank, and a bond
only ever shrinks to its structural value rL_i * n_i. That can never orphan the Tucker rank computed
just before it (n_i <= rL_i * rR_i^new = rL_i^2 * n_i holds automatically), so the no-truncation and
tolerance results are already minimal.
A hard rank cap breaks this. A max_tt_ranks cap can force rR_i below its structural value
rL_i * n_i. Then n_i, fixed moments earlier against the larger pre-cap bond, ends up > rL_i * rR_i
— a Tucker rank pointing into a subspace the truncated bond can no longer reach. It is structurally
redundant: the represented tensor is unaffected, but the reported rank is not minimal. (The symmetric
thing happens to a bond when a downstream max_tucker_ranks cap bites.)
Worked example — shape=(5,6,7), truncate the TT bonds to 2 but leave the Tucker ranks uncapped:
n_0 is fixed to 3 (= 1 * r_1 with the pre-cap bond r_1 = 3)
then the cap forces r_1 = 2 < rL_0 * n_0 = 1 * 3
=> n_0 = 3 > rL_0 * r_1 = 1 * 2 = 2 # orphaned: minimal n_0 is 2
This is deliberate: t3svd is the basic algorithm and does the minimum work. Re-tuning ranks is a
separate, opt-in step.
How to minimize: rank_adjustment_sweep#
rank_adjustment_sweep(direction) is a single lossless sweep that drops structurally-redundant
ranks (re-SVD each Tucker edge and TT bond with no cap). The represented tensor is unchanged.
'right_to_left'returns a right-orthogonal result;'left_to_right'returns a left-orthogonal result.
A single sweep reaches minimal ranks only if the input is already orthogonal in the opposite
direction. A left-orthogonal input already satisfies the forward (left) bounds, so a 'right_to_left'
sweep adds the backward (right) bounds and the result is fully minimal — and a t3svd result is
left-orthogonal, so this is the common path:
x2, _, _ = x.t3svd(max_tt_ranks=2) # left-orthogonal, possibly non-minimal
x3 = x2.rank_adjustment_sweep('right_to_left') # minimal, right-orthogonal (lossless)
On a general input (orthogonal in neither direction, or the same direction as the sweep) one sweep is a
partial reduction; compose both directions ('right_to_left' then 'left_to_right') for guaranteed
minimal ranks — e.g. to get a minimal, left-orthogonal result. This is why the operation is named a
“sweep”, not “minimize”: a single sweep only fully minimizes given the matching precondition.
Cost: it is one extra sweep, paid only when you ask for it. Keep in mind the cost of not minimizing —
non-minimal ranks make every later operation more expensive (storage, contraction), and retraction on a
non-minimal frame drops the redundant rank rather than preserving ranks strictly. (Minimal rank is not
a correctness precondition for any operation — inner/norm Hilbert–Schmidt faithfulness needs only an
orthogonal frame and gauged variations; the settled audit is
numerical_contracts.md.)
This is not specific to the Tucker extension#
Plain TT-SVD / TT-rounding (Oseledets) has the same phenomenon: hard per-bond rank caps applied in a single directional sweep can leave an upstream bond above its structural minimum, because a downstream cap shrinks what that bond can support after it was already fixed. Tolerance-based rounding does not exhibit it — each truncation, after orthogonalization, sees the true matricization singular values and adapts. The cure is the same: a structural sweep that propagates the post-cap bottlenecks. T3 simply adds one more flavor (a bond cap orphaning a Tucker rank).
See also#
contributor/t3svd_design_rationale.md— why T3-SVD and minimization are split, and whyrank_adjustment_sweepis a directional sweep rather than aminimize()method.backend/t3_svd.py—t3svd(basic algorithm) andrank_adjustment_sweep(the minimization).backend/ranks.py—compute_minimal_ranks(the structural minimal ranks),compute_raw_sweep_ranks(the ranks the sweep produces under caps).Tests:
test_t3svd_is_left_orthogonal_not_necessarily_minimal,test_rank_adjustment_sweep,test_compute_minimal_ranks_matches_matricization,test_compute_minimal_ranks_inequalities.docs/contributor/t3svd_verification.md— the accuracy + rank-parsimony test method for the truncation itself.