Skip to content

Repository files navigation

mmff94-ts

The MMFF94 force field, in pure TypeScript.

No WebAssembly. No native builds. No server. Every term of Halgren's force field — bond, angle, stretch-bend, torsion, out-of-plane, van der Waals, electrostatic — implemented from the papers, validated per-term against the original 761-molecule validation suite, and fast enough to hold its own against the canonical Fortran.

CI typing energies license

import { parse_sdf, calc_energy, optimize_lbfgs } from 'mmff94-ts';

const mol = parse_sdf(sdfText);        // V2000 SDF/MOL in, molecule out
const e = calc_energy(mol);            // every term + total (kcal/mol)
const opt = optimize_lbfgs(mol);       // gradient-based minimization

console.log(e.bond_stretch, e.angle_bend, e.torsion, /* … */ e.total);
console.log(opt.converged, opt.energy.total, opt.final_max_gradient);

Why this exists

JavaScript force fields to date have made one of two trades: ship the science as an opaque WebAssembly blob (heavy, sandbox-hostile, unreadable), or ship a toy (total energy only, no gradients, no validation). Neither serves real work.

mmff94-ts takes a third road — transcribe the science faithfully and prove it:

  • All seven terms, each with Halgren's published functional form: the buffered 14-7 vdW, the double-cubic stretch-bend, the three-term torsion Fourier series, the Wilson out-of-plane, the cubic bond and sextic angle with CB anharmonicity, and charge–charge electrostatics with the ×0.75 1–4 scaling.
  • Analytical gradients for every term, verified against central finite differences.
  • Numbers you can check. Not benchmarked "on typical molecules" — compared, term by term, against BatchMin's own output on Halgren's full validation suite.

Validation — measured, not claimed

Every number below is generated from the suite files and re-checked on every test run; nothing here is transcribed by hand.

Check Result
Atom typing 761/761 exact vs OpenBabel's MMFF94 typer
Per-term energies all seven terms ≤1e-4 kcal/mol vs BatchMin (759/761 within that gate; the rest are documented ERULE print-precision artifacts)
Partial charges ≤1e-3 e⁻ on 757/757 comparable molecules
Total energies 758/761 within 1e-3 kcal/mol
Gradient correctness finite-difference verified on every fixture and the 304-atom trp-cage; tests/gradient.test.ts prints the current worst
Independent implementations final energies match RDKit MMFF94 to ~0.5 kcal/mol and Tinker 26.2 to four decimals on shared minima

Five details worth knowing, because they're where hand-waved implementations hide:

  • Two suite entries (AN11A, DOZNIP) are excluded — Halgren himself flagged the type-76 anionic-nitrogen reference as unreliable. We document rather than average away.
  • Two ERULE residuals sit above 1e-4 only because BatchMin printed the empirical-rule parameters at 3 decimal places. The gate is the reference's print precision, not our arithmetic.
  • A third-party bug surfaced during benchmarking: Tinker 26.2 assigns a phantom −1 e net charge to neutral sulfones (hard-coded −0.5 base charge on type 107 with no sulfone compensation). We filed it upstream (TinkerTools/tinker#185) rather than quietly absorbing it into our comparison tables.
  • Nothing is dropped silently. Every interaction resolves through the stored parameter tables, then the part V empirical rules — and when neither exists, it leaves the energy. diagnose_molecule() counts all three outcomes and warns by default; the suite's own accounting is the Parameter diagnostics section of the validation report.
  • Drawings are normalized, not trusted. A sulfone written with dative S(+2)–O(−) bonds — what a SMILES round-trip can hand you — types as the same molecule as the double-bonded drawing, where MMFF94's charge-neutral S(IV)/S(VI) classes actually live (70.15 kcal/mol wrong before this, 4.90 after). The rule is narrow: N-oxides and Wittig ylides keep the charge-separated forms MMFF94 has types for.

The complete census — every residual, every exception, every exclusion with its reason — lives in the validation report.

Performance

The hot path is compiled once per molecule: every interaction parameter resolved into flat typed arrays, then energy and gradient evaluated allocation-free — no objects, no maps, no garbage collector.

On 29 drug-like molecules (OpenFF Industry set, 25–70 heavy+H atoms, QM-start geometries), minimizing to convergence:

engine converged median wall time
mmff94-ts — L-BFGS, pure JS 26/29 242 ms
Tinker 26.2 minimize — Fortran 29/29 296 ms

Same order as forty years of Fortran, in an interpreted language, on a 2014-era desktop core. Full methodology, caveats, and per-molecule data: docs/benchmark.md.

Quick start

npm install mmff94-ts        # not yet published — see Status
# meanwhile: git clone && npm install && npm run build
import { parse_sdf, assign_atom_types, assign_bci_charges, calc_energy, optimize_lbfgs } from 'mmff94-ts';

// Rich path: type and charge explicitly, inspect everything.
const mol = assign_bci_charges(assign_atom_types(parse_sdf(sdfText)));
const energy = calc_energy(mol);
console.log(energy.total);      // kcal/mol, with .bond_stretch, .angle_bend,
                                // .stretch_bend, .torsion, .out_of_plane,
                                // .van_der_waals, .electrostatic beside it

const result = optimize_lbfgs(mol);
console.log(result.converged);            // true
console.log(result.energy.total);         // energy at the minimum
console.log(result.final_max_gradient);   // convergence quality
console.log(result.molecule.atoms);       // the minimized geometry

// Simple path: bare SDF text straight to a minimized molecule.
const done = optimize_lbfgs(parse_sdf(sdfText));

Convergence defaults to max |gᵢ| < 0.05 or RMS gradient < 0.02 kcal/mol/Å (TINKER-style dual gate). criterion: 'max' restores the strict single-coordinate rule; 'rms' matches TINKER exactly.

A complete walkthrough — parsing, typing, charging, per-term analysis — is in examples/quickstart.ts, with the pipeline documented end-to-end in the walkthrough.

API surface

Function Purpose
parse_sdf(text) V2000 SDF/MOL → Molecule (bond-index-safe on malformed input)
assign_atom_types(mol) MMFF94 symbolic typing — aromaticity-aware, valence-driven
assign_bci_charges(typed) Bond-charge-increment partial charges (eq. 15 sharing)
calc_energy(typed) Per-term + total energy
calc_gradient(typed) Flat analytical gradient (3·N)
optimize_lbfgs(mol, opts?) Limited-memory BFGS with strong-Wolfe line search
optimize_steepest_descent(mol, opts?) Robust fallback minimizer
parameter_gap_report(typed) Atoms outside MMFF94's parameter space (hypervalent centers, untyped elements) — diagnostic, never silently wrong
diagnose_molecule(mol, opts?) Full parameter-resolution report: coordination gaps, empirical-rule use, omitted interactions. One extra energy evaluation; warns by default

Everything is plain data in and plain data out: no classes to instantiate, no state to manage, every function pure over its inputs — with one deliberate exception, diagnose_molecule, whose entire purpose is to speak up.

Documentation

Document Contents
Walkthrough the full pipeline end to end
Validation report the complete census — every residual, exception, exclusion
Benchmark optimizer methodology + per-molecule data
Numerical precision error-budget math
Implementer's notes resolved forensics: the trp-cage zwitterion, ERULE provenance, reference inconsistencies

Status

Implemented and gated: all seven energy terms, analytical gradients, BCI charges, L-BFGS and steepest-descent optimization, aromatic-ring perception, hypervalent-center diagnostics. 344 tests, 2 intentional skips.

Known limitations, stated plainly:

  • Explicit hydrogens required (MMFF94's own contract).
  • All-pairs nonbonded evaluation — O(N²) is fine at drug scale (≤ ~500 atoms), not protein scale. Cutoffs + neighbor lists are designed-for but not built.
  • Single-geometry minimization; no conformer ensemble generation yet.
  • MMFF94 only: the MMFF94s variant (planarized delocalized trigonal nitrogen, Halgren 1999) is not implemented.
  • Reads V2000 SDF/MOL; MOL2 and PDB readers not yet provided.

License

Citation

MMFF94 is Thomas A. Halgren's force field, published as the Merck Molecular Force Field series. If you use this library in scientific work, cite the papers whose content it implements, alongside the repository (see CITATION.cff for machine-readable metadata):

  1. Halgren, T. A. J. Comput. Chem. 1996, 17, 490–519 – Basis, form, scope, parameterization, and performance of MMFF94.
  2. Halgren, T. A. J. Comput. Chem. 1996, 17, 520–552 – MMFF94 van der Waals and electrostatic parameters for intermolecular interactions.
  3. Halgren, T. A. J. Comput. Chem. 1996, 17, 553–586 – Molecular geometries and vibrational frequencies (bond, angle, stretch-bend parameters).
  4. Halgren, T. A.; Nachbar, R. B. J. Comput. Chem. 1996, 17, 587–615 - Conformational energies and geometries (torsion parameters).
  5. Halgren, T. A. J. Comput. Chem. 1996, 17, 616–641 – Extension of MMFF94: the empirical rules this library's empirical.ts implements.
  6. Halgren, T. A. J. Comput. Chem. 1999, 20, 720–729 – MMFF VI: the MMFF94s option (not implemented here; see the limitations above).

The 761-molecule validation suite is Halgren's own, from https://server.ccl.net/cca/data/MMFF94/ (November 1998 revision).

About

Pure TypeScript MMFF94 — energy, gradients, geometry optimization

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages