Use and write .frc force-field files#

The classical force fields of the ffnn family – ReaxFF, OPLS, and the ones to come – read their parameters from one format: the MolSSI/SEAMM .frc force-field file (xnn.ffnn.common.frc), the format of the SEAMM force-field distribution. Besides the parameters, a .frc file carries the SMARTS templates that assign its own atom types to any structure, so a fixed-topology force field such as OPLS can be applied to a molecule 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.frc is the OPLS-AA distribution (variants oplsaa, the CL&P ionic-liquid extension CL&P, and their union oplsaa+), dreiding.frc carries the DREIDING generators (variants dreiding with Lennard-Jones nonbonds and dreiding/X6 with exponential-6 ones), and a dozen published ReaxFF fields live under reaxff/; all are copied verbatim from SEAMM (BSD-3-Clause, see src/xnn/ffnn/data/README.md for the commit and per-file provenance). xnn adds lopls (Siu et al. 2012) and oplsaa-1996 (the alkane and alcohol torsions of the original paper), both small files that #include oplsaa.frc and override a few rows.

Every model and reader accepts the same spec: a variant name ("oplsaa", "CHO_cho_2008"), a path to a .frc file, or "<path>.frc:<variant>" to pick one of several variants in a file.

Apply a force field to a molecule#

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

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

dre = Dreiding.from_atoms(ethanol, "dreiding", cutoff=12.0)
dre.topology.types              # ['C_3', 'C_3', 'O_3', 'H_', ...]
dre.topology.bond_orders        # DREIDING's rules read these too

reax = ReaxFF("CHO_cho_2008")   # ReaxFF needs no typing: species only

OPLS.from_atoms perceives the bonding with RDKit, assigns the library’s atom types from its templates (assign_atom_types()), derives the topology, and places impropers at trigonal centers the library has a pattern for. Dreiding.from_atoms does the same and additionally keeps the perceived bond orders, which its rules need to pick torsion barriers and stretch force constants. RDKit is an optional dependency:

pip install "xnn[ffnn]"

The lower-level pieces are available separately when you need them:

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

ff = read_forcefield("oplsaa")                  # resolved variant
types = assign_atom_types(ethanol, ff)          # SMARTS typing only
top = MolecularTopology.from_ase(ethanol, forcefield="oplsaa")
lib = read_opls("CL&P", strict=False)           # skip forms OPLS lacks

Query a force field directly#

ForceField exposes the lookups force-field codes need, with the equivalence and wildcard rules of the format:

ff.charge("opls_80")                     # -0.18 (through the NonB equivalence)
ff.nonbond("opls_80")                    # (3.5, 0.066): sigma / A, eps / kcal/mol
ff.bond("opls_80", "opls_81")            # -> ('quadratic_bond', key, Row)
ff.torsion("opls_85", "opls_18", "opls_18", "opls_85")[2].values
ff.improper("opls_89", "opls_89", "opls_88", "opls_88")   # center third
ff.templates["opls_80"]["smarts"]        # ['[CD4H3:1][#6]' ...]

Units are those of the format’s defaults (kcal/mol, Angstrom, degrees); @units and @type modifiers in the file are converted on reading, so a section written in kJ/nm or as rmin/epsilon comes out the same way.

Write a force field#

Trained parameters go back out in the same format:

trained = model.ff.export_library()      # OPLSLibrary, kcal/mol units
trained.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

The written file carries the templates, so it types structures like the original. Two caveats: a dihedral V0 constant has no column in torsion_opls and is dropped (it affects neither forces nor energy differences), and ReaxFF-nn network weights have no place in the format, so neural libraries keep using the JSON format of save().

The format in brief#

A file opens with !MolSSI forcefield 1: the word after ! is the dialect (MolSSI, or BIOSYM in legacy files) and the trailing number is the format version, the version of the file grammar. Version 1 is the current and only published one; it is what xnn implements and writes (FRC_FORMAT_VERSION), every shipped file declares it, FrcFile exposes it as format_version, and a newer number is parsed with a warning. It is not a parameter version: those sit in the Version column of every row (see below), and the newest wins. Everything else is organised in sections that 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 the variant <name>; later labels override earlier ones for the same key, which is how CL&P and lopls extend oplsaa, and within a section the newest version of each key wins. #include <file> splices another file in (local: resolves against the xnn data directory and any include_dirs you pass); #templates and #fragments hold JSON; #reference <n> holds provenance.

Adding a new force field#

Write its parameters as a .frc file (any unknown section kind is read from its column header; register known ones with register_section_schema() to get key symmetry and unit conversion), give it a #templates section, and write the small bridge that turns a ForceField into the model’s parameter tables – xnn.ffnn.models.oplslib.from_forcefield() and xnn.ffnn.models.ffield.from_forcefield() are the two existing examples. Files placed in src/xnn/ffnn/data/ are found by name.