Download CupOFMoCA-PROTAC/src/protac/CDK6CRBN.py from ChatterjeeLab/CupOFMoCA: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/ChatterjeeLab/CupOFMoCA/resolve/main/CupOFMoCA-PROTAC/src/protac/CDK6CRBN.py
- Command line
-
hf download hf://ChatterjeeLab/CupOFMoCA/CupOFMoCA-PROTAC/src/protac/CDK6CRBN.py
-
curl -L -o CDK6CRBN.py https://huggingface.co/ChatterjeeLab/CupOFMoCA/resolve/main/CupOFMoCA-PROTAC/src/protac/CDK6CRBN.py
15.3 kB
| """ | |
| CDK6-CRBN PROTAC Builder | |
| Target: CDK6 (palbociclib-derived warhead, piperazine-NH reactive) | |
| E3 Ligase: CRBN (pomalidomide / 4-amino-thalidomide, aryl-NH2 reactive) | |
| Source: Dominant chemistry in PROTAC-DB CDK6+CRBN (palbociclib core matches | |
| 58% of training PROTACs; piperazine-NH on warhead β diacid linker). | |
| Linker chemistry: same as EGFR-VHL / BRD9-VHL β diacid (cooh termini). | |
| Warhead-piperazine-NH + linker-COOH β tertiary amide (warhead end) | |
| E3-aryl-NH2 + linker-COOH β secondary amide (E3 end) | |
| Use with: --fix_termini cooh | |
| --protac_module CDK6CRBN | |
| Picking COOH chemistry over the alternative amine-termini path because: | |
| 1. Both warhead piperazine-NH and pomalidomide aryl-NH2 are nucleophilic | |
| primary/secondary amines β diacid linker is the natural fit | |
| 2. Diacid-termini generation produces far fewer liability-flagged linkers | |
| than the amine-termini path (avoids vinyl-amines, hemiaminals, hydroxyamines) | |
| Example training PROTAC (PROTAC-DB CDK6+CRBN, label=1): | |
| CC(=O)C1=C(C)C2=CN=C(NC3=CC=C(N4CCN(CC(=O)NCCOCCOCCOCCNC5=CC=CC6=C5C(=O)N(C5CCC(=O)NC5=O)C6=O)CC4)C=N3)N=C2N(C5CCCC5)C1=O | |
| """ | |
| 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 CDK6-CRBN chemistry | |
| # --------------------------------------------------------------------------- | |
| # Palbociclib with terminal piperazine-NH (linker attachment site). | |
| # 6-acetyl-pyrido-pyrimidine + cyclopentyl-N + amino-pyridyl-piperazine. | |
| # 58% substructure match against PROTAC-DB CDK6+CRBN entries. | |
| CDK6_WARHEAD_SMILES = "CC(=O)c1c(C)c2cnc(Nc3ccc(N4CCNCC4)nc3)nc2n(C2CCCC2)c1=O" | |
| # Pomalidomide / 4-amino-thalidomide (CRBN binder; aryl-NH2 reactive). | |
| CRBN_E3_SMILES = "Nc1cccc2c1C(=O)N(C3CCC(=O)NC3=O)C2=O" | |
| CDK6_WARHEAD_MOL = Chem.MolFromSmiles(CDK6_WARHEAD_SMILES) | |
| CRBN_E3_MOL = Chem.MolFromSmiles(CRBN_E3_SMILES) | |
| # SMARTS patterns | |
| _COOH_SMARTS = "[CX3](=[OX1])[OX2H1]" | |
| _NH2_SMARTS = "[NX3;H2]" | |
| _NH_SMARTS = "[NX3;H1;!$(NC=O);!$(NS=O);!$(NC#N)]" | |
| _AMIDE_SMARTS = "[NX3H0,NX3H1][CX3](=[OX1])[#6]" | |
| # Palbociclib core (loose β allows any N-substituent on the cyclopentyl-N) | |
| _CDK6_CORE_SMARTS = "[#6]C(=O)c1c([#6])c2cnc(*)nc2n([#6])c1=O" | |
| _THALIDOMIDE_SMARTS = "O=C1c2ccccc2C(=O)N1[C@@H]1CCC(=O)NC1=O" | |
| _TERMINAL_CHO_SMARTS = "[CX3H1](=[OX1])" | |
| class CDK6CRBN_PROTAC_Builder(PROTAC_Builder): | |
| """ | |
| PROTAC builder: palbociclib-piperazine CDK6 warhead + pomalidomide CRBN E3. | |
| Linker requirement: two COOH groups (diacid), one per end. | |
| Warhead piperazine-NH + linker COOH β tertiary amide. | |
| E3 aryl-NH2 + linker COOH β secondary amide. | |
| """ | |
| def __init__(self, warhead: Optional[Chem.Mol] = None, | |
| e3: Optional[Chem.Mol] = None): | |
| # Initialize patterns BEFORE super().__init__() β parent calls _verify_* | |
| self._cooh_pat = Chem.MolFromSmarts(_COOH_SMARTS) | |
| self._nh2_pat = Chem.MolFromSmarts(_NH2_SMARTS) | |
| self._nh_pat = Chem.MolFromSmarts(_NH_SMARTS) | |
| self._amide_pat = Chem.MolFromSmarts(_AMIDE_SMARTS) | |
| self._cdk6_core = Chem.MolFromSmarts(_CDK6_CORE_SMARTS) | |
| self._thalidomide = Chem.MolFromSmarts(_THALIDOMIDE_SMARTS) | |
| self._terminal_cho = Chem.MolFromSmarts(_TERMINAL_CHO_SMARTS) | |
| super().__init__( | |
| warhead=warhead if warhead is not None else CDK6_WARHEAD_MOL, | |
| e3=e3 if e3 is not None else CRBN_E3_MOL, | |
| warhead_name="palbociclib-piperazine (CDK6)", | |
| e3_name="pomalidomide (CRBN)", | |
| ) | |
| # Amide reactions | |
| # E3 aryl-NH2 + linker COOH β secondary aniline-amide | |
| self._amide_rxn_nh2 = AllChem.ReactionFromSmarts( | |
| "[NX3H2;$(Nc):1].[CX3:2](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:2](=[OX1:3])" | |
| ) | |
| # Warhead piperazine-NH + linker COOH β tertiary amide | |
| self._amide_rxn_nh = AllChem.ReactionFromSmarts( | |
| "[NX3H1;!$(NC=O);!$(NS=O):1].[CX3:2](=[OX1:3])[OX2H1]>>[NX3H0:1][CX3:2](=[OX1:3])" | |
| ) | |
| def _verify_warhead_reactive_group(self): | |
| if self.warhead_mol.HasSubstructMatch(self._nh_pat): | |
| print(f"β {self.warhead_name}: NH reactive group confirmed (piperazine)") | |
| else: | |
| print(f"β {self.warhead_name}: no reactive NH detected β check warhead") | |
| def _verify_e3_reactive_group(self): | |
| nh2_aryl = Chem.MolFromSmarts("[NX3;H2;$(Nc)]") | |
| if self.e3_mol.HasSubstructMatch(nh2_aryl): | |
| 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 β diacid (COOH at both ends) | |
| # ------------------------------------------------------------------ | |
| def _reactive_cooh_atoms(self, linker_mol: Chem.Mol) -> list: | |
| results = [] | |
| seen = set() | |
| # Direct COOH | |
| for m in linker_mol.GetSubstructMatches(self._cooh_pat): | |
| c_idx = m[0] | |
| if c_idx not in seen: | |
| seen.add(c_idx) | |
| results.append({"idx": c_idx, "restored": False}) | |
| # Terminal CHO β restore to COOH (post-assembly form) | |
| for m in linker_mol.GetSubstructMatches(self._terminal_cho): | |
| c_idx = m[0] | |
| if c_idx in seen: | |
| continue | |
| rw = Chem.RWMol(linker_mol) | |
| c_atom = rw.GetAtomWithIdx(c_idx) | |
| c_atom.SetNumExplicitHs(0) | |
| c_atom.SetNoImplicit(True) | |
| o_idx = rw.AddAtom(Chem.Atom("O")) | |
| rw.GetAtomWithIdx(o_idx).SetNumExplicitHs(1) | |
| rw.AddBond(c_idx, o_idx, Chem.BondType.SINGLE) | |
| rw.GetAtomWithIdx(c_idx).SetAtomMapNum(99) | |
| try: | |
| Chem.SanitizeMol(rw) | |
| except Exception: | |
| continue | |
| trial_rxn = AllChem.ReactionFromSmarts( | |
| "[NX3H2;$(Nc):1].[CX3:99](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:99](=[OX1:3])" | |
| ) | |
| if trial_rxn.RunReactants((self.e3_mol, rw.GetMol())): | |
| seen.add(c_idx) | |
| results.append({"idx": c_idx, "restored": True}) | |
| return results | |
| def find_linker_attachment_sites(self, linker_mol: Chem.Mol, | |
| amide_only: bool = False) -> list: | |
| cands = self._reactive_cooh_atoms(linker_mol) | |
| sites, seen = [], set() | |
| direct = [c for c in cands if not c["restored"]] | |
| restored = [c for c in cands if c["restored"]] | |
| for w in direct: | |
| for e in direct: | |
| 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": "cooh", | |
| "warhead_restored": False, | |
| "e3_site": e["idx"], "e3_type": "cooh", "e3_restored": False, | |
| }) | |
| for w in direct: | |
| for e in restored: | |
| 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": "cooh", | |
| "warhead_restored": False, | |
| "e3_site": e["idx"], "e3_type": "cooh", "e3_restored": True, | |
| }) | |
| for e in direct: | |
| key = (e["idx"], w["idx"]) | |
| if key not in seen and w["idx"] != e["idx"]: | |
| seen.add(key) | |
| sites.append({ | |
| "warhead_site": e["idx"], "warhead_type": "cooh", | |
| "warhead_restored": False, | |
| "e3_site": w["idx"], "e3_type": "cooh", "e3_restored": False, | |
| }) | |
| for w in restored: | |
| for e in restored: | |
| 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": "cooh", | |
| "warhead_restored": True, | |
| "e3_site": e["idx"], "e3_type": "cooh", "e3_restored": True, | |
| }) | |
| return sites | |
| # ------------------------------------------------------------------ | |
| # Assembly | |
| # ------------------------------------------------------------------ | |
| def _restore_cho_to_cooh(self, rw: Chem.RWMol, c_idx: int): | |
| c_atom = rw.GetAtomWithIdx(c_idx) | |
| c_atom.SetNumExplicitHs(0) | |
| c_atom.SetNoImplicit(True) | |
| o_idx = rw.AddAtom(Chem.Atom("O")) | |
| rw.GetAtomWithIdx(o_idx).SetNumExplicitHs(1) | |
| rw.AddBond(c_idx, o_idx, Chem.BondType.SINGLE) | |
| def _assemble_protac(self, linker_mol: Chem.Mol, warhead_site_idx: int, | |
| e3_site_idx: int, | |
| warhead_type: str = "cooh", e3_type: str = "cooh", | |
| warhead_restored: bool = False, | |
| e3_restored: bool = False) -> Optional[str]: | |
| rw = Chem.RWMol(linker_mol) | |
| if e3_restored: | |
| self._restore_cho_to_cooh(rw, e3_site_idx) | |
| if warhead_restored: | |
| self._restore_cho_to_cooh(rw, warhead_site_idx) | |
| try: | |
| Chem.SanitizeMol(rw) | |
| except Exception: | |
| return None | |
| rw.GetAtomWithIdx(e3_site_idx).SetAtomMapNum(99) | |
| tagged = rw.GetMol() | |
| # Step 1: E3 aryl-NH2 + linker COOH(tagged) β secondary aniline-amide | |
| step1_rxn = AllChem.ReactionFromSmarts( | |
| "[NX3H2;$(Nc):1].[CX3:99](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:99](=[OX1:3])" | |
| ) | |
| products1 = step1_rxn.RunReactants((self.e3_mol, tagged)) | |
| 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 | |
| rw2 = Chem.RWMol(intermediate) | |
| for atom in rw2.GetAtoms(): | |
| if atom.GetAtomMapNum() == 99: | |
| atom.SetAtomMapNum(0) | |
| intermediate = rw2.GetMol() | |
| # Step 2: warhead piperazine-NH + remaining COOH β tertiary amide | |
| products2 = self._amide_rxn_nh.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 has_warhead_core(self, protac_mol: Chem.Mol) -> bool: | |
| return protac_mol.HasSubstructMatch(self._cdk6_core) | |
| def has_e3_core(self, protac_mol: Chem.Mol) -> bool: | |
| glut = Chem.MolFromSmarts("O=C1NC(=O)CCC1") | |
| return protac_mol.HasSubstructMatch(glut) | |
| def has_both_amide_bonds(self, protac_mol: Chem.Mol) -> bool: | |
| return len(protac_mol.GetSubstructMatches(self._amide_pat)) >= 2 | |
| def no_unreacted_cooh(self, protac_mol: Chem.Mol) -> bool: | |
| return len(protac_mol.GetSubstructMatches(self._cooh_pat)) == 0 | |
| 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 _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 = CDK6_WARHEAD_MOL | |
| E3_MOL = CRBN_E3_MOL | |
| WARHEAD_SMILES = CDK6_WARHEAD_SMILES | |
| E3_SMILES = CRBN_E3_SMILES | |
| Builder = CDK6CRBN_PROTAC_Builder | |
| WH_REACTIVE_SMARTS = _NH_SMARTS | |
| E3_REACTIVE_SMARTS = "[NX3;H2;$(Nc)]" | |
| WARHEAD_PAT = Chem.MolFromSmarts(_CDK6_CORE_SMARTS) | |
| E3_PAT = Chem.MolFromSmarts("O=C1NC(=O)CCC1") | |
| # --------------------------------------------------------------------------- | |
| # Self-test | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| from rdkit import RDLogger | |
| RDLogger.DisableLog("rdApp.error") | |
| print("=" * 70) | |
| print("CDK6-CRBN PROTAC Builder β Self-Test") | |
| print(f"Warhead : CDK6 (palbociclib-piperazine-NH, NH reactive)") | |
| print(f"E3 : CRBN (pomalidomide aryl-NH2, NH2 reactive)") | |
| print(f"Chemistry: diacid linker + 2Γ amide bond formation") | |
| print("=" * 70) | |
| builder = CDK6CRBN_PROTAC_Builder() | |
| test_linkers = [ | |
| ("OC(=O)CCCCCCCCC(=O)O", "sebacic acid (C10 diacid)"), | |
| ("OC(=O)CCOCCOCCOC(=O)O", "PEG-diacid"), | |
| ("NCCN", "diamine β should fail"), | |
| ] | |
| print() | |
| for smiles, desc in test_linkers: | |
| linker_mol = Chem.MolFromSmiles(smiles) | |
| cooh_count = len(linker_mol.GetSubstructMatches(Chem.MolFromSmarts(_COOH_SMARTS))) | |
| print(f"Linker : {desc}") | |
| print(f" COOH sites: {cooh_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) | |