OkayBoboLi's picture
Add the PROTAC release
f12d3b3 verified
Raw History Blame Contribute Delete
14.7 kB
"""
AR-CRBN PROTAC Builder (v2, alkylation chemistry β€” in-distribution vs PROTAC-DB)
Target: Androgen Receptor (AR) β€” GSK2849466-derived warhead
E3 Ligase: CRBN β€” pomalidomide / 4-amino-thalidomide
Why "v2": the original ARCRBN.py used a COOH-armed enzalutamide-aniline warhead
+ 5-fluoro-thalidomide E3 (amine-termini linker, amide+SNAr chemistry). That
exact warhead+E3 pair has 0/276 substructure overlap with PROTAC-DB's AR-V7+CRBN
training entries, so DegradMaster collapses to ~0 on generated PROTACs (OOD).
This v2 builder uses the dominant PROTAC-DB AR-CRBN chemistry:
- Warhead: 2,2,3,3-tetramethyl-cyclobutyl-pivamide-(4-aminobenzamide) +
cyanochlorophenyl ether (GSK2849466 / SARM-binder family)
Reactive site: aniline-NH2 on the benzamide para position.
- E3: pomalidomide (4-amino-thalidomide).
Reactive site: aryl-NH2 on the isoindolinone 4-position.
- Linker: alkyl chain with terminal CH3 at both ends.
Assembly (mirrors the alkylation path in ErlotinibVHL.py):
Warhead-Ar-NH2 + linker-CH3 β†’ Warhead-Ar-N(H)-CH2-… (C-N alkylation)
E3-Ar-NH2 + linker-CH3 β†’ E3-Ar-N(H)-CH2-… (C-N alkylation)
Use with: --fix_termini methyl (linker CH3 termini are pinned)
--protac_module ARCRBNv2
"""
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 AR-CRBN chemistry
# ---------------------------------------------------------------------------
# GSK2849466-style AR antagonist with terminal aniline-NH2 (linker attachment).
# 2,2,3,3-tetramethyl-1-((4-aminobenzamido))-4-(4-cyano-3-chlorophenoxy)-cyclobutane.
# 39/276 PROTAC-DB AR-CRBN entries contain the cyclobutyl-pivamide core; 226/276
# contain the 4-aminobenzamide motif; together they cover the dominant active
# AR-CRBN PROTAC chemotype.
AR_WARHEAD_SMILES = "CC1(C)[C@H](NC(=O)c2ccc(N)cc2)C(C)(C)[C@H]1Oc1ccc(C#N)c(Cl)c1"
# Pomalidomide / 4-amino-thalidomide β€” aryl-NH2 reactive for linker alkylation.
# 52/276 PROTAC-DB AR-CRBN entries match this exact pomalidomide core; thalidomide-
# like bicyclic appears in 267/276.
CRBN_E3_SMILES = "Nc1cccc2c1C(=O)N(C3CCC(=O)NC3=O)C2=O"
AR_WARHEAD_MOL = Chem.MolFromSmiles(AR_WARHEAD_SMILES)
CRBN_E3_MOL = Chem.MolFromSmiles(CRBN_E3_SMILES)
# SMARTS patterns
_NH2_ARYL_SMARTS = "[NX3;H2;$(Nc)]" # primary aniline NH2
_AMIDE_SMARTS = "[NX3H0,NX3H1][CX3](=[OX1])[#6]"
_TERMINAL_CH3_SMARTS = "[CX4H3]"
_TERMINAL_CHO_SMARTS = "[CX3H1](=[OX1])"
# Warhead substructure for assembly verification (cyclobutyl-pivamide + cyanochlorophenoxy)
_AR_CORE_SMARTS = "CC(C)(C)[C](C(C)(C))[C]Oc1ccc(C#N)c(Cl)c1"
# E3 substructure: glutarimide + isoindolinone (thalidomide bicyclic)
_THALIDOMIDE_SMARTS = "O=C1c2ccccc2C(=O)N1[C@@H]1CCC(=O)NC1=O"
class ARCRBNv2_PROTAC_Builder(PROTAC_Builder):
"""
PROTAC builder: GSK2849466-aniline AR warhead + pomalidomide CRBN E3.
Linker requirement: terminal CH3 groups (alkyl), one per end.
Warhead aryl-NH2 + linker terminal CH3 β†’ secondary aniline + CH2 (C-N alkylation).
E3 aryl-NH2 + linker terminal CH3 β†’ secondary aniline + CH2 (C-N alkylation).
"""
def __init__(self, warhead: Optional[Chem.Mol] = None,
e3: Optional[Chem.Mol] = None):
# Initialize SMARTS patterns BEFORE super().__init__() β€” the parent
# constructor calls _verify_*_reactive_group which accesses these.
self._nh2_aryl_pat = Chem.MolFromSmarts(_NH2_ARYL_SMARTS)
self._amide_pat = Chem.MolFromSmarts(_AMIDE_SMARTS)
self._terminal_ch3 = Chem.MolFromSmarts(_TERMINAL_CH3_SMARTS)
self._terminal_cho = Chem.MolFromSmarts(_TERMINAL_CHO_SMARTS)
self._ar_core = Chem.MolFromSmarts(_AR_CORE_SMARTS)
self._thalidomide = Chem.MolFromSmarts(_THALIDOMIDE_SMARTS)
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="GSK2849466-aniline (AR)",
e3_name="pomalidomide (CRBN)",
)
# Alkylation reactions: aryl-NH2 + terminal CH3 β†’ aryl-NH-CH2-
# E3 reaction: pomalidomide aryl-NH2 + linker CH3
self._alkyl_rxn_e3 = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX4H3:2]>>[NX3H1:1][CX4H2:2]"
)
# Warhead reaction: AR aryl-NH2 + linker CH3
self._alkyl_rxn_wh = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX4H3:2]>>[NX3H1:1][CX4H2:2]"
)
# ------------------------------------------------------------------
# Verification
# ------------------------------------------------------------------
def _verify_warhead_reactive_group(self):
if self.warhead_mol.HasSubstructMatch(self._nh2_aryl_pat):
print(f"βœ“ {self.warhead_name}: aryl-NH2 reactive group confirmed (4-aminobenzamide)")
else:
print(f"⚠ {self.warhead_name}: no reactive aryl-NH2 detected β€” check warhead")
def _verify_e3_reactive_group(self):
if self.e3_mol.HasSubstructMatch(self._nh2_aryl_pat):
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 β€” looks for terminal CH3 on linker
# ------------------------------------------------------------------
def _reactive_terminal_c_atoms(self, linker_mol: Chem.Mol) -> list:
"""
Find terminal CH3 carbons that can act as alkylating sites.
Two-layer check:
1. SMARTS match on terminal CH3
2. Reaction trial vs E3 aryl-NH2 to confirm the alkylation fires.
"""
results = []
seen = set()
for m in linker_mol.GetSubstructMatches(self._terminal_ch3):
c_idx = m[0]
if c_idx in seen:
continue
rw = Chem.RWMol(linker_mol)
rw.GetAtomWithIdx(c_idx).SetAtomMapNum(98)
try:
Chem.SanitizeMol(rw)
except Exception:
continue
trial_rxn = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX4H3:98]>>[NX3H1:1][CX4H2:98]"
)
if trial_rxn.RunReactants((self.e3_mol, rw.GetMol())):
seen.add(c_idx)
results.append({"idx": c_idx, "type": "alkyl"})
return results
def find_linker_attachment_sites(self, linker_mol: Chem.Mol,
amide_only: bool = False) -> list:
"""
Find pairs of terminal CH3 atoms on the linker for alkylation assembly.
amide_only is accepted for interface compatibility but ignored here
(this chemistry has no amide path).
"""
cands = self._reactive_terminal_c_atoms(linker_mol)
sites = []
seen = set()
for w in cands:
for e in cands:
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": "alkyl",
"warhead_restored": False,
"e3_site": e["idx"],
"e3_type": "alkyl",
"e3_restored": False,
})
return sites
# ------------------------------------------------------------------
# Assembly: two sequential alkylation steps
# ------------------------------------------------------------------
def _assemble_protac(self, linker_mol: Chem.Mol, warhead_site_idx: int,
e3_site_idx: int,
warhead_type: str = "alkyl", e3_type: str = "alkyl",
warhead_restored: bool = False,
e3_restored: bool = False) -> Optional[str]:
rw = Chem.RWMol(linker_mol)
rw.GetAtomWithIdx(e3_site_idx).SetAtomMapNum(99)
tagged_linker = rw.GetMol()
try:
Chem.SanitizeMol(rw)
except Exception:
return None
# Step 1: E3 aryl-NH2 + linker CH3 β†’ secondary aniline
step1_rxn = AllChem.ReactionFromSmarts(
"[NX3H2;$(Nc):1].[CX4H3:99]>>[NX3H1:1][CX4H2:99]"
)
products1 = step1_rxn.RunReactants((self.e3_mol, tagged_linker))
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
# Clear the e3-side tag
rw2 = Chem.RWMol(intermediate)
for atom in rw2.GetAtoms():
if atom.GetAtomMapNum() == 99:
atom.SetAtomMapNum(0)
intermediate = rw2.GetMol()
# Step 2: warhead aryl-NH2 + remaining terminal CH3 β†’ secondary aniline
products2 = self._alkyl_rxn_wh.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 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 has_warhead_core(self, protac_mol: Chem.Mol) -> bool:
# Match the cyanochloro-aryloxy + tetramethyl-cyclobutyl signature
pat = Chem.MolFromSmarts("Oc1ccc(C#N)c(Cl)c1")
return protac_mol.HasSubstructMatch(pat)
def has_e3_core(self, protac_mol: Chem.Mol) -> bool:
# Match the glutarimide of thalidomide-class
pat = Chem.MolFromSmarts("O=C1NC(=O)CCC1")
return protac_mol.HasSubstructMatch(pat)
def has_both_amide_bonds(self, protac_mol: Chem.Mol) -> bool:
# In alkylation chemistry, no NEW amides form β€” the warhead's pivamide and
# the E3's two carbonyls already exist. Just need β‰₯2 amide-like patterns total.
return len(protac_mol.GetSubstructMatches(self._amide_pat)) >= 2
def no_unreacted_cooh(self, protac_mol: Chem.Mol) -> bool:
# Alkylation chemistry doesn't use COOH; if any free COOH appeared it's
# a builder error. Accept absence as true.
cooh = Chem.MolFromSmarts("[CX3](=[OX1])[OX2H1]")
return len(protac_mol.GetSubstructMatches(cooh)) == 0
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 = AR_WARHEAD_MOL
E3_MOL = CRBN_E3_MOL
WARHEAD_SMILES = AR_WARHEAD_SMILES
E3_SMILES = CRBN_E3_SMILES
Builder = ARCRBNv2_PROTAC_Builder
WH_REACTIVE_SMARTS = _NH2_ARYL_SMARTS
E3_REACTIVE_SMARTS = _NH2_ARYL_SMARTS
WARHEAD_PAT = Chem.MolFromSmarts("Oc1ccc(C#N)c(Cl)c1")
E3_PAT = Chem.MolFromSmarts("O=C1NC(=O)CCC1")
# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
from rdkit import RDLogger
RDLogger.DisableLog("rdApp.error")
print("=" * 70)
print("AR-CRBN v2 PROTAC Builder β€” Self-Test")
print(f"Warhead : AR (GSK2849466-aniline, aryl-NH2 reactive)")
print(f"E3 : CRBN (pomalidomide, aryl-NH2 reactive)")
print(f"Chemistry: alkyl linker + 2Γ— C-N alkylation (CH3 termini)")
print("=" * 70)
builder = ARCRBNv2_PROTAC_Builder()
test_linkers = [
("CCCCCCCC", "octane (terminal CH3 at both ends)"),
("CCOCCOCCOCC", "PEG3-dimethyl"),
("CC1CCC(C)CC1", "1,4-dimethylcyclohexane"),
("OC(=O)CCCCC(=O)O", "diacid β€” should fail (no terminal CH3)"),
("NCCCCCN", "diamine β€” should fail (no terminal CH3)"),
]
print()
for smiles, desc in test_linkers:
linker_mol = Chem.MolFromSmiles(smiles)
print(f"Linker : {desc}")
ch3_count = len(linker_mol.GetSubstructMatches(Chem.MolFromSmarts(_TERMINAL_CH3_SMARTS)))
print(f" Terminal CH3 sites: {ch3_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)