Fragment-based drug discovery (FBDD) starts from very small molecules (fragments, MW < 300 Da) that bind weakly to the target but occupy a well-defined sub-pocket. These fragments are then grown, linked, or merged into drug-like leads. FBDD has delivered approved drugs including Venetoclax (ABT-199) and Vemurafenib, demonstrating that weak-binding, small starting points can be optimised into potent inhibitors.
The key insight is that fragments have a higher "hit rate" in structural screening (X-ray or SPR) than drug-like molecules because they need to satisfy fewer constraints simultaneously. The trade-off is that binding is weak (Kd values in the μM–mM range), requiring sensitive biophysical techniques for detection.
The Rule of Three
Analogous to Lipinski's Rule of Five for drugs, the Rule of Three (Ro3) was proposed by Astex Therapeutics to define fragment-like chemical space:
| Property | Rule of Three | Rule of Five (drug-like) |
|---|---|---|
| Molecular weight | ≤ 300 Da | ≤ 500 Da |
| cLogP | ≤ 3 | ≤ 5 |
| H-bond donors | ≤ 3 | ≤ 5 |
| H-bond acceptors | ≤ 3 | ≤ 10 |
| Rotatable bonds | ≤ 3 | ≤ 10 |
| PSA | ≤ 60 Ų | ≤ 140 Ų |
from rdkit import Chem
from rdkit.Chem import Descriptors, Lipinski, rdMolDescriptors
from dataclasses import dataclass
@dataclass
class Ro3Result:
smiles: str
mw: float
clogp: float
hbd: int
hba: int
rotatable_bonds: int
tpsa: float
passes: bool
violations: list[str]
def ro3_filter(smiles: str) -> Ro3Result:
"""
Apply Astex Rule of Three fragment filter to a SMILES string.
Returns Ro3Result with pass/fail and list of violated rules.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return Ro3Result(smiles, 0, 0, 0, 0, 0, 0, False, ["Invalid SMILES"])
mw = Descriptors.ExactMolWt(mol)
clogp = Descriptors.MolLogP(mol)
hbd = Lipinski.NumHDonors(mol)
hba = Lipinski.NumHAcceptors(mol)
rot = rdMolDescriptors.CalcNumRotatableBonds(mol)
tpsa = rdMolDescriptors.CalcTPSA(mol)
violations = []
if mw > 300:
violations.append(f"MW={mw:.1f} > 300")
if clogp > 3:
violations.append(f"cLogP={clogp:.2f} > 3")
if hbd > 3:
violations.append(f"HBD={hbd} > 3")
if hba > 3:
violations.append(f"HBA={hba} > 3")
if rot > 3:
violations.append(f"RotBonds={rot} > 3")
if tpsa > 60:
violations.append(f"TPSA={tpsa:.1f} > 60 Ų")
return Ro3Result(smiles, mw, clogp, hbd, hba, rot, tpsa, len(violations) == 0, violations)
def build_fragment_library(
smiles_list: list[str],
max_violations: int = 0,
min_heavy_atoms: int = 7,
) -> list[str]:
"""
Filter a SMILES list to fragment-like compounds.
Args:
smiles_list: List of SMILES strings
max_violations: Allowed Ro3 violations (0 = strict Ro3, 1 = lenient)
min_heavy_atoms: Minimum heavy atom count (avoid too-small fragments)
Returns:
Filtered list of SMILES passing fragment criteria
"""
fragments = []
for smi in smiles_list:
mol = Chem.MolFromSmiles(smi)
if mol is None:
continue
if mol.GetNumHeavyAtoms() < min_heavy_atoms:
continue
result = ro3_filter(smi)
if len(result.violations) <= max_violations:
fragments.append(smi)
return fragments
Fragment Efficiency Metrics
Because fragments bind weakly in absolute terms, potency must be normalised by molecular size or lipophilicity. The two key metrics are:
- Ligand Efficiency (LE): ΔG per heavy atom. Good fragments have LE > 0.3 kcal/mol/HA.
- Lipophilic Ligand Efficiency (LLE, or LipE): pIC50 − cLogP. Rewards potency without relying on lipophilicity. Target LLE > 5 for drug-like compounds.
import numpy as np
from rdkit import Chem
from rdkit.Chem import Descriptors
def ligand_efficiency(pki: float, heavy_atom_count: int) -> float:
"""
Ligand Efficiency (LE) = ΔG / #HA.
ΔG ≈ -1.37 × pKi at 25°C (kcal/mol).
Args:
pki: Experimental or predicted pKi (= -log10(Ki in M))
heavy_atom_count: Number of non-hydrogen atoms
Returns:
LE in kcal/mol per heavy atom
"""
if heavy_atom_count == 0:
return 0.0
delta_g = -1.37 * pki # kcal/mol
return abs(delta_g) / heavy_atom_count
def lipophilic_efficiency(pic50: float, smiles: str) -> float:
"""
Lipophilic Ligand Efficiency (LLE) = pIC50 - cLogP.
Higher is better; drug-like threshold is LLE > 5.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return float("nan")
clogp = Descriptors.MolLogP(mol)
return pic50 - clogp
def efficiency_profile(smiles: str, pki: float) -> dict:
"""
Compute all fragment efficiency metrics for a compound.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return {}
n_ha = mol.GetNumHeavyAtoms()
mw = Descriptors.ExactMolWt(mol)
clogp = Descriptors.MolLogP(mol)
le = ligand_efficiency(pki, n_ha)
lle = pki - clogp
# Binding efficiency index (BEI) = pKi / MW (kDa)
bei = pki / (mw / 1000.0)
# Surface efficiency index (SEI) = pKi / PSA (1/100 Ų)
from rdkit.Chem import rdMolDescriptors
tpsa = rdMolDescriptors.CalcTPSA(mol)
sei = pki / (tpsa / 100.0) if tpsa > 0 else 0.0
return {
"smiles": smiles,
"mw": round(mw, 2),
"clogp": round(clogp, 2),
"n_heavy_atoms": n_ha,
"pki": pki,
"le": round(le, 3),
"lle": round(lle, 2),
"bei": round(bei, 2),
"sei": round(sei, 2),
}
Computational Fragment Docking
Fragment docking uses the same algorithms as standard molecular docking (see the Molecular Docking guide) but with adapted scoring to account for weak binding and smaller ligands. Key tools include AutoDock Vina, Glide HTVS, and SMINA. Fragment-specific considerations:
- Using larger docking boxes or multiple sub-pocket grids to allow fragments to explore the full binding site
- Post-docking MMO (minimum motion overlay) clustering to group similar binding modes
- Rescoring with per-atom LE to avoid scoring function bias toward heavy fragments
import subprocess
import os
from pathlib import Path
def dock_fragment_vina(
receptor_pdbqt: str,
fragment_pdbqt: str,
output_dir: str,
center: tuple[float, float, float],
box_size: tuple[float, float, float] = (25.0, 25.0, 25.0),
exhaustiveness: int = 16,
num_modes: int = 20,
) -> list[dict]:
"""
Dock a fragment using Vina with a larger box for wider site exploration.
Args:
receptor_pdbqt: Path to receptor .pdbqt
fragment_pdbqt: Path to fragment .pdbqt
output_dir: Directory for output files
center: (cx, cy, cz) box center (Angstroms)
box_size: (sx, sy, sz) box dimensions — larger for fragments
exhaustiveness: Vina exhaustiveness (higher for fragments)
num_modes: Number of poses to save
Returns:
List of dicts with pose_rank and score (kcal/mol)
"""
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, "out.pdbqt")
log_path = os.path.join(output_dir, "vina.log")
cmd = [
"vina",
"--receptor", receptor_pdbqt,
"--ligand", fragment_pdbqt,
"--out", output_path,
"--log", log_path,
"--center_x", str(center[0]),
"--center_y", str(center[1]),
"--center_z", str(center[2]),
"--size_x", str(box_size[0]),
"--size_y", str(box_size[1]),
"--size_z", str(box_size[2]),
"--exhaustiveness", str(exhaustiveness),
"--num_modes", str(num_modes),
]
result = subprocess.run(cmd, capture_output=True, text=True)
# Parse scores from log
poses = []
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) >= 2:
try:
rank = int(parts[0])
score = float(parts[1])
poses.append({"rank": rank, "score": score})
except ValueError:
pass
return poses
Fragment Growing Strategy
Once a fragment hit is confirmed in a crystal structure, the next step is fragment growing: adding atoms or small groups to the fragment while keeping the core binding mode intact. Growing is guided by exit vectors (bonds pointing into unexplored protein sub-pockets) identified from the crystal structure.
from rdkit import Chem
from rdkit.Chem import AllChem, draw
from rdkit.Chem.Scaffolds import MurckoScaffold
def enumerate_exit_vectors(smiles: str) -> list[int]:
"""
Enumerate candidate exit vectors (attachment points) on a fragment.
Exit vectors are atoms bonded to H that point outward from the ring system.
Returns list of heavy atom indices suitable for growing.
"""
mol = Chem.MolFromSmiles(smiles)
mol = Chem.AddHs(mol)
AllChem.EmbedMolecule(mol, AllChem.ETKDGv3())
mol = Chem.RemoveHs(mol)
exit_vectors = []
for atom in mol.GetAtoms():
# Sp3 or ring atom with at least one implicit/explicit H
if atom.GetTotalNumHs() > 0 and atom.GetAtomicNum() > 1:
exit_vectors.append(atom.GetIdx())
return exit_vectors
def grow_fragment(
fragment_smiles: str,
building_block_smiles: str,
attachment_idx: int,
) -> list[str]:
"""
Grow a fragment by attaching a building block at the specified atom index.
Uses RDKit's cross-coupling reaction template approach.
Returns list of product SMILES.
"""
# Convert attachment point to a marked amine for coupling
fragment_mol = Chem.MolFromSmiles(fragment_smiles)
bb_mol = Chem.MolFromSmiles(building_block_smiles)
if fragment_mol is None or bb_mol is None:
return []
# Amide coupling reaction SMARTS
reaction_smarts = "[C:1](=O)[OH:2].[NH2:3][C:4]>>[C:1](=O)[NH:3][C:4]"
rxn = AllChem.ReactionFromSmarts(reaction_smarts)
products = rxn.RunReactants((fragment_mol, bb_mol))
result = []
seen = set()
for prod_set in products:
for prod in prod_set:
try:
smi = Chem.MolToSmiles(prod)
if smi not in seen:
seen.add(smi)
result.append(smi)
except Exception:
continue
return result
Fragment Linking
When two fragments bind simultaneously to adjacent sub-pockets, they can be connected by a linker. Fragment linking is geometrically constrained: the linker must be short enough that the fragments don't have to move from their bound positions, and the exit vectors must be roughly co-linear with the linker geometry.
from rdkit import Chem
from rdkit.Chem import AllChem
import numpy as np
def estimate_linking_geometry(
frag1_exit_coords: np.ndarray,
frag2_exit_coords: np.ndarray,
) -> dict:
"""
Estimate minimum linker length and geometry for linking two fragments.
Args:
frag1_exit_coords: 3D coordinates of exit atom in fragment 1
frag2_exit_coords: 3D coordinates of exit atom in fragment 2
Returns:
Dict with distance, suggested linker atom count, and geometry type
"""
distance = np.linalg.norm(frag1_exit_coords - frag2_exit_coords)
# Approximate: each C-C bond adds ~1.5 Å, each C=C ~1.33 Å
# Rule of thumb: linker_atoms ≈ round(distance / 1.5) - 1
linker_atoms = max(1, round(distance / 1.5) - 1)
if distance < 4.0:
geometry_type = "direct_bond_or_1-atom_linker"
elif distance < 7.0:
geometry_type = "short_chain_2-3_atoms"
elif distance < 10.0:
geometry_type = "medium_chain_4-6_atoms"
else:
geometry_type = "long_linker_or_fragment_merge"
return {
"exit_vector_distance_A": round(distance, 2),
"suggested_linker_atoms": linker_atoms,
"geometry_type": geometry_type,
"note": "Rigid linkers (phenyl, alkyne) preferred for <6 Å; flexible for >8 Å"
}
# Practical linker options by distance range
LINKER_LIBRARY = {
"1-atom": ["C", "O", "N", "S"],
"2-atom": ["CC", "CO", "CN", "OC"],
"3-atom": ["CCC", "C=CC", "COC", "CNC", "C1CC1"],
"4-atom": ["CCCC", "CC=CC", "c1ccc1", "CCOC"],
"5-atom": ["c1cccc1", "CCCCC", "CC(C)CC"],
}
Practical FBDD Workflow Summary
| Stage | Goal | Computational Tool |
|---|---|---|
| Library design | Build 1,000–2,000 fragment library | ZINC, Enamine REAL, Ro3 filter |
| Primary screen | Docking / virtual screen to identify site binders | Vina, Glide, SMINA with LE-rescoring |
| Hit confirmation | Structural confirmation of binding mode | X-ray, SPR, NMR (experimental) |
| Fragment growing | Extend into unexplored sub-pockets | Fragmenstein, STRIFE, RDKit grow |
| Fragment merging | Combine overlapping fragments | FBDD-ML, manual medicinal chemistry |
| Fragment linking | Connect fragments on adjacent pockets | LINK, DeLinker, geometry analysis |
| Lead optimisation | Optimise for potency, selectivity, ADMET | Full docking pipeline + ADMET models |
Key Takeaways
- Fragments (MW < 300 Da, Ro3-compliant) explore binding sites more efficiently than drug-like molecules, giving higher structural hit rates in X-ray screens.
- Ligand Efficiency (LE > 0.3 kcal/mol/HA) and LLE (pIC50 − cLogP > 5) are the two most important metrics for prioritising fragment hits before growing.
- Computational fragment docking uses standard tools (Vina, SMINA) but needs larger docking boxes and more exhaustive sampling due to the small size of fragments.
- Fragment growing guided by crystal structure exit vectors is more reliable than fragment linking, which requires geometrically compatible binding modes.
- Tools like Fragmenstein and STRIFE can automate fragment merging and growing in a structure-aware manner, reducing the gap between computational and synthetic chemistry.
Try it live
Explore a related model hands-on: open Molecule Screening Engine — Similarity Search Live and change its parameters while it runs. Everything stays in your browser.
▶ Open Molecule Screening Engine — Similarity Search Live simulation