API Overview¶
This page documents the public API of Q2MM — the key classes, functions, and modules you'll use to build force field optimization workflows.
Models (q2mm.models)¶
Core data structures for force fields and molecules.
ForceField¶
Central data structure holding all force field parameters.
from q2mm.models.forcefield import ForceField
ff = ForceField(
bonds=[BondParam(...)],
angles=[AngleParam(...)],
torsions=[TorsionParam(...)],
vdws=[VdwParam(...)],
)
Attributes:
| Attribute | Type | Description |
|---|---|---|
bonds |
list[BondParam] |
Bond stretch parameters |
angles |
list[AngleParam] |
Angle bend parameters |
torsions |
list[TorsionParam] |
Torsion (dihedral) parameters |
vdws |
list[VdwParam] |
Van der Waals parameters |
Key methods:
vec = ff.get_param_vector() # flat array of all tunable parameters
ff.set_param_vector(new_vec) # update parameters from flat array
bounds = ff.get_bounds() # (lower, upper) bounds for each parameter
n = ff.n_params # total number of tunable parameters
ff2 = ff.copy() # deep copy
Factory methods:
ff = ForceField.from_mm3_fld("mol.fld") # load from MM3 .fld file
ff = ForceField.from_tinker_prm("mol.prm") # load from Tinker .prm file
Export methods:
ff.to_mm3_fld("optimized.fld") # export to MM3 .fld
ff.to_tinker_prm("optimized.prm") # export to Tinker .prm
ff.to_openmm_xml("forcefield.xml") # export to OpenMM XML
ff.to_openmm_xml("forcefield.xml", mol) # with AtomTypes/Residues
Q2MMMolecule¶
Molecular structure with coordinates, optional Hessian, and auto-detected
bonds/angles. Bonds are inferred from covalent radii — see the
tutorial for details on
bond_tolerance and when to increase it for transition states.
Constructors:
| Method | Input | Notes |
|---|---|---|
Q2MMMolecule(...) |
Raw symbols, geometry arrays | Full control, any data source |
Q2MMMolecule.from_xyz(path) |
XYZ file | Simplest option |
Q2MMMolecule.from_structure(s) |
Legacy Structure (from Gaussian, MacroModel) |
Preserves atom types and bond tables |
Q2MMMolecule.from_qcel(mol) |
QCElemental Molecule |
For MolSSI ecosystem workflows |
from q2mm.models.molecule import Q2MMMolecule
# From XYZ file (most common)
mol = Q2MMMolecule.from_xyz("ts.xyz", charge=-1, bond_tolerance=1.4)
# From raw arrays
mol = Q2MMMolecule(
symbols=["O", "H", "H"],
geometry=[[0.0, 0.0, 0.0], ...],
hessian=hessian_matrix, # optional, ndarray
atom_types=["1", "5", "5"],
bond_tolerance=1.3,
)
# From a Gaussian log (via parser)
from q2mm.parsers.gaussian import GaussLog
log = GaussLog("opt-freq.log", au_hessian=True)
mol = Q2MMMolecule.from_structure(log.structures[-1], charge=-1)
Parameters (raw constructor):
| Parameter | Type | Description |
|---|---|---|
symbols |
list[str] |
Element symbols (e.g. ["O", "H", "H"]) |
geometry |
array-like |
Cartesian coordinates (Å), shape (N, 3) |
hessian |
ndarray, optional |
Hessian matrix, shape (3N, 3N) |
atom_types |
list[str] |
MM atom type labels (default: element symbols) |
bond_tolerance |
float |
Multiplier on covalent radii sum for bond detection (default: 1.3; use 1.4+ for TS) |
Tip
Bonds and angles are auto-detected from atomic coordinates using covalent
radii — you don't need to specify them manually. Formats with explicit
bond tables (MOL2, MacroModel .mmo) preserve connectivity via
from_structure().
estimate_force_constants()¶
Extract bond and angle force constants from a QM Hessian using the Seminario method.
Input: Q2MMMolecule or list[Q2MMMolecule] — each molecule must have a Hessian.
Output: ForceField with estimated bond/angle force constants and
equilibrium values.
Note
The Seminario method is pure linear algebra (eigenvalue decomposition of 3×3 Hessian sub-blocks). It runs in < 1 ms for small molecules.
Force Field I/O (q2mm.models.ff_io)¶
Read and write force field files in different formats.
from q2mm.models.ff_io import (
load_mm3_fld, save_mm3_fld,
load_tinker_prm, save_tinker_prm,
save_openmm_xml,
)
| Function | Description |
|---|---|
load_mm3_fld(path) |
Load a ForceField from an MM3 .fld file |
save_mm3_fld(ff, path) |
Write a ForceField to an MM3 .fld file |
load_tinker_prm(path) |
Load a ForceField from a Tinker .prm file |
save_tinker_prm(ff, path) |
Write a ForceField to a Tinker .prm file |
save_openmm_xml(ff, path, molecule=None) |
Write a ForceField to an OpenMM ForceField XML file |
OpenMM XML Export¶
Two export modes are available for OpenMM:
ForceField XML — standalone format loadable by openmm.app.ForceField():
from q2mm.models.ff_io import save_openmm_xml
# Without topology (force definitions only)
save_openmm_xml(ff, "forcefield.xml")
# With topology (includes AtomTypes and Residues)
save_openmm_xml(ff, "forcefield.xml", molecule=mol)
System XML — serialize an exact OpenMM System (topology-specific):
from q2mm.backends.mm.openmm import OpenMMEngine
engine = OpenMMEngine()
engine.export_system_xml("system.xml", molecule, ff)
# Load back
system = OpenMMEngine.load_system_xml("system.xml")
| Parameter | Type | Default | Description |
|---|---|---|---|
ff |
ForceField |
(required) | Force field to export |
path |
str or Path |
(required) | Output file path |
molecule |
Q2MMMolecule, optional |
None |
Molecule(s) for <AtomTypes> and <Residues> sections |
Custom MM3 force definitions
The exported XML uses CustomBondForce, CustomAngleForce, and
CustomNonbondedForce with MM3 functional forms (cubic bond stretch,
sextic angle bend, buffered 14-7 vdW). This preserves the exact
physics of the Q2MM force field — standard harmonic approximations
are not used.
JaxEngine — Differentiable MM¶
Differentiable MM backend using JAX with OPLSAA-style energy functions
(harmonic bond/angle, 12-6 Lennard-Jones). Provides analytical gradients of
the energy with respect to force field parameters via jax.grad,
eliminating finite differences in parameter optimisation. Torsion energy
functions are implemented but not yet active for Q2MMMolecule.
from q2mm.backends.mm.jax_engine import JaxEngine
engine = JaxEngine()
# Standard MMEngine methods work as expected
energy = engine.energy(molecule, forcefield)
hessian = engine.hessian(molecule, forcefield) # analytical via jax.hessian
freqs = engine.frequencies(molecule, forcefield)
# NEW: analytical parameter gradients (not available in OpenMM/Tinker)
energy, grad = engine.energy_and_param_grad(molecule, forcefield)
# grad is dE/d(param_vector) — same shape as forcefield.get_param_vector()
Using analytical gradients with the optimizer:
from q2mm.optimizers.scipy_opt import ScipyOptimizer
# Pass jac='analytical' to use JAX gradients instead of finite differences
optimizer = ScipyOptimizer(method="L-BFGS-B", jac="analytical")
result = optimizer.optimize(objective)
Functional form differences
JaxEngine uses standard OPLSAA forms (harmonic bonds/angles, 12-6 LJ),
not MM3 forms (cubic stretch, sextic bend, buffered 14-7). Near
equilibrium geometries the results are very similar, but for exact
MM3 parity use OpenMMEngine. See issue #91 for planned MM3 JAX forms.
JaxMDEngine — Full-featured Differentiable MM¶
Wraps jax-md's OPLSAA module for production-quality molecular mechanics with analytical gradients. Supports periodic boundary conditions, electrostatics (Cutoff/Ewald/PME), and neighbor lists. Proper torsion math is implemented but currently inactive until molecule-level torsion detection lands (#127).
from q2mm.backends.mm.jax_md_engine import JaxMDEngine
engine = JaxMDEngine(box=(50.0, 50.0, 50.0))
# Standard MMEngine methods
energy = engine.energy(molecule, forcefield)
energy, grad = engine.energy_and_param_grad(molecule, forcefield)
# Per-term energy breakdown
breakdown = engine.energy_breakdown(molecule, forcefield)
# → {'bond': 1.2, 'angle': 0.5, 'torsion': 0.0, 'lj': -0.3, 'coulomb': 0.1, 'total': 1.5}
Electrostatics options:
from jax_md.mm_forcefields.nonbonded.electrostatics import (
CutoffCoulomb, EwaldCoulomb, PMECoulomb,
)
# Simple cutoff (default)
engine = JaxMDEngine(coulomb=CutoffCoulomb(r_cut=12.0))
# Ewald summation
engine = JaxMDEngine(coulomb=EwaldCoulomb(alpha=0.23, kmax=5))
# Particle Mesh Ewald
engine = JaxMDEngine(coulomb=PMECoulomb(grid_size=32, alpha=0.3))
Electrostatic charges
Partial charges are not yet optimizable through Q2MM's parameter vector. Coulomb energy is computed with zero charges by default. The Coulomb handler is still invoked so the API is forward-compatible when charge support is added.
Improper torsions
Improper torsions are not yet supported. The topology arrays are allocated empty. Support will be added when the Q2MM data model includes improper parameters.
When to use JaxMDEngine vs JaxEngine
JaxEngine is a lightweight gas-phase backend with hand-rolled energy functions — ideal for simple small-molecule TS force field work.
JaxMDEngine wraps jax-md's full OPLSAA implementation including torsions, electrostatics, and PBC. Use it when you need these features or want to leverage jax-md's ecosystem.
Optimizers (q2mm.optimizers)¶
Classes for defining objectives and running parameter optimization.
ReferenceData / ReferenceValue¶
Container for QM or experimental reference observations used as optimization targets.
Factory methods — auto-populate from QM outputs:
from q2mm.optimizers.objective import ReferenceData
# From a Gaussian formatted checkpoint (returns ref data + molecule)
ref, mol = ReferenceData.from_fchk("opt-freq.fchk", bond_tolerance=1.4)
# From a Gaussian log file (with optional frequencies)
ref, mol = ReferenceData.from_gaussian("opt-freq.log", include_frequencies=True)
# From an already-constructed molecule
ref = ReferenceData.from_molecule(mol, frequencies=freqs)
# Multi-molecule training set
ref = ReferenceData.from_molecules([mol1, mol2], frequencies_list=[f1, f2])
| Factory | Input | Returns | What it extracts |
|---|---|---|---|
from_fchk(path) |
.fchk file |
(ReferenceData, Q2MMMolecule) |
Bond lengths, angles, Hessian |
from_gaussian(path) |
.log file |
(ReferenceData, Q2MMMolecule) |
Bond lengths, angles, frequencies, Hessian |
from_molecule(mol) |
Q2MMMolecule |
ReferenceData |
Bond lengths, angles; optionally frequencies and eigenmatrix |
from_molecules(mols) |
list[Q2MMMolecule] |
ReferenceData |
Same as from_molecule for each, with sequential molecule_idx |
from_molecule() parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
mol |
Q2MMMolecule |
(required) | Molecule with geometry |
weights |
dict, optional |
{"bond_length": 10.0, "bond_angle": 5.0, "frequency": 1.0} |
Weight overrides by data type |
molecule_idx |
int |
0 |
Index for multi-molecule fits |
frequencies |
array-like, optional |
None |
Vibrational frequencies (cm⁻¹) |
skip_imaginary |
bool |
False |
Skip negative frequencies |
include_eigenmatrix |
bool |
False |
Add Hessian eigenmatrix training data |
eigenmatrix_diagonal_only |
bool |
False |
Only diagonal eigenmatrix elements |
from_gaussian() parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str or Path |
(required) | Path to Gaussian .log file |
weights |
dict, optional |
see above | Weight overrides |
bond_tolerance |
float |
1.3 |
Covalent-radii multiplier for bond detection (use 1.4+ for TS) |
charge |
int |
0 |
Molecular charge |
multiplicity |
int |
1 |
Spin multiplicity |
include_frequencies |
bool |
True |
Add frequency data from the log |
skip_imaginary |
bool |
False |
Skip imaginary frequencies |
au_hessian |
bool |
True |
Keep Hessian in atomic units (Hartree/Bohr²) |
from_fchk() parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str or Path |
(required) | Path to the Gaussian .fchk file |
weights |
dict, optional |
see above | Weight overrides |
bond_tolerance |
float |
1.3 |
Covalent-radii multiplier for bond detection |
charge |
int |
0 |
Molecular charge (overridden by file value if present) |
multiplicity |
int |
1 |
Spin multiplicity (overridden by file value if present) |
Bulk loaders — add data in batch to an existing ReferenceData:
ref = ReferenceData()
# Add all frequencies from an array
n = ref.add_frequencies_from_array(freqs, weight=1.0, skip_imaginary=True)
# Add eigenmatrix elements from a QM Hessian
n = ref.add_eigenmatrix_from_hessian(mol.hessian, diagonal_only=False)
| Method | Returns | Description |
|---|---|---|
add_frequencies_from_array(freqs) |
int (count added) |
Bulk-add all vibrational frequencies from a 1-D array |
add_eigenmatrix_from_hessian(hessian) |
int (count added) |
Decompose Hessian, extract eigenmatrix, add diagonal + off-diagonal elements with default weight scheme |
Manual entry — add individual observations:
ref = ReferenceData()
ref.add_energy(value=0.0, weight=1.0)
ref.add_frequency(value=1648.5, data_idx=0, weight=0.1)
ref.add_bond_length(value=0.9572, atom_indices=(0, 1), weight=10.0)
ref.add_bond_angle(value=104.52, atom_indices=(1, 0, 2), weight=5.0)
ref.add_torsion_angle(value=180.0, atom_indices=(0, 1, 2, 3), weight=2.0)
Each ReferenceValue has:
| Field | Type | Description |
|---|---|---|
kind |
str |
Type of observation (energy, frequency, bond_length, bond_angle, torsion_angle, eig_diagonal, eig_offdiagonal) |
value |
float |
Target value |
weight |
float |
Relative importance in the objective |
atom_indices |
tuple[int, ...], optional |
Atoms involved (for geometric properties) |
data_idx |
int |
Index into the raw data array (for matching frequencies/eigenvalues when atom_indices is not applicable) |
label |
str |
Human-readable label (used in error messages and debugging) |
molecule_idx |
int |
Index into the molecules list (default: 0) |
Properties:
| Property | Type | Description |
|---|---|---|
n_observations |
int |
Total number of reference entries |
values |
list[ReferenceValue] |
All reference observations |
ObjectiveFunction¶
Callable objective for scipy.optimize: maps a parameter vector to a scalar
score.
from q2mm.optimizers.objective import ObjectiveFunction
obj = ObjectiveFunction(
forcefield=ff,
engine=engine,
molecules=[mol],
reference=ref,
)
score = obj(param_vector) # f(param_vector) -> float
- Connects ForceField ↔ MM engine ↔ reference data
- Computes weighted sum-of-squares of residuals
- Caches MM engine handles for performance
Methods:
| Method | Signature | Description |
|---|---|---|
__call__(param_vector) |
ndarray → float |
Evaluate objective (sum of squared residuals). This is what scipy.optimize.minimize calls. |
residuals(param_vector) |
ndarray → ndarray |
Compute weighted residual vector. Used by least_squares method. |
reset() |
→ None |
Reset evaluation counter, history, and cached engine handles. Called automatically by ScipyOptimizer.optimize(). |
Attributes:
| Attribute | Type | Description |
|---|---|---|
forcefield |
ForceField |
The force field being optimized |
engine |
MMEngine |
MM backend (OpenMM, Tinker, etc.) |
molecules |
list[Q2MMMolecule] |
Training set structures |
reference |
ReferenceData |
QM/experimental reference observations |
n_eval |
int |
Number of objective evaluations so far |
history |
list[float] |
Score at each evaluation (for convergence tracking) |
ScipyOptimizer¶
Wraps scipy.optimize with sensible defaults for force field optimization.
from q2mm.optimizers.scipy_opt import ScipyOptimizer
optimizer = ScipyOptimizer(method="L-BFGS-B", eps=1e-3)
result = optimizer.optimize(objective)
print(result.summary())
print(f"Improvement: {result.improvement:.1%}")
Constructor parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
method |
str |
"L-BFGS-B" |
Optimization algorithm (see table below) |
maxiter |
int |
500 |
Maximum iterations |
ftol |
float |
1e-8 |
Function tolerance for convergence |
eps |
float |
1e-3 |
Finite-difference step size (scipy default ~1e-8 is too small for FF parameters) |
use_bounds |
bool |
True |
Use parameter bounds from ForceField.get_bounds() |
verbose |
bool |
True |
Log progress during optimization |
Optimization Methods¶
| Method | Type | Best for |
|---|---|---|
L-BFGS-B |
Quasi-Newton | Smooth problems (default) |
Nelder-Mead |
Simplex | Derivative-free, robust |
trust-constr |
Trust-region | Constrained optimization |
Powell |
Direction-set | Derivative-free |
least_squares |
Levenberg-Marquardt | Residual-based fitting |
Choosing a method
L-BFGS-B is the best starting point for most problems. Switch to Nelder-Mead if the objective is noisy or non-smooth. Use least_squares when you have many residuals and few parameters.
OptimizationResult¶
Returned by ScipyOptimizer.optimize().
result = optimizer.optimize(objective)
print(result.summary()) # human-readable summary
print(result.improvement) # fractional improvement (0–1)
print(result.history) # score at each evaluation
Attributes:
| Attribute | Type | Description |
|---|---|---|
success |
bool |
Whether the optimizer converged |
message |
str |
Convergence status message from scipy |
initial_score |
float |
Objective value before optimization |
final_score |
float |
Objective value after optimization |
n_iterations |
int |
Number of optimizer iterations |
n_evaluations |
int |
Total objective function evaluations |
initial_params |
ndarray |
Starting parameter vector |
final_params |
ndarray |
Optimized parameter vector |
history |
list[float] |
Score at each evaluation |
method |
str |
Optimization method used |
Properties and methods:
| Member | Returns | Description |
|---|---|---|
improvement |
float |
Fractional improvement: (initial - final) / initial. 0 = no change, 1 = perfect. |
summary() |
str |
Human-readable summary (method, scores, improvement, eval count) |
OptimizationLoop¶
GRAD→SIMP cycling loop: alternates full-space gradient optimization with sensitivity-based subspace simplex passes. See the Optimization Guide for detailed usage and examples.
from q2mm.optimizers.cycling import OptimizationLoop
loop = OptimizationLoop(
objective,
max_params=3,
max_cycles=10,
convergence=0.01,
)
result = loop.run()
print(result.summary())
Constructor parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
max_params |
int |
3 |
Parameters per simplex pass |
max_cycles |
int |
10 |
Maximum GRAD→SIMP iterations |
convergence |
float |
0.01 |
Stop when fractional improvement < this value |
full_method |
str |
"L-BFGS-B" |
Scipy method for full-space pass |
simp_method |
str |
"Nelder-Mead" |
Scipy method for subspace pass |
full_maxiter |
int |
200 |
Max iterations for gradient pass |
simp_maxiter |
int |
200 |
Max iterations for simplex pass |
sensitivity_metric |
str |
"simp_var" |
"simp_var" or "abs_d1" |
eps |
float |
1e-3 |
Finite-difference step size |
LoopResult¶
Returned by OptimizationLoop.run().
| Attribute | Type | Description |
|---|---|---|
success |
bool |
Whether the loop converged |
initial_score |
float |
Objective before optimization |
final_score |
float |
Objective after optimization |
n_cycles |
int |
Number of GRAD→SIMP cycles completed |
message |
str |
Convergence status message |
cycle_scores |
list[float] |
Score after each cycle (length = n_cycles + 1) |
selected_indices |
list[list[int]] |
Parameter indices selected per cycle |
sensitivity_results |
list[SensitivityResult] |
Sensitivity data per cycle |
improvement |
float |
Fractional improvement (property) |
summary() |
str |
Human-readable summary (method) |
SubspaceObjective¶
Wraps an ObjectiveFunction to expose only a subset of parameters.
from q2mm.optimizers.cycling import SubspaceObjective
sub = SubspaceObjective(objective, [0, 2, 4], ff.get_param_vector())
score = sub(sub_vector) # len(sub_vector) == 3
| Method | Signature | Description |
|---|---|---|
__call__(sub_vector) |
ndarray → float |
Evaluate objective on the subspace |
residuals(sub_vector) |
ndarray → ndarray |
Residuals on the subspace |
get_initial_vector() |
→ ndarray |
Initial values for active indices |
get_bounds() |
→ list[tuple] |
Bounds for active indices |
compute_sensitivity()¶
Standalone sensitivity analysis via central differentiation.
from q2mm.optimizers.cycling import compute_sensitivity
sens = compute_sensitivity(objective, metric="simp_var")
print(sens.ranking) # param indices sorted by sensitivity
print(sens.d1) # first derivatives
print(sens.simp_var) # d2/d1² metric
| Parameter | Type | Default | Description |
|---|---|---|---|
objective |
ObjectiveFunction |
required | The objective to analyse |
metric |
str |
"simp_var" |
Ranking metric ("simp_var" or "abs_d1") |
step_sizes |
ndarray, optional |
from ForceField.get_step_sizes() |
Per-parameter perturbation sizes |
Returns a SensitivityResult with fields d1, d2, simp_var, ranking, metric, n_evals.
Backends (q2mm.backends)¶
Abstract interfaces and concrete implementations for MM and QM engines.
MMEngine (abstract base)¶
from q2mm.backends.base import MMEngine
class MMEngine(ABC):
def energy(self, structure, forcefield) -> float: ...
def minimize(self, molecule, forcefield) -> tuple[float, list, ndarray]: ...
def frequencies(self, structure, forcefield) -> list[float]: ...
def hessian(self, structure, forcefield) -> ndarray: ...
Implementations: OpenMMEngine, TinkerEngine, JaxEngine, JaxMDEngine
QMEngine (abstract base)¶
from q2mm.backends.base import QMEngine
class QMEngine(ABC):
def energy(self, molecule) -> float: ...
def hessian(self, molecule) -> ndarray: ...
def optimize(self, molecule) -> Q2MMMolecule: ...
def frequencies(self, molecule) -> list[float]: ...
Implementations: Psi4Engine
Warning
QM engines are used for reference data generation, not during the optimization loop. A single Hessian calculation can take minutes to hours depending on molecule size and level of theory.
Parsers (q2mm.parsers)¶
File format parsers for reading computational chemistry output.
| Parser | Module | Description |
|---|---|---|
GaussLog |
q2mm.parsers |
Gaussian .log output files |
Mol2 |
q2mm.parsers |
MOL2 structure files |
JaguarIn / JaguarOut |
q2mm.parsers |
Jaguar input/output |
MacroModel / MacroModelLog |
q2mm.parsers |
MacroModel files |
MM3 |
q2mm.parsers |
MM3 force field files |
TinkerFF |
q2mm.parsers |
Tinker parameter files |
from q2mm.parsers import GaussLog
log = GaussLog("optimization.log")
structures = log.structures # list[Structure] — coordinates, atoms
eigenvalues = log.evals # frequency eigenvalues
eigenvectors = log.evecs # frequency eigenvectors
Software Compatibility¶
Feature support across force field formats and compute backends.
Force Field Formats¶
| Capability | MM3 .fld |
Tinker .prm |
OpenMM XML | AMBER .frcmod |
|---|---|---|---|---|
| Read (load parameters) | ✅ | ✅ | — | ✅ |
| Write (standalone) | ✅ | ✅ | ✅ | ✅ |
| Write (template-based) | ✅ | ✅ | — | ✅ |
| Bond stretch | ✅ | ✅ | ✅ | ✅ |
| Angle bend | ✅ | ✅ | ✅ | ✅ |
| Torsion (dihedral) | ✅ | ✅ | ✅ | ✅ |
| van der Waals | ✅ | ✅ | ✅ | ✅ |
| MM3 functional forms | ✅ native | ✅ native | ✅ custom forces | — |
Template-based vs standalone export
Template-based export updates parameters in an existing file, preserving headers, comments, and parameters that weren't optimised. Use this for round-trip compatibility with the original software.
Standalone export writes a minimal file from scratch — useful when no template exists (e.g., Seminario-estimated parameters).
MM Backends¶
| Capability | OpenMM | Tinker | JAX | JAX-MD |
|---|---|---|---|---|
| Single-point energy | ✅ | ✅ | ✅ | ✅ |
| Energy minimisation | ✅ | ✅ | ✅ | ✅ |
| Numerical Hessian | ✅ | ✅ | — | — |
| Analytical Hessian | — | — | ✅ jax.hessian |
✅ jax.hessian |
| Vibrational frequencies | ✅ | ✅ | ✅ | ✅ |
| Runtime parameter update | ✅ update_forcefield() |
❌ (re-writes files) | ✅ (all JIT-compiled) | ✅ (all JIT-compiled) |
| Analytical param gradients | — | — | ✅ energy_and_param_grad() |
✅ energy_and_param_grad() |
| Per-term energy breakdown | — | — | — | ✅ energy_breakdown() |
| System XML export | ✅ export_system_xml() |
— | — | — |
| Torsions | ✅ | ✅ | ❌ (#91) | ⚠️ engine ready, awaiting #127 |
| Electrostatics | ❌ (charges zeroed) | ✅ | — | ⚠️ infra only; charges zeroed |
| Periodic boundary conditions | ✅ | ✅ | — | ✅ |
| Functional forms | MM3 (cubic/sextic/14-7) | MM3 | OPLSAA (harmonic/LJ) | OPLSAA (harmonic/LJ) |
| Install | pip install openmm |
System binary | pip install "q2mm[jax]" |
pip install "q2mm[jax-md]" |
QM Backends¶
| Capability | Gaussian | Jaguar | Psi4 |
|---|---|---|---|
| Parse optimised geometry | ✅ .log / .fchk |
✅ .out |
✅ via QCElemental |
| Parse Hessian | ✅ | ✅ .in |
✅ |
| Parse frequencies | ✅ | ✅ .out |
✅ |
| Parse eigenvalues / eigenvectors | ✅ | ✅ .out |
✅ |
| Live QM engine | ❌ (file-based) | ❌ (file-based) | ✅ Psi4Engine |
Seminario Method¶
| Feature | Supported |
|---|---|
| Bond force constants | ✅ |
| Angle force constants | ✅ |
| Transition states (imaginary mode handling) | ✅ Methods C, D, E (Limé & Norrby, J. Comput. Chem. 2015) |
| Multiple molecules (ensemble averaging) | ✅ |
| Eigenmatrix training data | ✅ |
Transition State Methods C, D, E
At a transition state, one Hessian eigenvalue is negative (the reaction coordinate). Methods C, D, and E are different strategies for handling this during Seminario estimation:
- Method C (
replace_neg_eigenvalue): Replace the negative eigenvalue with a large positive value. Simple but distorts the eigenspectrum. - Method D (
keep_natural_eigenvalue): Keep the natural (negative) eigenvalue unchanged — gives lower RMS error but may produce force fields with negative force constants. - Method E (
estimate_force_constants_method_e): Run Method D first, detect problematic parameters (zero or negative force constants), and replace only those parameters with the corresponding Method C values.
See q2mm.models.seminario for the implementation.
Quick reference
For the best out-of-the-box experience, use OpenMM as your MM backend
and Gaussian .fchk or Psi4 for QM reference data. This combination
gives you fast runtime parameter updates, full Hessian access, and
export to both System XML and ForceField XML.
For fastest optimisation, use the JAX backend with
ScipyOptimizer(jac='analytical') to get exact analytical gradients —
no finite differences needed. Install with pip install "q2mm[jax]".
See Benchmarks for benchmarks across backends and optimization methods.