Protein-ligand interactions overlay#

Detect hydrogen bonds, salt bridges, π-cation, and π-stacking interactions between a protein and its bound ligand (using peppr), and render them as colored dashed lines over a ray-traced cartoon — H-bonds in blue, salt bridges in yellow, π-cation in orange, π-stacking in green (parallel) or smudge (T-shaped), following PLIP’s conventions.

import tempfile
from pathlib import Path

import matplotlib.pyplot as plt
import yaml
from biotite import structure as struc
from PIL import Image

import portein

# Send PyMOL renders to a tempdir so they don't clutter the notebook dir.
output_dir = Path(tempfile.mkdtemp())

Load the structure and split receptor/ligand#

PDB files don’t carry CONECT records by default, so we add bonds via biotite’s residue- template lookup before splitting into receptor and ligand.

pdb = portein.read_structure("../_data/7lc2.pdb")
chain_a = pdb[pdb.chain_id == "A"]
chain_a.bonds = struc.connect_via_residue_names(chain_a)

ligand = chain_a[chain_a.res_name == "GNP"]
receptor = chain_a[chain_a.res_name != "GNP"]

Detect interactions#

With rotate=True, we orient the receptor + ligand so the 2D projection maximizes the spread of the interaction atoms — usually a better view of the binding pocket than the default protein-area maximization in ProteinConfig.

interactions = portein.InteractionSet.find(receptor=receptor, ligand=ligand, rotate=True)

print(f"H-bonds: {len(interactions.hbonds)}")
print(f"Salt bridges: {len(interactions.salt_bridges)}")
print(f"π-cation: {len(interactions.pi_cation)}")
print(f"π-stacking: {len(interactions.pi_stacking)}")
[16:53:18] Can't kekulize mol.  Unkekulized atoms: 22 23 24 27 28 30 31
[16:53:18] Can't kekulize mol.  Unkekulized atoms: 22 23 24 27 28 30 31
/home/runner/work/portein/portein/portein/rotate.py:23: NumbaPerformanceWarning: np.dot() is faster on contiguous arrays, called on (Array(float64, 1, 'A', False, aligned=True), Array(float64, 1, 'A', False, aligned=True))
  m = find_best_projection(coords)
/home/runner/work/portein/portein/portein/rotate.py:23: NumbaPerformanceWarning: np.dot() is faster on contiguous arrays, called on (Array(float64, 2, 'C', False, aligned=True), Array(float64, 1, 'A', False, aligned=True))
  m = find_best_projection(coords)
/home/runner/work/portein/portein/portein/rotate.py:28: NumbaPerformanceWarning: np.dot() is faster on contiguous arrays, called on (Array(float64, 2, 'A', False, aligned=True), Array(float64, 2, 'F', False, aligned=True))
  matrix = rotate_to_maximize_bb_height(coords[:, :2]) @ matrix
H-bonds: 20
Salt bridges: 0
π-cation: 1
π-stacking: 2

Render with the interactions overlaid#

Pass the InteractionSet to Pymol(interactions=...). The render adds one PyMOL cmd.distance per interaction (with PLIP colors) and creates a named selection portein_interaction_residues that any layer can target — typically a thin-stick rendering of the participating side chains so the dashes have visible atoms to terminate at.

with open("../../configs/pymol_settings.yaml") as f:
    pymol_settings = yaml.safe_load(f)

protein_config = portein.ProteinConfig(
    pdb_file=interactions.combined,
    rotate=False,
    width=700,
    chain_to_color={"A": "lightblue"},
    output_prefix=str(output_dir / "interactions"),
)

image_file = portein.Pymol(
    protein=protein_config,
    layers=[
        # Protein as cartoon in backdrop
        portein.PymolConfig(
            representation="cartoon",
            pymol_settings=pymol_settings,
            selection="all",
            transparency=0.5,
        ),
        # Interacting residues and ligand as sticks
        [
            portein.PymolConfig(
                representation="sticks",
                pymol_settings={**pymol_settings, "stick_radius": 0.15},
                selection="portein_interaction_residues",
                color="salmon",
            ),
            portein.PymolConfig(
                representation="sticks",
                pymol_settings={**pymol_settings, "stick_radius": 0.25},
                selection="resn GNP",
                color="green",
            ),
        ],
    ],
    # Interactions as dashed lines
    interactions=interactions,
).run()

Crop and display#

cropped = portein.crop_to_content(Image.open(image_file))
DPI = 100
fig, ax = plt.subplots(figsize=(cropped.width / DPI, cropped.height / DPI), dpi=DPI)
ax.imshow(cropped)
ax.axis("off")
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
plt.show()
../_images/d737b06b5338176f53aac8f20cb3f8001fc739e932da1edbfba5adc5caf33cde.png