Equivariant API Reference¶
Native E(3)-equivariant algebra and neural-network primitives used by the atomistic and quantum-chemistry models (SchNet, PaiNN, NequIP, QHNet). Provides irreducible representations, tensor products, gated nonlinearities, spherical harmonics, radial bases, and graph utilities.
Core algebra (irreps, tensor products, gates)¶
Native E(3)-equivariant building-block library (irreps, Clebsch-Gordan, etc.).
A dependency-free, JAX/Flax-NNX-native reimplementation of the equivariant
primitives used by e3nn/e3nn-jax (Geiger & Smidt 2022, arXiv:2207.09453),
shared across opifex's quantum-SciML families (interatomic potentials,
equivariant Hamiltonian prediction).
CartesianLinear
¶
Bases: Module
Equivariant channel-mixing of stacked rank-2 Cartesian tensors.
Given an input of shape (..., in_channels, 3, 3) the layer produces
(..., out_channels, 3, 3) by mixing channels separately within each
irreducible Cartesian part (scalar / vector / symmetric-traceless). Because
each part carries a single O(3) irrep, an arbitrary learnable mixing over
the channel axis -- applied independently per part -- commutes with the
rotation action X -> R X R^T and is therefore equivariant (TensorNet uses
the same per-irrep linear mixing, arXiv:2306.06482 Eq. 6).
The three parts get independent (in_channels, out_channels) weight
matrices, so the layer is the Cartesian analogue of
:class:~opifex.neural.equivariant.EquivariantLinear.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
in_channels
|
int
|
Number of input Cartesian-tensor channels. |
required |
out_channels
|
int
|
Number of output channels. |
required |
rngs
|
Rngs
|
Random number generators (keyword-only); |
required |
CartesianTensor
¶
A rank-2 Cartesian tensor feature transforming as X -> R X R^T.
The underlying array has shape (..., 3, 3) (arbitrary leading / channel
dimensions). The object is a pytree whose single child is the array, so it
flows through jit / grad / vmap unchanged. This is the
TensorNet (arXiv:2306.06482) representation: an l <= 2 equivariant
feature stored as one Cartesian tensor rather than a spherical-harmonic
:class:~opifex.neural.equivariant.IrrepsArray.
rotate
¶
rotate(rotation: Float[Array, '3 3']) -> CartesianTensor
Apply the equivariant action X -> R X R^T for a rotation R.
tensor_product
¶
tensor_product(other: CartesianTensor) -> CartesianTensor
Equivariant tensor product via the matrix product X @ Y.
Equivariant because (R X R^T)(R Y R^T) = R (X Y) R^T (TensorNet,
Eq. 5). No Clebsch-Gordan tensor is required.
decompose
¶
decompose() -> tuple[Float[Array, ''], Float[Array, '... 3'], Float[Array, '... 3 3']]
Decompose into irreducible Cartesian parts (scalar, vector, symmetric).
Following TensorNet Eq. 1-4:
scalar-- the tracetr(X)(anl = 0invariant);vector-- the axial vector dual to the antisymmetric part(X - X^T)/2(anl = 1feature, rotating byR);symmetric-- the symmetric-traceless part(X + X^T)/2 - tr(X)/3 * I(anl = 2feature, rotating byR . R^T).
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
A |
Float[Array, '... 3']
|
|
from_parts
classmethod
¶
from_parts(scalar: Float[Array, ''], vector: Float[Array, '... 3'], symmetric: Float[Array, '... 3 3']) -> CartesianTensor
Assemble a Cartesian tensor from its irreducible parts.
Inverse of :meth:decompose: X = scalar/3 * I + A(vector) + symmetric
where A(vector) is the antisymmetric matrix with axial vector
vector and symmetric is the symmetric-traceless l = 2 part.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scalar
|
Float[Array, '']
|
The |
required |
vector
|
Float[Array, '... 3']
|
The |
required |
symmetric
|
Float[Array, '... 3 3']
|
The |
required |
Returns:
| Type | Description |
|---|---|
CartesianTensor
|
The reconstructed :class: |
tree_flatten
¶
Pytree flatten: the array is the single child, no aux-data.
tree_unflatten
classmethod
¶
tree_unflatten(_aux_data: None, children: tuple[Float[Array, '... 3 3']]) -> CartesianTensor
Pytree unflatten, bypassing shape re-validation for traced leaves.
Gate
¶
Gate(irreps_in: Irreps | str, *, even_act: Callable[[Array], Array] = gelu, odd_act: Callable[[Array], Array] = soft_odd, gate_act: Callable[[Array], Array] = sigmoid)
Bases: Module
Configured wrapper around :func:gate for use in module stacks.
Stores the activation choices (and validates the input irreps on call) so the
gate can be dropped into an nnx module sequence like a layer. It has no
learnable parameters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in
|
Irreps | str
|
Expected input layout (validated on call). |
required |
even_act
|
Callable[[Array], Array]
|
Activation for even scalars. Default :func: |
gelu
|
odd_act
|
Callable[[Array], Array]
|
Activation for odd scalars. Default :func: |
soft_odd
|
gate_act
|
Callable[[Array], Array]
|
Activation for the gate scalars. Default
:func: |
sigmoid
|
NormGate
¶
Bases: Module
Norm-gated equivariant nonlinearity.
Unlike :func:gate -- which consumes the rightmost scalars as dedicated
gates -- this gate drives every multiplicity from a learnable MLP of the
input scalars concatenated with the per-irrep norms of the non-scalar
channels (the gating signal is therefore rotation-invariant). The MLP output
replaces the scalar channels and scales each non-scalar multiplicity, so the
output layout equals the input layout. This is the nonlinearity used
throughout the QHNet self / pair interaction layers (Yu et al. 2023).
Scaling an equivariant feature by an invariant gate is equivariant, so the
module is rotation-equivariant; reusing :func:opifex.neural.equivariant.norm
gives a NaN-safe gradient at zero vectors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps
|
Irreps | str
|
Expected input layout (validated on call). Must carry at least
one |
required |
rngs
|
Rngs
|
Random number generators (keyword-only) seeding the MLP. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Irrep
¶
Irreps
¶
An ordered direct sum of mul x Irrep blocks (a steerable feature layout).
Construct from a string (Irreps("8x0e + 4x1o")), an :class:Irrep, another
:class:Irreps, or a tuple of (mul, Irrep) blocks.
IrrepsArray
¶
A JAX array tagged with its :class:Irreps layout (an equivariant feature).
The array's last axis is the feature axis and must equal irreps.dim. The
object is a pytree: the array is the single child (traced) and the
:class:Irreps is static aux-data, so equivariant features pass through
jit/grad/vmap unchanged.
chunks
property
¶
chunks: list[Float[Array, '... mul dim_l']]
Split the feature axis into per-block arrays of shape (..., mul, 2l+1).
tree_flatten
¶
Pytree flatten: array is the child, irreps is static aux-data.
tree_unflatten
classmethod
¶
tree_unflatten(aux_data: Irreps, children: tuple[Float[Array, '... dim']]) -> IrrepsArray
Pytree unflatten, bypassing dimension re-validation for traced leaves.
EquivariantLinear
¶
Bases: Module
An equivariant linear map between two :class:Irreps layouts.
For every output block, the layer sums learnable (mul_in, mul_out)
mixings over all input blocks carrying the same irrep (l, p). Output
blocks whose irrep is absent from the input are left at zero (no weights).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in
|
Irreps | str
|
Input layout. |
required |
irreps_out
|
Irreps | str
|
Output layout. |
required |
rngs
|
Rngs
|
Random number generators (keyword-only); |
required |
BesselBasis
¶
Bases: Module
Bessel radial basis b_n(r) = sqrt(2/r_c) sin(n pi r / r_c) / r.
Ported from MACE ../mace/mace/modules/radial.py:18 (eq. 7 of
arXiv:2206.07697). The frequencies n pi / r_c for n = 1..num_basis
are stored as a (non-trainable) buffer; the basis is a smooth, complete set
of invariant radial features.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_basis
|
int
|
Number of Bessel functions |
required |
cutoff
|
float
|
Cutoff radius |
required |
rngs
|
Rngs | None
|
Unused (the basis has no learnable parameters); accepted for a uniform constructor signature across equivariant modules. |
None
|
GaussianBasis
¶
Bases: Module
Gaussian radial basis g_n(r) = exp(-(r - mu_n)^2 / (2 sigma^2)).
Ported from MACE ../mace/mace/modules/radial.py:88. Centres mu_n are
evenly spaced on [0, r_c] and the width is the centre spacing
(sigma = r_c / (num_basis - 1)), giving the coefficient
coeff = -0.5 / sigma^2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_basis
|
int
|
Number of Gaussians (centres on |
required |
cutoff
|
float
|
Cutoff radius |
required |
rngs
|
Rngs | None
|
Unused (no learnable parameters); accepted for a uniform constructor signature. |
None
|
PiecewiseLinearBasis
¶
Bases: Module
Piecewise-linear (hat) radial basis on [0, cutoff].
Faithful to the isotropic basis of torch_harmonics's PiecewiseLinearFilterBasis
(torch-harmonics/torch_harmonics/filter_basis.py), the canonical filter basis for
discrete-continuous (DISCO) convolutions (Ocampo, Price & McEwen 2023, arXiv:2209.13603).
The num_basis hat functions have collocation spacing dr = 2 * cutoff / (num_basis + 1)
and half-width dr: phi_k(r) = max(0, 1 - |r - r_k| / dr) restricted to r <= cutoff,
with centres r_k = k * dr (odd num_basis) or (k + 0.5) * dr (even). Each function is
continuous and compactly supported, so no separate cutoff envelope is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_basis
|
int
|
Number of hat functions (>= 1). |
required |
cutoff
|
float
|
Support radius |
required |
rngs
|
Rngs | None
|
Unused (the basis is parameter-free); accepted for interface uniformity. |
None
|
SymmetricContraction
¶
SymmetricContraction(irreps_in: Irreps | str, irreps_out: Irreps | str, *, correlation: int, num_species: int, num_channels: int, rngs: Rngs)
Bases: Module
Per-element symmetric contraction up to a given correlation (body order).
Maps a per-channel single-particle basis A of layout irreps_in to
irreps_out features, channel-wise, summing the contributions of every
tensor-power order 1 .. correlation. Weights are indexed by atomic element,
so each species gets its own contraction (the MACE convention).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in
|
Irreps | str
|
Per-channel input irreps (multiplicity one each; the channel axis carries the multiplicity). |
required |
irreps_out
|
Irreps | str
|
Per-channel output irreps to keep. |
required |
correlation
|
int
|
Maximum tensor-power order |
required |
num_species
|
int
|
Number of distinct atomic elements (the per-element weight axis). |
required |
num_channels
|
int
|
Number of feature channels. |
required |
rngs
|
Rngs
|
Random number generators (keyword-only) seeding the weights. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ChannelwiseTensorProduct
¶
ChannelwiseTensorProduct(irreps_in1: Irreps | str, irreps_in2: Irreps | str, irreps_out: Irreps | str, *, internal_weights: bool = True, rngs: Rngs)
Bases: Module
Channel-wise (e3nn "uuu") Clebsch-Gordan tensor product.
The two inputs and the output share one uniform multiplicity mul; each
selection-rule-allowed path (i_1, i_2) -> i_3 whose output irrep is present
in irreps_out couples the inputs channel-by-channel and carries a single
(mul,) weight -- O(mul) parameters per path rather than the
:class:FullyConnectedTensorProduct's O(mul^3). This is the connection
QHNet's self / pair interaction layers use (../AIRS/OpenDFT/QHBench/QH9/
models/QHNet.py); the unweighted forward equals
e3nn.elementwise_tensor_product.
The per-channel weights are the module's internal parameters by default, but a
per-sample weights array (width :attr:weight_numel) can be supplied to
:meth:__call__ -- the path QHNet uses to inject per-edge radial /
inner-product modulation into the pair coupling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in1
|
Irreps | str
|
First input layout (single uniform multiplicity). |
required |
irreps_in2
|
Irreps | str
|
Second input layout (same uniform multiplicity). |
required |
irreps_out
|
Irreps | str
|
Output layout; only paths producing an irrep present here are instantiated. Each output irrep should appear once. |
required |
internal_weights
|
bool
|
If |
True
|
rngs
|
Rngs
|
Random number generators (keyword-only); |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the inputs and output do not share one multiplicity. |
FullyConnectedTensorProduct
¶
FullyConnectedTensorProduct(irreps_in1: Irreps | str, irreps_in2: Irreps | str, irreps_out: Irreps | str, *, rngs: Rngs)
Bases: Module
Fully-connected Clebsch-Gordan tensor product with learnable path weights.
Every selection-rule-allowed path (i_1, i_2) -> i_3 whose output irrep is
present in irreps_out carries an independent (mul_1, mul_2, mul_3)
weight; the contributions are summed per output block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in1
|
Irreps | str
|
Layout of the first input. |
required |
irreps_in2
|
Irreps | str
|
Layout of the second input. |
required |
irreps_out
|
Irreps | str
|
Desired output layout; only paths producing an irrep that appears here are instantiated. |
required |
rngs
|
Rngs
|
Random number generators (keyword-only); |
required |
TensorProduct
¶
Bases: Protocol
Protocol for an equivariant bilinear map of two :class:IrrepsArray s.
Concrete implementations (the dense Clebsch-Gordan
:class:FullyConnectedTensorProduct, or a future Cartesian /
cuEquivariance-backed variant) expose the same call signature so they are
substitutable.
apply_scalar_weights
¶
apply_scalar_weights(features: IrrepsArray, weights: Float[Array, '... num_irreps']) -> IrrepsArray
Scale each multiplicity of features by an invariant scalar weight.
Multiplying a steerable feature's multiplicity by a rotation-invariant scalar
is equivariant, so this realises the radial / scalar-gated modulation used by
the NequIP convolution and the equivariant Hamiltonian pair head (one weight
per irrep, weights.shape[-1] == features.irreps.num_irreps).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
IrrepsArray
|
The steerable feature to scale. |
required |
weights
|
Float[Array, '... num_irreps']
|
Per-multiplicity scalars of width |
required |
Returns:
| Name | Type | Description |
|---|---|---|
An |
IrrepsArray
|
class: |
IrrepsArray
|
multiplicity scaled by its weight. |
inner_product
¶
inner_product(x: IrrepsArray, y: IrrepsArray, *, normalize: bool = True) -> IrrepsArray
Return the per-multiplicity dot product of x and y as 0e scalars.
x and y must share their layout. Each mul x (l, p) block yields
mul invariant scalars <x_u, y_u> (optionally divided by dim = 2l+1,
the e3nn "component" path normalisation used by QHNet's InnerProduct).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
IrrepsArray
|
First steerable feature. |
required |
y
|
IrrepsArray
|
Second steerable feature with |
required |
normalize
|
bool
|
If |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IrrepsArray
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
norm
¶
norm(x: IrrepsArray, *, squared: bool = False) -> IrrepsArray
Return the per-irrep Euclidean norm of x as 0e scalars.
Each mul x (l, p) block contributes mul scalar norms, one per
multiplicity, so the output layout replaces every block's irrep by 0e
while keeping its multiplicity (e.g. 2x0e + 3x1o -> 2x0e + 3x0e).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
IrrepsArray
|
Input steerable feature. |
required |
squared
|
bool
|
If |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IrrepsArray
|
class: |
IrrepsArray
|
as |
rms_normalize
¶
rms_normalize(x: IrrepsArray, *, eps: float = 1e-06) -> IrrepsArray
Scale each sample of x to unit root-mean-square (equivariant RMSNorm).
Divides the whole feature vector by the per-sample scalar
sqrt(mean(x**2) + eps). That scalar is rotation-invariant (each irrep
block's squared norm is preserved by rotation, so their mean is too), so
dividing by it preserves the transformation law -- this is an equivariant
analogue of RMSNorm. It bounds the feature magnitude before operations that
square the features (the self / pair interaction tensor products), without
which an unnormalised trunk's growing activations blow up the squared output.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
IrrepsArray
|
Input steerable feature; the RMS is taken over its last (feature) axis. |
required |
eps
|
float
|
Numerical floor added to the mean square before the square root. |
1e-06
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IrrepsArray
|
class: |
IrrepsArray
|
to unit RMS. |
from_irreps_array
¶
from_irreps_array(features: IrrepsArray) -> CartesianTensor
Convert an 1x0e + 1x1o + 1x2e IrrepsArray back to a Cartesian tensor.
Inverse of :func:to_irreps_array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
IrrepsArray
|
An :class: |
required |
Returns:
| Type | Description |
|---|---|
CartesianTensor
|
The reconstructed :class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
to_irreps_array
¶
to_irreps_array(tensor: CartesianTensor) -> IrrepsArray
Convert a :class:CartesianTensor to an 1x0e + 1x1o + 1x2e IrrepsArray.
The scalar (trace), vector (antisymmetric dual) and symmetric-traceless parts
map to the l = 0 / l = 1 / l = 2 blocks respectively. The l = 2
block uses the change-of-basis derived from the canonical spherical-harmonic
evaluator (see :func:_symmetric_to_l2_basis), so the result is exactly
consistent with :func:opifex.geometry.algebra.wigner.wigner_d: a rotation of
tensor rotates the blocks by D^l(R).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tensor
|
CartesianTensor
|
A rank-2 Cartesian tensor feature with leading shape |
required |
Returns:
| Name | Type | Description |
|---|---|---|
An |
IrrepsArray
|
class: |
IrrepsArray
|
|
radius_graph
¶
radius_graph(positions: Float[Array, 'n 3'], cutoff: float, *, max_edges: int, self_loops: bool = False) -> tuple[Array, Array]
Build the radius-graph edge index for a point cloud.
Two nodes i and j are connected (with i the sender and j the
receiver) when |positions[i] - positions[j]| < cutoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positions
|
Float[Array, 'n 3']
|
Node coordinates of shape |
required |
cutoff
|
float
|
Connection radius |
required |
max_edges
|
int
|
Static upper bound on the number of returned edges; the output
arrays have this length, padded with |
required |
self_loops
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
Array
|
A pair |
Array
|
|
scatter_max
¶
scatter_max(data: Float[Array, 'edges ...'], index: Array, *, num_segments: int) -> Float[Array, 'num_segments ...']
Segment-wise maximum of data addressed by index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Float[Array, 'edges ...']
|
Per-edge values of shape |
required |
index
|
Array
|
Destination node index per edge, shape |
required |
num_segments
|
int
|
Number of output segments (nodes). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_segments ...']
|
Per-segment maximum of shape |
scatter_mean
¶
scatter_mean(data: Float[Array, 'edges ...'], index: Array, *, num_segments: int) -> Float[Array, 'num_segments ...']
Average data within segments addressed by index.
Empty segments map to zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Float[Array, 'edges ...']
|
Per-edge values of shape |
required |
index
|
Array
|
Destination node index per edge, shape |
required |
num_segments
|
int
|
Number of output segments (nodes). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_segments ...']
|
Per-segment mean of shape |
scatter_sum
¶
scatter_sum(data: Float[Array, 'edges ...'], index: Array, *, num_segments: int) -> Float[Array, 'num_segments ...']
Sum data into segments addressed by index.
output[index[e]] += data[e] for every edge e (negative indices, e.g.
the -1 padding from :func:radius_graph, are dropped by
segment_sum).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Float[Array, 'edges ...']
|
Per-edge values of shape |
required |
index
|
Array
|
Destination node index per edge, shape |
required |
num_segments
|
int
|
Number of output segments (nodes). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_segments ...']
|
Aggregated array of shape |
cosine_cutoff
¶
cosine_cutoff(radius: Float[Array, ...], cutoff: float) -> Float[Array, ...]
Behler cosine cutoff envelope 0.5 (cos(pi r / r_c) + 1).
From Behler (J. Chem. Phys. 134, 074106, 2011), the original atom-centred
symmetry-function cutoff. Decays smoothly from 1 at r = 0 to 0
at r = r_c (with vanishing first derivative there) and is masked to zero
beyond r_c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
radius
|
Float[Array, ...]
|
Distances of shape |
required |
cutoff
|
float
|
Cutoff radius |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ...]
|
Envelope values of shape |
polynomial_cutoff
¶
Smooth polynomial cutoff envelope decaying from 1 to 0 on [0, r_c].
Ported from MACE ../mace/mace/modules/radial.py:113 (eq. 8 of
arXiv:2206.07697); equivalent to e3nn_jax.poly_envelope. The envelope
and its first p derivatives vanish at r_c::
f(r) = 1 - (p+1)(p+2)/2 (r/r_c)^p
+ p(p+2) (r/r_c)^{p+1}
- p(p+1)/2 (r/r_c)^{p+2}
masked to zero for r >= r_c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
radius
|
Float[Array, ...]
|
Distances of shape |
required |
cutoff
|
float
|
Cutoff radius |
required |
p
|
int
|
Polynomial order controlling smoothness (default |
6
|
Returns:
| Type | Description |
|---|---|
Float[Array, ...]
|
Envelope values of shape |
Spherical harmonics¶
Real spherical harmonics Y_l(r) for E(3)-equivariant networks.
A native, dependency-free port of the recursive spherical-harmonics algorithm of
e3nn-jax (Geiger & Smidt 2022, arXiv:2207.09453; reference
../e3nn-jax/e3nn_jax/_src/spherical_harmonics/recursive.py). Higher degrees
are built from lower ones by contracting two spherical-harmonic blocks with the
real Clebsch-Gordan tensor::
Y_l = cste(l) / norm(l) * einsum('...i,...j,ijk->...k', Y_l1, Y_l2, C)
where l1 = l - 2**floor(log2(l-1)) and l2 = l - l1. The per-degree
normalization constant norm(l) is computed numerically from the value of the
contraction at the "north pole" index (rather than symbolically via sympy as
in the reference), which is exact for the integer Clebsch-Gordan tables.
The output uses the same real-spherical-harmonic basis as
:func:opifex.geometry.algebra.wigner.wigner_d, so equivariance
Y_l(R r) = D^l(R) Y_l(r) holds. Y_0 is constant and Y_1 is
proportional to the (normalized) direction. The implementation is
jit/grad/vmap clean and handles batched inputs.
References
../e3nn-jax/e3nn_jax/_src/spherical_harmonics/recursive.py-- the recurrence, thel1/l2split, and theintegral/componentnormalization constants.../e3nn-jax/e3nn_jax/_src/spherical_harmonics/__init__.py-- thenormalize(project onto the sphere) andnormalizationoptions.
spherical_harmonics
¶
spherical_harmonics(out: Irreps | int, vectors: Float[Array, '... 3'], *, normalize: bool = True, normalization: str = 'integral') -> IrrepsArray
Real spherical harmonics Y_l(r) for degrees l = 0..lmax.
The harmonics share the real basis of
:func:opifex.geometry.algebra.wigner.wigner_d, so they are equivariant:
Y_l(R r) = D^l(R) Y_l(r).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
out
|
Irreps | int
|
Either an integer |
required |
vectors
|
Float[Array, '... 3']
|
Cartesian coordinates of shape |
required |
normalize
|
bool
|
If |
True
|
normalization
|
str
|
One of |
'integral'
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IrrepsArray
|
class: |
IrrepsArray
|
irreps and an array of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Tensor products¶
Equivariant tensor product of two steerable features via Clebsch-Gordan.
The tensor product couples two :class:IrrepsArray s into a third by contracting
each pair of input blocks with the real Clebsch-Gordan tensor. For input irreps
ir_1 and ir_2, the selection rule
|l_1 - l_2| <= l_3 <= l_1 + l_2 (with parity p_1 p_2) lists the allowed
output irreps; the contraction
einsum("...ui,...vj,ijk->...uvk", x_1, x_2, C) (with C the
Clebsch-Gordan tensor) is equivariant by construction.
Ported from e3nn-jax (../e3nn-jax/e3nn_jax/_src/tensor_products.py:122
and the uvw connection of
../e3nn-jax/e3nn_jax/_src/legacy/core_tensor_product.py). The
fully-connected variant gives every path (i_1, i_2) -> i_3 a learnable
(mul_1, mul_2, mul_3) weight:
einsum("uvw,ijk,...ui,...vj->...wk", w, C, x_1, x_2). Normalisation follows
the e3nn default config (irrep_normalization="component",
path_normalization="element"): the per-path scale
alpha = dim(ir_3) / sum_paths(mul_1 mul_2) (cf.
_normalize_instruction_path_weights) is folded into the weight
initialisation standard deviation, so the forward pass uses the raw
Clebsch-Gordan tensor.
TensorProduct
¶
Bases: Protocol
Protocol for an equivariant bilinear map of two :class:IrrepsArray s.
Concrete implementations (the dense Clebsch-Gordan
:class:FullyConnectedTensorProduct, or a future Cartesian /
cuEquivariance-backed variant) expose the same call signature so they are
substitutable.
FullyConnectedTensorProduct
¶
FullyConnectedTensorProduct(irreps_in1: Irreps | str, irreps_in2: Irreps | str, irreps_out: Irreps | str, *, rngs: Rngs)
Bases: Module
Fully-connected Clebsch-Gordan tensor product with learnable path weights.
Every selection-rule-allowed path (i_1, i_2) -> i_3 whose output irrep is
present in irreps_out carries an independent (mul_1, mul_2, mul_3)
weight; the contributions are summed per output block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in1
|
Irreps | str
|
Layout of the first input. |
required |
irreps_in2
|
Irreps | str
|
Layout of the second input. |
required |
irreps_out
|
Irreps | str
|
Desired output layout; only paths producing an irrep that appears here are instantiated. |
required |
rngs
|
Rngs
|
Random number generators (keyword-only); |
required |
ChannelwiseTensorProduct
¶
ChannelwiseTensorProduct(irreps_in1: Irreps | str, irreps_in2: Irreps | str, irreps_out: Irreps | str, *, internal_weights: bool = True, rngs: Rngs)
Bases: Module
Channel-wise (e3nn "uuu") Clebsch-Gordan tensor product.
The two inputs and the output share one uniform multiplicity mul; each
selection-rule-allowed path (i_1, i_2) -> i_3 whose output irrep is present
in irreps_out couples the inputs channel-by-channel and carries a single
(mul,) weight -- O(mul) parameters per path rather than the
:class:FullyConnectedTensorProduct's O(mul^3). This is the connection
QHNet's self / pair interaction layers use (../AIRS/OpenDFT/QHBench/QH9/
models/QHNet.py); the unweighted forward equals
e3nn.elementwise_tensor_product.
The per-channel weights are the module's internal parameters by default, but a
per-sample weights array (width :attr:weight_numel) can be supplied to
:meth:__call__ -- the path QHNet uses to inject per-edge radial /
inner-product modulation into the pair coupling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
irreps_in1
|
Irreps | str
|
First input layout (single uniform multiplicity). |
required |
irreps_in2
|
Irreps | str
|
Second input layout (same uniform multiplicity). |
required |
irreps_out
|
Irreps | str
|
Output layout; only paths producing an irrep present here are instantiated. Each output irrep should appear once. |
required |
internal_weights
|
bool
|
If |
True
|
rngs
|
Rngs
|
Random number generators (keyword-only); |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the inputs and output do not share one multiplicity. |
Radial bases and cutoffs¶
Radial bases and cutoff envelopes for E(3)-equivariant networks.
Interatomic-distance edge features are invariant scalars (they depend only on
|r_i - r_j|), so these objects return plain arrays rather than
:class:~opifex.neural.equivariant.IrrepsArray; downstream they are tagged as
0e channels. The formulae are ported from the MACE radial module (Batatia
et al. 2022, "MACE: Higher Order Equivariant Message Passing Neural Networks for
Fast and Accurate Force Fields", arXiv:2206.07697):
- :class:
BesselBasis--../mace/mace/modules/radial.py:18(eq. 7). - :class:
GaussianBasis--../mace/mace/modules/radial.py:88. - :func:
polynomial_cutoff--../mace/mace/modules/radial.py:113(eq. 8), equivalent toe3nn_jax.poly_envelope.
The cosine envelope :func:cosine_cutoff follows Behler (J. Chem. Phys. 134,
074106, 2011), the original ACSF cutoff function.
BesselBasis
¶
Bases: Module
Bessel radial basis b_n(r) = sqrt(2/r_c) sin(n pi r / r_c) / r.
Ported from MACE ../mace/mace/modules/radial.py:18 (eq. 7 of
arXiv:2206.07697). The frequencies n pi / r_c for n = 1..num_basis
are stored as a (non-trainable) buffer; the basis is a smooth, complete set
of invariant radial features.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_basis
|
int
|
Number of Bessel functions |
required |
cutoff
|
float
|
Cutoff radius |
required |
rngs
|
Rngs | None
|
Unused (the basis has no learnable parameters); accepted for a uniform constructor signature across equivariant modules. |
None
|
GaussianBasis
¶
Bases: Module
Gaussian radial basis g_n(r) = exp(-(r - mu_n)^2 / (2 sigma^2)).
Ported from MACE ../mace/mace/modules/radial.py:88. Centres mu_n are
evenly spaced on [0, r_c] and the width is the centre spacing
(sigma = r_c / (num_basis - 1)), giving the coefficient
coeff = -0.5 / sigma^2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_basis
|
int
|
Number of Gaussians (centres on |
required |
cutoff
|
float
|
Cutoff radius |
required |
rngs
|
Rngs | None
|
Unused (no learnable parameters); accepted for a uniform constructor signature. |
None
|
PiecewiseLinearBasis
¶
Bases: Module
Piecewise-linear (hat) radial basis on [0, cutoff].
Faithful to the isotropic basis of torch_harmonics's PiecewiseLinearFilterBasis
(torch-harmonics/torch_harmonics/filter_basis.py), the canonical filter basis for
discrete-continuous (DISCO) convolutions (Ocampo, Price & McEwen 2023, arXiv:2209.13603).
The num_basis hat functions have collocation spacing dr = 2 * cutoff / (num_basis + 1)
and half-width dr: phi_k(r) = max(0, 1 - |r - r_k| / dr) restricted to r <= cutoff,
with centres r_k = k * dr (odd num_basis) or (k + 0.5) * dr (even). Each function is
continuous and compactly supported, so no separate cutoff envelope is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_basis
|
int
|
Number of hat functions (>= 1). |
required |
cutoff
|
float
|
Support radius |
required |
rngs
|
Rngs | None
|
Unused (the basis is parameter-free); accepted for interface uniformity. |
None
|
polynomial_cutoff
¶
Smooth polynomial cutoff envelope decaying from 1 to 0 on [0, r_c].
Ported from MACE ../mace/mace/modules/radial.py:113 (eq. 8 of
arXiv:2206.07697); equivalent to e3nn_jax.poly_envelope. The envelope
and its first p derivatives vanish at r_c::
f(r) = 1 - (p+1)(p+2)/2 (r/r_c)^p
+ p(p+2) (r/r_c)^{p+1}
- p(p+1)/2 (r/r_c)^{p+2}
masked to zero for r >= r_c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
radius
|
Float[Array, ...]
|
Distances of shape |
required |
cutoff
|
float
|
Cutoff radius |
required |
p
|
int
|
Polynomial order controlling smoothness (default |
6
|
Returns:
| Type | Description |
|---|---|
Float[Array, ...]
|
Envelope values of shape |
cosine_cutoff
¶
cosine_cutoff(radius: Float[Array, ...], cutoff: float) -> Float[Array, ...]
Behler cosine cutoff envelope 0.5 (cos(pi r / r_c) + 1).
From Behler (J. Chem. Phys. 134, 074106, 2011), the original atom-centred
symmetry-function cutoff. Decays smoothly from 1 at r = 0 to 0
at r = r_c (with vanishing first derivative there) and is masked to zero
beyond r_c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
radius
|
Float[Array, ...]
|
Distances of shape |
required |
cutoff
|
float
|
Cutoff radius |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ...]
|
Envelope values of shape |
Graph utilities¶
Neighbour graph construction and segment-scatter for message passing.
These utilities turn a point cloud into the (senders, receivers) edge index
used by equivariant message-passing networks, and aggregate per-edge messages
back onto nodes.
-
:func:
radius_graphfollows the dense pairwise-distance + fixed-size mask approach of../e3nn-jax/e3nn_jax/_src/radius_graph.py(it usesjnp.where(mask, size=...)to return a statically shaped edge list). The neighbour-list concept is that of../jax-md/jax_md/partition.py; unlikejax-md's cell-list partitioning, this implementation is a simple denseO(N^2)pairwise computation -- correct andjit-friendly for the small to medium molecules typical of interatomic-potential workloads, but not intended for very largeN. -
:func:
scatter_sum, :func:scatter_meanand :func:scatter_maxwrapjax.ops.segment_*(cf.../e3nn-jax/e3nn_jax/_src/scatter.py).
Static-shape contract: radius_graph returns edge arrays of fixed length
max_edges (padded with -1), so the output shape does not depend on the
data -- a requirement for jit. Callers must size max_edges to an upper
bound on the true edge count; excess edges beyond max_edges are dropped.
radius_graph
¶
radius_graph(positions: Float[Array, 'n 3'], cutoff: float, *, max_edges: int, self_loops: bool = False) -> tuple[Array, Array]
Build the radius-graph edge index for a point cloud.
Two nodes i and j are connected (with i the sender and j the
receiver) when |positions[i] - positions[j]| < cutoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positions
|
Float[Array, 'n 3']
|
Node coordinates of shape |
required |
cutoff
|
float
|
Connection radius |
required |
max_edges
|
int
|
Static upper bound on the number of returned edges; the output
arrays have this length, padded with |
required |
self_loops
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
Array
|
A pair |
Array
|
|
scatter_sum
¶
scatter_sum(data: Float[Array, 'edges ...'], index: Array, *, num_segments: int) -> Float[Array, 'num_segments ...']
Sum data into segments addressed by index.
output[index[e]] += data[e] for every edge e (negative indices, e.g.
the -1 padding from :func:radius_graph, are dropped by
segment_sum).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Float[Array, 'edges ...']
|
Per-edge values of shape |
required |
index
|
Array
|
Destination node index per edge, shape |
required |
num_segments
|
int
|
Number of output segments (nodes). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_segments ...']
|
Aggregated array of shape |
scatter_mean
¶
scatter_mean(data: Float[Array, 'edges ...'], index: Array, *, num_segments: int) -> Float[Array, 'num_segments ...']
Average data within segments addressed by index.
Empty segments map to zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Float[Array, 'edges ...']
|
Per-edge values of shape |
required |
index
|
Array
|
Destination node index per edge, shape |
required |
num_segments
|
int
|
Number of output segments (nodes). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_segments ...']
|
Per-segment mean of shape |
scatter_max
¶
scatter_max(data: Float[Array, 'edges ...'], index: Array, *, num_segments: int) -> Float[Array, 'num_segments ...']
Segment-wise maximum of data addressed by index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Float[Array, 'edges ...']
|
Per-edge values of shape |
required |
index
|
Array
|
Destination node index per edge, shape |
required |
num_segments
|
int
|
Number of output segments (nodes). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_segments ...']
|
Per-segment maximum of shape |