Molecular graphs, equivariance and docking
Irrep-safe batch and instance normalization
Free account with email, Google or GitHub. Your code runs in an isolated PyTorch sandbox.
Your task
Normalize each active irrep copy with one scale shared by all of its components, so a rotation of the input still produces the same rotation of the output. Center and bias only even scalar copies; scale vectors and other irreps without centering them. Return normalized features, the statistics used, and the next running statistics without mutating the supplied state.
Function signature
equivariant_batch_normalization(
x: torch.Tensor,
valid: torch.Tensor,
irreps: torch.Tensor,
multiplicities: torch.Tensor,
running_mean: torch.Tensor,
running_var: torch.Tensor,
weight: torch.Tensor,
bias: torch.Tensor,
options: dict,
) -> dict[str, torch.Tensor]Worked example
Center scalars, scale whole vectors
The even scalar values 1 and 3 have mean 2 and centered second moment 1. With eps=1, they become \pm1/\sqrt2. Vectors are not centered. Their component mean-square is 1/3, so every component shares scale \sqrt3/2. Padded scalar components stay zero; the running vector variance becomes (1+1/3)/2=2/3.
import torch
x = torch.tensor([[[[[1.0, 0.0, 0.0]], [[1.0, 0.0, 0.0]]]], [[[[3.0, 0.0, 0.0]], [[0.0, 1.0, 0.0]]]]], dtype=torch.float64, requires_grad=True)
valid = torch.tensor([[True], [True]], dtype=torch.bool)
irreps = torch.tensor([[0, 1], [1, -1]], dtype=torch.int64)
multiplicities = torch.tensor([1, 1], dtype=torch.int64)
running_mean = torch.tensor([[0.0], [0.0]], dtype=torch.float64, requires_grad=True)
running_var = torch.tensor([[1.0], [1.0]], dtype=torch.float64, requires_grad=True)
weight = torch.tensor([[1.0], [1.0]], dtype=torch.float64, requires_grad=True)
bias = torch.tensor([[0.0], [0.0]], dtype=torch.float64, requires_grad=True)
options = {
'eps': 1.0,
'momentum': 0.5,
}
result = equivariant_batch_normalization(x=x, valid=valid, irreps=irreps, multiplicities=multiplicities, running_mean=running_mean, running_var=running_var, weight=weight, bias=bias, options=options)Expected return
{
'output': torch.tensor([[[[[-0.7071067811865475, 0.0, 0.0]], [[0.8660254037844387, 0.0, 0.0]]]], [[[[0.7071067811865475, 0.0, 0.0]], [[0.0, 0.8660254037844387, 0.0]]]]], dtype=torch.float64),
'mean': torch.tensor([[[2.0], [0.0]]], dtype=torch.float64),
'second_moment': torch.tensor([[[1.0], [0.3333333333333333]]], dtype=torch.float64),
'running_mean': torch.tensor([[1.0], [0.0]], dtype=torch.float64),
'running_var': torch.tensor([[1.0], [0.6666666666666666]], dtype=torch.float64),
}Parameters
| Parameter | Shape and meaning |
|---|---|
xtorch.Tensor | [B, S, T, C, D]Packed real irrep features; observed active values finite |
validtorch.Tensor | [B, S]True for observed positions |
irrepstorch.Tensor | [T, 2]int64 [T,2], (degree\ge 0, inversion parity \pm 1), \mathrm{dimension2}\cdot l+1\le D |
multiplicitiestorch.Tensor | [T]int64 [T], number of active copies in [0,C] |
running_meantorch.Tensor | [T, C]Finite only at active \mathrm{even}-\mathrm{l0} slots; all other slots ignored |
running_vartorch.Tensor | [T, C]Finite nonnegative at active copies; padded slots ignored |
weighttorch.Tensor | [T, C]Learned per-irrep-copy gain, shared across components; padding ignored |
biastorch.Tensor | [T, C]Learned invariant scalar shift only at active 0\mathrm{e} slots; other slots ignored |
optionsdict | Scalar settings. Omitted keys use the defaults below. |
Return
dict[str, torch.Tensor]
Return exactly the named fields applicable to this operation.
Required behavior
- Inputs
x [B,S,T,C,D]packBouter instances,Ssample positions,Treal irrep types, at mostCmultiplicity copies per type, and padded component widthD. Each\mathrm{irreps}[t]=(l,p),l\ge 0,pin {-1,+1};d_{t}=2\cdot l+1\le D.multiplicities[t]is an integer in[0,C]. - Copy
cis active iffc<\mathrm{multiplicities}[t]; componentmis active iffm<d_{t}.valid[B,S]usesTruefor observed positions.BorSmay be zero;T,C,D\ge 1. Active observed values, active running state and enabled active affine parameters are finite. - Required centered squares, reductions,
\epsilon-shifted moments, gains and final affine arithmetic must also be representable and finite in the input dtype: arbitrary finitefloat32inputs near10^{20}whose squares overflow are outside the numerical domain. - Nonfinite values at masked positions, padded copies/components, forbidden biases or nonexistent scalar-
meanslots are ignored before arithmetic. Allweight/biasentries are also ignored before arithmetic whenaffine=False, even if nonfinite. Active running state still must be finite in instance mode because it is returned unchanged. - The irrep basis must be orthonormal. No per-component affine parameters are allowed.
defaultsmatch original DiffDock's pinned e3nn0.5.0BatchNorm:mode='train',instance=False,normalization='component',reduce='mean',affine=True,\varepsilon=10^{-5},\mathrm{momentum}=0.1. - mode may be 'train'/'eval'; normalization 'component'/'norm'; reduce '
mean'/'max';\varepsilon>0and0\le \mathrm{momentum}\le 1are finite. Floating tensors usefloat32orfloat64. - Eps must remain finite and at least the smallest positive NORMAL number in x.dtype
(\mathrm{float32}:1.1754943508222875\times 10^{-38}, \mathrm{float64}:2.2250738585072014\times 10^{-308}). - This rejects underflowing and subnormal
\varepsilon, such asfloat32\varepsilon 10^{-50}or10^{-40}, whose normalization backward can overflow even for zero features. As with forward arithmetic, VJP claims require finite representable first-order intermediates and gradients for the supplied cotangents. - Source DiffDock passes
[N_{\mathrm{nodes}},\mathrm{irreps.dim}], equivalent here toB=N_{\mathrm{nodes}},S=1, so default statistics mix nodes across complexes.Bdoes not automaticallymeanmolecular graph. The optionalinstance=Truevariant uses eachBrow independently and is not DiffDock's default. - ONLY
l=0,p=+1('0\mathrm{e}') is a scalar for centering andbias. The oddl=0pseudoscalar ('0\mathrm{o}') is NEVER centered or biased. - For current statistics (
mode='train'ORinstance=True), compute each active0\mathrm{e}copy'smean\mu: global mode pools allvalid(b,s)observations equally; instance mode averagesvalidswithin thatb. All othermeanslots are zero. Empty statistics havemeanzero. - Global eval instead uses supplied
running_meanfor active0\mathrm{e}copies. Subtract\mufrom0\mathrm{e}fields only, leaving odd scalars/vectors/higher-degree tensors uncentered; sanitize masks and component padding again. - For current statistics, compute energy
e[b,s,t,c]=\sum_{m<d_{t}} \mathrm{centered\_x}^{2}. - In
normalization='component', divide byd_t, not paddedD; 'norm' uses the component sum. First reduce overvalidSpositions independently perBrow: 'mean' averagesvalidenergies, 'max' chooses the largestvalidenergy (firstvalidposition at a tie). Empty instances have zero and do not participate in global averaging. instance=Trueuses each instance's statistic directly. Otherwise average these per-instance statistics EQUALLY over nonemptyBrows, even if theirvalidcounts differ. This is the source's sample-then-batch reduction hierarchy with an explicit ragged-mask extension.- Scalar centering still pools individual
validobservations globally, so under ragged masks its weights can differ from the final norm-statistic weights. - For example counts
3and1with0\mathrm{e}values(0,0,0)and (4) give global\mu=1and\mathrm{second\_moment}=(1+9)/2=5, not pooled variance3. Global eval uses suppliedrunning_varinstead. - This stored quantity is a biased variance for centered
0\mathrm{e}and a second moment/RMS energy for all otherirreps; there is non/(n-1)correction. - For each full active irrep copy,
output\mathrm{centered\_x}/\sqrt{\mathrm{second\_moment}+\varepsilon}, multiplied byweight[t,c]ifaffine=True. Only active0\mathrm{e}copies then addbias[t,c]. Ifaffine=Falseignore suppliedweight/bias, with defined zero derivatives for them. - One copy's gain is shared across all
d_tcomponents. Invalid positions and padded copies/components return zero, including after a nonzero scalarbias. There is no nonlinearity or cross-copy mixing. - Global train with any observed position returns functional next running statistics:
\mathrm{running\_mean}'=(1-\mathrm{momentum})\,\mathrm{old\_mean}+\mathrm{momentum}\,\mufor active0\mathrm{e}copies, and\mathrm{running\_var}'=(1-\mathrm{momentum})\,\mathrm{old\_var}+\mathrm{momentum}\,\mathrm{second\_moment}for every active copy. - With no observations, preserve supplied active running state. Eval OR instance mode leaves supplied active state unchanged; e3nn instance mode has no state, and this API's pass-through tensors merely keep a uniform functional interface. Nonexistent
meanslots and padded state slots return zero. - Active
running_varis finite and nonnegative, even when it is unused for normalizing the current batch. Source e3nn detaches buffer updates; this exercise instead returns DIFFERENTIABLE pure-functional EMA tensors, explicitly grading their VJPs as a teaching adaptation. Never mutate inputs or detach values. - Return
output[B,S,T,C,D],mean[G,T,C],second_moment[G,T,C],running_mean[T,C],running_var[T,C], whereG=1for global andG=Bfor instance mode. Mean is zero outside0\mathrm{e}, and both diagnostics are zero at padded copies. - Empty current statistics are zero; global eval diagnostics still describe the supplied running state. Max-mode VJPs are promised away from meaningful winner ties; zero-energy ties have zero first derivative through the square.
- Unsupported configuration or negative active running variance raises
ValueError; invalid irrep/multiplicity shapes or nonorthonormal bases are outside contract.
Why this matters
Components of one irrep must share a gain. Only even invariant scalars may be centered or shifted by an ordinary learned bias.
Starter code
You write the body of this function in the BioTorch workspace. Your code runs in an isolated PyTorch sandbox; Run checks the public examples and Submit grades the full test suite, including gradients.
import torch
def equivariant_batch_normalization(
x: torch.Tensor, # (B, S, T, C, D)
valid: torch.Tensor, # (B, S)
irreps: torch.Tensor, # (T, 2)
multiplicities: torch.Tensor, # (T,)
running_mean: torch.Tensor, # (T, C)
running_var: torch.Tensor, # (T, C)
weight: torch.Tensor, # (T, C)
bias: torch.Tensor, # (T, C)
options: dict,
) -> dict[str, torch.Tensor]:
"""Return dict[str, torch.Tensor]:
output: (B, S, T, C, D).
mean: (G, T, C).
second_moment: (G, T, C).
running_mean: (T, C).
running_var: (T, C)."""
# Implement the operation. Inputs and test setup are supplied by BioTorch.
raise NotImplementedError("Implement equivariant_batch_normalization")
Ready to implement it?
3 progressive hints, a solution walkthrough, and grading that checks values, shapes and gradients are waiting in the workspace.
Free account with email, Google or GitHub. Your code runs in an isolated PyTorch sandbox.
Sources and further reading
A primitive exercise, not an end-to-end reproduction of the named models. No affiliation or endorsement is implied.