contract#
- t3toolbox.backend.contractions.contract(subscripts, *operands, **group_lens)#
def contract( subscripts: str, # grouped einsum, e.g. 'WCa,Caib,WCi->WCb': UPPERCASE = a group of # ZERO OR MORE axes; lowercase = one axis. Explicit '->' required. *operands: NDArray, # one array per comma-separated input term **group_lens: int, # len_<G> (e.g. len_W=2) = axis count of group <G>. Required only # when the subscripts cannot pin it; verified when redundant. ) -> NDArray:
Grouped einsum: an einsum whose UPPERCASE letters each stand for a GROUP of zero or more axes.
The group sizes are solved from the operand ndims, every group is expanded into ordinary single-axis letters, and one einsum runs on the operands exactly as given – no reshape, no data movement, every axis of every group independently shardable. Dispatch follows the house convention (inferred numpy/jax; numpy uses the greedy pairwise BLAS path, jax one fused einsum).
Full usage guide with examples:
docs/grouped_contractions.md; block-letter conventions (Wprobe stack,Ktangent stack,Cframe stack, frame-inner order) are indocs/batching_and_stacking.md; implementation internals:docs/contributor/contractions_internals.md. Group axes follow einsum’s usual extent rules: mismatched sizes for the same group axis raise, except that a size-1 axis broadcasts (standard einsum semantics on both backends).A batched matvec where the matrix batch
Cis shared and the vector batchWrides:>>> import numpy as np >>> from t3toolbox.backend.contractions import contract >>> A = np.random.default_rng(0).standard_normal((2, 4, 3)) # C=(2,), axes (i, o) >>> x = np.random.default_rng(1).standard_normal((5, 2, 3)) # W=(5,), C=(2,), axis o >>> y = contract('Cio,WCo->WCi', A, x) >>> y.shape (5, 2, 4) >>> bool(np.allclose(y, np.einsum('cio,wco->wci', A, x))) # |W|=|C|=1 by hand True
The SAME string handles any group sizes, including empty (a group of zero axes just vanishes):
>>> contract('Cio,WCo->WCi', np.ones((4, 3)), np.ones(3)).shape # W=(), C=() (4,) >>> contract('Cio,WCo->WCi', np.ones((2, 2, 4, 3)), np.ones((5, 6, 2, 2, 3))).shape (5, 6, 2, 2, 4)
When the ndims cannot pin a split, say which axes are which via
len_<G>– the error says exactly what is missing (and this is decided from the subscripts alone, never from the shapes, so a call site either always needs the argument or never does):>>> zo = np.ones((5, 2, 3)); za = np.ones((5, 2, 6)) # W=(5,), C=(2,) >>> contract('WCo,WCa->Cao', zo, za) Traceback (most recent call last): ... ValueError: contract('WCo,WCa->Cao'): the operand ndims do not determine ... >>> contract('WCo,WCa->Cao', zo, za, len_W=1).shape # sum over the probe stack W (2, 6, 3)