Tutorial 4: EXESS Interaction Energy¶
What you get: A single number (in Hartrees) quantifying how strongly a ligand interacts with its protein pocket — computed from first principles, no force field fitting required.
Time |
~2–5 minutes (cloud compute) |
Skill level |
Beginner |
Prerequisites |
Python 3.10+, |
Why This Matters¶
In structure-based drug design, you constantly ask: how tightly does this molecule sit in the binding pocket? Docking scores approximate this with empirical potentials. Molecular mechanics (MM-GBSA, MM-PBSA) improve on it. But quantum mechanics gives you the most physically rigorous answer — it captures polarization, charge transfer, and dispersion effects that classical methods miss.
The catch? QM on an entire protein is impossible. EXESS solves this with fragment-based quantum mechanics: it breaks your system into amino-acid-sized pieces, computes QM energies for each piece and their pairwise/three-body interactions, then assembles the result. You get a QM-quality interaction energy without needing a supercomputer.
What this is (and isn’t)
This interaction energy is not a binding free energy — it doesn’t include entropy, solvation, or conformational sampling. Think of it as a high-quality electronic interaction energy for a single pose. It’s most useful for rank-ordering poses or comparing ligands in the same pocket.
Quick Start: Get an Interaction Energy in 10 Lines¶
import json
from rush import exess
from rush.client import RunOpts, download_object
out = exess.interaction_energy(
"tyk2_ejm_31_t.json", # QDX Topology file for TYK2 + ligand EJM-31
93, # Fragment index of the ligand
method="RestrictedHF", # Explicit: minimal method for testing
basis="STO-3G", # Explicit: minimal basis for testing
frag_keywords=exess.FragKeywords(
level="Trimer", # Include up to 3-body interactions
dimer_cutoff=10.0, # Å — pairs within this distance
trimer_cutoff=5.0, # Å — trimers within this distance
cutoff_type="Centroid",
distance_metric="Min",
),
run_opts=RunOpts(name="Tutorial: Interaction Energy"),
collect=True,
)
# Extract the result
json_bytes = download_object(out[0]["path"])
result = json.loads(json_bytes.decode())
print(f"Interaction energy: {result['qmmbe']['expanded_hf_energy']} Eh")
Example output:
Interaction energy: -0.08234 Eh
That negative value means the ligand is stabilized by its protein environment. The magnitude tells you the strength — more negative = stronger interaction.
About the default method
By default, EXESS uses RestrictedHF/STO-3G — the simplest possible quantum chemistry method with a minimal basis set. This runs fast and is great for testing your workflow, but the absolute energies are not quantitatively meaningful. For production work, use at least method="RestrictedHF", basis="cc-pVDZ" or a correlated method like RI-MP2. The relative trends (which ligand binds more tightly) are often preserved even at low levels of theory.
Where to get the input file¶
The tyk2_ejm_31_t.json topology file is in the rush-py repo’s tests/data/ folder. It’s a QDX Topology — a JSON format that encodes atomic coordinates, fragment definitions, charges, and connectivity.
Understanding the Parameters¶
Parameter |
What it controls |
Rule of thumb |
|---|---|---|
|
Which fragment is your ligand |
Check your topology to find the right index |
|
Include 3-body corrections |
More accurate but slower; |
|
How far out to include fragment pairs (Å) |
10 Å captures most important interactions; increase for charged systems |
|
How far out for 3-body terms (Å) |
Keep smaller than dimer_cutoff; 3-body terms decay faster |
|
Restrict which fragments participate |
Use to limit to the binding pocket (faster, often sufficient) |
End-to-End: From PDB to Interaction Energy¶
Don’t have a QDX Topology file? Start from a PDB. Rush’s Prepare Complex module handles protonation, missing atoms, and charge assignment automatically.
Step 1: Prepare the system¶
import json
from pathlib import Path
from rush.client import RunOpts
from rush.prepare_complex import prepare_complex
trc = prepare_complex(
Path("1hsg.pdb"),
ligand_names=["MK1", "HOH"], # Residue names for ligands/waters
run_opts=RunOpts(name="Tutorial: Prepare Complex"),
collect=True,
)
# Inspect what Prepare Complex determined about charges
print("Charged residues:")
for i, (name, charge) in enumerate(
zip(trc.residues.seqs, trc.topology.fragment_formal_charges)
):
if int(charge) != 0:
print(f" {i:>4} {name}: {int(charge):+}")
What Prepare Complex does under the hood
It uses PDBFixer to fill missing atoms/residues, PDB2PQR for protonation states, and RDKit for ligand processing. The output TRC (Topology + Residues + Coordinates) is fully annotated with connectivity and formal charges — everything EXESS needs.
Step 2: Focus on the binding pocket¶
Running QM on the entire protein is wasteful. Most residues far from the ligand contribute negligibly to the interaction energy. Use get_fragments_near_fragment to select just the pocket:
# Find fragments within 5 Å of the ligand
lig_idx = trc.residues.seqs.index("MK1")
pocket_frags = trc.topology.get_fragments_near_fragment(lig_idx, 5.0) + [lig_idx]
print(f"Pocket contains {len(pocket_frags)} fragments (out of {len(trc.residues.seqs)} total)")
Step 3: Run the interaction energy calculation¶
from rush import exess
# EXESS needs the Topology (not the full TRC)
with open("1hsg_t.json", "w") as f:
json.dump(trc.topology.to_json(), f, indent=2)
out = exess.interaction_energy(
"1hsg_t.json",
lig_idx,
frag_keywords=exess.FragKeywords(
level="Dimer",
included_fragments=pocket_frags,
),
run_opts=RunOpts(name="Tutorial: Interaction Energy E2E"),
collect=True,
)
json_bytes = download_object(out[0]["path"])
result = json.loads(json_bytes.decode())
print(f"Interaction energy: {result['qmmbe']['expanded_hf_energy']} Eh")
The second output
exess.interaction_energy returns two outputs. The first is the JSON with energies. The second contains additional exported data (density matrices, etc.) — only populated if you request exports. See the Exports tutorial for details.
Interpreting the Results¶
The key value is result['qmmbe']['expanded_hf_energy']:
Negative → ligand is stabilized by the protein (favorable interaction)
More negative → stronger interaction
Units are Hartrees (1 Eh ≈ 627.5 kcal/mol)
For comparing two ligands in the same pocket, the difference in interaction energies is more meaningful than the absolute values — especially at lower levels of theory.
Try It Yourself¶
The complete example script and data files are in the rush-py repository:
# Clone the repo (if you haven't already)
git clone https://github.com/talo/rush-py.git
cd rush-py
# The example script and data are at:
# examples/exess-interaction-energy/04_exess_interaction_energy.py
# examples/exess-interaction-energy/data/
# Run the full example
cd examples/exess-interaction-energy
python 04_exess_interaction_energy.py
The data files (TYK2 topology and PDB) are included in the repository — no separate download needed.
See Also¶
Exports tutorial — extract electron density, ESP, and other properties
Optimization tutorial — relax geometries before computing interaction energies
QM/MM tutorial — run dynamics simulations with mixed QM/MM
Full example script — runnable version with both examples