t3toolbox 2026.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- t3toolbox/__init__.py +76 -0
- t3toolbox/backend/__init__.py +7 -0
- t3toolbox/backend/apply.py +388 -0
- t3toolbox/backend/common.py +459 -0
- t3toolbox/backend/contractions.py +2812 -0
- t3toolbox/backend/entries.py +298 -0
- t3toolbox/backend/fitting.py +289 -0
- t3toolbox/backend/fv_conversions.py +190 -0
- t3toolbox/backend/fv_operations.py +330 -0
- t3toolbox/backend/linalg.py +527 -0
- t3toolbox/backend/optimizers.py +409 -0
- t3toolbox/backend/probing.py +1447 -0
- t3toolbox/backend/ranks.py +473 -0
- t3toolbox/backend/sampling_derivatives.py +1634 -0
- t3toolbox/backend/stacking.py +519 -0
- t3toolbox/backend/t3_constructors.py +95 -0
- t3toolbox/backend/t3_conversions.py +198 -0
- t3toolbox/backend/t3_linalg.py +605 -0
- t3toolbox/backend/t3_operations.py +360 -0
- t3toolbox/backend/t3_orthogonalization.py +409 -0
- t3toolbox/backend/t3_svd.py +514 -0
- t3toolbox/backend/tt_operations.py +186 -0
- t3toolbox/backend/tt_orthogonalization.py +89 -0
- t3toolbox/backend/tv_operations.py +603 -0
- t3toolbox/backend/ufv_conversions.py +327 -0
- t3toolbox/backend/ufv_masking.py +148 -0
- t3toolbox/backend/ufv_operations.py +235 -0
- t3toolbox/backend/uniform_fitting.py +474 -0
- t3toolbox/backend/ut3_constructors.py +185 -0
- t3toolbox/backend/ut3_conversions.py +168 -0
- t3toolbox/backend/ut3_linalg.py +153 -0
- t3toolbox/backend/ut3_masking.py +118 -0
- t3toolbox/backend/ut3_operations.py +202 -0
- t3toolbox/backend/ut3_orthogonalization.py +194 -0
- t3toolbox/backend/ut3_sampling.py +252 -0
- t3toolbox/backend/ut3_svd.py +229 -0
- t3toolbox/backend/utv_operations.py +506 -0
- t3toolbox/backend/utv_sampling.py +557 -0
- t3toolbox/backend/wt3_operations.py +130 -0
- t3toolbox/corewise.py +390 -0
- t3toolbox/fitting.py +547 -0
- t3toolbox/frame_variations_format.py +1316 -0
- t3toolbox/manifold.py +1462 -0
- t3toolbox/optimizers.py +228 -0
- t3toolbox/safety.py +244 -0
- t3toolbox/tucker_tensor_train.py +4384 -0
- t3toolbox/uniform_frame_variations_format.py +1071 -0
- t3toolbox/uniform_manifold.py +1250 -0
- t3toolbox/uniform_tucker_tensor_train.py +909 -0
- t3toolbox/weighted_tucker_tensor_train.py +669 -0
- t3toolbox-2026.0.0.dist-info/METADATA +169 -0
- t3toolbox-2026.0.0.dist-info/RECORD +55 -0
- t3toolbox-2026.0.0.dist-info/WHEEL +5 -0
- t3toolbox-2026.0.0.dist-info/licenses/LICENSE +21 -0
- t3toolbox-2026.0.0.dist-info/top_level.txt +1 -0
t3toolbox/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Authors: Nick Alger and Blake Christierson
|
|
2
|
+
# Copyright: MIT License (2026)
|
|
3
|
+
# Github: https://github.com/NickAlger/T3Toolbox
|
|
4
|
+
# Documentation: https://nickalger.github.io/T3Toolbox/index.html
|
|
5
|
+
"""T3Toolbox: Tucker tensor trains (T3) -- a Tucker decomposition whose central core is
|
|
6
|
+
stored as a tensor train.
|
|
7
|
+
|
|
8
|
+
This package root re-exports the **frontend** surface: the tensor classes (ragged and
|
|
9
|
+
uniform), the frame/variations/tangent classes, the geometry singletons, the Gauss-Newton
|
|
10
|
+
fitting models, and the optimizers. **Backend users import submodules explicitly** (e.g.
|
|
11
|
+
``from t3toolbox.backend import probing``) -- the backend is namespaced by module and
|
|
12
|
+
deliberately not re-exported here. Naming conventions: ``docs/naming_conventions.md``.
|
|
13
|
+
"""
|
|
14
|
+
from t3toolbox.tucker_tensor_train import TuckerTensorTrain
|
|
15
|
+
from t3toolbox.uniform_tucker_tensor_train import UniformTuckerTensorTrain
|
|
16
|
+
from t3toolbox.frame_variations_format import T3Frame, T3Variations, t3_orthogonal_representations
|
|
17
|
+
from t3toolbox.uniform_frame_variations_format import UT3Frame, UT3Variations, ut3_orthogonal_representations
|
|
18
|
+
from t3toolbox.manifold import T3Tangent, MANIFOLD, COREWISE
|
|
19
|
+
from t3toolbox.uniform_manifold import UT3Tangent, UNIFORM_MANIFOLD, UNIFORM_COREWISE
|
|
20
|
+
from t3toolbox.fitting import (
|
|
21
|
+
GaussNewtonModel,
|
|
22
|
+
UniformGaussNewtonModel,
|
|
23
|
+
apply_model,
|
|
24
|
+
entries_model,
|
|
25
|
+
probe_model,
|
|
26
|
+
apply_derivatives_model,
|
|
27
|
+
entries_derivatives_model,
|
|
28
|
+
probe_derivatives_model,
|
|
29
|
+
)
|
|
30
|
+
from t3toolbox.optimizers import gradient_descent, mc_sgd, adam, newton_cg
|
|
31
|
+
from t3toolbox import safety
|
|
32
|
+
from t3toolbox.safety import safe, unsafe
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from importlib.metadata import version as _pkg_version
|
|
36
|
+
__version__ = _pkg_version('t3toolbox')
|
|
37
|
+
except Exception: # not installed (e.g. PYTHONPATH use) -- keep in sync with pyproject.toml
|
|
38
|
+
__version__ = '2026.0.0'
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# tensors
|
|
42
|
+
'TuckerTensorTrain',
|
|
43
|
+
'UniformTuckerTensorTrain',
|
|
44
|
+
# frames / variations / tangents
|
|
45
|
+
'T3Frame',
|
|
46
|
+
'T3Variations',
|
|
47
|
+
'T3Tangent',
|
|
48
|
+
'UT3Frame',
|
|
49
|
+
'UT3Variations',
|
|
50
|
+
'UT3Tangent',
|
|
51
|
+
't3_orthogonal_representations',
|
|
52
|
+
'ut3_orthogonal_representations',
|
|
53
|
+
# geometries
|
|
54
|
+
'MANIFOLD',
|
|
55
|
+
'COREWISE',
|
|
56
|
+
'UNIFORM_MANIFOLD',
|
|
57
|
+
'UNIFORM_COREWISE',
|
|
58
|
+
# fitting models
|
|
59
|
+
'GaussNewtonModel',
|
|
60
|
+
'UniformGaussNewtonModel',
|
|
61
|
+
'apply_model',
|
|
62
|
+
'entries_model',
|
|
63
|
+
'probe_model',
|
|
64
|
+
'apply_derivatives_model',
|
|
65
|
+
'entries_derivatives_model',
|
|
66
|
+
'probe_derivatives_model',
|
|
67
|
+
# optimizers
|
|
68
|
+
'gradient_descent',
|
|
69
|
+
'mc_sgd',
|
|
70
|
+
'adam',
|
|
71
|
+
'newton_cg',
|
|
72
|
+
# safety mode
|
|
73
|
+
'safety',
|
|
74
|
+
'safe',
|
|
75
|
+
'unsafe',
|
|
76
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""The pure-functional backend: stateless functions on raw core-tuple / supercore data.
|
|
2
|
+
|
|
3
|
+
Deliberately empty: backend users import submodules explicitly (e.g.
|
|
4
|
+
``from t3toolbox.backend import probing``) -- each module namespaces one
|
|
5
|
+
(representation family) x (operation kind) cell, and each carries its own ``__all__``.
|
|
6
|
+
See ``docs/naming_conventions.md`` for the family prefix grammar and the module map.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Authors: Nick Alger and Blake Christierson
|
|
2
|
+
# Copyright: MIT License (2026)
|
|
3
|
+
# Github: https://github.com/NickAlger/T3Toolbox
|
|
4
|
+
# Documentation: https://nickalger.github.io/T3Toolbox/index.html
|
|
5
|
+
"""The ``apply`` sampling type: contract a T3 with vectors in ALL modes -> one scalar per sample.
|
|
6
|
+
|
|
7
|
+
Holds the t3 + tv ops, the ambient/tangent/corewise transposes, and the frame sweeps for applies.
|
|
8
|
+
Specializes the general probing machinery (containment probe ⊃ apply ⊃ entries; this module
|
|
9
|
+
imports from ``probing``, never the reverse). Costs and the role in Riemannian least-squares:
|
|
10
|
+
``docs/entries_apply_probe.md``.
|
|
11
|
+
"""
|
|
12
|
+
import numpy as np
|
|
13
|
+
import typing as typ
|
|
14
|
+
|
|
15
|
+
import t3toolbox.backend.t3_conversions as t3_conversions
|
|
16
|
+
import t3toolbox.backend.contractions as contractions
|
|
17
|
+
from t3toolbox.backend.common import *
|
|
18
|
+
import math
|
|
19
|
+
import t3toolbox.backend.probing as probing
|
|
20
|
+
from t3toolbox.backend.probing import compute_xi, compute_mu, compute_dxi, compute_sigma_hat, _sigma_step
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
't3_apply',
|
|
24
|
+
't3_apply_ambient_transpose',
|
|
25
|
+
'tv_apply',
|
|
26
|
+
'tv_apply_transpose',
|
|
27
|
+
'tv_precompute_apply_frame_sweep',
|
|
28
|
+
'tv_apply_jacobian_from_sweep',
|
|
29
|
+
'tv_apply_transpose_from_sweep',
|
|
30
|
+
't3_apply_corewise_transpose',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def t3_apply(
|
|
34
|
+
x: typ.Union[
|
|
35
|
+
typ.Tuple[typ.Sequence[NDArray], typ.Sequence[NDArray]], # (tucker_cores, tt_cores)
|
|
36
|
+
typ.Tuple[NDArray, NDArray], # (tucker_supercore, tt_supercore)
|
|
37
|
+
],
|
|
38
|
+
vecs: typ.Union[
|
|
39
|
+
typ.Sequence[NDArray], # len=d, elm_shape=vsw+(Ni,), ragged
|
|
40
|
+
NDArray, # shape=(d,) + vsw +(Ni,), uniform
|
|
41
|
+
],
|
|
42
|
+
) -> NDArray:
|
|
43
|
+
'''Contract a Tucker tensor train with vectors in all indices.
|
|
44
|
+
'''
|
|
45
|
+
use_jax = tree_contains_jax((x, vecs))
|
|
46
|
+
is_uniform = is_ndarray(x[0]) # supercore -> real lax.scan over the mode axis (like entries)
|
|
47
|
+
xnp, _, xscan = get_backend(is_uniform, use_jax)
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
tucker_cores, tt_cores = x
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
|
|
54
|
+
vsc = tucker_cores[0].shape[:-2] # core/frame stack C (the batch of T3s)
|
|
55
|
+
vsw = vecs[0].shape[:-1] # vec stack W (the probe-like vectors), base-inner: W outer, C inner
|
|
56
|
+
|
|
57
|
+
def _func(mu_WCa, v_B_G):
|
|
58
|
+
v_Wo, B_Cpo, G_Capb = v_B_G
|
|
59
|
+
mu_WCb = contractions.WCa_Caib_Wo_Cio_to_WCb(
|
|
60
|
+
mu_WCa, G_Capb, v_Wo, B_Cpo,
|
|
61
|
+
)
|
|
62
|
+
return mu_WCb, (0,)
|
|
63
|
+
|
|
64
|
+
mu_WCa = xnp.ones(vsw + vsc + (tt_cores[0].shape[-3],)) # W + C
|
|
65
|
+
v_B_G = (vecs, tucker_cores, tt_cores)
|
|
66
|
+
mu_WCz, _ = xscan(_func, mu_WCa, v_B_G)
|
|
67
|
+
|
|
68
|
+
result = xnp.sum(mu_WCz, axis=-1)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def t3_apply_ambient_transpose(
|
|
73
|
+
c: NDArray, # residual, shape=W+C
|
|
74
|
+
ww: typ.Sequence[NDArray], # apply vectors, len=d, elm_shape=W+(Ni,)
|
|
75
|
+
sum_over_probes: bool = False, # True: W becomes the CP rank (ambient J^T r)
|
|
76
|
+
) -> typ.Sequence[NDArray]: # canonical (CP) factors. len=d, ith elm_shape=stack_shape+(R, Ni)
|
|
77
|
+
'''Ambient transpose of :py:func:`t3_apply`: back-project ``c`` into CP factors.
|
|
78
|
+
|
|
79
|
+
The *ambient* adjoint -- the transpose of ``apply`` as a linear map on the **full tensor space**
|
|
80
|
+
(``X -> ( <X, w0^W (x) ... (x) w_{d-1}^W> )_W``). Frame-free; the back-projection
|
|
81
|
+
``c * (w0 (x) ... (x) w_{d-1})`` is rank-1, whose natural representation is a **canonical (CP)
|
|
82
|
+
decomposition** (``apply`` consumes one vector per mode; its adjoint emits one scaled vector per
|
|
83
|
+
mode). This is distinct from the *corewise* transpose (gradient w.r.t. a base point's cores) and
|
|
84
|
+
the *tangent* transpose (Riemannian gradient) -- see ``docs/transposes.md`` for the full taxonomy.
|
|
85
|
+
|
|
86
|
+
- ``sum_over_probes=False`` (primary): ``W`` is a passthrough stacking axis -- a ``W (+ C)`` stack
|
|
87
|
+
of rank-1 CP tensors (CP rank ``R=1``).
|
|
88
|
+
- ``sum_over_probes=True``: ``W`` becomes the CP **rank** -- one rank-``|W|`` CP tensor
|
|
89
|
+
``sum_W c_W * (w0^W (x) ...)`` (the ambient ``J^T r``). Cheap as CP (``O(d |W| N)``, the shared
|
|
90
|
+
rank index stays implicit); the ``|W|^2`` cost of a *dense* Tucker tensor train is incurred only
|
|
91
|
+
if you convert with ``t3_conversions.t3_from_canonical``.
|
|
92
|
+
|
|
93
|
+
Returns the CP ``factors`` (``c`` folded into the first), in the layout
|
|
94
|
+
``t3_conversions.t3_from_canonical`` consumes.
|
|
95
|
+
'''
|
|
96
|
+
use_jax = tree_contains_jax((c, ww))
|
|
97
|
+
xnp, _, _ = get_backend(False, use_jax)
|
|
98
|
+
c = xnp.asarray(c)
|
|
99
|
+
|
|
100
|
+
nW = ww[0].ndim - 1 # probe stack rank (ww[i] is W + (Ni,))
|
|
101
|
+
W = ww[0].shape[:nW] # probe stack
|
|
102
|
+
C = c.shape[nW:] # frame stack (c is W + C)
|
|
103
|
+
nC = len(C)
|
|
104
|
+
|
|
105
|
+
if sum_over_probes:
|
|
106
|
+
# canonical rank |W|, stack C: w_i flattened over W into the rank axis, broadcast over C;
|
|
107
|
+
# c folded into F_0 as F_0[C, s, n] = c_flat[s, C] * w0_flat[s, n].
|
|
108
|
+
m = int(np.prod(W, dtype=int)) # |W|
|
|
109
|
+
c_flat = xnp.moveaxis(c.reshape((m,) + C), 0, nC) # (m,) + C -> C + (m,)
|
|
110
|
+
factors = []
|
|
111
|
+
for i, w in enumerate(ww):
|
|
112
|
+
w_flat = w.reshape((1,) * nC + (m, w.shape[-1])) # broadcastable to C + (m, Ni)
|
|
113
|
+
if i == 0:
|
|
114
|
+
factors.append(c_flat[..., None] * w_flat) # C + (m, N0)
|
|
115
|
+
else:
|
|
116
|
+
factors.append(w_flat * xnp.ones(C + (1, 1))) # materialize C + (m, Ni)
|
|
117
|
+
else:
|
|
118
|
+
# canonical rank 1, stack W + C: c folded into F_0 as F_0[W, C, 0, n] = c[W, C] * w0[W, n].
|
|
119
|
+
c_exp = c.reshape(W + C + (1, 1)) # W + C + (1, 1)
|
|
120
|
+
factors = []
|
|
121
|
+
for i, w in enumerate(ww):
|
|
122
|
+
w_exp = w.reshape(W + (1,) * nC + (1, w.shape[-1])) # broadcastable to W + C + (1, Ni)
|
|
123
|
+
if i == 0:
|
|
124
|
+
factors.append(c_exp * w_exp) # W + C + (1, N0)
|
|
125
|
+
else:
|
|
126
|
+
factors.append(w_exp * xnp.ones(C + (1, 1))) # materialize W + C + (1, Ni)
|
|
127
|
+
|
|
128
|
+
return tuple(factors)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def tv_apply(
|
|
132
|
+
ww: typ.Sequence[NDArray], # apply vectors, len=d, elm_shape=W+(Ni,)
|
|
133
|
+
variation: typ.Tuple[
|
|
134
|
+
typ.Sequence[NDArray], # var_tucker_cores. len=d, elm_shape=K+C+(nOi,Ni)
|
|
135
|
+
typ.Sequence[NDArray], # var_tt_cores. len=d, elm_shape=K+C+(rLi,nUi,rRi)
|
|
136
|
+
],
|
|
137
|
+
frame: typ.Tuple[
|
|
138
|
+
typ.Sequence[NDArray], # up_tucker_cores U. len=d
|
|
139
|
+
typ.Sequence[NDArray], # down_tt_cores O. len=d
|
|
140
|
+
typ.Sequence[NDArray], # left_tt_cores P. len=d
|
|
141
|
+
typ.Sequence[NDArray], # right_tt_cores Q. len=d
|
|
142
|
+
], # frame order = T3Frame.data = (up, down, left, right)
|
|
143
|
+
) -> NDArray: # the scalar apply(v, ww), one per stack element; shape = W + K + C
|
|
144
|
+
'''Apply a tangent vector in all modes: contract the dense tangent with ``ww`` in every index.
|
|
145
|
+
|
|
146
|
+
The all-modes special case of probing -- a single left-to-right pass (mu-hat via P, then the
|
|
147
|
+
perturbation sigma via Q), contracted at the terminal bond. No right (nu) / central (eta) sweeps,
|
|
148
|
+
no per-mode assembly. See Section 6.2.2 (Algorithms 6-7) of Alger et al. (2026).
|
|
149
|
+
|
|
150
|
+
See Also
|
|
151
|
+
--------
|
|
152
|
+
tv_entries
|
|
153
|
+
tv_probe
|
|
154
|
+
'''
|
|
155
|
+
up_tucker_cores, down_tt_cores, left_tt_cores, right_tt_cores = frame
|
|
156
|
+
var_tucker_cores, var_tt_cores = variation
|
|
157
|
+
|
|
158
|
+
xis = compute_xi(up_tucker_cores, ww) # xi-hat_i = U_i^T w_i
|
|
159
|
+
dxis = compute_dxi(var_tucker_cores, ww) # delta-xi_i = dU_i^T w_i
|
|
160
|
+
mus = compute_mu(left_tt_cores, xis) # frame left sweep via P
|
|
161
|
+
|
|
162
|
+
return _apply_from_xis(xis, dxis, mus, right_tt_cores, down_tt_cores, var_tt_cores)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def tv_apply_transpose(
|
|
166
|
+
c: NDArray, # residual, shape = W + C (one per probe-set, per base point)
|
|
167
|
+
ww: typ.Sequence[NDArray], # the apply vectors, len=d, elm_shape=W+(Ni,)
|
|
168
|
+
frame: typ.Tuple[typ.Sequence[NDArray], typ.Sequence[NDArray],
|
|
169
|
+
typ.Sequence[NDArray], typ.Sequence[NDArray]], # (up, down, left, right)
|
|
170
|
+
sum_over_probes: bool = False,
|
|
171
|
+
) -> typ.Tuple[typ.Sequence[NDArray], typ.Sequence[NDArray]]: # (dU_tildes, dG_tildes) = T3Variations.data
|
|
172
|
+
'''Apply the transpose of :py:func:`tv_apply` -- back-project a residual ``c`` into a tangent.
|
|
173
|
+
|
|
174
|
+
The adjoint of the (linear-in-the-variation) all-modes apply. Needs only the frame sweep
|
|
175
|
+
(xi-hat, mu-hat, nu-hat, eta-hat) and a single-term scatter assembly (it skips the adjoint
|
|
176
|
+
perturbation sweep that tv_probe_transpose runs). With ``sum_over_probes=False`` the probe
|
|
177
|
+
stack W becomes the output tangent stack; with ``True`` it is summed (the ``J^T r`` back-projection).
|
|
178
|
+
|
|
179
|
+
See Also
|
|
180
|
+
--------
|
|
181
|
+
tv_apply
|
|
182
|
+
tv_entries_transpose
|
|
183
|
+
'''
|
|
184
|
+
frame_sweep = tv_precompute_apply_frame_sweep(frame, ww)
|
|
185
|
+
return tv_apply_transpose_from_sweep(c, ww, frame, frame_sweep, sum_over_probes)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def tv_precompute_apply_frame_sweep(
|
|
189
|
+
frame: typ.Tuple[
|
|
190
|
+
typ.Sequence[NDArray], # up_tucker_cores U. len=d
|
|
191
|
+
typ.Sequence[NDArray], # down_tt_cores O. len=d
|
|
192
|
+
typ.Sequence[NDArray], # left_tt_cores P. len=d
|
|
193
|
+
typ.Sequence[NDArray], # right_tt_cores Q. len=d
|
|
194
|
+
], # frame order = T3Frame.data = (up, down, left, right)
|
|
195
|
+
ww: typ.Sequence[NDArray], # apply vectors, len=d, elm_shape=W+(Ni,)
|
|
196
|
+
) -> typ.Tuple[
|
|
197
|
+
typ.Sequence[NDArray], # xis. len=d, elm_shape=W+C+(nUi,)
|
|
198
|
+
typ.Sequence[NDArray], # mus. len=d, elm_shape=W+C+(rLi,)
|
|
199
|
+
]: # lean frame sweep -- (xis, mus) only
|
|
200
|
+
'''The all-modes apply **frame sweep** (lean): the frame edge variables ``(xi-hat, mu-hat)`` that
|
|
201
|
+
depend only on the frame frame and the apply vectors ``ww`` -- NOT on the tangent or residual.
|
|
202
|
+
Computing them is the expensive, ``W``-scaled part of the apply Jacobian; precomputed **once per
|
|
203
|
+
frame** and reused across every ``J`` / ``Jᵀ`` of an inner solve (the reuse hook for ``fitting.py``).
|
|
204
|
+
|
|
205
|
+
**Lean ``(xis, mus)`` only** (not the right ``nu`` / down ``eta`` sweeps): the all-modes apply
|
|
206
|
+
forward AND its adjoint-state transpose use only ``(xi, mu)`` -- the transpose recomputes the right
|
|
207
|
+
context as ``sigma_hat`` from the residual rather than storing ``nu``/``eta``, halving the
|
|
208
|
+
``W``-scaling memory (apply on the manifold, §6.2.2 of Alger et al. (2026)). Probe, which leaves a
|
|
209
|
+
mode free, needs the full sweep -- :py:func:`tv_precompute_probe_frame_sweep`.
|
|
210
|
+
|
|
211
|
+
See Also
|
|
212
|
+
--------
|
|
213
|
+
tv_apply_jacobian_from_sweep
|
|
214
|
+
tv_apply_transpose_from_sweep
|
|
215
|
+
tv_precompute_probe_frame_sweep
|
|
216
|
+
'''
|
|
217
|
+
up_tucker_cores, down_tt_cores, left_tt_cores, right_tt_cores = frame
|
|
218
|
+
xis = compute_xi(up_tucker_cores, ww)
|
|
219
|
+
mus = compute_mu(left_tt_cores, xis)
|
|
220
|
+
return xis, mus
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def tv_apply_jacobian_from_sweep(
|
|
224
|
+
variation: typ.Tuple[
|
|
225
|
+
typ.Sequence[NDArray], # var_tucker_cores. len=d, elm_shape=K+C+(nOi,Ni)
|
|
226
|
+
typ.Sequence[NDArray], # var_tt_cores. len=d, elm_shape=K+C+(rLi,nUi,rRi)
|
|
227
|
+
],
|
|
228
|
+
ww: typ.Sequence[NDArray], # apply vectors, len=d, elm_shape=W+(Ni,) -- for the variation's dxis
|
|
229
|
+
frame: typ.Tuple[
|
|
230
|
+
typ.Sequence[NDArray], # up_tucker_cores U. len=d (unused; for a uniform call signature)
|
|
231
|
+
typ.Sequence[NDArray], # down_tt_cores O. len=d
|
|
232
|
+
typ.Sequence[NDArray], # left_tt_cores P. len=d (unused)
|
|
233
|
+
typ.Sequence[NDArray], # right_tt_cores Q. len=d
|
|
234
|
+
], # frame order = T3Frame.data = (up, down, left, right)
|
|
235
|
+
frame_sweep: typ.Tuple[
|
|
236
|
+
typ.Sequence[NDArray], # xis
|
|
237
|
+
typ.Sequence[NDArray], # mus
|
|
238
|
+
], # = tv_precompute_apply_frame_sweep(frame, ww) (lean)
|
|
239
|
+
) -> NDArray: # the scalar apply(v, ww), one per stack element; shape = W + K + C
|
|
240
|
+
'''Forward all-modes apply of a tangent vector reusing a precomputed frame sweep -- the bare ``𝒥`` with
|
|
241
|
+
the frame edge variables injected. Equivalent to :py:func:`tv_apply`, but it takes the lean
|
|
242
|
+
``(xis, mus)`` from ``frame_sweep`` instead of recomputing them (the reuse hook for ``fitting.py``).
|
|
243
|
+
Only the variation-dependent ``dxis`` is computed here. No gauge projector ``Π``.
|
|
244
|
+
|
|
245
|
+
See Also
|
|
246
|
+
--------
|
|
247
|
+
tv_precompute_apply_frame_sweep
|
|
248
|
+
tv_apply
|
|
249
|
+
'''
|
|
250
|
+
var_tucker_cores, var_tt_cores = variation
|
|
251
|
+
_, down_tt_cores, _, right_tt_cores = frame
|
|
252
|
+
xis, mus = frame_sweep
|
|
253
|
+
dxis = compute_dxi(var_tucker_cores, ww) # variation-dependent; not part of the frame sweep
|
|
254
|
+
return _apply_from_xis(xis, dxis, mus, right_tt_cores, down_tt_cores, var_tt_cores)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def tv_apply_transpose_from_sweep(
|
|
258
|
+
c: NDArray, # residual, shape = W + K + C (K optional)
|
|
259
|
+
ww: typ.Sequence[NDArray], # apply vectors (one-hot e_index for entries), len=d, elm_shape=W+(Ni,)
|
|
260
|
+
frame: typ.Tuple[
|
|
261
|
+
typ.Sequence[NDArray], # up_tucker_cores U. len=d (unused; uniform call signature)
|
|
262
|
+
typ.Sequence[NDArray], # down_tt_cores O. len=d
|
|
263
|
+
typ.Sequence[NDArray], # left_tt_cores P. len=d (unused)
|
|
264
|
+
typ.Sequence[NDArray], # right_tt_cores Q. len=d
|
|
265
|
+
], # frame order = T3Frame.data = (up, down, left, right)
|
|
266
|
+
frame_sweep: typ.Tuple[
|
|
267
|
+
typ.Sequence[NDArray], # xis
|
|
268
|
+
typ.Sequence[NDArray], # mus
|
|
269
|
+
], # = tv_precompute_apply_frame_sweep(frame, ww) (lean: no nu/eta)
|
|
270
|
+
sum_over_probes: bool = False,
|
|
271
|
+
) -> typ.Tuple[typ.Sequence[NDArray], typ.Sequence[NDArray]]: # (dU_tildes, dG_tildes) = T3Variations.data
|
|
272
|
+
'''Transpose of the all-modes apply reusing a precomputed frame sweep -- the bare ``𝒥ᵀ``, by the
|
|
273
|
+
**adjoint-state** method (the scalar residual ``c`` seeds one reverse ``sigma_hat`` sweep; see
|
|
274
|
+
:py:func:`_apply_transpose_adjoint`). Takes the **lean** ``(xis, mus)`` sweep + the frame cores ``O,
|
|
275
|
+
Q`` (it recomputes the right context rather than storing ``nu``/``eta`` -- half the memory). Reuse
|
|
276
|
+
hook for ``fitting.py`` (one frame sweep feeds the forward and this transpose). Full ``W + K + C``
|
|
277
|
+
(the residual ``c`` may carry the tangent stack ``K``). No gauge projector ``Π``.
|
|
278
|
+
|
|
279
|
+
See Also
|
|
280
|
+
--------
|
|
281
|
+
tv_precompute_apply_frame_sweep
|
|
282
|
+
tv_apply_transpose
|
|
283
|
+
'''
|
|
284
|
+
_, down_tt_cores, _, right_tt_cores = frame
|
|
285
|
+
xis, mus = frame_sweep
|
|
286
|
+
return _apply_transpose_adjoint(c, ww, xis, mus, down_tt_cores, right_tt_cores, sum_over_probes)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def t3_apply_corewise_transpose(
|
|
290
|
+
c: NDArray, # residual, shape=W+C
|
|
291
|
+
ww: typ.Sequence[NDArray], # apply vectors, len=d, elm_shape=W+(Ni,)
|
|
292
|
+
core_pair: typ.Tuple[
|
|
293
|
+
typ.Sequence[NDArray], # tucker_cores, len=d, elm_shape=C+(ni,Ni)
|
|
294
|
+
typ.Sequence[NDArray], # tt_cores, len=d, elm_shape=C+(ri,ni,r(i+1))
|
|
295
|
+
],
|
|
296
|
+
sum_over_probes: bool = False, # True: sum the apply stack W (the gradient J^T r)
|
|
297
|
+
) -> typ.Tuple[
|
|
298
|
+
typ.Tuple[NDArray, ...], # tucker-core gradients, same shapes as tucker_cores
|
|
299
|
+
typ.Tuple[NDArray, ...], # tt-core gradients, same shapes as tt_cores
|
|
300
|
+
]:
|
|
301
|
+
'''Corewise (non-manifold) transpose of :py:func:`t3_apply`: gradient of the
|
|
302
|
+
measurement w.r.t. the cores of the frame ``core_pair``, treated as independent variables.
|
|
303
|
+
|
|
304
|
+
The adjoint of the *core parametrization* ``cores -> apply(X(cores), ww)`` at the base point -- the
|
|
305
|
+
gradient a core-wise optimizer (Adam, L-BFGS) needs. Returns gradients shaped exactly like
|
|
306
|
+
``(tucker_cores, tt_cores)`` -- a gradient, NOT a tensor (so no ``|W|`` blow-up: the apply stack
|
|
307
|
+
collapses into the fixed-size cores). Distinct from the *ambient* transpose (a free CP tensor) and
|
|
308
|
+
the *tangent* transpose (a Riemannian tangent); see ``docs/transposes.md``.
|
|
309
|
+
|
|
310
|
+
Implemented by the Section 6.3 ("corewise simplification") substitution into the tangent transpose:
|
|
311
|
+
feed the frame's own cores in place of the orthogonal frames (``P, Q, O -> G_i``), with ``U_i`` no
|
|
312
|
+
longer required orthogonal -- i.e. :py:func:`tv_apply_transpose` at frame ``(U, G, G, G)``. No
|
|
313
|
+
orthogonality is required. ``sum_over_probes=True`` sums the apply stack ``W`` (the gradient
|
|
314
|
+
``J^T r``); ``False`` keeps ``W`` as a stack (one core-gradient set per probe).
|
|
315
|
+
|
|
316
|
+
Math reference: Section 6.3, Alger et al. (2026), "Tucker Tensor Train Taylor Series"
|
|
317
|
+
(arXiv:2603.21141).
|
|
318
|
+
'''
|
|
319
|
+
tucker_cores, tt_cores = core_pair
|
|
320
|
+
return tv_apply_transpose(
|
|
321
|
+
c, ww, (tucker_cores, tt_cores, tt_cores, tt_cores), sum_over_probes=sum_over_probes,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _apply_from_xis(xis, dxis, mus, right_tt_cores, down_tt_cores, var_tt_cores):
|
|
326
|
+
'''Run the perturbation sigma sweep (via Q) to its TERMINAL carry and contract the final bond.
|
|
327
|
+
|
|
328
|
+
Shared tail of tv_apply and tv_entries (they differ only in how xis/dxis are formed).
|
|
329
|
+
'''
|
|
330
|
+
use_jax = tree_contains_jax((xis, dxis, mus, right_tt_cores))
|
|
331
|
+
is_uniform = not isinstance(xis, typ.Sequence)
|
|
332
|
+
xnp, xmap, xscan = get_backend(is_uniform, use_jax)
|
|
333
|
+
|
|
334
|
+
def _func(sigma, x):
|
|
335
|
+
Q, O, dG, xi, dxi, mu = x
|
|
336
|
+
return _sigma_step(sigma, Q, O, dG, xi, dxi, mu), (0,)
|
|
337
|
+
|
|
338
|
+
# carry sigma is W+K+C; take the leading stack from dxis (carries K), not xis (W+C only).
|
|
339
|
+
rR0 = right_tt_cores[0].shape[-3]
|
|
340
|
+
init = xnp.zeros(dxis[0].shape[:-1] + (rR0,))
|
|
341
|
+
sigma_terminal, _ = xscan(_func, init, (right_tt_cores, down_tt_cores, var_tt_cores, xis, dxis, mus))
|
|
342
|
+
return xnp.sum(sigma_terminal, axis=-1) # contract the terminal bond -> W + K + C
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _apply_transpose_adjoint(c, ww, xis, mus, down_tt_cores, right_tt_cores, sum_over_probes):
|
|
346
|
+
'''Adjoint-state assembly shared by apply/tv_entries_transpose_from_sweep -- the **low-memory,
|
|
347
|
+
K-aware** transpose (replaces the old scatter). The scalar residual ``c`` seeds one reverse
|
|
348
|
+
``sigma_hat`` sweep (recomputing the right context, so ``nu``/``eta`` are never stored); then
|
|
349
|
+
|
|
350
|
+
dxi-hat_k = mu-hat_{k-1} . O_k . sigma-hat_k # over the down mode nO
|
|
351
|
+
dG-tilde_k = mu-hat_{k-1} (x) xi-hat_k (x) sigma-hat_k # over (rL, nU, rR)
|
|
352
|
+
dU-tilde_k = dxi-hat_k (x) w_k # over (nO, N)
|
|
353
|
+
|
|
354
|
+
Uses only the lean frame sweep ``(xis, mus)`` (half the memory of the ``(xis, mus, nus, etas)`` the
|
|
355
|
+
scatter stored -- the ``W``-scaling ``nu``/``eta`` are gone), at the cost of the ``sigma_hat`` sweep
|
|
356
|
+
per transpose. Full ``W + K + C``: the residual ``c`` may carry the tangent stack ``K`` (the output
|
|
357
|
+
space of a ``K``-stacked forward ``tv_apply``), which rides into the variation gradient -- the
|
|
358
|
+
capability the scatter lacked. ``sum_over_probes=True`` sums the probe stack ``W`` (the ``J^T r``
|
|
359
|
+
back-projection); ``False`` keeps it as the output tangent stack. ``K``/``C`` always kept.'''
|
|
360
|
+
is_uniform = is_ndarray(mus)
|
|
361
|
+
sigma_hats = compute_sigma_hat(right_tt_cores, xis, c) # polymorphic reverse sweep
|
|
362
|
+
|
|
363
|
+
if is_uniform:
|
|
364
|
+
# d-prefixed WKC (3b-6a/c), vectorized over the core index d (NOT a per-core loop -> no jit
|
|
365
|
+
# unroll). ww is the packed apply/one-hot probe supercore (d,)+W+(N,) -> n_probe = len(W). The
|
|
366
|
+
# ragged loop below is the oracle.
|
|
367
|
+
n_probe = ww.ndim - 2
|
|
368
|
+
dxi_hats = contractions.dWCa_dCaib_dWKCb_to_dWKCi(mus, down_tt_cores, sigma_hats)
|
|
369
|
+
if sum_over_probes:
|
|
370
|
+
dG_tildes = contractions.dWCa_dWCi_dWKCb_to_dKCaib(mus, xis, sigma_hats, n_probe)
|
|
371
|
+
dU_tildes = contractions.dWo_dWKCa_to_dKCao(ww, dxi_hats)
|
|
372
|
+
else:
|
|
373
|
+
dG_tildes = contractions.dWCa_dWCi_dWKCb_to_dWKCaib(mus, xis, sigma_hats, n_probe)
|
|
374
|
+
dU_tildes = contractions.dWo_dWKCa_to_dWKCao(ww, dxi_hats)
|
|
375
|
+
return dU_tildes, dG_tildes
|
|
376
|
+
|
|
377
|
+
n_probe = ww[0].ndim - 1
|
|
378
|
+
dxi_hats = tuple(contractions.WCa_Caib_WKCb_to_WKCi(mu, O, sh) # dxi_hat = mu . O . sigma_hat
|
|
379
|
+
for mu, O, sh in zip(mus, down_tt_cores, sigma_hats))
|
|
380
|
+
if sum_over_probes:
|
|
381
|
+
dG_tildes = tuple(contractions.WCa_WCi_WKCb_to_KCaib(mu, xi, sh, n_probe)
|
|
382
|
+
for mu, xi, sh in zip(mus, xis, sigma_hats))
|
|
383
|
+
dU_tildes = tuple(contractions.Wo_WKCa_to_KCao(w, dxh) for w, dxh in zip(ww, dxi_hats))
|
|
384
|
+
else:
|
|
385
|
+
dG_tildes = tuple(contractions.WCa_WCi_WKCb_to_WKCaib(mu, xi, sh, n_probe)
|
|
386
|
+
for mu, xi, sh in zip(mus, xis, sigma_hats))
|
|
387
|
+
dU_tildes = tuple(contractions.Wo_WKCa_to_WKCao(w, dxh) for w, dxh in zip(ww, dxi_hats))
|
|
388
|
+
return dU_tildes, dG_tildes # (var_tucker, var_tt) = T3Variations.data
|