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