A Cartesian tensor of rank can be split into irreducible Cartesian tensors (ICTs) Wen, 2026. An ICT is a tensor that is symmetric and traceless. An ICT of rank , called its weight, has independent components: a scalar for , a vector for , a symmetric traceless matrix for , and so on. The weights of the ICTs of run from 0 to , and a weight can occur more than once; each occurrence is a channel, numbered . The tensor is a sum of parts, one for each ICT , and two operators move between the tensor and its ICTs:
the extraction operator takes the tensor to an ICT, ;
the embedding operator takes the ICT back to the part of the tensor it carries, .
Summed over all weights and channels, the parts give back . natto builds
both operators for every weight and channel, keyed by (ell, p).
Dual basis¶
By default are the mapping tensors of the paper and their duals, both exact. We start with a rank-2 tensor and build its operators:
import numpy as np
from natto import act, get_embedding_operators, get_extraction_operators
T = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 12.0]])
G = get_embedding_operators(n=2)
G_tilde = get_extraction_operators(n=2)
print(f"channels (ell, p): {list(G)}")channels (ell, p): [(0, 1), (1, 1), (2, 1)]
G and G_tilde are dictionaries with one operator per ICT, keyed by (ell, p):
ell is the weight of the ICT, and p numbers the channels of that weight, counting
from 1. A rank-2 tensor has components, which split as : one
scalar, one vector and one symmetric traceless matrix. So each of the weights 0, 1 and
2 occurs once, and p is always 1. At higher rank a weight can occur several times,
as shown below.
Each operator prints as a sum of Kronecker deltas and Levi-Civita symbols. Lower-case letters are the indices of the ICT, and upper-case letters those of the tensor. For weight 1:
print(f"G~ of weight 1: {G_tilde[1, 1]}")
print(f"G of weight 1: {G[1, 1]}")G~ of weight 1: +1/2 ε_ABa
G of weight 1: +1 ε_ABa
act applies an operator to its input. takes T to its weight-1 ICT, a
vector, and takes that vector back to the weight-1 part of T, its antisymmetric
part:
X = act(G_tilde[1, 1], T)
T_1 = act(G[1, 1], X)
print(f"X, the weight-1 ICT of T:\n{X}\n")
print(f"G · X, the weight-1 part of T:\n{T_1}")X, the weight-1 ICT of T:
[-1. 2. -1.]
G · X, the weight-1 part of T:
[[ 0. -1. -2.]
[ 1. 0. -1.]
[ 2. 1. 0.]]
Arrays and einsum rules¶
Internally, act turns the operator into a numerical array and contracts it with the
input by numpy.einsum. evaluate gives you both pieces: the array, and the einsum
rule that contracts it with the input. That is useful to store the array, or to apply
one operator to many tensors without evaluating it again.
from natto import evaluate
G_tilde_array, rule = evaluate(G_tilde[1, 1])
X = np.einsum(rule, G_tilde_array, T)
print(f"einsum rule: {rule}\n")
print(f"G~ of weight 1 as an array, shape {G_tilde_array.shape}:\n{G_tilde_array}\n")
print(f"X, the weight-1 ICT of T:\n{X}")einsum rule: aAB,...AB->...a
G~ of weight 1 as an array, shape (3, 3, 3):
[[[ 0. 0. 0. ]
[ 0. 0. 0.5]
[ 0. -0.5 0. ]]
[[ 0. 0. -0.5]
[ 0. 0. 0. ]
[ 0.5 0. 0. ]]
[[ 0. 0.5 0. ]
[-0.5 0. 0. ]
[ 0. 0. 0. ]]]
X, the weight-1 ICT of T:
[-1. 2. -1.]
X is the same ICT as with act. In the rule, a is the index of the
ICT and A, B those of the tensor, in the order of the array’s axes. The ... lets T carry leading batch axes: a stack of
tensors of shape (m, 3, 3) gives a stack of m ICTs.
Several channels of one weight¶
A rank-3 tensor has components, which split as
: one ICT of weight 0, three of weight 1, two of
weight 2 and one of weight 3. Weight 1 therefore has three channels, p = 1, 2, 3, and
weight 2 has two:
G_tilde = get_extraction_operators(n=3)
print(f"channels (ell, p): {list(G_tilde)}")channels (ell, p): [(0, 1), (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (3, 1)]
The three channels of weight 1 are three different vectors a rank-3 tensor carries,
roughly one for each pair of its indices that can be contracted away, and each has its
own extraction and embedding operator. Every builder also takes ell, to build the
operators of one weight only:
G_tilde = get_extraction_operators(n=3, ell=1)
print(f"channels (ell, p): {list(G_tilde)}")channels (ell, p): [(1, 1), (1, 2), (1, 3)]
Orthonormal basis¶
With basis="orthonormal", the mapping tensors of each weight are orthonormalized,
with their Gram matrix. An orthonormal mapping tensor is
its own dual, so the same both extracts and embeds; the extraction and
embedding operators of a channel share one array and differ only in their rule. The
orthonormalization is irrational in general, so these operators have float
coefficients and no printed form, and are used through act or evaluate:
G_hat = get_embedding_operators(n=2, basis="orthonormal")
G_hat_tilde = get_extraction_operators(n=2, basis="orthonormal")
X = act(G_hat_tilde[1, 1], T)
T_1 = act(G_hat[1, 1], X)
print(f"X in the orthonormal basis:\n{X}\n")
print(f"G^ · X, the weight-1 part of T:\n{T_1}")X in the orthonormal basis:
[-1.41421356 2.82842712 -1.41421356]
G^ · X, the weight-1 part of T:
[[ 0. -1. -2.]
[ 1. 0. -1.]
[ 2. 1. 0.]]
Compare with the dual basis: the ICT is times the dual one,
, since the two bases scale the mapping tensor differently, but the part
of T it carries is the same antisymmetric matrix.
Intrinsic symmetry¶
A tensor with an intrinsic symmetry carries fewer channels, and symmetry builds the
operators of its class. The symmetry is written as index equalities: "ij=ji" is a
symmetric rank-2 tensor, "ij=-ji" an antisymmetric one, and "ijkl=jikl=klij" the
elasticity tensor.
A symmetric rank-2 tensor has no antisymmetric part, so of the channels of a general rank-2 tensor, weight 1 is gone:
S = (T + T.T) / 2
G_tilde = get_extraction_operators(n=2, symmetry="ij=ji")
print(f"channels (ell, p): {list(G_tilde)}\n")
print(f"G~ of weight 0: {G_tilde[0, 1]}")
print(f"X, the weight-0 ICT of S: {act(G_tilde[0, 1], S)}")channels (ell, p): [(0, 1), (2, 1)]
G~ of weight 0: +1/3 δ_AB
X, the weight-0 ICT of S: 6.0
- Wen, M. (2026). Reusable Operators for Irreducible Cartesian Tensor Decomposition and Coupling. arXiv Preprint arXiv:2609.05971. 10.48550/arXiv.2609.05971