Tutorial 6: EXESS QM/MM Dynamics¶
What you get: A trajectory of atomic positions over time — a molecular movie where the critical region (your ligand, active site, or reaction center) is treated with quantum mechanics while the rest uses fast classical force fields.
Time |
~5–30 minutes depending on system size and timesteps |
Skill level |
Intermediate |
Prerequisites |
Python 3.10+, |
Why This Matters¶
Static structures tell you where atoms are. Dynamics tells you how they move — which bonds vibrate, which water molecules exchange, how flexible a binding pocket is. Classical molecular dynamics (MD) handles this well for proteins, but it can’t describe bond breaking, charge redistribution, or electronic effects in your region of interest.
QM/MM gives you the best of both worlds: quantum mechanics for the part that matters (a ligand, a catalytic site, a metal center) and molecular mechanics for everything else. EXESS extends this further with Q4ML — you can mix QM, MM, and ML (AIMNet neural network) regions in a single simulation.
Quick Start: QM/MM on a Protein–Ligand System¶
from rush import exess
from rush.client import RunOpts, save_object
out = exess.qmmm(
"6a5j_t.json", # Topology (atoms, coordinates, fragments)
500, # Number of MD timesteps
"6a5j_r.json", # Residues (residue names and assignments)
qm_fragments=[6], # Treat fragment 6 with quantum mechanics
ml_fragments=[], # No ML region → everything else is MM
run_opts=RunOpts(name="Tutorial: QM/MM"),
collect=True,
)
This runs 500 timesteps of molecular dynamics where fragment 6 (e.g., a ligand or key residue) is computed with Hartree-Fock QM, and the remaining protein + solvent fragments use classical MM (OpenMM).
⚠️ Tutorial Basis Set Warning
This tutorial example code uses STO-3G, a minimal basis set chosen purely for speed so examples run quickly. STO-3G is not suitable for research or production calculations. For meaningful results, use at least
cc-pVDZor larger. See the electronic structure methods reference for available basis sets.
Minimal Example: Two Water Molecules from Scratch¶
To understand exactly what inputs QM/MM needs, let’s build a system manually — two water molecules, all treated with QM:
import json
from rush import exess
from rush.client import RunOpts
from rush import Topology, exess
from rush.mol import Element, Fragment, Residue, Residues
# Define two water molecules
topology = Topology(
symbols=[Element.O, Element.H, Element.H, Element.O, Element.H, Element.H],
geometry=[
0.0000, 0.0000, 0.0000, # Water 1: O
0.7570, 0.5860, 0.0000, # H
-0.7570, 0.5860, 0.0000, # H
2.8000, 0.0000, 0.0000, # Water 2: O
3.5570, 0.5860, 0.0000, # H
2.0430, 0.5860, 0.0000, # H
],
fragments=[Fragment([0, 1, 2]), Fragment([3, 4, 5])],
)
residues = Residues(
residues=[Residue([0, 1, 2]), Residue([3, 4, 5])],
seqs=["HOH", "HOH"],
)
# Write input files
with open("molecule_t.json", "w") as f:
json.dump(topology.to_json(), f)
with open("molecule_r.json", "w") as f:
json.dump(residues.to_json(), f)
# Run all-QM dynamics
out = exess.qmmm(
topology_path="molecule_t.json",
residues_path="molecule_r.json",
n_timesteps=100,
trajectory=exess.Trajectory(include_waters=True),
ml_fragments=[],
mm_fragments=[], # No ML or MM → everything is QM
run_opts=RunOpts(name="Tutorial: QM/MM Water Dimer"),
collect=True,
)
Fragment assignment logic
EXESS requires that every fragment is assigned to exactly one of QM, MM, or ML. Setting ml_fragments=[] and mm_fragments=[] forces all fragments into the QM region. You can also explicitly set qm_fragments=[0, 1] for clarity.
Working with the Trajectory¶
The output is a JSON object containing geometries — a list of coordinate arrays, one per timestep. Each geometry is a flat list of floats ([x1, y1, z1, x2, y2, z2, ...]), matching the order of atoms in your Topology.
import json
from itertools import batched
from pathlib import Path
from rush import Topology
out_file = save_object(out["path"])
with open(out_file) as f:
geometries = json.load(f)["geometries"]
print(f"Trajectory has {len(geometries)} frames")
# Load the original topology and compare first/last frames
topology = Topology.from_json(Path("molecule_t.json"))
print("\nAtoms at First Step:")
for x, y, z in batched(topology.geometry, 3):
print(f" ({x:>7.4f}, {y:>7.4f}, {z:>7.4f})")
topology.geometry = geometries[-1]
print("\nAtoms at Final Step:")
for x, y, z in batched(topology.geometry, 3):
print(f" ({x:>7.4f}, {y:>7.4f}, {z:>7.4f})")
This example uses 6a5j, a solution NMR structure of a small peptide. During the gas-phase NVE simulation, the peptide backbone flexes and side chains reorient. The surrounding MM environment would normally provide confining forces; in this gas-phase simulation, the structure is free to explore conformational space.
Example Output¶
Explore the QM/MM trajectory visualization. Or run the code above to generate this yourself!
Try It Yourself¶
The complete example is available in the rush-py repository:
You can download the files directly or clone the repository to run the full example.
See Also¶
Optimization tutorial — relax structures before running dynamics
Interaction energy tutorial — compute binding energies from trajectory snapshots
Exports tutorial — extract electronic properties during simulations