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