OkayBoboLi's picture
Add the PROTAC release
f12d3b3 verified
Raw History Blame Contribute Delete
15.3 kB
"""
CDK6-CRBN PROTAC Builder
Target: CDK6 (palbociclib-derived warhead, piperazine-NH reactive)
E3 Ligase: CRBN (pomalidomide / 4-amino-thalidomide, aryl-NH2 reactive)
Source: Dominant chemistry in PROTAC-DB CDK6+CRBN (palbociclib core matches
58% of training PROTACs; piperazine-NH on warhead β†’ diacid linker).
Linker chemistry: same as EGFR-VHL / BRD9-VHL β€” diacid (cooh termini).
Warhead-piperazine-NH + linker-COOH β†’ tertiary amide (warhead end)
E3-aryl-NH2 + linker-COOH β†’ secondary amide (E3 end)
Use with: --fix_termini cooh
--protac_module CDK6CRBN
Picking COOH chemistry over the alternative amine-termini path because:
1. Both warhead piperazine-NH and pomalidomide aryl-NH2 are nucleophilic
primary/secondary amines β†’ diacid linker is the natural fit
2. Diacid-termini generation produces far fewer liability-flagged linkers
than the amine-termini path (avoids vinyl-amines, hemiaminals, hydroxyamines)
Example training PROTAC (PROTAC-DB CDK6+CRBN, label=1):
CC(=O)C1=C(C)C2=CN=C(NC3=CC=C(N4CCN(CC(=O)NCCOCCOCCOCCNC5=CC=CC6=C5C(=O)N(C5CCC(=O)NC5=O)C6=O)CC4)C=N3)N=C2N(C5CCCC5)C1=O
"""
from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit import RDLogger
from typing import Optional
try:
from .protac_builder import PROTAC_Builder
except ImportError:
from protac_builder import PROTAC_Builder
RDLogger.DisableLog("rdApp.error")
# ---------------------------------------------------------------------------
# Component SMILES β€” chosen to match dominant PROTAC-DB CDK6-CRBN chemistry
# ---------------------------------------------------------------------------
# Palbociclib with terminal piperazine-NH (linker attachment site).
# 6-acetyl-pyrido-pyrimidine + cyclopentyl-N + amino-pyridyl-piperazine.
# 58% substructure match against PROTAC-DB CDK6+CRBN entries.
CDK6_WARHEAD_SMILES = "CC(=O)c1c(C)c2cnc(Nc3ccc(N4CCNCC4)nc3)nc2n(C2CCCC2)c1=O"
# Pomalidomide / 4-amino-thalidomide (CRBN binder; aryl-NH2 reactive).
CRBN_E3_SMILES = "Nc1cccc2c1C(=O)N(C3CCC(=O)NC3=O)C2=O"
CDK6_WARHEAD_MOL = Chem.MolFromSmiles(CDK6_WARHEAD_SMILES)
CRBN_E3_MOL = Chem.MolFromSmiles(CRBN_E3_SMILES)
# SMARTS patterns
_COOH_SMARTS = "[CX3](=[OX1])[OX2H1]"
_NH2_SMARTS = "[NX3;H2]"
_NH_SMARTS = "[NX3;H1;!$(NC=O);!$(NS=O);!$(NC#N)]"
_AMIDE_SMARTS = "[NX3H0,NX3H1][CX3](=[OX1])[#6]"
# Palbociclib core (loose β€” allows any N-substituent on the cyclopentyl-N)
_CDK6_CORE_SMARTS = "[#6]C(=O)c1c([#6])c2cnc(*)nc2n([#6])c1=O"
_THALIDOMIDE_SMARTS = "O=C1c2ccccc2C(=O)N1[C@@H]1CCC(=O)NC1=O"
_TERMINAL_CHO_SMARTS = "[CX3H1](=[OX1])"
class CDK6CRBN_PROTAC_Builder(PROTAC_Builder):
"""
PROTAC builder: palbociclib-piperazine CDK6 warhead + pomalidomide CRBN E3.
Linker requirement: two COOH groups (diacid), one per end.
Warhead piperazine-NH + linker COOH β†’ tertiary amide.
E3 aryl-NH2 + linker COOH β†’ secondary amide.
"""
def __init__(self, warhead: Optional[Chem.Mol] = None,
e3: Optional[Chem.Mol] = None):
# Initialize patterns BEFORE super().__init__() β€” parent calls _verify_*
self._cooh_pat = Chem.MolFromSmarts(_COOH_SMARTS)
self._nh2_pat = Chem.MolFromSmarts(_NH2_SMARTS)
self._nh_pat = Chem.MolFromSmarts(_NH_SMARTS)
self._amide_pat = Chem.MolFromSmarts(_AMIDE_SMARTS)
self._cdk6_core = Chem.MolFromSmarts(_CDK6_CORE_SMARTS)
self._thalidomide = Chem.MolFromSmarts(_THALIDOMIDE_SMARTS)
self._terminal_cho = Chem.MolFromSmarts(_TERMINAL_CHO_SMARTS)
super().__init__(
warhead=warhead if warhead is not None else CDK6_WARHEAD_MOL,
e3=e3 if e3 is not None else CRBN_E3_MOL,
warhead_name="palbociclib-piperazine (CDK6)",
e3_name="pomalidomide (CRBN)",
)
# Amide reactions
# E3 aryl-NH2 + linker COOH β†’ secondary aniline-amide
self._amide_rxn_nh2 = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX3:2](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:2](=[OX1:3])"
)
# Warhead piperazine-NH + linker COOH β†’ tertiary amide
self._amide_rxn_nh = AllChem.ReactionFromSmarts(
"[NX3H1;!$(NC=O);!$(NS=O):1].[CX3:2](=[OX1:3])[OX2H1]>>[NX3H0:1][CX3:2](=[OX1:3])"
)
def _verify_warhead_reactive_group(self):
if self.warhead_mol.HasSubstructMatch(self._nh_pat):
print(f"βœ“ {self.warhead_name}: NH reactive group confirmed (piperazine)")
else:
print(f"⚠ {self.warhead_name}: no reactive NH detected β€” check warhead")
def _verify_e3_reactive_group(self):
nh2_aryl = Chem.MolFromSmarts("[NX3;H2;$(Nc)]")
if self.e3_mol.HasSubstructMatch(nh2_aryl):
print(f"βœ“ {self.e3_name}: aryl-NH2 reactive group confirmed (4-amino-thalidomide)")
else:
print(f"⚠ {self.e3_name}: no reactive aryl-NH2 detected β€” check E3")
# ------------------------------------------------------------------
# Linker attachment site detection β€” diacid (COOH at both ends)
# ------------------------------------------------------------------
def _reactive_cooh_atoms(self, linker_mol: Chem.Mol) -> list:
results = []
seen = set()
# Direct COOH
for m in linker_mol.GetSubstructMatches(self._cooh_pat):
c_idx = m[0]
if c_idx not in seen:
seen.add(c_idx)
results.append({"idx": c_idx, "restored": False})
# Terminal CHO β†’ restore to COOH (post-assembly form)
for m in linker_mol.GetSubstructMatches(self._terminal_cho):
c_idx = m[0]
if c_idx in seen:
continue
rw = Chem.RWMol(linker_mol)
c_atom = rw.GetAtomWithIdx(c_idx)
c_atom.SetNumExplicitHs(0)
c_atom.SetNoImplicit(True)
o_idx = rw.AddAtom(Chem.Atom("O"))
rw.GetAtomWithIdx(o_idx).SetNumExplicitHs(1)
rw.AddBond(c_idx, o_idx, Chem.BondType.SINGLE)
rw.GetAtomWithIdx(c_idx).SetAtomMapNum(99)
try:
Chem.SanitizeMol(rw)
except Exception:
continue
trial_rxn = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX3:99](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:99](=[OX1:3])"
)
if trial_rxn.RunReactants((self.e3_mol, rw.GetMol())):
seen.add(c_idx)
results.append({"idx": c_idx, "restored": True})
return results
def find_linker_attachment_sites(self, linker_mol: Chem.Mol,
amide_only: bool = False) -> list:
cands = self._reactive_cooh_atoms(linker_mol)
sites, seen = [], set()
direct = [c for c in cands if not c["restored"]]
restored = [c for c in cands if c["restored"]]
for w in direct:
for e in direct:
key = (w["idx"], e["idx"])
if key not in seen and w["idx"] != e["idx"]:
seen.add(key)
sites.append({
"warhead_site": w["idx"], "warhead_type": "cooh",
"warhead_restored": False,
"e3_site": e["idx"], "e3_type": "cooh", "e3_restored": False,
})
for w in direct:
for e in restored:
key = (w["idx"], e["idx"])
if key not in seen and w["idx"] != e["idx"]:
seen.add(key)
sites.append({
"warhead_site": w["idx"], "warhead_type": "cooh",
"warhead_restored": False,
"e3_site": e["idx"], "e3_type": "cooh", "e3_restored": True,
})
for e in direct:
key = (e["idx"], w["idx"])
if key not in seen and w["idx"] != e["idx"]:
seen.add(key)
sites.append({
"warhead_site": e["idx"], "warhead_type": "cooh",
"warhead_restored": False,
"e3_site": w["idx"], "e3_type": "cooh", "e3_restored": False,
})
for w in restored:
for e in restored:
key = (w["idx"], e["idx"])
if key not in seen and w["idx"] != e["idx"]:
seen.add(key)
sites.append({
"warhead_site": w["idx"], "warhead_type": "cooh",
"warhead_restored": True,
"e3_site": e["idx"], "e3_type": "cooh", "e3_restored": True,
})
return sites
# ------------------------------------------------------------------
# Assembly
# ------------------------------------------------------------------
def _restore_cho_to_cooh(self, rw: Chem.RWMol, c_idx: int):
c_atom = rw.GetAtomWithIdx(c_idx)
c_atom.SetNumExplicitHs(0)
c_atom.SetNoImplicit(True)
o_idx = rw.AddAtom(Chem.Atom("O"))
rw.GetAtomWithIdx(o_idx).SetNumExplicitHs(1)
rw.AddBond(c_idx, o_idx, Chem.BondType.SINGLE)
def _assemble_protac(self, linker_mol: Chem.Mol, warhead_site_idx: int,
e3_site_idx: int,
warhead_type: str = "cooh", e3_type: str = "cooh",
warhead_restored: bool = False,
e3_restored: bool = False) -> Optional[str]:
rw = Chem.RWMol(linker_mol)
if e3_restored:
self._restore_cho_to_cooh(rw, e3_site_idx)
if warhead_restored:
self._restore_cho_to_cooh(rw, warhead_site_idx)
try:
Chem.SanitizeMol(rw)
except Exception:
return None
rw.GetAtomWithIdx(e3_site_idx).SetAtomMapNum(99)
tagged = rw.GetMol()
# Step 1: E3 aryl-NH2 + linker COOH(tagged) β†’ secondary aniline-amide
step1_rxn = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX3:99](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:99](=[OX1:3])"
)
products1 = step1_rxn.RunReactants((self.e3_mol, tagged))
if not products1:
return None
intermediate = None
for prod_tuple in products1:
try:
m = prod_tuple[0]
Chem.SanitizeMol(m)
intermediate = m
break
except Exception:
continue
if intermediate is None:
return None
rw2 = Chem.RWMol(intermediate)
for atom in rw2.GetAtoms():
if atom.GetAtomMapNum() == 99:
atom.SetAtomMapNum(0)
intermediate = rw2.GetMol()
# Step 2: warhead piperazine-NH + remaining COOH β†’ tertiary amide
products2 = self._amide_rxn_nh.RunReactants((self.warhead_mol, intermediate))
if not products2:
return None
for prod_tuple in products2:
try:
protac_mol = prod_tuple[0]
Chem.SanitizeMol(protac_mol)
return Chem.MolToSmiles(protac_mol)
except Exception:
continue
return None
# ------------------------------------------------------------------
# Validation
# ------------------------------------------------------------------
def has_warhead_core(self, protac_mol: Chem.Mol) -> bool:
return protac_mol.HasSubstructMatch(self._cdk6_core)
def has_e3_core(self, protac_mol: Chem.Mol) -> bool:
glut = Chem.MolFromSmarts("O=C1NC(=O)CCC1")
return protac_mol.HasSubstructMatch(glut)
def has_both_amide_bonds(self, protac_mol: Chem.Mol) -> bool:
return len(protac_mol.GetSubstructMatches(self._amide_pat)) >= 2
def no_unreacted_cooh(self, protac_mol: Chem.Mol) -> bool:
return len(protac_mol.GetSubstructMatches(self._cooh_pat)) == 0
def check_attachment_site_compatibility(self, protac_mol: Chem.Mol) -> bool:
return self.has_warhead_core(protac_mol) and self.has_e3_core(protac_mol)
def _basic_validation(self, protac_mol: Chem.Mol) -> dict:
checks = {
"has_warhead_core": self.has_warhead_core(protac_mol),
"has_e3_core": self.has_e3_core(protac_mol),
"has_both_amide_bonds": self.has_both_amide_bonds(protac_mol),
"no_unreacted_cooh": self.no_unreacted_cooh(protac_mol),
"attachment_site_valid": self.check_attachment_site_compatibility(protac_mol),
}
checks["_passed"] = all([
checks["has_warhead_core"],
checks["has_e3_core"],
checks["has_both_amide_bonds"],
checks["no_unreacted_cooh"],
])
return checks
# ---------------------------------------------------------------------------
# Standard interface aliases
# ---------------------------------------------------------------------------
WARHEAD_MOL = CDK6_WARHEAD_MOL
E3_MOL = CRBN_E3_MOL
WARHEAD_SMILES = CDK6_WARHEAD_SMILES
E3_SMILES = CRBN_E3_SMILES
Builder = CDK6CRBN_PROTAC_Builder
WH_REACTIVE_SMARTS = _NH_SMARTS
E3_REACTIVE_SMARTS = "[NX3;H2;$(Nc)]"
WARHEAD_PAT = Chem.MolFromSmarts(_CDK6_CORE_SMARTS)
E3_PAT = Chem.MolFromSmarts("O=C1NC(=O)CCC1")
# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
from rdkit import RDLogger
RDLogger.DisableLog("rdApp.error")
print("=" * 70)
print("CDK6-CRBN PROTAC Builder β€” Self-Test")
print(f"Warhead : CDK6 (palbociclib-piperazine-NH, NH reactive)")
print(f"E3 : CRBN (pomalidomide aryl-NH2, NH2 reactive)")
print(f"Chemistry: diacid linker + 2Γ— amide bond formation")
print("=" * 70)
builder = CDK6CRBN_PROTAC_Builder()
test_linkers = [
("OC(=O)CCCCCCCCC(=O)O", "sebacic acid (C10 diacid)"),
("OC(=O)CCOCCOCCOC(=O)O", "PEG-diacid"),
("NCCN", "diamine β€” should fail"),
]
print()
for smiles, desc in test_linkers:
linker_mol = Chem.MolFromSmiles(smiles)
cooh_count = len(linker_mol.GetSubstructMatches(Chem.MolFromSmarts(_COOH_SMARTS)))
print(f"Linker : {desc}")
print(f" COOH sites: {cooh_count}")
try:
protac_smi = builder.assemble(linker_mol, return_all=False)
protac_mol = Chem.MolFromSmiles(protac_smi)
passed, checks = builder.validate_protac(protac_mol)
tag = "VALID βœ“" if passed else "ASSEMBLED but INVALID βœ—"
print(f" Assembly: {tag}")
for k, v in checks.items():
print(f" {'βœ“' if v else 'βœ—'} {k}")
props = builder.get_protac_properties(protac_mol)
print(f" MW: {props['mw']} Da LogP: {props['logp']} HeavyAtoms: {props['heavy_atoms']}")
except Exception as e:
print(f" Assembly: FAILED β€” {e}")
print("-" * 70)