Source code for xnn.common.models.base
"""The contract every model obeys.
A model is an ``nn.Module`` that maps an :class:`AtomicGraph` to a dict with at
least:
{"node_energy": (N,), "energy": (B,)}
Models do **not** compute forces/stress themselves -- that is added uniformly by
:class:`xnn.common.models.outputs.ForceStressOutput` via autograd, so every model gets
correct, conservative forces for free and the code lives in exactly one place.
"""
from __future__ import annotations
from abc import abstractmethod
import torch
from torch import nn
from ..data import AtomicGraph
[docs]
class InteratomicPotential(nn.Module):
"""Abstract base class every interatomic-potential model must subclass.
A model is an :class:`torch.nn.Module` that maps an :class:`AtomicGraph`
to a dict of tensors with at least the keys ``"node_energy"`` (shape
``(N,)``, per-atom energies) and ``"energy"`` (shape ``(B,)``, per-structure
energies). Models do **not** compute forces or stress themselves; those are
added uniformly by
:class:`xnn.common.models.outputs.ForceStressOutput` via autograd, so every
model gets correct conservative forces for free.
Notes
-----
The radial cutoff (in Angstrom, used to build neighbor lists) is set as the
instance attribute ``cutoff`` in each subclass ``__init__``. It is
intentionally not declared as a bare class annotation here, so subclasses
remain compatible with :func:`torch.jit.script`.
"""
# `cutoff` (radial cutoff in Angstrom, used to build neighbor lists) is set
# as an instance attribute in each subclass __init__. It is intentionally
# not a bare class annotation here, so subclasses stay torch.jit.script-able.
[docs]
@abstractmethod
def forward(self, data: AtomicGraph) -> dict[str, torch.Tensor]:
"""Compute per-atom and per-structure energies for a graph.
Parameters
----------
data : AtomicGraph
The batched atomic graph to evaluate.
Returns
-------
dict of str to torch.Tensor
A mapping containing at least ``"node_energy"`` of shape ``(N,)``
(per-atom energies) and ``"energy"`` of shape ``(B,)`` (per-structure
energies), where ``N`` is the number of atoms and ``B`` the number of
structures in the batch.
Raises
------
NotImplementedError
Always, unless overridden by a concrete subclass.
"""
raise NotImplementedError
[docs]
@classmethod
@abstractmethod
def from_config(cls, cfg) -> "InteratomicPotential":
"""Construct a model instance from a configuration dataclass.
Parameters
----------
cfg : ModelConfig
The model configuration dataclass describing hyperparameters.
Returns
-------
InteratomicPotential
A newly constructed model instance.
Raises
------
NotImplementedError
Always, unless overridden by a concrete subclass.
"""
raise NotImplementedError
[docs]
def aggregate_energy(self, node_energy: torch.Tensor,
data: AtomicGraph) -> torch.Tensor:
"""Sum per-atom energies into per-structure energies (locality).
Parameters
----------
node_energy : torch.Tensor
Per-atom energies of shape ``(N,)``.
data : AtomicGraph
The batched graph, providing ``num_graphs`` and the ``batch`` vector
that maps each atom to its structure index.
Returns
-------
torch.Tensor
Per-structure total energies of shape ``(B,)``, where
``B == data.num_graphs``.
"""
energy = torch.zeros(data.num_graphs, dtype=node_energy.dtype,
device=node_energy.device)
energy.index_add_(0, data.batch, node_energy)
return energy