Working with Force-field Files#

The classical force-fields such as ReaxFF, OPLS, DREIDING read their parameters from the MolSSI/SEAMM .frc file format (see xnn.ffnn.common.frc), developed by MolSSI and used in SEAMM force-field distribution. A .frc file also carries SMARTS templates that assign its atom types to the molecules. Thus, any fixed-topology field can be adopted without knowing its type names.

What ships with xnn#

from xnn.ffnn.common import list_forcefields

sorted(list_forcefields())
# ['CL&P', 'dreiding', 'dreiding/X6', 'lopls', 'oplsaa', 'oplsaa+', 'oplsaa-1996',
#  'reaxff/CHLiOFSi_Yun_2017', ..., 'reaxff/CHO_cho_2008', ...]

oplsaa is the OPLS-AA distribution (CL&P adds the ionic-liquid extension, oplsaa+ is their union), dreiding and dreiding/X6 are the two DREIDING nonbond forms, and a dozen published ReaxFF fields live under reaxff/. All are copied verbatim from SEAMM (BSD-3-Clause). xnn adds lopls (Siu et al. 2012) and oplsaa-1996 (the torsions of the original paper), which #include oplsaa.frc and override a few rows, and the electrolyte-water ReaxFF field of Fedkin et al. (2019), reaxff/CHOLiNaKCsFClI_Fedkin_2019.

Every model and reader takes the same spec: a variant name, a path to a .frc file, or "<path>.frc:<variant>".

Apply a force field to a molecule#

RDKit (ffnn extra) perceives the bonds and the templates assign the types:

from ase.build import molecule
from xnn.ffnn.models import OPLS, ReaxFF, Dreiding

ethanol = molecule("CH3CH2OH")
opls = OPLS.from_atoms(ethanol, "oplsaa", cutoff=12.0)
opls.topology.types                # ['opls_80', 'opls_99', 'opls_96', ...]

dre = Dreiding.from_atoms(ethanol, "dreiding", cutoff=12.0)
dre.topology.bond_orders           # DREIDING's rules read these

reax = ReaxFF("CHO_cho_2008")      # ReaxFF needs no typing

The pieces are available on their own:

from xnn.ffnn.common import read_forcefield, assign_atom_types
from xnn.ffnn.models import MolecularTopology

ff = read_forcefield("oplsaa")
types = assign_atom_types(ethanol, ff)
top = MolecularTopology.from_ase(ethanol, forcefield="oplsaa")

ff.charge("opls_80")                      # -0.18
ff.nonbond("opls_80")                     # (3.5, 0.066): sigma / A, eps / kcal/mol
ff.bond("opls_80", "opls_81")             # ('quadratic_bond', key, Row)
ff.templates["opls_80"]["smarts"]         # ['[CD4H3:1][#6]', ...]

Units are the format’s defaults (kcal/mol, Å, degrees); @units and @type modifiers in a file are converted on reading.

Write a force field#

Trained parameters go back out in the same format:

lib = opls.ff.export_library()
lib.save_frc("my_opls.frc", name="my-opls")
OPLS.from_atoms(ethanol, "my_opls.frc")        # and straight back in

reax.export_library().save("ffield.json")      # ReaxFF-nn weights: JSON only

from xnn.ffnn.models.ffield import write_ffield
write_ffield(reax.export_library(), "ffield.refit")   # classical ReaxFF for LAMMPS

The written .frc file carries the templates. A dihedral V0 constant has no column in the format and is dropped (it changes neither forces nor energy differences); ReaxFF-nn network weights have no place in it and stay in JSON. write_ffield writes the ffield text format of LAMMPS pair_style reaxff, so a ReaxFF field refit with conventions="lammps" (the default for classical fields) runs there unchanged.

The format in brief#

A file opens with !MolSSI forcefield 1 (dialect and format version). Sections start at a # line and run to the next one:

#quadratic_bond oplsaa          <- kind, label

> E = K2 * (R - R0)^2           <- annotation
@units K2 kJ/mol/nm^2           <- modifier (optional)

!Version    Ref  I        J        R0      K2     <- column header
2023.01.29  1    opls_18  opls_18  1.5290  268.00 <- data row

#define <name> lists, per functional form, the labelled sections that make up a variant; later labels override earlier ones, which is how CL&P and lopls extend oplsaa. #include <file> splices another file in, #templates holds the SMARTS JSON, #reference the provenance. Within a section the newest Version of each key wins.

To add a force field, write its parameters as a .frc file with a #templates section (unknown section kinds are read from their column header; register_section_schema() adds key symmetry and unit rules) and a small bridge from ForceField to the model’s tables, as xnn.ffnn.models.oplslib.from_forcefield() does. Files under src/xnn/ffnn/data/ are found by name.