""" 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")