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.
Files changed (55) hide show
  1. t3toolbox/__init__.py +76 -0
  2. t3toolbox/backend/__init__.py +7 -0
  3. t3toolbox/backend/apply.py +388 -0
  4. t3toolbox/backend/common.py +459 -0
  5. t3toolbox/backend/contractions.py +2812 -0
  6. t3toolbox/backend/entries.py +298 -0
  7. t3toolbox/backend/fitting.py +289 -0
  8. t3toolbox/backend/fv_conversions.py +190 -0
  9. t3toolbox/backend/fv_operations.py +330 -0
  10. t3toolbox/backend/linalg.py +527 -0
  11. t3toolbox/backend/optimizers.py +409 -0
  12. t3toolbox/backend/probing.py +1447 -0
  13. t3toolbox/backend/ranks.py +473 -0
  14. t3toolbox/backend/sampling_derivatives.py +1634 -0
  15. t3toolbox/backend/stacking.py +519 -0
  16. t3toolbox/backend/t3_constructors.py +95 -0
  17. t3toolbox/backend/t3_conversions.py +198 -0
  18. t3toolbox/backend/t3_linalg.py +605 -0
  19. t3toolbox/backend/t3_operations.py +360 -0
  20. t3toolbox/backend/t3_orthogonalization.py +409 -0
  21. t3toolbox/backend/t3_svd.py +514 -0
  22. t3toolbox/backend/tt_operations.py +186 -0
  23. t3toolbox/backend/tt_orthogonalization.py +89 -0
  24. t3toolbox/backend/tv_operations.py +603 -0
  25. t3toolbox/backend/ufv_conversions.py +327 -0
  26. t3toolbox/backend/ufv_masking.py +148 -0
  27. t3toolbox/backend/ufv_operations.py +235 -0
  28. t3toolbox/backend/uniform_fitting.py +474 -0
  29. t3toolbox/backend/ut3_constructors.py +185 -0
  30. t3toolbox/backend/ut3_conversions.py +168 -0
  31. t3toolbox/backend/ut3_linalg.py +153 -0
  32. t3toolbox/backend/ut3_masking.py +118 -0
  33. t3toolbox/backend/ut3_operations.py +202 -0
  34. t3toolbox/backend/ut3_orthogonalization.py +194 -0
  35. t3toolbox/backend/ut3_sampling.py +252 -0
  36. t3toolbox/backend/ut3_svd.py +229 -0
  37. t3toolbox/backend/utv_operations.py +506 -0
  38. t3toolbox/backend/utv_sampling.py +557 -0
  39. t3toolbox/backend/wt3_operations.py +130 -0
  40. t3toolbox/corewise.py +390 -0
  41. t3toolbox/fitting.py +547 -0
  42. t3toolbox/frame_variations_format.py +1316 -0
  43. t3toolbox/manifold.py +1462 -0
  44. t3toolbox/optimizers.py +228 -0
  45. t3toolbox/safety.py +244 -0
  46. t3toolbox/tucker_tensor_train.py +4384 -0
  47. t3toolbox/uniform_frame_variations_format.py +1071 -0
  48. t3toolbox/uniform_manifold.py +1250 -0
  49. t3toolbox/uniform_tucker_tensor_train.py +909 -0
  50. t3toolbox/weighted_tucker_tensor_train.py +669 -0
  51. t3toolbox-2026.0.0.dist-info/METADATA +169 -0
  52. t3toolbox-2026.0.0.dist-info/RECORD +55 -0
  53. t3toolbox-2026.0.0.dist-info/WHEEL +5 -0
  54. t3toolbox-2026.0.0.dist-info/licenses/LICENSE +21 -0
  55. 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