Extending the Force Field¶
This page shows how to add custom interaction styles, type schemas, potential kernels, and export formatters.
The four-step extension chain¶
Adding a new interaction form (e.g., Morse bonds) requires four components:
- Type — declares which parameters exist (
D,alpha,r0) - Style — constructs types and converts them to a potential
- Potential — implements the numerical kernel (
calc_energy,calc_forces) - Formatters — serialize parameters for each export backend (LAMMPS, GROMACS, XML)
All four are required for a complete, reusable extension.
Step 1: define the Type¶
Subclass the appropriate Type base. For bonds, subclass BondType. Store the parameters in the parent via super().__init__(name, **kwargs).
from molpy.core.forcefield import AtomType, BondType
class MorseBondType(BondType):
def __init__(self, name: str, itom: AtomType, jtom: AtomType,
D: float, alpha: float, r0: float):
super().__init__(name, itom, jtom, D=D, alpha=alpha, r0=r0)
Step 2: define the Style¶
Subclass BondStyle. Implement def_type() to create your custom type, and to_potential() to convert all types into the numerical kernel.
from molpy.core.forcefield import BondStyle
class MorseBondStyle(BondStyle):
def def_type(self, itom, jtom, D, alpha, r0, name=""):
if not name:
name = f"{itom.name}-{jtom.name}"
typ = MorseBondType(name, itom, jtom, D=D, alpha=alpha, r0=r0)
self.types.add(typ)
return typ
def to_potential(self):
types = self.get_types(MorseBondType)
return MorseBondPotential(
D={t.name: t["D"] for t in types},
alpha={t.name: t["alpha"] for t in types},
r0={t.name: t["r0"] for t in types},
)
Step 3: define the Potential¶
Subclass the appropriate potential base. Implement calc_energy and calc_forces. Use TypeIndexedArray for vectorized parameter lookup by type name.
import numpy as np
from molpy.potential.bond.base import BondPotential
from molpy.potential.utils import TypeIndexedArray
class MorseBondPotential(BondPotential):
name = "morse"
def __init__(self, D, alpha, r0):
self.D = TypeIndexedArray(D).reshape(-1, 1)
self.alpha = TypeIndexedArray(alpha).reshape(-1, 1)
self.r0 = TypeIndexedArray(r0).reshape(-1, 1)
def calc_energy(self, r, bond_idx, bond_types):
dr = r[bond_idx[:, 1]] - r[bond_idx[:, 0]]
dr_norm = np.linalg.norm(dr, axis=1, keepdims=True)
exp_term = 1.0 - np.exp(
-self.alpha[bond_types] * (dr_norm - self.r0[bond_types])
)
return float(np.sum(self.D[bond_types] * exp_term ** 2))
def calc_forces(self, r, bond_idx, bond_types):
# Similar implementation with gradient of Morse potential
...
Always validate the potential against known values (energy at r0 should be zero, energy should increase monotonically away from r0).
Step 4: register param formatters¶
Each export backend has a ForceFieldFormatter subclass that inherits from the format's FieldFormatter (for data field name mapping) and adds _param_formatters (for Style/Type parameter serialization).
Register your custom Style's param formatter on the appropriate subclass:
from molpy.io.forcefield.lammps import LammpsForceFieldFormatter
def _format_morse_bond(typ) -> list[float]:
"""Format MorseBondType parameters for LAMMPS: D alpha r0"""
return [typ.params.kwargs["D"], typ.params.kwargs["alpha"], typ.params.kwargs["r0"]]
LammpsForceFieldFormatter.register_param_formatter(MorseBondStyle, _format_morse_bond)
Repeat for each backend. Registrations are isolated per subclass — adding a formatter to one backend does not affect others. This isolation is enforced by __init_subclass__ copying the registry.
Using the custom interaction¶
import molpy as mp
ff = mp.AtomisticForcefield(name="custom", units="real")
a_style = ff.def_atomstyle("full")
c = a_style.def_type("C", mass=12.011)
o = a_style.def_type("O", mass=15.999)
morse = ff.def_style(MorseBondStyle("morse"))
morse.def_type(c, o, D=100.0, alpha=1.8, r0=1.43)
pot = morse.to_potential()
print(pot.D["C-O"]) # [100.]
Checklist¶
- Custom
Typesubclass with explicit parameters in__init__ - Custom
Stylesubclass withdef_type()andto_potential() - Custom
Potentialsubclass withcalc_energy()andcalc_forces() - Validate potential: energy at equilibrium = 0, monotonic increase away from it
- Register formatters for each writer backend (LAMMPS, GROMACS, XML)
- Write tests: type creation, potential values, export round-trip
- Tests in
tests/test_potential/andtests/test_io/test_forcefield/