Molecular docking#

AutoDock Vina predicts how a small molecule, the ligand, binds to a protein, the receptor. It is interfaced by the VinaApp in the biotite.application_v2.autodock subpackage. Let’s dock biotin into its famous binding partner streptavidin. As we would like to check how well the docking works, we download a high-resolution crystal structure of the complex, which we split into the receptor and the reference binding pose of the ligand. The docking requires bonds and formal charges, hence both are included when loading the structure.

from tempfile import gettempdir
import biotite.database.rcsb as rcsb
import biotite.structure as struc
import biotite.structure.io.pdbx as pdbx

pdbx_file = pdbx.BinaryCIFFile.read(
    rcsb.fetch("2rtg", "bcif", gettempdir())
)
structure = pdbx.get_structure(
    pdbx_file, model=1, include_bonds=True, extra_fields=["charge"]
)
# The structure is a homodimer, one monomer is sufficient
structure = structure[structure.chain_id == "B"]
receptor = structure[struc.filter_amino_acids(structure)]
ref_ligand = structure[structure.res_name == "BTN"]

The ligand should not be taken from the complex, as it is already in its bound conformation. Instead we take the idealized model of biotin from the Chemical Component Dictionary, that comes with the required bonds and charges.

import biotite.structure.info as info

ligand = info.residue("BTN")

Now we let Vina place the ligand into the receptor. The search space is a box, defined by its center and size. Here we cheat a little bit by centering the box at the position of the reference ligand, which we would not know for an unknown complex.

import biotite.application_v2.autodock as autodock

app = autodock.VinaApp()
result = app.run(
    ligand,
    receptor,
    center=struc.centroid(ref_ligand),
    # Edge length of the box in Å
    size=[20, 20, 20],
    # A fixed seed and a single thread make the run reproducible
    seed=42,
    cpu=1,
).result()
print(result.energies)
[-6.82 -5.88 -5.81 -5.79 -5.49 -5.49 -5.49 -5.3  -5.22]

Vina reports multiple binding modes, sorted from best to worst predicted binding energy. The coordinates of the ligand in each binding mode are given by VinaResult.ligand_coord, which we combine with the input ligand to an AtomArrayStack. Note that Vina removes nonpolar hydrogen atoms, so their coordinates are NaN. As we compare only the heavy atoms with the reference pose anyway, we simply remove all hydrogen atoms.

import numpy as np

docked_ligand = struc.from_template(ligand, result.ligand_coord)
docked_ligand = docked_ligand[..., struc.filter_heavy(docked_ligand)]
ref_ligand = ref_ligand[struc.filter_heavy(ref_ligand)]
# Ensure that both models have the same atom order
docked_ligand = docked_ligand[..., info.standardize_order(docked_ligand)]
ref_ligand = ref_ligand[info.standardize_order(ref_ligand)]
# No superimposition, as the position in the binding pocket matters
rmsd = struc.rmsd(ref_ligand, docked_ligand)
for energy, deviation in zip(result.energies, rmsd):
    print(f"{energy:6.2f} kcal/mol  {deviation:5.2f} Å")
 -6.82 kcal/mol   0.44 Å
 -5.88 kcal/mol   6.75 Å
 -5.81 kcal/mol   6.36 Å
 -5.79 kcal/mol   2.37 Å
 -5.49 kcal/mol   8.03 Å
 -5.49 kcal/mol   6.34 Å
 -5.49 kcal/mol   6.33 Å
 -5.30 kcal/mol   5.44 Å
 -5.22 kcal/mol   6.41 Å

The binding mode with the best predicted energy is also the one that is closest to the experimentally determined pose. Let’s look at it visually:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(6.0, 4.0))
ax.scatter(result.energies, rmsd, marker="+", color="black")
ax.set_xlabel("Binding energy (kcal/mol)")
ax.set_ylabel("RMSD to reference (Å)")
fig.tight_layout()
../../_images/vina_6_0.png

Vina can also treat selected side chains of the receptor as flexible during the docking. For this purpose, a boolean mask of the flexible residues is given to the flexible parameter of VinaApp.run().