Lesson 10 of 13 · 12 min
Analyzing Ligand Binding Sites
A ligand binding site is the shell of protein residues that line the cavity a bound small molecule occupies. Characterizing it programmatically turns a static complex into a quantitative description of shape, chemistry, and contacts you can compare across structures. In a PDB file the ligand is stored as HETATM records that share the flag with solvent, ions, and additives, so you must select only the ligand of interest and drop water before defining a pocket. This lesson isolates the HETATM ligand, expands a 5 angstrom residue shell in both PyMOL and Biopython, and catalogs the contacts, using EGFR kinase bound to erlotinib (ligand code AQ4) as the worked example.
Isolating the Ligand
# PyMOL command console
fetch 1m17, async=0
# HETATM ligand only: erlotinib carries the residue name AQ4
select ligand, resn AQ4
remove solvent
# byres expands to complete residues; keep only protein atoms
# with any atom within 5 A of the ligand
select pocket, byres (polymer within 5 of ligand)
# one line per pocket residue, plus a count
iterate pocket and name CA, print(f"{chain}/{resn}-{resi}")
print("pocket residues:", cmd.count_atoms("pocket and name CA"))A/LEU-694
A/GLY-695
A/VAL-702
A/ALA-719
A/LYS-721
A/GLU-738
A/MET-742
A/LEU-764
A/ILE-765
A/THR-766
A/GLN-767
A/LEU-768
A/MET-769
A/PRO-770
A/PHE-771
A/GLY-772
A/CYS-773
A/LEU-820
A/THR-830
A/ASP-831
pocket residues: 20
Pocket Residues With Biopython
from Bio.PDB import PDBList, PDBParser, NeighborSearch, Selection
PDBList().retrieve_pdb_file("1m17", pdir=".", file_format="pdb")
structure = PDBParser(QUIET=True).get_structure("1m17", "pdb1m17.ent")
model = structure[0]
# Ligand atoms: HETATM residues whose hetfield is 'H_AQ4'
ligand_atoms = [a for a in model.get_atoms()
if a.get_parent().id[0] == "H_AQ4"]
# NeighborSearch builds a KD-tree over every atom in the model
atoms = Selection.unfold_entities(model, "A")
ns = NeighborSearch(atoms)
# Any standard residue with an atom <= 5.0 A from any ligand atom
pocket = set()
for lig_atom in ligand_atoms:
for res in ns.search(lig_atom.coord, 5.0, level="R"):
if res.id[0] == " ": # standard amino acid, not HETATM/water
pocket.add(res)
for res in sorted(pocket, key=lambda r: r.id[1]):
print(res.get_parent().id, res.resname, res.id[1])
print("pocket residues:", len(pocket))A LEU 694
A GLY 695
A VAL 702
A ALA 719
A LYS 721
A GLU 738
A MET 742
A LEU 764
A ILE 765
A THR 766
A GLN 767
A LEU 768
A MET 769
A PRO 770
A PHE 771
A GLY 772
A CYS 773
A LEU 820
A THR 830
A ASP 831
pocket residues: 20
Characterizing Pocket Chemistry
import numpy as np
from collections import Counter
# Side-chain chemical classes
CLASS = {
"ARG": "positive", "LYS": "positive", "HIS": "positive",
"ASP": "negative", "GLU": "negative",
"SER": "polar", "THR": "polar", "ASN": "polar", "GLN": "polar",
"CYS": "polar", "TYR": "polar",
"GLY": "nonpolar", "ALA": "nonpolar", "VAL": "nonpolar",
"LEU": "nonpolar", "ILE": "nonpolar", "MET": "nonpolar",
"PRO": "nonpolar", "PHE": "nonpolar", "TRP": "nonpolar",
}
lig_coords = np.array([a.coord for a in ligand_atoms])
rows = []
for res in sorted(pocket, key=lambda r: r.id[1]):
res_coords = np.array([a.coord for a in res])
# minimum ligand-to-residue heavy-atom distance
dmin = np.linalg.norm(
res_coords[:, None, :] - lig_coords[None, :, :], axis=-1
).min()
rows.append((res.resname, res.id[1], CLASS[res.resname], dmin))
classes = Counter(c for _, _, c, _ in rows)
polar = classes["polar"] + classes["positive"] + classes["negative"]
frac_polar = polar / len(rows)
for resname, resi, klass, dmin in rows:
print(f"{resname}{resi:<4} {klass:<9} {dmin:5.2f} A")
print(f"\ncomposition: {dict(classes)}")
print(f"polar fraction: {frac_polar:.2f}")LEU694 nonpolar 3.27 A
GLY695 nonpolar 4.50 A
VAL702 nonpolar 4.19 A
ALA719 nonpolar 3.31 A
LYS721 positive 3.70 A
GLU738 negative 3.81 A
MET742 nonpolar 4.63 A
LEU764 nonpolar 3.43 A
ILE765 nonpolar 4.45 A
THR766 polar 3.41 A
GLN767 polar 3.15 A
LEU768 nonpolar 3.50 A
MET769 nonpolar 2.70 A
PRO770 nonpolar 3.11 A
PHE771 nonpolar 3.45 A
GLY772 nonpolar 3.24 A
CYS773 polar 4.73 A
LEU820 nonpolar 3.45 A
THR830 polar 3.65 A
ASP831 negative 3.11 A
composition: {'nonpolar': 13, 'positive': 1, 'negative': 2, 'polar': 4}
polar fraction: 0.35
The profile reads as a classic ATP pocket: thirteen nonpolar residues dominate, holding the polar fraction to 0.35, while the closest contact at 2.70 angstrom is the hinge residue Met769, whose backbone amide donates a hydrogen bond to the quinazoline N1 of erlotinib (labeled N2 in the AQ4 atom nomenclature). Thr766 is the gatekeeper, Asp831 is the DFG aspartate, and Glu738 is the alpha-C helix glutamate that salt-bridges the catalytic Lys721, so the handful of polar and charged residues sit exactly where selectivity and catalysis are decided. A contact near 2.7 to 3.2 angstrom to a nitrogen or oxygen flags a likely hydrogen bond, whereas 3.5 to 4.7 angstrom contacts are van der Waals packing.
Try it yourself: Re-run the Biopython analysis on a more polar site, such as 1STP (streptavidin bound to biotin, ligand BTN) or 3ERT (estrogen receptor with 4-hydroxytamoxifen, ligand OHT). Tighten the cutoff to 4.0 angstrom to keep only first-shell contacts, then compare the polar fraction against the hydrophobic ATP pocket above. Which residue makes the closest contact, and is the nearest atom on the backbone or the side chain?