| name | cuequivariance |
| description | Define custom groups (Irrep subclasses), build segmented tensor products with CG coefficients, create equivariant polynomials and IrDictPolynomials, and use built-in descriptors (linear, tensor products, spherical harmonics). Use when working with cuequivariance group theory, irreps, or segmented polynomials. |
cuequivariance: Groups, Irreps, and Segmented Polynomials
Overview
cuequivariance (imported as cue) provides two core abstractions:
- Group theory:
Irrep subclasses define irreducible representations of Lie groups (SO3, O3, SU2, or custom). Irreps manages collections with multiplicities.
- Segmented polynomials:
SegmentedTensorProduct describes tensor contractions over segments of varying shape, linked by Path objects carrying Clebsch-Gordan coefficients. SegmentedPolynomial wraps multiple STPs into a polynomial with named inputs/outputs. Two higher-level wrappers attach group representations:
EquivariantPolynomial — dense operands with IrrepsAndLayout metadata
IrDictPolynomial — operands already split by irrep, with per-group Irreps metadata for the dict[Irrep, Array] workflow
Defining a custom group
Subclass cue.Irrep (a frozen dataclass) and implement:
from __future__ import annotations
import dataclasses
import re
from typing import Iterator
import numpy as np
import cuequivariance as cue
@dataclasses.dataclass(frozen=True)
class Z2(cue.Irrep):
odd: bool
@classmethod
def regexp_pattern(cls) -> re.Pattern:
return re.compile(r"(odd|even)")
@classmethod
def from_string(cls, string: str) -> Z2:
return cls(odd=string == "odd")
def __repr__(rep: Z2) -> str:
return "odd" if rep.odd else "even"
def __mul__(rep1: Z2, rep2: Z2) -> Iterator[Z2]:
return [Z2(odd=rep1.odd ^ rep2.odd)]
@classmethod
def clebsch_gordan(cls, rep1: Z2, rep2: Z2, rep3: Z2) -> np.ndarray:
if rep3 in rep1 * rep2:
return np.array([[[[1]]]])
else:
return np.zeros((0, 1, 1, 1))
@property
def dim(rep: Z2) -> int:
return 1
def __lt__(rep1: Z2, rep2: Z2) -> bool:
return rep1.odd < rep2.odd
@classmethod
def iterator(cls) -> Iterator[Z2]:
for odd in [False, True]:
yield Z2(odd=odd)
def discrete_generators(rep: Z2) -> np.ndarray:
if rep.odd:
return -np.ones((1, 1, 1))
else:
return np.ones((1, 1, 1))
def continuous_generators(rep: Z2) -> np.ndarray:
return np.zeros((0, rep.dim, rep.dim))
def algebra(self) -> np.ndarray:
return np.zeros((0, 0, 0))
irreps = cue.Irreps(Z2, "3x odd + 2x even")
Required methods summary
| Method | Returns | Purpose |
|---|
regexp_pattern() | re.Pattern | Parse string like "1", "0e", "odd" |
from_string(s) | Irrep | Construct from matched string |
__repr__ | str | Canonical string form |
__mul__(a, b) | Iterator[Irrep] | Selection rule for tensor product |
clebsch_gordan(a, b, c) | ndarray (n, d1, d2, d3) | CG coefficients |
dim (property) | int | Dimension of representation |
__lt__(a, b) | bool | Ordering (dimension first, then custom) |
iterator() | Iterator[Irrep] | All irreps, trivial first |
continuous_generators() | ndarray (lie_dim, dim, dim) | Lie algebra generators |
discrete_generators() | ndarray (n, dim, dim) | Finite symmetry generators |
algebra() | ndarray (lie_dim, lie_dim, lie_dim) | Structure constants |
Built-in groups
cue.SO3(l): 3D rotations. l is a non-negative integer. dim = 2l+1. String: "0", "1", "2".
cue.O3(l, p): 3D rotations + parity. p=+1 (even) or p=-1 (odd). String: "0e", "1o", "2e".
cue.SU2(j): Spin group. j is a non-negative half-integer. String: "0", "1/2", "1".
Irreps and layout
irreps = cue.Irreps("SO3", "16x0 + 4x1 + 2x2")
irreps.dim
for mul, ir in irreps:
print(mul, ir, ir.dim)
IrrepsLayout controls memory order within each (mul, ir) block:
cue.ir_mul: data ordered as (ir.dim, mul) — used by all descriptors and ir_dict internally
cue.mul_ir: data ordered as (mul, ir.dim) — used by nnx dict[Irrep, Array] and PyTorch
IrrepsAndLayout combines irreps with a layout into a Rep:
rep = cue.IrrepsAndLayout(cue.Irreps("SO3", "4x0 + 2x1"), cue.ir_mul)
rep.dim
Building a SegmentedTensorProduct from scratch
The subscripts string uses Einstein notation. Operands are comma-separated, coefficient modes follow +.
d = cue.SegmentedTensorProduct.from_subscripts("ij,j,i")
d.add_segment(0, (3, 4))
d.add_segment(1, (4,))
d.add_segment(2, (3,))
d.add_path(0, 0, 0, c=1.0)
poly = cue.SegmentedPolynomial.eval_last_operand(d)
[y] = poly(M_flat, x)
Multi-segment STP (how descriptors work internally)
Descriptors build STPs with multiple segments per operand. Each segment corresponds to an irrep block:
d = cue.SegmentedTensorProduct.from_subscripts("uv,iu,iv")
s_in_0 = d.add_segment(1, (3, 2))
s_out_0 = d.add_segment(2, (3, 5))
d.add_path((2, 5), s_in_0, s_out_0, c=1.0)
s_in_1 = d.add_segment(1, (1, 4))
s_out_1 = d.add_segment(2, (1, 3))
d.add_path((4, 3), s_in_1, s_out_1, c=1.0)
Weights operand
For weighted tensor products (subscript starting with uvw or uv), the first operand is always weights. The weight segment shape is (mul_1, mul_2, ...) matching the multiplicity modes. The weights operand gets new_scalars() irreps since weights are invariant.
CG coefficients as path coefficients
d = cue.SegmentedTensorProduct.from_subscripts("uvw,iu,jv,kw+ijk")
for cg in cue.clebsch_gordan(ir1, ir2, ir3):
d.add_path((mul1, mul2, mul3), seg_in1, seg_in2, seg_out, c=cg)
Descriptors
All descriptors come in two variants:
- Original — returns
EquivariantPolynomial with dense operands
_ir_dict — returns IrDictPolynomial with operands already split by irrep
EquivariantPolynomial descriptors
e = cue.descriptors.fully_connected_tensor_product(
16 * cue.Irreps("SO3", "0 + 1 + 2"),
16 * cue.Irreps("SO3", "0 + 1 + 2"),
16 * cue.Irreps("SO3", "0 + 1 + 2"),
)
e = cue.descriptors.channelwise_tensor_product(
64 * cue.Irreps("SO3", "0 + 1"), cue.Irreps("SO3", "0 + 1"),
cue.Irreps("SO3", "0 + 1"), simplify_irreps3=True,
)
e = cue.descriptors.full_tensor_product(
cue.Irreps("SO3", "2x0 + 1x1"), cue.Irreps("SO3", "0 + 1"),
)
e = cue.descriptors.elementwise_tensor_product(
cue.Irreps("SO3", "4x0 + 4x1"), cue.Irreps("SO3", "4x0 + 4x1"),
)
e = cue.descriptors.linear(
cue.Irreps("SO3", "4x0 + 2x1"),
cue.Irreps("SO3", "3x0 + 5x1"),
)
e = cue.descriptors.spherical_harmonics(cue.SO3(1), [0, 1, 2, 3])
e = cue.descriptors.symmetric_contraction(
64 * cue.Irreps("SO3", "0 + 1 + 2"),
64 * cue.Irreps("SO3", "0 + 1"),
(1, 2, 3),
)
IrDictPolynomial descriptors
Each _ir_dict variant returns an IrDictPolynomial whose polynomial is already split by irrep. The input_irreps and output_irreps tuples describe the operand groups.
desc = cue.descriptors.channelwise_tensor_product_ir_dict(
64 * cue.Irreps("SO3", "0 + 1"),
cue.Irreps("SO3", "0 + 1"),
cue.Irreps("SO3", "0 + 1"),
)
desc = cue.descriptors.fully_connected_tensor_product_ir_dict(irreps1, irreps2, irreps3)
desc = cue.descriptors.full_tensor_product_ir_dict(irreps1, irreps2)
desc = cue.descriptors.elementwise_tensor_product_ir_dict(irreps1, irreps2)
desc = cue.descriptors.linear_ir_dict(irreps_in, irreps_out)
desc = cue.descriptors.spherical_harmonics_ir_dict(cue.O3(1, -1), [0, 1, 2, 3])
desc = cue.descriptors.symmetric_contraction_ir_dict(irreps_in, irreps_out, (1, 2, 3))
IrDictPolynomial
IrDictPolynomial pairs a SegmentedPolynomial (already split by irrep) with the Irreps that describe each operand group.
desc = cue.descriptors.channelwise_tensor_product_ir_dict(
32 * cue.Irreps("SO3", "0 + 1"),
cue.Irreps("SO3", "0 + 1"),
cue.Irreps("SO3", "0 + 1"),
)
desc.polynomial
desc.input_irreps
desc.output_irreps
scaled_poly = desc.polynomial * 0.5
for i, op in enumerate(desc.polynomial.inputs):
print(f"Input {i}: size={op.size}, num_segments={op.num_segments}")
Contract: for each (mul, ir) block in input_irreps / output_irreps, the corresponding polynomial operand has size mul * ir.dim.
split_polynomial_by_irreps
The low-level function underlying _ir_dict descriptors. Splits one polynomial operand at irrep boundaries:
poly = e.polynomial
poly = cue.split_polynomial_by_irreps(poly, 2, irreps_sh)
poly = cue.split_polynomial_by_irreps(poly, 1, irreps_in)
poly = cue.split_polynomial_by_irreps(poly, -1, irreps_out)
EquivariantPolynomial key methods
e.inputs
e.outputs
e.polynomial
[out] = e(weights, input1, input2)
e_ready = e.squeeze_modes().flatten_coefficient_modes()
e_split = e.split_operand_by_irrep(1).split_operand_by_irrep(-1)
e_scaled = e * 0.5
e_fused = e.fuse_stps()
normalize_paths_for_operand
Called internally by descriptors. Normalizes path coefficients so that a random input produces unit-variance output for the specified operand. Critical for numerical stability.
SegmentedPolynomial structure
poly = e.polynomial
poly.num_inputs
poly.num_outputs
poly.inputs
poly.outputs
poly.operations
for op, stp in poly.operations:
print(op.buffers)
print(stp.subscripts)
SegmentedOperand
operand = poly.inputs[0]
operand.num_segments
operand.segments
operand.size
operand.ndim
operand.all_same_segment_shape()
operand.segment_shape
Custom equivariant polynomial from scratch
import numpy as np
import cuequivariance as cue
cg = cue.clebsch_gordan(cue.SO3(1), cue.SO3(1), cue.SO3(0))
d = cue.SegmentedTensorProduct.from_subscripts("uvw,iu,jv,kw+ijk")
d.add_segment(1, (3, 4))
d.add_segment(2, (3, 4))
d.add_segment(3, (1, 16))
for c in cg:
d.add_path((4, 4, 16), 0, 0, 0, c=c)
d = d.normalize_paths_for_operand(-1)
poly = cue.SegmentedPolynomial.eval_last_operand(d)
ep = cue.EquivariantPolynomial(
[
cue.IrrepsAndLayout(cue.Irreps("SO3", "4x1").new_scalars(d.operands[0].size), cue.ir_mul),
cue.IrrepsAndLayout(cue.Irreps("SO3", "4x1"), cue.ir_mul),
cue.IrrepsAndLayout(cue.Irreps("SO3", "4x1"), cue.ir_mul),
],
[cue.IrrepsAndLayout(cue.Irreps("SO3", "16x0"), cue.ir_mul)],
poly,
)
w = np.random.randn(ep.inputs[0].dim)
x = np.random.randn(ep.inputs[1].dim)
y = np.random.randn(ep.inputs[2].dim)
[out] = ep(w, x, y)
Key file locations
| Component | Path |
|---|
Irrep base class | cuequivariance/group_theory/representations/irrep.py |
Rep base class | cuequivariance/group_theory/representations/rep.py |
SO3 | cuequivariance/group_theory/representations/irrep_so3.py |
O3 | cuequivariance/group_theory/representations/irrep_o3.py |
SU2 | cuequivariance/group_theory/representations/irrep_su2.py |
Irreps | cuequivariance/group_theory/irreps_array/irreps.py |
IrrepsLayout | cuequivariance/group_theory/irreps_array/irreps_layout.py |
IrrepsAndLayout | cuequivariance/group_theory/irreps_array/irreps_and_layout.py |
SegmentedTensorProduct | cuequivariance/segmented_polynomials/segmented_tensor_product.py |
SegmentedPolynomial | cuequivariance/segmented_polynomials/segmented_polynomial.py |
EquivariantPolynomial | cuequivariance/group_theory/equivariant_polynomial.py |
IrDictPolynomial | cuequivariance/group_theory/ir_dict_polynomial.py |
| Descriptors | cuequivariance/group_theory/descriptors/ |
| Tensor product descriptors | cuequivariance/group_theory/descriptors/irreps_tp.py |
spherical_harmonics | cuequivariance/group_theory/descriptors/spherical_harmonics_.py |
symmetric_contraction | cuequivariance/group_theory/descriptors/symmetric_contractions.py |