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