Download CupOFMoCA-PROTAC/src/protac/ARCRBNv2.py from ChatterjeeLab/CupOFMoCA: direct link, hf CLI and curl.
- Browser
- Download file 14.7 kB
-
https://huggingface.co/ChatterjeeLab/CupOFMoCA/resolve/main/CupOFMoCA-PROTAC/src/protac/ARCRBNv2.py
- Command line
-
hf download hf://ChatterjeeLab/CupOFMoCA/CupOFMoCA-PROTAC/src/protac/ARCRBNv2.py
-
curl -L -o ARCRBNv2.py https://huggingface.co/ChatterjeeLab/CupOFMoCA/resolve/main/CupOFMoCA-PROTAC/src/protac/ARCRBNv2.py
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) | |