Extending the Force Field¶
A new interaction style, or a whole new category of terms, is a registration from Python. Nothing is rebuilt: no Rust, no wheel, no writer arm to add.
The force-field IR (molrs's, which adopts the LAMMPS standard) is a
protocol. A category says how many atoms a term has and which Frame
block its terms live in; a style says its ordered parameters, each with a
dimension, and its energy, as an expression or as a Python kernel. Anything of
that form registers into the registry every PotentialCompiler reads, and from
then on it is typed, priced, saved and read back exactly like a built-in.
molpy keeps no parallel IR: mp.ff mirrors molrs.ff submodule by submodule
(mp.ff.ir.StyleSpec is molrs.ff.ir.StyleSpec, a registered style's record).
The IR's vocabulary (ParamSpec, StyleSpec, CategorySpec, the refusals)
is mp.ff.ir; the registry, with the class you subclass to declare a style
(StyleDeclaration), is mp.ff.style_registry; the compiler is
mp.ff.compile; the kernels are mp.ff.potential; and the force-field
container is mp.ff.forcefield.
A new style in 30 lines¶
A Kremer-Grest bead-spring chain: LAMMPS bond_style fene, declared by its
expression, and a typifier that types every bead B and every bond with it.
import molpy as mp
from molpy.ff.forcefield import ForceField
from molpy.ff.compile import PotentialCompiler
from molpy.ff.ir import ParamSpec
from molpy.ff.style_registry import StyleDeclaration
from molpy.ff.typifier import TypeAssignment, Typifier
class Fene(StyleDeclaration): # LAMMPS bond_style fene, by its expression
category, name = "bond", "fene"
params = [ParamSpec("k", "E/L^2"), ParamSpec("r0", "L"), ParamSpec("epsilon", "E"), ParamSpec("sigma", "L")]
expression = ("-0.5*k*r0^2*log(1-(r/r0)^2)"
"+step(2^(1/6)*sigma-r)*(4*epsilon*((sigma/r)^12-(sigma/r)^6)+epsilon)")
class BeadSpring(Typifier): # every bead B, every bond FENE; lj units
def source_forcefield(self):
return ForceField("bead-spring", units="lj")
def assign(self, graph):
bead = {"type": ("full", "B", (), {"mass": 1.0})}
spring = {"type": ("fene", "B-B", ("B", "B"),
{"k": 30.0, "r0": 1.5, "epsilon": 1.0, "sigma": 1.0})}
bonds = graph.links.exact_bucket(mp.Bond)
return TypeAssignment([bead] * len(graph.atoms), links={mp.Bond: [spring] * len(bonds)},
styles=[("atom", "full", {}), ("bond", "fene", {})])
typifier = BeadSpring()
frame = typifier.typify(chain).to_frame() # chain: an mp.Atomistic of bonded beads
ff = typifier.forcefield()
energy, forces = PotentialCompiler(ff).compile(frame).calc_energy_forces(frame)
mp.io.write_mrec_frame("chain.mrec", frame, forcefield=ff) # the expression travels along
Defining the class registers the style; Fene.unregister() takes it out
again. The record carries the expression, so a process that registered
nothing reads chain.mrec back with
mp.io.read_mrec_forcefield("chain.mrec").to_forcefield() and
prices it identically. molpy's test suite runs this snippet as written
(tests/test_ff/test_user_style.py), and proves the energy and forces
equal the analytic FENE sum and that a fresh process prices the record bit for
bit.
The pieces¶
Name (mp.ff.style_registry.…; ParamSpec and IrError on mp.ff.ir, ExplicitTerms on mp.ff.compile) |
What it does |
|---|---|
StyleDeclaration |
Subclass it: category, name, params, and an expression and/or a kernel method. The subclass statement registers it. |
ParamSpec(name, dim, *, kind, default, mix, indexed, …) |
One parameter: its name and dimension ("E/L^2"; E L A Q M for energy, length, angle, charge, mass). params also takes a {name: dim} dict. |
register_style(category, name, *, params, expression=None, kernel=None, …) |
The same as a function. |
register_category(name, arity, *, coordinate="compound", order="reversible") |
A new category of 2–5 atoms; its terms live in the block f"{name}s". |
styles(category=None), categories(), evaluate(...), unregister_style(category, name) |
Introspection (styles returns StyleSpec records, categories CategorySpec records), a style's energy on a batch of coordinates, removal of a custom style. |
ExplicitTerms(category, style, atoms, **params).compile() |
Any style's kernel over explicit instances (atom indices, one parameter row per term), no typifier needed. |
IrError |
Every refusal is a subclass of it (a ValueError) named after what was refused (SealedError, NoKernelError, UnboundVariableError, …). |
Parameters arrive as stored. An angle value (dimension A) is in degrees,
as the force field stores it, and the expression converts it with
(pi/180), defining pi last (Lepton has no named constants):
k*(theta-theta0*(pi/180))^2; pi=3.141592653589793. Coordinates (theta,
phi) are radians.
Variables by category: r (bond, pair), theta (angle), phi
(dihedral), phi and chi = abs(phi) (improper); any category with points
can use distance(p1,p3), angle(p1,p2,p3), dihedral(p1,p2,p3,p4). Pair
expressions also see q1, q2 and the self rows x1, x2 of a parameter
x. The grammar is Lepton's: + - * / ^, exp log sqrt sin cos tan asin acos
atan abs min max step delta select, definitions after ;.
A Python kernel instead of (or beside) an expression: a method
kernel(self, q, **params) -> (e, de_dq) vectorised over the terms, or for a
compound category kernel(self, x, **params) -> (e, grad) with x of shape
(n, arity, 3). With both, they must agree; registration checks the
derivative against a central difference.
A new category¶
A Urey-Bradley 1-3 spring as its own category, typed through a custom relation kind of the graph:
mp.ff.style_registry.register_category("urey_bradley", 3)
class UreyBradley(mp.ff.style_registry.StyleDeclaration):
category, name = "urey_bradley", "harmonic"
params = {"k_ub": "E/L^2", "r_ub": "L"}
expression = "k_ub*(distance(p1,p3)-r_ub)^2"
A typifier's TypeAssignment then keys its rows by the kind name
(links={"urey_bradleys": rows}, after graph.register_kind("urey_bradleys", 3)),
and the terms land in the frame's urey_bradleys block.
Engines¶
Exporting to an engine is molrs's job, and molpy adds no formatter of its
own: LammpsEngine.generate_inputs (and the minimize / md runs built on
it) takes every *_style line and coefficient from molrs's LAMMPS writer
(mp.io.write_lammps_forcefield_str), so it writes whatever that can write —
hybrid styles and angle charmm with its Urey-Bradley term included — and a
style an engine cannot hold is refused by molrs, by name, never written
half-formed. The .mrec record is the format that always holds a registered
style: its expression travels with it.
Checklist¶
- The style declares its parameters in LAMMPS
*_coefforder, each with its dimension - An expression, a kernel, or both (then they agree)
- A test: energy and forces against a hand formula, and a
.mrecround trip if the style is to be saved