TuckerTensorTrain.t3svd ======================= .. py:method:: t3toolbox.tucker_tensor_train.TuckerTensorTrain.t3svd(max_tt_ranks = None, max_tucker_ranks = None, rtol = None, atol = None, assume_orthogonal = False) .. code-block:: python 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. :param self: The Tucker tensor train. ``structure=((N0,...,N(d-1)), (n0,...,n(d-1)), (1,r1,...r(d-1),1))`` :type self: TuckerTensorTrain :param max_tt_ranks: 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``). :type max_tt_ranks: int or Sequence[int], optional :param max_tucker_ranks: 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``). :type max_tucker_ranks: int or Sequence[int], optional :param rtol: 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=()``. :type rtol: float, optional :param atol: Absolute tolerance for truncation (in the Frobenius norm), applied **per truncation step** (see Notes). Default: no ``atol`` truncation (``None``). Requires ``stack_shape=()``. :type atol: float, optional :param assume_orthogonal: 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 :py:meth:`is_right_orthogonal`. A *left*-orthogonal input (e.g. a prior :py:meth:`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). :type assume_orthogonal: bool, optional :returns: * :py:class:`TuckerTensorTrain` -- New Tucker tensor train representing the same tensor (or a truncated version), but with modified cores * Tuple[:py:class:`NDArray`,...] -- Singular values associated with edges between Tucker cores and TT cores * Tuple[:py:class:`NDArray`,...] -- Singular values associated with edges between adjacent TT cores .. rubric:: 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 :py:meth:`rank_adjustment_sweep` (the result is left-orthogonal, so ``'right_to_left'`` minimizes it). See ``docs/t3svd_minimal_ranks.md``. .. seealso:: :py:meth:`.TuckerTensorTrain.rank_adjustment_sweep`, :py:meth:`.TuckerTensorTrain.t3svd_dense`, :py:meth:`.TuckerTensorTrain.get_minimal_ranks` .. rubric:: 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) # doctest: +IGNORE_EXCEPTION_DETAIL 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 :py:meth:`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 :py:meth:`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