.. include:: /tutorial/preamble.rst
Interface to OpenMM
===================
.. currentmodule:: biotite.interface.openmm
This subpackage provides an interface to the `OpenMM `_
molecular simulation toolkit, expanding the structure capabilities of
*Biotite* into the realm of molecular dynamics.
Like other :mod:`biotite.interface` subpackages, *OpenMM* objects are created with
``to_()`` functions (:func:`to_topology()`, :func:`to_system()`) and converted back
to an :class:`.AtomArray` with ``from_()`` functions
(:func:`from_topology()`, :func:`from_system()`, :func:`from_states()`, etc.).
Like in the :doc:`RDKit interface tutorial `, explaining all the functionalities
of *OpenMM* would exceed the scope of this tutorial.
Please refer to the `OpenMM documentation `_
for further information.
This tutorial will only give a few examples of how *OpenMM* and *Biotite* work in
tandem.
First example: MD simulation of lysozyme
----------------------------------------
Here the aim is to run the prominent
`'Lysozyme in water' `_ example in *OpenMM*.
For structure preparation before and analysis afterwards, we will use *Biotite*.
.. jupyter-execute::
import numpy as np
import openmm
import openmm.app as app
from openmm.unit import angstrom, kelvin, nanometer, picosecond
import biotite.database.rcsb as rcsb
import biotite.interface.openmm as openmm_interface
import biotite.interface.pymol as pymol_interface
import biotite.structure.io.pdbx as pdbx
import biotite.structure as struc
BUFFER = 10
atoms = pdbx.get_structure(
pdbx.BinaryCIFFile.read(rcsb.fetch("1aki", "bcif")), model=1, include_bonds=True
)
# Remove solvent, as proper solvent addition is handled by OpenMM
molecule = atoms[struc.filter_amino_acids(atoms)]
# Create a box with some buffer around the protein
# This box will be used as starting simulation box
box_dim = np.max(molecule.coord, axis=0) - np.min(molecule.coord, axis=0)
molecule.box = np.diag(box_dim + BUFFER)
The :class:`.AtomArray` is converted to an :class:`openmm.topology.Topology` simply
by calling :func:`to_topology()`.
.. jupyter-execute::
# Create an OpenMM Topology from the AtomArray
topology = openmm_interface.to_topology(molecule)
The lysozyme structure we use does not have hydrogen atoms, but we require them for the
MD simulation.
Hence, we use functionality from *OpenMM* to model the missing hydrogen atoms.
.. jupyter-execute::
force_field = openmm.app.ForceField("amber14-all.xml", "amber14/tip3pfb.xml")
# Add hydrogen atoms and water
modeller = app.Modeller(topology, molecule.coord * angstrom)
modeller.addHydrogens(force_field)
modeller.addSolvent(force_field)
topology = modeller.topology
The last prerequisite for the MD simulation is the energy minimization to get a
proper starting conformation.
After that, we can finally run the simulation for the given number of steps.
After a certain number of steps, we record the current state (i.e. conformation) of the
system for later analysis.
.. jupyter-execute::
TIME_STEP = 0.004
FRAME_STEP = 2.0
N_FRAMES = 60
system = force_field.createSystem(
topology,
nonbondedMethod=app.PME,
nonbondedCutoff=1 * nanometer,
constraints=app.HBonds,
)
integrator = openmm.LangevinMiddleIntegrator(
300 * kelvin, 1 / picosecond, TIME_STEP * picosecond
)
simulation = app.Simulation(topology, system, integrator)
simulation.context.setPositions(modeller.positions)
simulation.minimizeEnergy()
# Run simulation and record the current state (the coordinates)
# every FRAME_STEP picoseconds
states = []
for i in range(N_FRAMES):
simulation.step(FRAME_STEP // TIME_STEP)
states.append(simulation.context.getState(getPositions=True))
While each state contains information about the current atom coordinates, box dimensions
etc., it misses the topology of the system, i.e. residues, atoms and bonds.
Hence, to get the trajectory as :class:`.AtomArrayStack` via :func:`from_states()`,
we need to provide an :class:`.AtomArray` as template.
However, we cannot use the original input lysozyme structure from above, as hydrogen
atoms were added meanwhile and thus the topology has changed.
To solve this problem we obtain an updated template first using :func:`from_topology()`.
.. jupyter-execute::
template = openmm_interface.from_topology(topology)
trajectory = openmm_interface.from_states(template, states)
# Center protein in box
trajectory.coord -= struc.centroid(
trajectory.coord[:, struc.filter_amino_acids(trajectory)]
)[:, np.newaxis, :]
trajectory.coord += np.sum(trajectory.box / 2, axis=-2)[:, np.newaxis, :]
# Remove segmentation over periodic boundary
trajectory = struc.remove_pbc(trajectory)
Now we could analyze the trajectory using functionality from :mod:`biotite.structure`,
as exemplified in the :doc:`trajectory tutorial <../structure/trajectories>`, or use
some custom analysis.
For this tutorial we will simply visualize the trajectory using
:mod:`biotite.interface.pymol`.
.. jupyter-execute::
pymol_object = pymol_interface.PyMOLObject.from_structure(trajectory)
pymol_object.color("biotite_lightorange", struc.filter_amino_acids(trajectory))
# Draw simulation box
# As the box does not extend much during the simulation, simply draw the mean size
box = pymol_interface.draw_box(
np.mean(trajectory.box, axis=0), color=(0, 0, 0), width=1
)
# Create an isometric view
pymol_interface.cmd.turn("y", 45)
pymol_interface.cmd.turn("x", 45)
pymol_object.zoom(buffer=15)
pymol_interface.cmd.set("orthoscopic", 1)
pymol_interface.cmd.mset()
pymol_interface.play(
(1400, 1400), format="mp4", html_attributes="loop autoplay width=700"
)
Second example: Simulating water in vacuum
------------------------------------------
Although simulating a single molecule of water is a frankly boring application,
it demonstrates the usage of custom forces in *OpenMM*:
Instead of creating a :class:`openmm.openmm.System` from a
:class:`openmm.app.topology.Topology`, we could alternatively creating it manually,
allowing us to customize the forces. And because ``Force`` objects use atom indices to
define interacting atoms and :func:`to_system()` creates a system with the same
atom ordering as the input :class:`.AtomArray`, we can directly use atom indices
obtained in *Biotite* to create forces in *OpenMM*.
We begin by constructing the molecule from the *Chemical Component Dictionary*.
.. jupyter-execute::
import biotite.structure.info as info
TIME_STEP = 0.001
FRAME_STEP = 0.1
N_FRAMES = 120
water = info.residue("HOH")
system = openmm_interface.to_system(water)
For the sake of brevity the force field will only contain bonded forces for bond
length and angle stretching. This is also the reason a box was not set in the system:
For bonded forces, periodic boundaries simply do not matter.
.. jupyter-execute::
# Adopted from TIP3P
# (https://github.com/openmm/openmmforcefields/blob/main/amber/files/tip3p.xml)
BOND_LENGTH_IDEAL = 0.09572
BOND_LENGTH_STIFFNESS = 462750.4
BOND_ANGLE_IDEAL = 1.82421813418
BOND_ANGLE_STIFFNESS = 836.8
length_force = openmm.HarmonicBondForce()
for i, j, _ in water.bonds.as_array():
length_force.addBond(i, j, BOND_LENGTH_IDEAL, BOND_LENGTH_STIFFNESS)
system.addForce(length_force)
angle_force = openmm.HarmonicAngleForce()
angle_force.addAngle(
np.where(water.atom_name == "H1")[0][0],
np.where(water.atom_name == "O")[0][0],
np.where(water.atom_name == "H2")[0][0],
BOND_ANGLE_IDEAL,
BOND_ANGLE_STIFFNESS,
)
_ = system.addForce(angle_force)
Now we are prepared to run the simulation.
Here we cannot use a ``Simulation`` object as convenient wrapper, as we have bypassed
the creation of a ``Topology`` this time.
Hence, we use the ``Context`` and ``Integrator`` directly.
.. jupyter-execute::
integrator = openmm.LangevinMiddleIntegrator(
300 * kelvin, 1 / picosecond, TIME_STEP * picosecond
)
context = openmm.Context(system, integrator)
context.setPositions(water.coord * angstrom)
states = []
for i in range(N_FRAMES):
integrator.step(FRAME_STEP // TIME_STEP)
states.append(context.getState(getPositions=True))
Finally we can obtain the trajectory as :class:`.AtomArrayStack` and visualize the
trajectory, analogous to the snippets shown above.
.. jupyter-execute::
# In this case we can reuse the input structure as template,
# as not atoms have been added or removed
trajectory = openmm_interface.from_states(water, states)
# Superimpose the frames
trajectory, _ = struc.superimpose(trajectory[0], trajectory)
# Remove the box from the previous visualization
del box
pymol_object = pymol_interface.PyMOLObject.from_structure(trajectory)
# Visualize as stick model
pymol_interface.cmd.set("stick_radius", 0.15)
pymol_interface.cmd.set("sphere_scale", 0.25)
pymol_interface.cmd.set("sphere_quality", 4)
# Visualize docked model
pymol_object.show("spheres")
pymol_object.show("sticks")
pymol_object.set("stick_color", "black")
pymol_object.orient()
pymol_interface.cmd.mset()
# Play as looping GIF
pymol_interface.play((300, 300))