Equivariant graph convolutionΒΆ

For nodes \(i\) and neighbors \(j\), a common convolution is

\[ f'_i = \frac{1}{\sqrt{z}} \sum_{j \in \mathcal{N}(i)} f_j \otimes_{w(\lVert r_{ij}\rVert)} Y(r_{ij}), \]

where spherical harmonics encode edge directions, a radial network produces tensor-product weights, and scatter-sum aggregates messages.

The following sketch exposes all important array shapes:

import mlx.core as mx
from e3nn_mlx import math, o3, scatter_sum

irreps_node = o3.Irreps("8x0e + 4x1o")
irreps_sh = o3.Irreps.spherical_harmonics(2)

tp = o3.FullyConnectedTensorProduct(
    irreps_node,
    irreps_sh,
    irreps_node,
    internal_weights=False,
    shared_weights=False,
)

# positions: (nodes, 3); edge_src/edge_dst: (edges,)
edge_vec = positions[edge_dst] - positions[edge_src]
edge_length = mx.linalg.norm(edge_vec, axis=-1)
edge_attr = o3.spherical_harmonics(
    irreps_sh, edge_vec, normalize=True, normalization="component"
)

radial = math.soft_one_hot_linspace(
    edge_length,
    start=0.0,
    end=max_radius,
    number=num_basis,
    basis="smooth_finite",
    cutoff=True,
)
weights = radial_network(radial)  # (edges, tp.weight_numel)
messages = tp(node_features[edge_src], edge_attr, weights)
output = scatter_sum(messages, edge_dst, positions.shape[0])

Distance-derived weights are rotation invariant; spherical harmonics and the tensor product carry the directional transformation law. Summation is equivariant because it combines only features with the same representation.

For a complete trainable implementation, use Convolution or inspect its source.