Beyond 2D Fingerprints: Encoding Protein-Ligand Interactions for Machine Learning

Jul 12, 2024·
Yassir Boulaamane
Yassir Boulaamane
· 4 min read

A 2D fingerprint throws away the pose. Docking spent its time on contacts, hydrogen-bond geometry, and stacking, and a topology-only vector keeps none of that. Use a 2D fingerprint when the ligand alone is the object. Use a 3D interaction fingerprint when the pose is the object.

PLEC, SPLIF, and E3FP are three different objects. PLEC encodes which groups touch. SPLIF encodes the geometry of those contacts. E3FP never sees the protein.

Three fingerprints, three questionsPLEC encodes contact chemistry between protein and ligand. SPLIF encodes the spatial geometry of interacting fragments. E3FP encodes ligand shape and pharmacophore and does not see the protein.Match the fingerprint to the questionPLECWho touches whomProtein and ligandAffinity, reactivitySPLIFHow they sitProtein and ligandPose vs a referenceE3FPLigand shapeLigand onlyScaffold hopping
Figure 1. PLEC and SPLIF need the receptor. E3FP does not, so two poses of the same ligand can differ while a protein contact cannot appear in the vector. The bit lengths below are the starting points in the snippets: 16384 for PLEC, 4096 for SPLIF and for E3FP.

PLEC

PLEC is an ECFP-style fingerprint of the interface. It describes the chemical neighborhood of contacting atom pairs, on both the ligand and the protein. The model sees which groups are next to which residue environments, and at what depth you asked for.

Use it when the chemistry of the contact is the signal: affinity, or reactivity at a site.

import oddt
import oddt.fingerprints as fps
from oddt.toolkits.rdk import readfile

# Load receptor and ligand
receptor = next(readfile('pdb', 'receptor.pdb'))
receptor.protein = True

ligand = next(readfile('sdf', 'docked_pose.sdf'))

# Calculate PLEC fingerprint
# depth_ligand and depth_protein control circular environment depth
plec = fps.PLEC(ligand, receptor, depth_ligand=2, depth_protein=4, size=16384)

size is the bit length. A longer vector collides less and costs more memory. 16384 is the starting point given here for ensemble docking.

If the task is reactivity at one catalytic site, a hand-built distance can be appended. The coordinate below is a placeholder.

import numpy as np
from oddt.interactions import close_contacts

# Example: compute distance from ligand centroid to a metal/active site atom
# Replace with your actual active site coordinate
active_site_coord = np.array([12.3, 45.6, 7.8])
ligand_centroid = ligand.atoms.coords.mean(axis=0)
dist_to_site = np.linalg.norm(ligand_centroid - active_site_coord)

# Append to PLEC vector
plec_extended = np.append(plec, dist_to_site)

close_contacts is imported in the original snippet and not called. The distance is a centroid-to-point norm.

SPLIF

SPLIF encodes the geometry of interacting fragments rather than a catalog of atom types. Stacking and T-shaped contacts sit in that geometry, without a hard-coded interaction list. That makes it less tied to one docking program’s interaction labels.

Use it when you want poses that reproduce the 3D binding geometry of a reference ligand.

# SPLIF is also available through ODDT
splif = fps.SPLIF(ligand, receptor, depth_ligand=1, depth_protein=1, size=4096)

The vectors are sparser than PLEC. Tanimoto similarity on them is the geometric comparison in this note.

from oddt.utils import binary_tanimoto

reference_splif = fps.SPLIF(reference_ligand, receptor)
similarity = binary_tanimoto(splif, reference_splif)
print(f"Geometric similarity to reference: {similarity:.3f}")

E3FP

E3FP is a circular fingerprint in Euclidean space. It grows out from atoms through distance, not through bonds. The same 2D structure in two conformations gets two fingerprints. The protein is absent, so this is a ligand-shape method: ligand-based screening and scaffold hopping.

# Install e3fp: pip install e3fp
from e3fp.pipeline import fprints_from_mol
from rdkit import Chem
from rdkit.Chem import AllChem

# Load mol with a 3D conformer
mol = Chem.MolFromSmiles('your_smiles_here')
mol = Chem.AddHs(mol)
AllChem.EmbedMolecule(mol, AllChem.ETKDGv3())
AllChem.MMFFOptimizeMolecule(mol)

# Generate E3FP fingerprints for all conformers
fprints = fprints_from_mol(mol, fprint_params={'bits': 4096, 'level': 5})

# Convert to dense array for ML
import numpy as np
e3fp_array = np.array(fprints[0].to_vector(sparse=False), dtype=np.float32)

For several poses, this note mean-pools the vectors.

# Mean-pool across poses - preserves pharmacophoric signal while reducing noise
fprint_arrays = [np.array(fp.to_vector(sparse=False)) for fp in fprints]
e3fp_consensus = np.mean(fprint_arrays, axis=0)

Which one

PLECSPLIFE3FP
SeesProtein and ligandProtein and ligandLigand only
EncodesContact chemistrySpatial geometryShape and pharmacophore
Ensemble noiseTolerates it wellModerateNeeds aggregation
UseReactivity, affinityGeometric screeningLigand-based screening

Across an ensemble of receptor conformations, PLEC has a frame of reference the protein supplies, so the encoding moves less when the pose jitters. E3FP has to be aggregated or the conformer noise is the feature.

None of the three is complete. The pipelines this note describes do best when a primary fingerprint is joined to a few hand-built features that the fingerprint is not learning. Profile what the model uses before you swap the whole representation.

2D fingerprints stay the right tool for a fast ligand-only screen. For a model whose question is the binding geometry, the 3D choice above is the one that matches the question.