Molecular graphs, equivariance and docking
One EGNN scalar-feature and coordinate update
EGNN
Free account with email, Google or GitHub. Your code runs in an isolated PyTorch sandbox.
Your task
Build scalar messages on supplied directed graph edges, then update node features and coordinates. Coordinate changes must follow relative displacement vectors so the update respects rigid motions.
Function signature
egnn_coordinate_update(
activations: dict,
parameters: dict,
masks: dict,
structure: dict,
coord_aggregation: str,
coord_scale: float,
residual: bool,
) -> tuple[torch.Tensor, torch.Tensor]Worked example
Follow one directed coordinate update
The edge sends node 0 into receiver 1, whose relative vector is x_1-x_0=[1,0,0]. The supplied edge network outputs \operatorname{SiLU}(1)=1/(1+e^{-1})\approx0.7311. With one neighbor and coordinate scale 1, only node 1 moves, to approximately [1.7311, 0, 0]. The zero final node map leaves scalar features [1, 2] unchanged.
import torch
activations = {
'h': torch.tensor([[1.0], [2.0]], dtype=torch.float64, requires_grad=True),
'xyz': torch.tensor([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=torch.float64, requires_grad=True),
'edge_attr': torch.tensor([[0.0]], dtype=torch.float64, requires_grad=True),
}
parameters = {
'w_edge1': torch.tensor([[0.0], [0.0], [0.0], [0.0]], dtype=torch.float64, requires_grad=True),
'b_edge1': torch.tensor([1.0], dtype=torch.float64, requires_grad=True),
'w_edge2': torch.tensor([[0.0]], dtype=torch.float64, requires_grad=True),
'b_edge2': torch.tensor([1.0], dtype=torch.float64, requires_grad=True),
'w_coord': torch.tensor([1.0], dtype=torch.float64, requires_grad=True),
'w_node1': torch.tensor([[0.0], [0.0]], dtype=torch.float64, requires_grad=True),
'b_node1': torch.tensor([1.0], dtype=torch.float64, requires_grad=True),
'w_node2': torch.tensor([[0.0]], dtype=torch.float64, requires_grad=True),
'b_node2': torch.tensor([0.0], dtype=torch.float64, requires_grad=True),
}
masks = {
'node_valid': torch.tensor([True, True], dtype=torch.bool),
'edge_valid': torch.tensor([True], dtype=torch.bool),
}
structure = {
'edge_index': torch.tensor([[1], [0]], dtype=torch.int64),
}
coord_aggregation = 'mean'
coord_scale = 1.0
residual = True
result = egnn_coordinate_update(activations=activations, parameters=parameters, masks=masks, structure=structure, coord_aggregation=coord_aggregation, coord_scale=coord_scale, residual=residual)Expected return
(torch.tensor([[1.0], [2.0]], dtype=torch.float64), torch.tensor([[0.0, 0.0, 0.0], [1.7310585786300048, 0.0, 0.0]], dtype=torch.float64))Parameters
| Parameter | Shape and meaning |
|---|---|
activationsdict | Supplied intermediate tensors. |
parametersdict | Supplied model parameters, keyed as shown below. |
masksdict | Supplied masks. Each field below defines what True includes or permits. |
structuredict | Fields supplied together in this dictionary. |
coord_aggregationstr | Required "sum" or "mean" for coordinate messages only; feature messages always sum by receiver. |
coord_scalefloat | Required finite multiplier on aggregated relative-coordinate messages; it may be signed. |
residualbool | Required flag. True adds the feature update to h and requires matching output width; False returns the update alone. |
Return
tuple[torch.Tensor, torch.Tensor]
Return these tensors in the stated order.
Required behavior
- Inputs
h [N,C],xyz[N,3],edge_index[2,E],edge_attr[E,A],node_valid[N],edge_valid[E]haveN,C,M,\mathrm{Cout}\ge 1,E,A\ge 0. Each column is(\mathrm{receiver} i, \mathrm{sender} j); indices are in[0,N).Truemeans valid. - An edge is active only when its edge mask and both endpoint masks are
True. Directed edges are used exactly as supplied; self edges and repeated edges are allowed and count separately. Mask paddedh/xyzand inactiveedge_attrbefore use; padded values may beNaN/Inf. - For each active edge let
d=\mathrm{xyz\_i}-\mathrm{xyz\_j},q=\operatorname{dot}(d,d),z=\operatorname{concat}(h_{i},h_{j},[q],\mathrm{edge\_attr\_e}),m=\operatorname{SiLU}(\operatorname{SiLU}(z\mathbin{@}w_{\mathrm{edge1}}+b_{\mathrm{edge1}})\mathbin{@}w_{\mathrm{edge2}}+b_{\mathrm{edge2}}). Inactivemis zero. - Scalar aggregation is ALWAYS
M_i=\sum_{e:\,\mathrm{receiver}(e)=i}m_e. Coordinate coefficienta_{e}=\operatorname{dot}(m_{e},w_{\mathrm{coord}})has no bias. - Let
D_i=\sum_{e:\,\mathrm{receiver}(e)=i}d_e a_e, then divideD_iby\max(\text{number of active incoming edges},1)only ifcoord_aggregation='mean'; 'sum' leaves it unchanged.coord_scaleis any finite scalar and\mathrm{xyz\_next\_i}=\mathrm{xyz\_i}+\mathrm{coord\_scale}\cdot D_{i}. - No distance normalization, square root,
\varepsilon, or stop-gradient is used; coincident coordinates are safe. Compute:u_{i}=\operatorname{SiLU}(\operatorname{concat}(h_{i},M_{i})\mathbin{@}w_{\mathrm{node1}}+b_{\mathrm{node1}})\mathbin{@}w_{\mathrm{node2}}+b_{\mathrm{node2}}. - If
residual=True, require\mathrm{Cout}=Cand returnh_{\mathrm{next}}=h+u; ifFalsereturnu. The arguments coord_aggregation, coord_scale, and residual are required. - Matrix shapes are
w_edge1[2C+1+A,M],w_edge2[M,M],w_coord[M],w_node1[C+M,M],w_node2[M,\mathrm{Cout}], with matching biasesb_edge1[M],b_edge2[M],b_node1[M],b_node2[\mathrm{Cout}]. Returnh_next[N,\mathrm{Cout}],xyz_next[N,3], both zero at invalid nodes. - Empty neighborhoods give no coordinate shift but still apply the node feature MLP. No learned
xyzbias, edge attention, dropout, or graph rebuilding is included; strided inputs and empty edge sets are supported. - Use
SiLU(z)=z/(1+e^{-z})for each named scalar activation.
Why this matters
A scalar computed from distances can scale a relative vector without breaking rotation, reflection or translation symmetry.
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 egnn_coordinate_update(
activations: dict,
parameters: dict,
masks: dict,
structure: dict,
coord_aggregation: str,
coord_scale: float,
residual: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return tuple[torch.Tensor, torch.Tensor]:
h_next: (N, Cout).
xyz_next: (N, 3)."""
# Implement the operation. Inputs and test setup are supplied by BioTorch.
raise NotImplementedError("Implement egnn_coordinate_update")
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.