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) with len(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) with len(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 rtol truncation (None). Requires stack_shape=().

  • atol (float, optional) – Absolute tolerance for truncation (in the Frobenius norm), applied per truncation step (see Notes). Default: no atol truncation (None). Requires stack_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 with is_right_orthogonal(). A left-orthogonal input (e.g. a prior t3svd() 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 cores

  • Tuple[NDArray,…] – Singular values associated with edges between Tucker cores and TT cores

  • Tuple[NDArray,…] – Singular values associated with edges between adjacent TT cores

Return type:

Tuple[TuckerTensorTrain, Tuple[NDArray, Ellipsis], Tuple[NDArray, Ellipsis]]

Notes

rtol/atol bound the truncation error at each step (each unfolding/matricization SVD truncation), not the overall error. The per-step errors accumulate in quadrature over the 2d-1 steps (d-1 TT unfoldings + d Tucker matricizations), so the realized error can exceed the requested tolerance by up to a factor sqrt(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.md for 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_ranks may be False) – exactly as the paper’s algorithm (and Oseledets’ TT-SVD) do. To reduce to minimal ranks, follow with rank_adjustment_sweep() (the result is left-orthogonal, so 'right_to_left' minimizes it). See docs/t3svd_minimal_ranks.md.

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 rtol truncates 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/atol need 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, t3svd does not guarantee minimal ranks – a hard cap can leave a Tucker rank above its structural bound (here n_0 = 3 > rL_0*r_1 = 1*2 = 2). The result is left-orthogonal; reduce it to minimal with rank_adjustment_sweep() ('right_to_left', since a t3svd result 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=True skips the initial orthogonalization, asserting the input is already right-orthogonal (verify with is_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