OkayBoboLi's picture
Add the PROTAC release
f12d3b3 verified
Raw History Blame Contribute Delete
14.1 kB
"""
AR-CRBN PROTAC Builder (mixed chemistry: amide on warhead side, SNAr on E3 side)
Target: Androgen Receptor (AR T878A)
E3 Ligase: CRBN (pomalidomide/thalidomide-based binder)
Source: PROTAC-DB ID 2525
Reference PROTAC (PROTAC-DB #2525, MW = 777 Da):
CN(C1=CC=C(C#N)C(Cl)=C1)[C@H]1CC[C@H](NC(=O)C2=CC=C(N3CCN(CC4CN(C5=CC=C6
C(=O)N(C7CCC(=O)NC7=O)C(=O)C6=C5)C4)CC3)C=C2)CC1
Linker: C1CN(CC2CNC2)CCN1 (piperazine-azetidine diamine, MW 155 Da)
Linker chemistry: DIAMINE termini (fix_termini='amine')
The generated linker has two free secondary N atoms.
Warhead provides a COOH electrophile; E3 provides an electrophilic aromatic C-F.
Warhead-COOH + linker-N β†’ secondary amide (warhead side)
E3-Ar-F + linker-N β†’ C-N aromatic bond via SNAr (E3 side)
Warhead (COOH-armed, MW β‰ˆ 388 Da):
4-[((1R,4R)-4-(N-methyl-N-(4-cyano-3-chlorophenyl)amino)cyclohexyl)amino]benzoic
acid. The para-COOH reacts with the linker's secondary amine β†’ amide.
E3 Ligand (5-fluorothalidomide, MW β‰ˆ 276 Da):
2-(2,6-dioxopiperidin-3-yl)-5-fluoroisoindoline-1,3-dione.
C5-F undergoes SNAr with the linker's secondary amine β†’ aromatic C-N bond.
"""
from rdkit import Chem
from rdkit.Chem import AllChem, Descriptors
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
# ---------------------------------------------------------------------------
# AR antagonist scaffold with COOH arm β€” reacts with linker piperazine-N β†’ amide
AR_WARHEAD_SMILES = "OC(=O)c1ccc(N[C@H]2CC[C@H](N(C)c3ccc(C#N)c(Cl)c3)CC2)cc1"
# 5-fluorothalidomide (CRBN binder): C5-F undergoes SNAr with linker azetidine-N
CRBN_E3_SMILES = "Fc1ccc2c(c1)C(=O)N([C@@H]1CCC(=O)NC1=O)C2=O"
AR_WARHEAD_MOL = Chem.MolFromSmiles(AR_WARHEAD_SMILES)
CRBN_E3_MOL = Chem.MolFromSmiles(CRBN_E3_SMILES)
# Standard interface aliases
WARHEAD_MOL = AR_WARHEAD_MOL
E3_MOL = CRBN_E3_MOL
WARHEAD_SMILES = AR_WARHEAD_SMILES
E3_SMILES = CRBN_E3_SMILES
# Reactive SMARTS (used by batch_report.py to locate reactive atoms in the free fragments)
WH_REACTIVE_SMARTS = "[CX3](=[OX1])[OX2H1]" # warhead COOH carbonyl C
E3_REACTIVE_SMARTS = "[c][F]" # E3 isoindoline C5 bearing F
# Linker attachment SMARTS β€” linker must expose two secondary amines
_NH_SMARTS = "[NX3;!H0;!$(NC=O);!$(NS=O);!$(N=O)]"
# Fallback substructure patterns for batch_report.py assembly check
WARHEAD_PAT = Chem.MolFromSmarts("[NX3](C)c1ccc(C#N)c(Cl)c1") # N-methyl-4-cyano-3-Cl-aniline
E3_PAT = Chem.MolFromSmarts("O=C1NC(=O)CCC1") # glutarimide (CRBN)
class ARCRBN_PROTAC_Builder(PROTAC_Builder):
"""
PROTAC builder for AR-CRBN (PROTAC-DB #2525).
Warhead side: COOH (electrophile) + linker secondary amine β†’ amide bond.
E3 side: isoindoline C5-F (SNAr electrophile) + linker secondary amine β†’ aromatic C-N.
Use with --fix_termini amine in defog_guided.py.
"""
_COOH_SMARTS = "[CX3](=[OX1])[OX2H1]"
_AR_F_SMARTS = "[c][F]"
_NH_SMARTS = "[NX3;!H0;!$(NC=O);!$(NS=O);!$(N=O)]"
_AMIDE_SMARTS = "[NX3H0,NX3H1][CX3](=[OX1])[#6]"
# Aromatic C-N in assembled PROTAC (isoindoline C5 bonded to non-amide N)
_AR_CN_SMARTS = "c1ccc2c(c1)C(=O)N(*)C2=O"
def __init__(self, warhead: Optional[Chem.Mol] = None,
e3: Optional[Chem.Mol] = None):
# Initialize patterns before super().__init__() which calls _verify_*
self._cooh_pat = Chem.MolFromSmarts(self._COOH_SMARTS)
self._ar_f_pat = Chem.MolFromSmarts(self._AR_F_SMARTS)
self._nh_pat = Chem.MolFromSmarts(self._NH_SMARTS)
self._amide_pat = Chem.MolFromSmarts(self._AMIDE_SMARTS)
self._ar_cn_pat = Chem.MolFromSmarts(self._AR_CN_SMARTS)
self._ar_core = Chem.MolFromSmarts("[NX3](C)c1ccc(C#N)c(Cl)c1")
self._glutarimide = Chem.MolFromSmarts("O=C1NC(=O)CCC1")
super().__init__(
warhead=warhead if warhead is not None else AR_WARHEAD_MOL,
e3=e3 if e3 is not None else CRBN_E3_MOL,
warhead_name="AR antagonist (COOH-armed)",
e3_name="5-fluorothalidomide (CRBN binder, SNAr)",
)
# Warhead side: linker secondary amine + warhead COOH β†’ secondary amide
self._amide_rxn = AllChem.ReactionFromSmarts(
"[NX3;!H0;!$(NC=O);!$(NS=O):1].[CX3:2](=[OX1:3])[OX2H1]"
">>[N:1][CX3:2](=[OX1:3])"
)
# E3 side: linker secondary amine + E3 aromatic C-F β†’ aromatic C-N (SNAr)
self._snAr_rxn = AllChem.ReactionFromSmarts(
"[NX3;!H0;!$(NC=O);!$(NS=O):1].[c:2][F,Cl]>>[c:2][N:1]"
)
def _verify_warhead_reactive_group(self):
if self.warhead_mol is not None and self.warhead_mol.HasSubstructMatch(self._cooh_pat):
print("βœ“ AR warhead: COOH reactive group confirmed")
else:
print("⚠ AR warhead: no COOH detected β€” check warhead SMILES")
def _verify_e3_reactive_group(self):
if self.e3_mol is not None and self.e3_mol.HasSubstructMatch(self._ar_f_pat):
print("βœ“ CRBN E3: aromatic C-F (SNAr) reactive group confirmed")
else:
print("⚠ CRBN E3: no Ar-F detected β€” check E3 SMILES")
# ------------------------------------------------------------------
# Linker attachment site detection
# ------------------------------------------------------------------
def _reactive_amine_atoms(self, linker_mol: Chem.Mol) -> list:
"""Find secondary/primary amine N atoms on the linker (attachment sites)."""
results, seen = [], set()
for m in linker_mol.GetSubstructMatches(self._nh_pat):
n_idx = m[0]
if n_idx not in seen:
seen.add(n_idx)
results.append({"idx": n_idx})
return results
def find_linker_attachment_sites(self, linker_mol: Chem.Mol,
amide_only: bool = False) -> list:
"""Return all ordered (warhead_site, e3_site) amine pairs for assembly."""
amines = self._reactive_amine_atoms(linker_mol)
sites, seen = [], set()
for w in amines:
for e in amines:
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": "amine",
"warhead_restored": False,
"e3_site": e["idx"],
"e3_type": "amine",
"e3_restored": False,
})
return sites
# ------------------------------------------------------------------
# Assembly: amide (warhead) + SNAr (E3)
# ------------------------------------------------------------------
def _assemble_protac(self, linker_mol: Chem.Mol,
warhead_site_idx: int, e3_site_idx: int,
warhead_type: str = "amine", e3_type: str = "amine",
warhead_restored: bool = False,
e3_restored: bool = False) -> Optional[str]:
"""
Step 1: tagged linker-N (e3 side) + E3 aromatic C-F β†’ SNAr intermediate.
Step 2: remaining linker-N (warhead side) + Warhead-COOH β†’ PROTAC (amide).
"""
# Tag the e3-side amine on the linker
rw = Chem.RWMol(linker_mol)
rw.GetAtomWithIdx(e3_site_idx).SetAtomMapNum(99)
try:
Chem.SanitizeMol(rw)
except Exception:
return None
tagged_linker = rw.GetMol()
# Step 1: tagged linker-N + E3 Ar-F β†’ SNAr (aromatic C-N bond)
step1_rxn = AllChem.ReactionFromSmarts(
"[NX3;!H0;!$(NC=O):99].[c:2][F,Cl]>>[c:2][N:99]"
)
products1 = step1_rxn.RunReactants((tagged_linker, self.e3_mol))
if not products1:
return None
intermediate = None
for prod_tuple in products1:
try:
mol = prod_tuple[0]
Chem.SanitizeMol(mol)
intermediate = mol
break
except Exception:
continue
if intermediate is None:
return None
# Clear atom map tag
rw2 = Chem.RWMol(intermediate)
for atom in rw2.GetAtoms():
if atom.GetAtomMapNum() == 99:
atom.SetAtomMapNum(0)
intermediate = rw2.GetMol()
# Step 2: remaining linker-N + Warhead-COOH β†’ amide
products2 = self._amide_rxn.RunReactants((intermediate, self.warhead_mol))
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 check_attachment_site_compatibility(self, protac_mol: Chem.Mol) -> bool:
has_amide = len(protac_mol.GetSubstructMatches(self._amide_pat)) >= 1
no_cooh = len(protac_mol.GetSubstructMatches(self._cooh_pat)) == 0
no_ar_f = not protac_mol.HasSubstructMatch(self._ar_f_pat)
has_cores = self.has_warhead_core(protac_mol) and self.has_e3_core(protac_mol)
return (has_amide and no_cooh and no_ar_f) or has_cores
def has_warhead_core(self, protac_mol: Chem.Mol) -> bool:
return protac_mol.HasSubstructMatch(self._ar_core)
def has_e3_core(self, protac_mol: Chem.Mol) -> bool:
return protac_mol.HasSubstructMatch(self._glutarimide)
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_amide_bond": len(protac_mol.GetSubstructMatches(self._amide_pat)) >= 1,
"no_unreacted_cooh": len(protac_mol.GetSubstructMatches(self._cooh_pat)) == 0,
"no_unreacted_ar_f": not protac_mol.HasSubstructMatch(self._ar_f_pat),
}
checks["attachment_site_valid"] = self.check_attachment_site_compatibility(protac_mol)
checks["_passed"] = all([
checks["has_warhead_core"],
checks["has_e3_core"],
checks["has_amide_bond"],
checks["no_unreacted_cooh"],
checks["no_unreacted_ar_f"],
])
return checks
Builder = ARCRBN_PROTAC_Builder
# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
RDLogger.DisableLog("rdApp.error")
print("=" * 70)
print("AR-CRBN PROTAC Builder β€” Self-Test (amide + SNAr chemistry)")
print(f"Warhead : AR antagonist with COOH arm (amide coupling)")
print(f"E3 : 5-fluorothalidomide (CRBN binder, SNAr at C5-F)")
print(f"Linker : diamine (two secondary N termini)")
print(f"Chemistry: linker-N + COOH β†’ amide (WH) | linker-N + Ar-F β†’ C-N (E3)")
print("=" * 70)
builder = ARCRBN_PROTAC_Builder()
builder._verify_warhead_reactive_group()
builder._verify_e3_reactive_group()
# Reference linker from PROTAC-DB #2525: piperazine-azetidine diamine
# Two secondary amines: piperazine-NH (warhead side) and azetidine-NH (E3 side)
test_linkers = [
("C1CN(CC2CNC2)CCN1", "Piperazine-azetidine (PROTAC-DB #2525 reference)"),
("NCCOCCOCCN", "PEG-diamine (two primary amines)"),
("N1CCNCC1", "Piperazine (two secondary amines)"),
("OC(=O)CCCCC(=O)O", "Diacid β€” no amines, should fail"),
("NCCCN", "Propane-1,3-diamine"),
("C1CCN(CC1)CCN", "Piperazine-ethylamine"),
]
print()
for smiles, desc in test_linkers:
linker_mol = Chem.MolFromSmiles(smiles)
print(f"Linker : {desc}")
if linker_mol is None:
print(" [SKIP] unparseable SMILES")
print("-" * 70)
continue
try:
result = builder.assemble(linker_mol, return_all=False)
except ValueError:
result = None
if result:
m = Chem.MolFromSmiles(result)
mw = Descriptors.MolWt(m) if m else "?"
val = builder._basic_validation(m) if m else {}
passed = val.get("_passed", False)
print(f" {'βœ“' if passed else '~'} Assembled (MW={mw:.0f} Da): {result[:80]}...")
if not passed:
fails = [k for k, v in val.items() if not v and not k.startswith("_")]
print(f" Validation fails: {fails}")
else:
print(f" βœ— Assembly failed")
print("-" * 70)
# Verify reference PROTAC (PROTAC-DB #2525) passes validation
print("\nReference PROTAC (PROTAC-DB #2525) validation:")
ref_smi = ("CN(C1=CC=C(C#N)C(Cl)=C1)[C@H]1CC[C@H](NC(=O)C2=CC=C"
"(N3CCN(CC4CN(C5=CC=C6C(=O)N(C7CCC(=O)NC7=O)C(=O)C6=C5)"
"C4)CC3)C=C2)CC1")
ref_mol = Chem.MolFromSmiles(ref_smi)
if ref_mol:
mw = Descriptors.MolWt(ref_mol)
val = builder._basic_validation(ref_mol)
print(f" MW = {mw:.0f} Da (expected ~777 Da)")
for k, v in val.items():
if not k.startswith("_"):
print(f" {'βœ“' if v else 'βœ—'} {k}")
print(f" {'βœ“ PASSED' if val['_passed'] else 'βœ— FAILED'}")
else:
print(" [ERROR] could not parse reference SMILES")