TuckerTensorTrain.t3svd#
- t3toolbox.tucker_tensor_train.TuckerTensorTrain.t3svd(max_tt_ranks=None, max_tucker_ranks=None, rtol=None, atol=None, assume_orthogonal=False)#
def t3svd( self: 'TuckerTensorTrain', max_tt_ranks: typ.Union[int, Sequence[int]] = None, # scalar (caps all) or len=d+1 max_tucker_ranks: typ.Union[int, Sequence[int]] = None, # scalar (caps all) or len=d rtol: float = None, atol: float = None, assume_orthogonal: bool = False, ) -> Tuple[ 'TuckerTensorTrain', # new_x Tuple[NDArray,...], # Tucker singular values, len=d Tuple[NDArray,...], # TT singular values, len=d+1 ]:
Compute (truncated) T3-SVD of Tucker tensor train.
- Parameters:
self (TuckerTensorTrain) – The Tucker tensor train.
structure=((N0,...,N(d-1)), (n0,...,n(d-1)), (1,r1,...r(d-1),1))max_tt_ranks (int or Sequence[int], optional) – Maximum TT-ranks
ri. A scalar caps every bond; a sequence is per-bond, e.g.,(1,5,5,5,1)withlen(max_tt_ranks)=d+1. Default: no max TT rank truncation (None).max_tucker_ranks (int or Sequence[int], optional) – Maximum Tucker ranks
ni. A scalar caps every mode; a sequence is per-mode, e.g.,(5,5,5)withlen(max_tucker_ranks)=d. Default: no max Tucker rank truncation (None).rtol (float, optional) – Relative tolerance for truncation (in the Frobenius norm), applied per truncation step (see Notes – the overall error can be larger). Default: no
rtoltruncation (None). Requiresstack_shape=().atol (float, optional) – Absolute tolerance for truncation (in the Frobenius norm), applied per truncation step (see Notes). Default: no
atoltruncation (None). Requiresstack_shape=().assume_orthogonal (bool, optional) – If
True, skip the initial orthogonalization, asserting the input is already right-orthogonal (every Tucker core down-orthogonal and every TT core right-orthogonal – the form the left-to-right sweep needs). Not enforced – a wrong assertion silently corrupts the result; verify first withis_right_orthogonal(). A left-orthogonal input (e.g. a priort3svd()result) is not the right form; reverse it yourself (a left-orthogonal T3 reversed is right-orthogonal) if you want to skip. Default:False(always orthogonalize – safe).
- Returns:
TuckerTensorTrain– New Tucker tensor train representing the same tensor (or a truncated version), but with modified coresTuple[
NDArray,…] – Singular values associated with edges between Tucker cores and TT coresTuple[
NDArray,…] – Singular values associated with edges between adjacent TT cores
- Return type:
Tuple[TuckerTensorTrain, Tuple[NDArray, Ellipsis], Tuple[NDArray, Ellipsis]]
Notes
rtol/atolbound the truncation error at each step (each unfolding/matricization SVD truncation), not the overall error. The per-step errors accumulate in quadrature over the2d-1steps (d-1TT unfoldings +dTucker matricizations), so the realized error can exceed the requested tolerance by up to a factorsqrt(2d-1)(the generalized Oseledets bound):||x - x2|| <= sqrt( sum of per-step truncation errors^2 ) <= sqrt(2d-1) * (per-step tol).
See
docs/contributor/t3svd_verification.mdfor the bound and its proof.This is the basic algorithm: it does not guarantee minimal ranks. The result is always left-orthogonal, but under a hard rank cap it can leave a Tucker rank / bond above its structural minimum (
has_minimal_ranksmay beFalse) – exactly as the paper’s algorithm (and Oseledets’ TT-SVD) do. To reduce to minimal ranks, follow withrank_adjustment_sweep()(the result is left-orthogonal, so'right_to_left'minimizes it). Seedocs/t3svd_minimal_ranks.md.See also
TuckerTensorTrain.rank_adjustment_sweep(),TuckerTensorTrain.t3svd_dense(),TuckerTensorTrain.get_minimal_ranks()Examples
No truncation – re-represents the same tensor, with ranks reduced to minimal:
>>> import numpy as np >>> import t3toolbox.tucker_tensor_train as t3 >>> np.random.seed(0) >>> x = t3.TuckerTensorTrain.randn((5, 6, 3), (4, 4, 3), (1, 3, 2, 1)) >>> x2, ss_tucker, ss_tt = x.t3svd() >>> print(np.allclose(x.to_dense(), x2.to_dense())) # same tensor True >>> print(x2.tucker_ranks, x2.tt_ranks) # reduced to minimal ranks (3, 4, 2) (1, 3, 2, 1)
The singular values ARE the singular values of the dense matrix unfoldings (the numerically-zero tail is dropped – which is what reduces the ranks):
>>> import numpy as np >>> import t3toolbox.tucker_tensor_train as t3 >>> np.random.seed(0) >>> x = t3.TuckerTensorTrain.randn((5, 6, 3), (4, 4, 3), (1, 3, 2, 1)) >>> _, ss_tucker, ss_tt = x.t3svd() >>> dense_svals = np.linalg.svd(x.to_dense().reshape(5, 6 * 3), compute_uv=False) >>> print(np.allclose(ss_tt[1], dense_svals[:len(ss_tt[1])])) # leading values match the unfolding's True >>> print(len(ss_tt[1]), int(np.sum(dense_svals > 1e-9))) # kept TT rank == numerical rank of the unfolding 3 3 >>> # (the Tucker singular values ss_tucker[i] relate to the mode-i matricizations the same way)
Truncation – a smooth tensor has gradually decaying unfolding spectra, so
rtoltruncates meaningfully (a sharp random spectrum would not):>>> import numpy as np >>> import t3toolbox.tucker_tensor_train as t3 >>> i, j, k = np.ogrid[1:9, 1:9, 1:9] >>> x = t3.TuckerTensorTrain.t3svd_dense(1.0 / (i + j + k))[0] # exact T3 of a smooth tensor >>> _, full_tucker_ss, full_tt_ss = x.t3svd() # original (untruncated) spectra >>> xt, _, _ = x.t3svd(rtol=1e-3) # truncate at rtol >>> print(x.tt_ranks, '->', xt.tt_ranks) # rtol drops the small singular values (1, 8, 8, 1) -> (1, 3, 3, 1) >>> # accuracy: ||x - xt|| <= sqrt(dropped singular-value energy at the chosen ranks) [Oseledets] >>> dropped_sq = (sum(float(np.sum(s[r:]**2)) for s, r in zip(full_tt_ss, xt.tt_ranks)) ... + sum(float(np.sum(s[r:]**2)) for s, r in zip(full_tucker_ss, xt.tucker_ranks))) >>> print(bool(np.linalg.norm(x.to_dense() - xt.to_dense()) <= np.sqrt(dropped_sq))) True >>> # parsimony: each chosen rank <= #{ original singular values >= tau }, tau = rtol * ||xt|| >>> tau = 1e-3 * np.linalg.norm(xt.to_dense()) >>> tt_ok = all(r <= max(1, int(np.sum(s >= tau))) for s, r in zip(full_tt_ss, xt.tt_ranks)) >>> tk_ok = all(n <= max(1, int(np.sum(s >= tau))) for s, n in zip(full_tucker_ss, xt.tucker_ranks)) >>> print(tt_ok, tk_ok) True True
Stacked T3s truncate vectorized over the stack – max-rank only (
rtol/atolneed a single T3, since different slices could truncate to different ranks):>>> import numpy as np >>> import t3toolbox.tucker_tensor_train as t3 >>> np.random.seed(0) >>> xs = t3.TuckerTensorTrain.randn((5, 6, 3), (4, 4, 3), (1, 3, 2, 1), stack_shape=(2,)) >>> xs2, _, _ = xs.t3svd(max_tucker_ranks=2, max_tt_ranks=2) >>> print(xs2.stack_shape, xs2.tucker_ranks, xs2.tt_ranks) (2,) (2, 2, 2) (1, 2, 2, 1) >>> xs.t3svd(rtol=1e-3) Traceback (most recent call last): ... ValueError
Under truncation,
t3svddoes not guarantee minimal ranks – a hard cap can leave a Tucker rank above its structural bound (heren_0 = 3 > rL_0*r_1 = 1*2 = 2). The result is left-orthogonal; reduce it to minimal withrank_adjustment_sweep()('right_to_left', since at3svdresult is left-orthogonal):>>> import numpy as np >>> import t3toolbox.tucker_tensor_train as t3 >>> np.random.seed(0) >>> x = t3.TuckerTensorTrain.randn((5, 6, 7), (4, 5, 6), (1, 3, 2, 1)) >>> x2, _, _ = x.t3svd(max_tt_ranks=2) # cap TT bonds, leave Tucker uncapped >>> print(x2.is_left_orthogonal(), x2.has_minimal_ranks, x2.tucker_ranks) True False (3, 4, 2) >>> x3 = x2.rank_adjustment_sweep('right_to_left') # reduce to minimal ranks (lossless) >>> print(x3.has_minimal_ranks, x3.tucker_ranks, np.allclose(x3.to_dense(), x2.to_dense())) True (2, 4, 2) True
assume_orthogonal=Trueskips the initial orthogonalization, asserting the input is already right-orthogonal (verify withis_right_orthogonal()first – it is not checked):>>> import numpy as np >>> import t3toolbox.tucker_tensor_train as t3 >>> np.random.seed(0) >>> x = t3.TuckerTensorTrain.randn((6, 7, 8), (5, 6, 7), (1, 4, 3, 1)) >>> xr = x.down_orthogonalize_tucker_cores().right_orthogonalize_tt_cores() >>> print(xr.is_right_orthogonal()) True >>> a, _, _ = xr.t3svd(max_tt_ranks=2, assume_orthogonal=True) # skip the redundant sweep >>> b, _, _ = xr.t3svd(max_tt_ranks=2) >>> print(np.allclose(a.to_dense(), b.to_dense())) True