Batching and stacking in T3Toolbox — the complete reference#
Batch/stack axes —
stack_shape, theC/W/Kblocks,K-stacked (tangent-stacked) tangents,vmap/jitover these objects — are the most subtle part of the library. This file is the source of truth for what the conventions are and why; the “Start here” section is enough for most use. (Rules for extending the batching machinery, and the decision history, live incontributor/batching_internals.md.)
Start here — the mental model in one screen#
New to stacking here? Read this section; it is enough to use the library. The numbered sections below are the full reference.
Stacking = leading batch axes on the cores, so one object holds many. Every TuckerTensorTrain /
T3Frame / T3Variations stores its cores as core.shape == stack_shape + (tensor/rank axes). With
stack_shape = (2, 3) you carry a 2×3 grid of T3s/bases/variations in one object, and operations
vectorize over them.
There are three kinds of batch — three “blocks” — that batch different things, on different arrays. Keeping them straight is the whole game; conflating them is the #1 source of confusion:
C— the frame/core stack. A batch of whole T3s / base points on the manifold. Lives on every core (it isstack_shape). Mnemonic: on every Core.W— the probe stack. A batch of probe-vector sets. Lives on the probe vectorswwonly, not the cores. Mnemonic: the w vectors.K— the tangent stack. A batch of tangent vectors attached at the same base point. Lives on the variation cores only. Mnemonic: the k tangent vectors at each frame.
One object can carry more than one block at once. Order is always frame-inner: W + K + C + (axes)
(C innermost, adjacent to the indices; W/K outermost). Why that order? See §3 — it is exactly
what lets a shared frame broadcast over its batch for free.
One concrete example (the picture that makes it click)#
Take d = 3 modes, a batch of 2 base points (C = (2,)), with 3 tangent vectors at each
(K = (3,)), probed by 4 probe-sets (W = (4,)). Then every array’s shape is:
Array |
Blocks |
Shape |
|---|---|---|
frame Tucker core |
|
|
frame TT core |
|
|
variation core |
|
|
probe vector |
|
|
forward probe |
|
|
Read it as: 2 base points, each with its own frame; 3 tangent vectors attached at each; probe every
one against 4 probe-sets → 4 × 3 × 2 probe results per mode. The frame (C) is shared
across the K tangents at it — never copied; frame-inner broadcasting (§3) handles that for free.
⚠️ The word “stack” itself means three different things in this codebase (
stack_shape; the tree↔object machinery instacking.py; the uniform supercore representation). When confused, first decide which. See §1.
Shape-notation legend (how to read the shapes in the code)#
The codebase annotates shapes in trailing comments and encodes them in names. The whole scheme:
A core’s shape is written
stack_shape + (tensor/rank axes), e.g.Bi.shape = stack_shape + (nᵢ, Nᵢ).Capital letters are grouped batch blocks, each standing for zero or more axes:
Cframe/core stack,Wprobe stack,Ktangent stack. An empty block contributes no axes (so the same code handles no-stack and stacked).lowercase letters are single axes — tensor modes / ranks (
n,N,r) or contraction legs (a,i,b,o,j).Order is frame-inner:
W + K + C + (axes)(probe outer, tangent middle, frame inner).Body locals suffix their layout:
mu_WCais an array with axesW + C + (a,).Grouped contractions are einsum strings:
contract('WCa,Caib,WCi->WCb', ...)reads as a per-operand block+leg signature (operand 1 =W+C+(a,); operand 2 = aC-only core with legsa,i,b; operand 3 =W+C+(i,); output frame-innerW+C+(b,)).A leading
din a subscripts term (dWCa) is the uniform layer’s supercore/derivative axis (seedocs/uniform_supercore_layout.md).
Glossary#
Term |
Meaning |
|---|---|
|
the leading batch axes shared by all of an object’s cores; is the frame/core stack |
frame / core stack |
a batch of T3s / base points, on every core ( |
probe stack |
a batch of probe-vector sets, on |
tangent stack |
a batch of tangent vectors at one frame, on the variation cores only; |
frame-inner |
the ordering rule: |
|
a |
heterogeneous stack |
one T3 whose cores have different-but-broadcastable stacks (frame |
“the split is recovered” |
|
|
transpose flag (§11): |
ragged / uniform / weighted |
the three representations. ragged (tuples of arrays) is the default; uniform (supercores + masks) is the jit/GPU mirror of the whole stack ( |
The block letters
W/K/Care deliberately disjoint from the core/variation symbols (U,P,Q,O,G,H,B) and fromJᵀ/tensor/T3, so a shape comment is never ambiguous.
0. TL;DR#
The word “stack” means three different things in this codebase. Keep them apart (§1).
There are three distinct batch blocks —
C(frame/core stack),W(probe stack),K(tangent stack). They batch different things and live on different subsets of the operands (§2).Library-wide convention is frame-inner: the core stack
Cis innermost (adjacent to the tensor indices); the extra stacksW,Kare outermost. Orders:W+C,K+C,W+K+C(§3).There are two machineries for batch axes: a leading
...in einsum (for one broadcastable prefix) and the grouped-einsum interpretercontractions.contract(for two independent blocks on different operand subsets) (§4).A T3 may have heterogeneous-but-broadcastable core stacks (e.g. frame cores
C, one variation coreK+C). This is first-class in the backend;t3_broadcast_to_common_stackmaterializes it when a uniform-stack object is required (§5).Backend functions accept raw tuples and tolerate heterogeneous stacks; frontend dataclasses (
validate) require a uniform stack. That asymmetry explains where broadcasting is lazy vs materialized (§5).The
K/Csplit is not stored — it is recovered from the(frame, variations)pairing (§6).jaxpytree registration makesT3Tangentvmap/jit-able; the frame and variations are both leaves (the same-frame guard is numerical, not identity), so the frame flows as traced data with no per-frame recompile (§7).
1. “Stacking” means three different things#
stack_shape— leading batch axes on ONE object’s cores. Every core of aTuckerTensorTrain/T3Frame/T3Variationsiscore.shape == stack_shape + (tensor/rank axes). A single leading'...'in an einsum rides these axes along for free. This is the common case and the meaning of “stacked” 95% of the time. Caveat: a plain'...'carries exactly one shared/broadcast prefix.backend/stacking.py— converting a Python tree of separate objects ↔ one stacked object.stack(tree, axes)/unstack(S, axes)/tree_zip/apply_func_to_leaf_subtrees. This is its own tree machinery — not jax pytrees, and not the same as meaning (1). It is whatT3Tangent.unstack_*/stack_*,T3Frame.stack, etc. are built on.The uniform supercore (
ut3_*,ufv_*,uniform_*) — a separate representation. One stacked supercore array + boolean rank masks, built forjax.lax.scanvectorization, GPU efficiency, and compile-oncejit— a faster mirror of the ragged layer, optimizers included. Seedocs/uniform_equivalence_contract.mdand the otheruniform_*notes.
When something is confusing, first ask which of these three you are dealing with.
2. The three batch blocks: C, W, K#
Within meaning (1), there are three semantically distinct things one might batch. They are not interchangeable, and a single object can carry more than one at once.
Block |
Name |
Batches… |
Lives on… |
Appears in |
|---|---|---|---|---|
|
frame / core stack |
a batch of T3 objects / base points |
the cores (every core) |
everywhere; |
|
probe stack |
a batch of probe vectors |
the probe vectors |
probing ( |
|
tangent stack |
a batch of tangent vectors sharing one frame |
the variations only (not the frame cores) |
|
Key facts:
Cis shared acrossWandK. A probe stackWof probe vectors is applied to aC-batch of T3s → output carries both. AK-batch of tangents shares oneC-batch of base points.WandKlive on different operands thanC.Wis only onww;Kis only on the variation cores;Cis only on the cores. This is exactly why a single'...'is not enough (§4):'...'broadcasts a shared prefix, butW/Kare present on a subset of operands.In the transpose of probing, the probe stack and the tangent stack coincide:
K == W(each probe residual becomes one tangent) — whensum_over_probes=False; setting itTruesumsWaway. See §11 for the full story on transposes andsum_over_probes.
Concrete shapes (frame-inner, see §3):
a T3 core:
C + (rL, n, rR)(tt core) orC + (n, N)(tucker core)a probe vector
ww[i]:W + (N_i,)a
T3Variationscore:K + C + (rL, n, rR)orK + C + (n, N)a forward probe output
zz[i]:W + C + (N_i,)(=t3_probeoutputW + C)a transpose-probe non-summed output: a
T3TangentwhoseK == W, i.e. variationsK + C
3. The frame-inner convention (and why)#
Rule (library-wide): order batch axes by how core-bound they are. The frame/core stack C is
INNERMOST (adjacent to the indices). Extra stacks — probe W, tangent K — are OUTERMOST.
So the canonical orderings are:
variation cores:
K + C + coreforward probe /
apply/entries:W + Cfull probe of a
K-stacked tangent:W + K + Ctranspose non-summed:
K + C(withK == W)
Why this exact order — the broadcasting mechanism#
This is the reason for the convention, and the thing most worth internalizing.
The bulk of the tangent/T3 operations combine a frame (carrying only C) with a variation
(carrying K + C, or W + C) using plain '...' einsums (to_dense, the gauge projections,
tv_project_t3_onto_tangent_space, corewise linalg). numpy/jax '...' broadcasting is right-aligned.
With frame-inner (
Cinnermost),Cis the trailing suffix ofK + C. So aC-stacked frame core broadcasts cleanly under aK + C-stacked variation core: the trailingCaxes align, and the leadingKaxes are replicated for free — exactly the semantics “the one base point is shared by allKtangent vectors at it.” Same forW + C.With frame-outer (
C + K), the trailing axes would beK,Cwould be the prefix, and the free right-aligned broadcast would mismatch. You would have to insert explicit transposes/reshapes at every boundary.
The custom grouped-block contractions (§4) are flops-neutral to block order (they reshape each block to one flat axis regardless), so they are made to follow the same frame-inner order purely for one consistent layout with no boundary-transpose copies. That is the whole justification — broadcasting determines the order; the contractions just conform.
Mnemonic: “a frame broadcasts over its batch only when the frame axes are on the inside.”
4. The two machineries for batch axes#
(a) One broadcastable prefix → a leading '...' in einsum#
If all operands share one batch prefix — or one operand’s prefix is a frame that broadcasts
under another’s via frame-inner (§3) — a leading '...' handles it for free. This covers to_dense,
the gauge projections, project_*, corewise_*, tv_to_dense/_t3, orthogonalization, etc.
This is why most of the library “just works” with stacking: you write the einsum with '...' and
negative axes, and C/K/W ride along.
(b) Two independent blocks on DIFFERENT operand subsets → contractions.contract#
When two independent batch blocks live on different subsets of the operands, a single '...'
cannot express it: right-aligned broadcasting would force the two blocks to align, but they are on
different operands and must stay independent. The canonical case is probing: the core/frame stack
C (on the cores) and the probe stack W (on the probe vectors only).
So probing is built on the grouped-einsum interpreter backend.contractions.contract:
The subscripts are a standard einsum string with one extension: an UPPERCASE letter is a GROUP of zero or more axes, lowercase letters are single axes — e.g.
contract('WCa,Caib,WCi->WCb', mu, P, xi). An empty group contributes no axes, so the same call handles no-stack / one-stack / both-stack.Each group’s axis count is solved from the operand ndims (a small exact linear system). Whether the ndims can pin a split is a property of the subscripts alone — so a call site either never needs help or always does, and in the latter case
contractraises, naming exactly thelen_<G>=keyword to pass (e.g.contract('WCo,WCa->Cao', z, eta, len_W=1)). Groups that always travel together ('WKCi,Cio->WKCo'— nothing separatesWfromKanywhere) need no split at all: only their combined total matters, and any supplied split is verified but changes nothing.The groups then expand into ordinary single-axis letters and one einsum runs on the operands exactly as given — no reshape, no data movement.
Output order is frame-inner:
W(andK) outer,Cinner. E.g.'...->WCb'returnsW_shape + C_shape + (b,).
A third independent block (forward-probing a K-stacked tangent — W probes, K tangents, C
frame) is just a third capital letter in the string: contract('WKCa,Caib,WCi->WKCb', ...).
Decision rule: if your two batches are on the same operands → '...'. If they are on different
operand subsets and must remain independent → contract. (Full usage guide, with examples and the
guarantees spelled out: grouped_contractions.)
Sharding for multi-GPU: any axis of any group shards freely. contract never reshapes, so every
group sub-axis is an honest einsum axis; sharding a batch/free axis costs zero collectives, and
sharding a summed axis costs exactly the all-reduce the mathematics requires. (This is checked by
compiling every subscripts string in the library under virtual devices —
tests/test_contractions_sharding.py; the history of why it is checked, not just argued, is
contributor material: contributor/batching_internals.md.)
5. Heterogeneous-but-broadcastable tuples, and the backend/frontend split#
A subtle but pervasive pattern: a single T3’s cores may have different (but broadcastable) stacks.
The archetype is a tangent term: the frame cores carry C, but one variation core carries K + C.
Per §3 this is a deliberate, valid layout — frame-inner makes C the suffix of K + C, so the
operation broadcasts the frame over K.
The
'...'-einsum ops handle this for free (gauge,project_*). They never materialize the broadcast — they keep the frame atCand let'...'replicate it. This is the efficient pattern (no|K|copies of the frame).The one reshape-based primitive,
to_dense, is broadcast-aware viat3_broadcast_to_common_stack(backend/t3_operations.py): computenp.broadcast_shapesof all core stacks,broadcast_toeach core up, then contract. No-op for a uniform-stack T3.
Backend tuples vs frontend dataclasses — the validate asymmetry#
This is the thing that decides where broadcasting is lazy vs materialized:
Backend functions take raw
(tucker_cores, tt_cores)tuples and tolerate heterogeneous stacks. Heterogeneous-but-broadcastable tuples are first-class there.Frontend dataclasses validate a UNIFORM stack.
TuckerTensorTrain.validate()(andT3Frame/T3Variations) hard-require every core to share onestack_shape. So any builder that produces a class instance must materialize the broadcast to uniform:tv_to_t3→ must broadcast frame→K+Cbefore concatenating the doubled-rank cores (its output is a validatedTuckerTensorTrain;[U_i ; V_i]is one array).frontend
bvf.fv_to_t3→ mustt3_broadcast_to_common_stackthe mixed-stack term before wrapping inTuckerTensorTrain.whereas
to_denseproduces a bare array, so it can broadcast lazily inside the contraction.
Rule of thumb: bare-array output → broadcast lazily; validated-class-instance output → materialize the broadcast to a uniform stack.
6. The K/C split is recovered, not stored#
T3Tangent bundles a T3Frame (stack C) with a T3Variations (stack K + C). The split is
derived, never stored as a field (minimal dataclasses):
frame_stack_shape(C)= frame.stack_shapestack_shape(K + C)= variations.stack_shapetangent_stack_shape(K)=the part ofvariations.stack_shapethat exceedsframe.stack_shape— i.e.Cis the trailing suffix ofK + C, andKis the leading remainder.
check_fv_pair enforces exactly this: frame.stack_shape must be the trailing (inner) suffix of
variations.stack_shape. (It does not require equality — that was the pre-K-stack invariant.)
T3FrameWeights is frame-like, and follows the same rule. A tangent metric
(weighting.md) is one metric per base point, so it carries C — not K + C — even
though it is absorbed into the variations. Where it batches and what it acts on are different questions;
conflating them is what put an over-strict K + C stack check on the weight predicate until it was fixed.
So pairing a weight with variations uses the trailing-suffix rule (fv_weights_consistent, blind to the
frame like the variations themselves), and check_fw_pair — the weight↔frame analog of check_fv_pair —
enforces the exact weights.stack_shape == frame.stack_shape wherever the frame is present.
Because the split is not recoverable from a bare tree of objects, the two-axis stack/unstack (§ below) must name which stack it peels.
Two-axis stack/unstack on T3Tangent#
A single monolithic stack/unstack cannot faithfully invert two stacks (the K/C split isn’t in
a bare tree). So T3Tangent has two explicit pairs, each peeling one named stack:
unstack_tangents/stack_tangents— peel the tangent stackK. Yields aK-shaped tree of tangents that share the frame (sameT3Frameobject → same tangent space → mutually linalg-compatible). “For each vector within the frame.”stack_tangentsguards that all leaves share the same frame (the same-frame check, as ininner/+).unstack_frame/stack_frame— peel the frame stackC. Yields aC-shaped tree of single-base-point tangents at distinct bases (distinct tangent spaces). “For each frame.”stack_frameplacesCinnermost (variation stack →K + C), which needs interior-axis stacking that the componentT3Variations.stackcan’t do — hence it is a real op, not user-assembled.
T3Frame/T3Variations keep their single plain stack/unstack (each is a single-stack object).
Backend functionals (tv_unstack_tangent_stack/tv_stack_tangent_stack/tv_unstack_frame_stack/
tv_stack_frame_stack in tv_operations.py) do the array/tree work; the T3Tangent methods are
thin wrappers doing the compatibility checks.
7. jax pytrees and vmap — how batching meets autodiff#
The frozen dataclasses are registered jax pytrees (if jax_available: at the end of each module), so they
can be jit/vmap/grad-ed:
TuckerTensorTrain,T3Frame,T3Variations: leaves = the cores (x.data), no aux.T3Tangent: the frame AND the variations are leaves (no aux). Both the fixed frame and the moving tangent vector flow as traced data. Consequences:vmapover aT3Tangentmaps over both the frame and variation leaves. For the batch-of-tangents-sharing-one-frame picture (vmap-over-K, frame fixed), close the frame over and map a function of the variations:vmap(lambda v: f(T3Tangent(frame, v))). TheK-stacked forward probe does not usevmapinternally — it uses the genuine 3-block (W,K,C) contractions (§4): consistent with thecontractinterpreter (the blessed mechanism for independent blocks), no PythonKloop on the numpy path, and low-level einsums fold into XLA at least as well as avmap(which can add layout/transpose churn over a long function).The same-frame guard is NUMERICAL (
safety.frames_equal), not object identity. Two tangents share a tangent space iff their frame cores are value-equal (with anis-identity fast path); the check is safe-mode + eager-only (skips undersafety.unsafe()/ any trace). So it survives a jit round-trip (a reconstructed, value-equal frame passes instead of false-failing) andinner/+jit cleanly.Payoff: NO per-frame recompile. Because the frame is a leaf (traced data, not a compile-time aux constant), a tangent or
GaussNewtonModelcrossing ajitboundary compiles once across all bases (traces=1) — you jit the frontend matvec directly. To grad w.r.t. the variations only, close the frame over; grad-w.r.t.-the-frame is also available (the frame is a leaf).T3Frame/T3Tangentare@dataclass(frozen=True, eq=False): value hash/eq on ndarray fields is ambiguous, so they stay identity-hashed — but the frame is a leaf-bearing pytree node, notaux_data.
The uniform layer does not
vmap— by construction, and loudly. A stackedUniformTuckerTensorTraincannot bevmap-ed over its stack with anyin_axes: axis 0 slices the mode axisd(the supercore layout is(d,) + stack_shape + (...)) andvalidaterejects the wreckage; axis 1 — the actual stack axis — also fails, because the rank masks ride as staticaux_datawith their own per-element stack axes andvmapcannot slice aux. Every route errors at validation; nothing convention-violating comes out. This is not a gap to fix: batching over uniform objects is what the nativeCstack with packed contractions is for. The one legitimatevmappattern is at the raw-supercore level — map over the stack axis of the bare arrays and construct an unstacked object inside with the (shared) unstacked masks. Ragged objectsvmapfine with default axes (their cores carry the stack leading).
vmap/jit invocation across the ops is checked cheaply in tests/test_dispatch.py (a jit-compile
of an op proves no hidden numpy — a stray np.* on a tracer raises).
8. Decoding the names (so you can read the code)#
The naming scheme encodes axis layout — once you know it, the einsums read themselves.
Body locals suffix the axis layout:
C_aib,mu_WCa,B0_b_j_c,xi_WCp. Capitals = grouped index blocks (C/W/K), lowercase = single axes, a leadingd= a stacked/derivative (uniform supercore) axis.apply/entriesuse the sameW(vec/index stack) andC(core stack) as everywhere else.Grouped contractions are
contract('WCa,Caib,WCi->WCb', ...)calls: read each term as a grouped-block einsum signature; output is frame-inner (Wouter,Cinner).Paper ↔ code (Appendix A of the T4S paper):
U=up_tucker,P=left_tt,Q=right_tt,O=down_tt (calleddown_tt_cores, not “outer”);δU=tucker_variations (V),δG=tt_variations (H). The block lettersW/K/Care deliberately disjoint from these core/variation symbols, soVhere means only the Tucker variation andGonly the TT core — no overload.
9. Gotchas (the ones that have actually bitten)#
corewise_dot/corewise_normcollapse EVERY axis (stacks included) to a scalar. To keep the stack (vectorized linalg), usecorewise.corewise_stack_dot(X, Y, n_stack).Validity checkers and residuals are per-stack-element.
is_orthogonal,is_gauged,is_consistent,is_left/right_orthogonal,allclose, and the*_residualproperties reduce over the non-stack axes and return an array of shapestack_shape— a scalar when unstacked, an array when stacked (stack elements can differ: e.g.frame.is_orthogonal()on a(2,)stack →[ True False]). Reduce with.all()for one verdict; the safe-mode preconditions do exactly this at the call site, so they require all stack elements to pass (seedocs/numerical_contracts.md). One asymmetry worth knowing: the structuralhas_minimal_ranksis a scalar in the ragged layer (one core shape ⇒ ranks are shared across the stack, so there is nothing per-element to report) but per-element in the uniform layer (ranks vary per stack element — the determinantal variety,docs/uniform_ranks_and_varieties.md); the numerical checkers are per-element in both layers.to_denseand the doubled-rankto_t3are not symmetric about broadcasting —to_densebroadcasts lazily (bare array out),to_t3materializes (validated class out). See §5.apply/entriesareW+C(vec/index stack OUTER, T3 stack INNER), like everything else.Canonical core-tuple orderings (frontend takes precedence):
TuckerTensorTrain.data = (tucker_cores, tt_cores);T3Frame.data = (up, down, left, right);T3Variations.data = (tucker_variations, tt_variations);(frame, variations)pairs are frame-first. Pass.datastraight through; do not reorder.
10. Where to look (file/function map)#
Concern |
Look at |
|---|---|
The |
|
The grouped-einsum interpreter ( |
|
Probing (2-block |
|
Tree ↔ stacked-object (meaning 2) |
|
Two-axis stack/unstack |
|
The |
|
jax pytree registration + |
bottom of |
The validate/uniform-stack requirement |
|
11. Transposes, adjoints, and sum_over_probes#
Every transpose in the library — probe_transpose, and the apply/entries adjoints on both
T3Tangent and TuckerTensorTrain — takes a sum_over_probes flag. This is the one
place where the probe stack W does something subtle, so here is the whole story.
The mental model#
The atomic operation is single-probe: one set of vectors ww, one residual. Everything batched is
that atom lifted over the stacks W/K/C (§2). The transpose lifts the same way:
sum_over_probes=False(default) is the primary transpose. It lifts the atomic adjoint overW, soWstays a passthrough stacking axis — one tangent (or tensor) per probe. This is the plain Jacobian-transpose; it is well-defined on its own, with no reference to summing. HereWbehaves exactly likeCandK: unstack → transpose each → restack.sum_over_probes=Trueis a derived convenience that additionally contractsW:sum_over_probes=True == (sum over the W axes of sum_over_probes=False)
It exists because, in optimization, the
Wprobes are the outputs of one operator on one shared input — the forward broadcasts that single input across allWprobes, and the transpose of a broadcast is a sum. Summing the per-probe contributions gives the one gradient / back-projectionJᵀr.
The trap (which has caught the authors): do not read True as “the real transpose” and False as
a special case. It is the reverse — False is the fundamental object, True is Σ_W applied on top.
Which mode do I want?#
Using
JandJᵀas standalone linear operators, or you want one output per probe →False(default).Assembling a gradient
g = Jᵀr, or a Gauss-Newton Hessian applyJᵀJ vin an optimizer (one residual vector over theWdata points → one gradient) →True.
Either way the two agree by the invariant above, so when in doubt use False and sum the W axes
yourself once you actually need the gradient.
Shape contract#
The residual lives in the forward’s output space; the transpose maps it back. False keeps W as
an output stack; True sums W away; K and C always pass through.
transpose |
residual in |
|
|
|---|---|---|---|
|
|
tangent stack |
tangent stack |
|
|
tangent stack |
tangent stack |
|
|
CP factors, stack |
CP factors, stack |
|
|
core grads, stack |
core grads, stack |
The apply/entries adjoints accept a K block in the residual too — it is optional, so a residual
of shape W + C and one of shape W + K + C are both valid. Their forward outputs
are W + K + C (tangent) and W + C (plain), so the residual-in column is just “the forward output,
adjoint-mapped back.”