Skip to content

Commit e391261

Browse files
Update conformer energies example (#2101)
* Update conformer energies example * Unify some prose * Avoid units diversion
1 parent 3ded979 commit e391261

File tree

2 files changed

+77
-84
lines changed

2 files changed

+77
-84
lines changed

examples/conformer_energies/conformer_energies.ipynb

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,9 @@
2121
"metadata": {
2222
"tags": []
2323
},
24-
"outputs": [
25-
{
26-
"data": {
27-
"application/vnd.jupyter.widget-view+json": {
28-
"model_id": "cfce2d5082774a069a379ab1dd3237eb",
29-
"version_major": 2,
30-
"version_minor": 0
31-
},
32-
"text/plain": []
33-
},
34-
"metadata": {},
35-
"output_type": "display_data"
36-
}
37-
],
24+
"outputs": [],
3825
"source": [
39-
"import openmm\n",
40-
"from openff.interchange import Interchange\n",
41-
"from openff.units.openmm import from_openmm\n",
26+
"from openff.interchange.drivers.openmm import get_openmm_energies\n",
4227
"from rdkit.Chem import rdMolAlign\n",
4328
"\n",
4429
"from openff.toolkit import ForceField, Molecule\n",
@@ -67,7 +52,27 @@
6752
{
6853
"data": {
6954
"application/vnd.jupyter.widget-view+json": {
70-
"model_id": "089a1b3c120542d79f178ee6179c8cf4",
55+
"model_id": "3dd98e55d74b4334adb0f4e207bd0874",
56+
"version_major": 2,
57+
"version_minor": 0
58+
},
59+
"text/plain": []
60+
},
61+
"metadata": {},
62+
"output_type": "display_data"
63+
},
64+
{
65+
"name": "stderr",
66+
"output_type": "stream",
67+
"text": [
68+
"/Users/mattthompson/.local/share/mamba/envs/openff-toolkit-test-rdkit/lib/python3.12/site-packages/nglview/__init__.py:12: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.\n",
69+
" import pkg_resources\n"
70+
]
71+
},
72+
{
73+
"data": {
74+
"application/vnd.jupyter.widget-view+json": {
75+
"model_id": "f792d6accb2846c1b4dcee7c9d80a849",
7176
"version_major": 2,
7277
"version_minor": 0
7378
},
@@ -129,18 +134,17 @@
129134
},
130135
"outputs": [],
131136
"source": [
132-
"# Load the openff-2.1.0 force field appropriate for vacuum calculations (without constraints)\n",
133-
"forcefield = ForceField(\"openff_unconstrained-2.2.0.offxml\")"
137+
"# Load the openff-2.2.1 force field appropriate for vacuum calculations (without constraints)\n",
138+
"forcefield = ForceField(\"openff_unconstrained-2.2.1.offxml\")"
134139
]
135140
},
136141
{
137142
"cell_type": "markdown",
138143
"metadata": {},
139144
"source": [
140-
"We'll now apply the Sage force field to the molecule to produce an [`Interchange`], and then an OpenMM [`Simulation`] object. Interchange can produce inputs for a whole host of other MD engines though!\n",
145+
"We'll now apply the Sage force field to the molecule to produce an [`Interchange`], which stores the result of parametrizing with this force field\n",
141146
"\n",
142-
"[`Interchange`]: https://docs.openforcefield.org/projects/interchange/en/stable/_autosummary/openff.interchange.Interchange.html#openff.interchange.Interchange\n",
143-
"[`Simulation`]: http://docs.openmm.org/latest/api-python/generated/openmm.app.simulation.Simulation.html#openmm.app.simulation.Simulation"
147+
"[`Interchange`]: https://docs.openforcefield.org/projects/interchange/en/stable/_autosummary/openff.interchange.Interchange.html#openff.interchange.Interchange"
144148
]
145149
},
146150
{
@@ -161,11 +165,8 @@
161165
],
162166
"source": [
163167
"print(f\"Parametrizing {molecule.name} (may take a moment to calculate charges)...\")\n",
164-
"interchange = Interchange.from_smirnoff(forcefield, [molecule])\n",
165-
"print(\"Done.\")\n",
166-
"\n",
167-
"integrator = openmm.VerletIntegrator(1 * openmm.unit.femtoseconds)\n",
168-
"simulation = interchange.to_openmm_simulation(integrator)"
168+
"interchange = forcefield.create_interchange(molecule.to_topology())\n",
169+
"print(\"Done.\")"
169170
]
170171
},
171172
{
@@ -191,21 +192,20 @@
191192
"minimized_molecule = Molecule(molecule)\n",
192193
"minimized_molecule.conformers.clear()\n",
193194
"\n",
195+
"\n",
194196
"for conformer in molecule.conformers:\n",
195-
" # Tell the OpenMM Simulation the positions of this conformer\n",
196-
" simulation.context.setPositions(conformer.to_openmm())\n",
197+
" # Use this conformer to update the positions of the Interchange object\n",
198+
" interchange.positions = conformer\n",
197199
"\n",
198-
" # Keep a record of the initial energy\n",
199-
" initial_energies.append(simulation.context.getState(getEnergy=True).getPotentialEnergy())\n",
200+
" # Get the (total) initial energy from this conformer and store it\n",
201+
" initial_energies.append(get_openmm_energies(interchange).total_energy)\n",
200202
"\n",
201-
" # Perform the minimization\n",
202-
" simulation.minimizeEnergy()\n",
203+
" # Minimize using Interchange.minimize, which wraps OpenMM\n",
204+
" interchange.minimize(engine=\"openmm\")\n",
203205
"\n",
204206
" # Record minimized energy and positions\n",
205-
" min_state = simulation.context.getState(getEnergy=True, getPositions=True)\n",
206-
"\n",
207-
" minimized_energies.append(min_state.getPotentialEnergy())\n",
208-
" minimized_molecule.add_conformer(from_openmm(min_state.getPositions()))"
207+
" minimized_energies.append(get_openmm_energies(interchange).total_energy)\n",
208+
" minimized_molecule.add_conformer(interchange.positions.to(\"angstrom\"))"
209209
]
210210
},
211211
{
@@ -225,7 +225,7 @@
225225
{
226226
"data": {
227227
"application/vnd.jupyter.widget-view+json": {
228-
"model_id": "a4750d0da648475bb17cf557ef89b691",
228+
"model_id": "c7b7dec6bc7b4c498bab35c31cedb4c3",
229229
"version_major": 2,
230230
"version_minor": 0
231231
},
@@ -261,16 +261,16 @@
261261
"text": [
262262
"ruxolitinib: 10 conformers\n",
263263
"Conformer Initial PE Minimized PE RMSD\n",
264-
" 1 / 10 : -88.682 kcal/mol -124.600 kcal/mol 0.456 Å\n",
265-
" 2 / 10 : -83.492 kcal/mol -121.554 kcal/mol 0.466 Å\n",
266-
" 3 / 10 : -43.748 kcal/mol -121.709 kcal/mol 0.951 Å\n",
267-
" 4 / 10 : -81.885 kcal/mol -123.365 kcal/mol 0.695 Å\n",
268-
" 5 / 10 : -83.609 kcal/mol -120.864 kcal/mol 0.674 Å\n",
269-
" 6 / 10 : -85.390 kcal/mol -123.760 kcal/mol 0.604 Å\n",
270-
" 7 / 10 : -82.167 kcal/mol -121.679 kcal/mol 0.973 Å\n",
271-
" 8 / 10 : -91.164 kcal/mol -123.829 kcal/mol 0.413 Å\n",
272-
" 9 / 10 : -89.107 kcal/mol -124.535 kcal/mol 0.808 Å\n",
273-
" 10 / 10 : -86.407 kcal/mol -124.416 kcal/mol 0.610 Å\n"
264+
" 1 / 10 : -88.258 kcal/mol -123.916 kcal/mol 0.452 Å\n",
265+
" 2 / 10 : -82.884 kcal/mol -120.706 kcal/mol 0.433 Å\n",
266+
" 3 / 10 : -43.067 kcal/mol -120.913 kcal/mol 0.949 Å\n",
267+
" 4 / 10 : -81.524 kcal/mol -122.732 kcal/mol 0.693 Å\n",
268+
" 5 / 10 : -82.897 kcal/mol -120.303 kcal/mol 0.789 Å\n",
269+
" 6 / 10 : -84.764 kcal/mol -122.897 kcal/mol 0.588 Å\n",
270+
" 7 / 10 : -81.634 kcal/mol -120.972 kcal/mol 1.004 Å\n",
271+
" 8 / 10 : -90.702 kcal/mol -123.087 kcal/mol 0.374 Å\n",
272+
" 9 / 10 : -88.588 kcal/mol -123.813 kcal/mol 0.757 Å\n",
273+
" 10 / 10 : -85.671 kcal/mol -123.590 kcal/mol 0.634 Å\n"
274274
]
275275
}
276276
],
@@ -322,8 +322,8 @@
322322
" output.append(\n",
323323
" [\n",
324324
" i + 1,\n",
325-
" init_energy.value_in_unit(openmm.unit.kilocalories_per_mole),\n",
326-
" min_energy.value_in_unit(openmm.unit.kilocalories_per_mole),\n",
325+
" init_energy.m_as(\"kilocalories_per_mole\"),\n",
326+
" min_energy.m_as(\"kilocalories_per_mole\"),\n",
327327
" minimization_rms,\n",
328328
" ]\n",
329329
" )\n",
@@ -361,7 +361,7 @@
361361
"name": "python",
362362
"nbconvert_exporter": "python",
363363
"pygments_lexer": "ipython3",
364-
"version": "3.10.10"
364+
"version": "3.12.11"
365365
},
366366
"widgets": {
367367
"application/vnd.jupyter.widget-state+json": {

examples/conformer_energies/conformer_energies.py

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
import argparse
22

3-
import openmm
4-
from openff.interchange import Interchange
5-
from openff.units.openmm import from_openmm
3+
from openff.interchange.drivers.openmm import get_openmm_energies
64
from rdkit.Chem import rdMolAlign
75

8-
from openff.toolkit import ForceField, Molecule, RDKitToolkitWrapper
6+
from openff.toolkit import ForceField, Molecule
7+
from openff.toolkit.utils import get_data_file_path
98

109

1110
def compute_conformer_energies_from_file(filename):
12-
# Load in the molecule and its conformers.
13-
# Note that all conformers of the same molecule are loaded as separate Molecule objects
14-
# If using a OFF Toolkit version before 0.7.0, loading SDFs through RDKit and OpenEye may provide
15-
# different behavior in some cases. So, here we force loading through RDKit to ensure the correct behavior
16-
rdktkw = RDKitToolkitWrapper()
17-
loaded_molecules = Molecule.from_file(filename, toolkit_registry=rdktkw)
18-
# The logic below only works for lists of molecules, so if a
19-
# single molecule was loaded, cast it to list
11+
# First, load conformers from an SDF file.
12+
loaded_molecules = Molecule.from_file(
13+
get_data_file_path("molecules/ruxolitinib_conformers.sdf"),
14+
)
15+
16+
# Normalize to list
2017
try:
2118
loaded_molecules = [*loaded_molecules]
2219
except TypeError:
2320
loaded_molecules = [loaded_molecules]
2421

25-
# Collatate all conformers of the same molecule
26-
# NOTE: This isn't necessary if you have already loaded or created multi-conformer molecules;
27-
# it is just needed because our SDF reader does not automatically collapse conformers.
22+
# from_file loads each entry in the SDF into its own molecule,
23+
# so collapse conformers into the same molecule
2824
molecule = loaded_molecules.pop(0)
2925
for next_molecule in loaded_molecules:
3026
if next_molecule == molecule:
@@ -45,13 +41,12 @@ def compute_conformer_energies_from_file(filename):
4541
+ f" ({molecule.name})"
4642
)
4743

48-
# Load the openff-2.1.0 force field appropriate for vacuum calculations (without constraints)
49-
forcefield = ForceField("openff_unconstrained-2.1.0.offxml")
44+
# Load the openff-2.2.1 force field appropriate for vacuum calculations (without constraints)
45+
forcefield = ForceField("openff_unconstrained-2.2.1.offxml")
46+
# Create an Interchange object, which stores the result of parametrizing with this force field
5047
print(f"Parametrizing {molecule.name} (may take a moment to calculate charges)...")
51-
interchange = Interchange.from_smirnoff(forcefield, [molecule])
48+
interchange = forcefield.create_interchange(molecule.to_topology())
5249
print("Done.")
53-
integrator = openmm.VerletIntegrator(1 * openmm.unit.femtoseconds)
54-
simulation = interchange.to_openmm_simulation(integrator)
5550

5651
# We'll store energies in two lists
5752
initial_energies = []
@@ -62,20 +57,18 @@ def compute_conformer_energies_from_file(filename):
6257
minimized_molecule.conformers.clear()
6358

6459
for conformer in molecule.conformers:
65-
# Tell the OpenMM Simulation the positions of this conformer
66-
simulation.context.setPositions(conformer.to_openmm())
60+
# Use this conformer to update the positions of the Interchange object
61+
interchange.positions = conformer
6762

68-
# Keep a record of the initial energy
69-
initial_energies.append(simulation.context.getState(getEnergy=True).getPotentialEnergy())
63+
# Get the (total) initial energy from this conformer and store it
64+
initial_energies.append(get_openmm_energies(interchange).total_energy)
7065

71-
# Perform the minimization
72-
simulation.minimizeEnergy()
66+
# Minimize using Interchange.minimize, which wraps OpenMM
67+
interchange.minimize(engine="openmm")
7368

7469
# Record minimized energy and positions
75-
min_state = simulation.context.getState(getEnergy=True, getPositions=True)
76-
77-
minimized_energies.append(min_state.getPotentialEnergy())
78-
minimized_molecule.add_conformer(from_openmm(min_state.getPositions()))
70+
minimized_energies.append(get_openmm_energies(interchange).total_energy)
71+
minimized_molecule.add_conformer(interchange.positions.to("angstrom"))
7972

8073
n_confs = molecule.n_conformers
8174
print(f"{molecule.name}: {n_confs} conformers")
@@ -122,8 +115,8 @@ def compute_conformer_energies_from_file(filename):
122115
output.append(
123116
[
124117
i + 1,
125-
init_energy.value_in_unit(openmm.unit.kilocalories_per_mole),
126-
min_energy.value_in_unit(openmm.unit.kilocalories_per_mole),
118+
init_energy.m_as("kilocalories_per_mole"),
119+
min_energy.m_as("kilocalories_per_mole"),
127120
minimization_rms,
128121
]
129122
)

0 commit comments

Comments
 (0)