OpenMM Backend¶
The OpenMMBackend is Q2MM's most versatile backend, supporting both
Harmonic and MM3 functional forms. It runs in-process via the
OpenMM Python API, avoiding subprocess overhead.
Installation¶
Q2MM develops and tests its core on Python 3.15, but OpenMM 8.6.1 has no cp315 wheels. Use a separate Python 3.14 environment for its required CPU correctness lane. The package also runs core checks on Python 3.14; it is not a soft-provisioning fallback. Affected native and pairwise checks require successful image/package preparation and nonzero executed tests. See runtime installation notes.
OpenMM is available via conda-forge:
Or with pip:
For GPU support (via CUDA), install the CUDA plugin package:
This provides CUDA plugin binaries that JIT-compile kernels via NVRTC, supporting all NVIDIA architectures including Blackwell (RTX 5090). Works on Linux, WSL2, and native Windows — requires an NVIDIA GPU and a compatible driver (≥ 535).
WSL2 recommended for GPU benchmarks
For GPU benchmarks and the full CUDA stack (JAX CUDA + JAX-MD + OpenMM CUDA), WSL2 is the recommended environment on Windows. Native Windows supports OpenMM CUDA but not JAX CUDA or JAX-MD.
Avoid OpenCL for GPU acceleration
OpenCL on modern NVIDIA GPUs (e.g. RTX 5090) gives very poor GPU utilisation (~14%). Always prefer CUDA over OpenCL when an NVIDIA GPU is available. The auto-detection order (CUDA > OpenCL > CPU) ensures CUDA is selected first when both plugins are installed.
Verify installation
Platform detection¶
OpenMMBackend auto-detects the fastest available compute platform:
| Priority | Platform | Notes |
|---|---|---|
| 1 | CUDA | Requires NVIDIA GPU + CUDA toolkit |
| 2 | OpenCL | AMD/Intel GPUs |
| 3 | CPU | Multi-threaded, available everywhere |
| 4 | Reference | Single-threaded, for debugging only |
Override with the platform_name constructor parameter if needed.
Supported energy terms¶
| Term | Harmonic Mode | MM3 Mode |
|---|---|---|
| Bonds | ✅ Harmonic | ✅ Cubic/quartic |
| Angles | ✅ Harmonic | ✅ Sextic |
| Torsions | ✅ | ✅ |
| Improper torsions | Existing cosine model | Existing cosine model |
| vdW (LJ 12-6) | ✅ | — |
| vdW (Buckingham exp-6) | — | ✅ |
| Electrostatics | ❌ | ❌ |
| 1-4 scaling | ✅ AMBER (ε/2) | None (MM3) |
Nonbonded topology and CMAP grids¶
Nonbonded exclusions are derived from the bond graph, even if the molecule
has no angle or torsion records. Pairs separated by one or two bonds are
excluded; harmonic-mode pairs whose shortest path is three bonds retain
half-strength vdW interactions. MM3 keeps its existing unscaled 1-4 interactions.
Atom types declared in nonbonded_excluded_atom_types have no nonbonded
interactions, including with other excluded centers.
CMAP supplies a fixed two-dihedral energy correction. The backend converts
CmapGrid's phi-major ordering and -180-degree origin to
OpenMM's phi-fast ordering and zero-degree origin,
and converts kcal/mol to kJ/mol once. Only even grid resolutions are
supported: an odd resolution would require resampling, so preparation rejects
it rather than choosing an interpolation policy. This conversion retains
OpenMM's native signed-dihedral convention; it does not establish signed-torsion
equivalence with other backends.
Preparation gates¶
Nonzero canonical bond dipoles and nondefault vdW reduction raise
PreparationError before layout or native-state construction in both forms.
Existing Urey-Bradley, CMAP, and cosine improper handling is retained.
The separate stretch-bend limitation is unchanged. See the
shared term-presence rules.
Proper and improper terms containing known native wildcard tokens
are rejected even when other valid terms make the system nonempty.
Configuration¶
from q2mm.backends.mm import OpenMMBackend
backend = OpenMMBackend(
platform_name=None, # auto-detect (CUDA > OpenCL > CPU > Reference)
precision=None, # "single", "mixed", or "double" (GPU only; default: "mixed")
)
| Parameter | Type | Default | Description |
|---|---|---|---|
platform_name |
str \| None |
None |
Force a specific OpenMM platform |
precision |
str \| None |
None |
GPU precision mode; ignored on CPU |
Runtime parameter updates¶
OpenMMBackend.prepare(PreparationRequest(...)) returns a prepared session
that owns a reusable OpenMM Context. Each typed request carries a full
parameter vector, and the session updates the context's force parameters
without rebuilding the system. This makes iterative optimization fast while
keeping all evaluations behind the typed prepared-session contract.
Capabilities¶
| Prepared-session operation | Supported | Notes |
|---|---|---|
energy(EnergyRequest) |
✅ | Returns kcal/mol |
minimize(MinimizationRequest) |
✅ | OpenMM L-BFGS minimizer |
hessian(HessianRequest) |
✅ | Numerical (finite-difference) |
frequencies(FrequencyRequest) |
✅ | From numerical Hessian |
parameter_gradient(ParameterGradientRequest) |
✅ | Exact for bond/angle/torsion; vdW via finite differences |
Capability.REUSABLE_STATE |
✅ | Prepared session reuses the OpenMM context |
Serialization¶
Standalone force-field XML can be written with
q2mm.io.save_openmm_xml(force_field, path, molecule=...). This exports a
limited MM3 subset, not an arbitrary prepared system. Unsupported
Urey-Bradley, stretch-bend, CMAP, improper, bond-dipole, reduced-site,
nonbonded-exclusion, and wildcard content raises ValueError before the
destination is replaced. Native integer-zero wildcard types are rejected
in vdW records as well as bonded terms. Every nonzero vdW reduction,
including 1.0, is rejected because this format does not retain that declaration.
Nonempty bond-order selectors and non-generic bond contexts are also rejected:
the XML records contain atom classes, not those additional selectors.
Class names must be unpadded, and repeated bond/angle class tuples or vdW
classes fail instead of allowing native matching to select an arbitrary
definition.
When molecules are supplied, selected force-field rows must map to their
actual atom classes. Element-only fallback or source-row bindings that
would require a different native class mapping are rejected, not silently
dropped. Use matching explicit classes or the prepared-System serializer.
Proper torsions use OpenMM's <Proper> schema. Only one Fourier component
per atom-class tuple is supported: OpenMM's custom torsion loader selects
the first matching definition rather than summing repeated definitions.
Multiple components are therefore rejected instead of partially exported.
The XML's vdW expression is Buckingham exp-6; this format does not imply
complete equivalence with a backend's physical policies.
For topology-specific serialization of an already prepared OpenMM system,
use q2mm.io.openmm.save_openmm_system_xml(prepared, path) and
q2mm.io.openmm.load_openmm_system_xml(path). These are I/O functions, separate
from the backend's typed evaluation interface.
Limitations¶
- Numerical Hessians —
hessian()uses finite differences, not analytical second derivatives. Accurate but slower than JAX's analytical Hessian. - Partial analytical gradients —
parameter_gradient()provides exact gradients for bond, angle, and torsion parameters via OpenMM global-parameter derivatives. vdW parameter gradients are supplemented via central finite differences for positive epsilon, using a step no larger than 1% of epsilon or 1e-4 kcal/mol. Radii use a 1e-4 Angstrom step, switching to a second-order forward difference when a central step would reach zero. Unused rows and rows without non-excluded partners have zero derivatives. Zero epsilon in same-row pairs has a finite one-sided derivative; zero epsilon mixed with another parameter row is not differentiable and is rejected, even when both epsilons are zero. Negative interacting parameters and unrepresentable steps are rejected rather than perturbed outside the supported domain. - Unsupported populated terms — bond dipoles and reduced vdW sites are rejected during preparation.
- No electrostatics — charge optimization is not supported.
Example¶
from q2mm.backends.contracts import EnergyRequest, FrequencyRequest, PreparationRequest
from q2mm.backends.mm.openmm import OpenMMBackend
from q2mm.io.amber import load_amber_frcmod
from q2mm.io.xyz import load_xyz
# Load molecule and force field
mol = load_xyz("molecule.xyz")
ff = load_amber_frcmod("params.frcmod")
backend = OpenMMBackend()
session = backend.prepare(PreparationRequest(case_id="example", molecule=mol, force_field=ff))
params = session.layout.vector(ff)
# Single-point energy
e = session.energy(EnergyRequest(parameters=params)).energy
print(f"Energy: {e:.4f} kcal/mol")
# Frequencies
freqs = session.frequencies(FrequencyRequest(parameters=params)).frequencies
print(f"Frequencies: {freqs}")