""" KRAS-VHL PROTAC Builder Target: KRAS (aminocyanothiophene-tetrahydroquinoline-isoxazole-pyrimidine warhead, ACBI3 family) E3 Ligase: VHL (VH032 hydroxyproline/thiazolyl-benzyl binder) Reference: ACBI3 (Astex/Boehringer Ingelheim); DegradMaster case_study_2 Warhead (ACBI3-derived KRAS binder, MW ≈ 478 Da): Aminocyanothiophene fused to a tetrahydroquinoline; the thiophene-fused ring is decorated with an isoxazole–pyrimidine. The pyrimidine bears a 2-methyl-1,4-diazepane whose distal N-H is the linker attachment point (analogous to the gefitinib piperazine NH in EGFR-VHL). E3 Ligand (VH032, MW ≈ 431 Da): Hydroxyproline + tert-leucine + 4-(4-methylthiazol-5-yl)benzyl cap. Free NH2 on tert-leucine alpha-amine is the reactive group. NOTE on chemistry: ACBI3 itself uses a click-derived 1,2,3-triazole linker joining the warhead alkyl chain to a VHL-side azide-bearing fragment. For generation/scoring purposes we mirror EGFR-VHL chemistry (diacid linker + 2× amide bond formation), since the diazepane NH and VH032 NH2 are both direct amide-coupling sites and this preserves a single linker chemistry across all three VHL targets in the study. """ 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 — derived from ACBI3 (DegradMaster case_study_2, c0 reference) # --------------------------------------------------------------------------- # ACBI3-derived KRAS warhead with diazepane-NH (linker stripped) KRAS_WARHEAD_SMILES = ( "C[C@H]1CNCCCN1c1nccc(-c2noc([C@@]3(C)CCCc4sc(N)c(C#N)c43)n2)n1" ) # VH032: same VHL ligand used by EGFR-VHL and BRD9-VHL modules VHL_E3_SMILES = "N[C@H](C(=O)N1C[C@H](O)C[C@H]1C(=O)NCc1ccc(-c2scnc2C)cc1)C(C)(C)C" # Pre-built Mol objects KRAS_WARHEAD_MOL = Chem.MolFromSmiles(KRAS_WARHEAD_SMILES) VHL_E3_MOL = Chem.MolFromSmiles(VHL_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]" # Isoxazole–pyrimidine biaryl (signature of the ACBI3 KRAS warhead) _ISOXAZOLE_PYRIMIDINE_SMARTS = "c1onc(n1)-c1ccnc(n1)" _HYDROXYPROLINE_SMARTS = "[C@@H]1C[C@H](O)CN1" _TERMINAL_CHO_SMARTS = "[CX3H1](=[OX1])" _TERMINAL_CH3_SMARTS = "[CX4H3]" class KRASVHL_PROTAC_Builder(PROTAC_Builder): """ PROTAC builder: ACBI3-derived KRAS warhead + VH032 VHL E3. Linker requirement: two COOH groups (diacid), one per end. Warhead NH (diazepane) + linker COOH → tertiary amide. E3 NH2 (tert-leucine) + linker COOH → secondary amide. """ def __init__(self, warhead: Optional[Chem.Mol] = None, e3: Optional[Chem.Mol] = None): super().__init__( warhead=warhead if warhead is not None else KRAS_WARHEAD_MOL, e3=e3 if e3 is not None else VHL_E3_MOL, warhead_name="ACBI3-diazepane (KRAS)", e3_name="VHL binder NH2 (VH032)", ) 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._iso_pyrim = Chem.MolFromSmarts(_ISOXAZOLE_PYRIMIDINE_SMARTS) self._hyp = Chem.MolFromSmarts(_HYDROXYPROLINE_SMARTS) self._terminal_cho = Chem.MolFromSmarts(_TERMINAL_CHO_SMARTS) self._terminal_ch3 = Chem.MolFromSmarts(_TERMINAL_CH3_SMARTS) # --- Amide bond reactions (diacid linker path) --- self._amide_rxn_nh2 = AllChem.ReactionFromSmarts( "[NX3H2:1].[CX3:2](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:2](=[OX1:3])" ) self._amide_rxn_nh = AllChem.ReactionFromSmarts( "[NX3H1;!$(NC=O);!$(NS=O):1].[CX3:2](=[OX1:3])[OX2H1]>>[NX3H0:1][CX3:2](=[OX1:3])" ) # --- C-N alkylation reactions (alkyl/PEG linker path) --- self._cn_rxn_nh2 = AllChem.ReactionFromSmarts( "[NX3H2:1].[CX4H3:2]>>[NX3H1:1][CX4H2:2]" ) self._cn_rxn_nh = AllChem.ReactionFromSmarts( "[NX3H1;!$(NC=O);!$(NS=O):1].[CX4H3:2]>>[NX3H0:1][CX4H2:2]" ) # ------------------------------------------------------------------ # Verification # ------------------------------------------------------------------ def _verify_warhead_reactive_group(self): pat = Chem.MolFromSmarts(_NH_SMARTS) if self.warhead_mol.HasSubstructMatch(pat): print(f"✓ {self.warhead_name}: NH reactive group confirmed (diazepane)") else: print(f"⚠ {self.warhead_name}: no reactive NH detected — check warhead") def _verify_e3_reactive_group(self): pat = Chem.MolFromSmarts(_NH2_SMARTS) if self.e3_mol.HasSubstructMatch(pat): print(f"✓ {self.e3_name}: NH2 reactive group confirmed (tert-leucine)") else: print(f"⚠ {self.e3_name}: no reactive NH2 detected — check E3") # ------------------------------------------------------------------ # Linker attachment site detection # ------------------------------------------------------------------ def _reactive_cooh_atoms(self, linker_mol: Chem.Mol) -> list: results = [] seen = set() 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}) 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 step1_rxn = AllChem.ReactionFromSmarts( "[NX3H2:1].[CX3:99](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:99](=[OX1:3])" ) if step1_rxn.RunReactants((self.e3_mol, rw.GetMol())): seen.add(c_idx) results.append({"idx": c_idx, "restored": True}) return results def _reactive_terminal_c_atoms(self, linker_mol: Chem.Mol) -> list: 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: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: amide_cands = self._reactive_cooh_atoms(linker_mol) alkyl_cands = [] if amide_only else self._reactive_terminal_c_atoms(linker_mol) sites = [] seen = set() def _add(w, e, wtype, etype): 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": wtype, "warhead_restored": w.get("restored", False), "e3_site": e["idx"], "e3_type": etype, "e3_restored": e.get("restored", False), }) direct = [c for c in amide_cands if not c["restored"]] restored = [c for c in amide_cands if c["restored"]] for w in direct: for e in direct: _add(w, e, "cooh", "cooh") for w in direct: for e in restored: _add(w, e, "cooh", "cooh") for e in direct: _add(e, w, "cooh", "cooh") for w in restored: for e in restored: _add(w, e, "cooh", "cooh") if not amide_only: for w in alkyl_cands: for e in alkyl_cands: _add(w, e, "alkyl", "alkyl") for w in amide_cands: for e in alkyl_cands: _add(w, e, "cooh", "alkyl") _add(e, w, "alkyl", "cooh") 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_type == "cooh" and e3_restored: self._restore_cho_to_cooh(rw, e3_site_idx) if warhead_type == "cooh" and 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_linker = rw.GetMol() if e3_type == "cooh": step1_rxn = AllChem.ReactionFromSmarts( "[NX3H2:1].[CX3:99](=[OX1:3])[OX2H1]>>[NX3H1:1][CX3:99](=[OX1:3])" ) else: step1_rxn = AllChem.ReactionFromSmarts( "[NX3H2: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: mol = prod_tuple[0] Chem.SanitizeMol(mol) intermediate = mol 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() if warhead_type == "cooh": step2_rxn = self._amide_rxn_nh else: step2_rxn = self._cn_rxn_nh products2 = step2_rxn.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: has_amide = len(protac_mol.GetSubstructMatches(self._amide_pat)) >= 2 no_cooh = len(protac_mol.GetSubstructMatches(self._cooh_pat)) == 0 has_cores = self.has_warhead_core(protac_mol) and self.has_e3_core(protac_mol) return (has_amide and no_cooh) or has_cores def has_warhead_core(self, protac_mol: Chem.Mol) -> bool: return protac_mol.HasSubstructMatch(self._iso_pyrim) def has_e3_core(self, protac_mol: Chem.Mol) -> bool: return protac_mol.HasSubstructMatch(self._hyp) 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 _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 (used by defog_guided.py / batch_report.py) # --------------------------------------------------------------------------- WARHEAD_MOL = KRAS_WARHEAD_MOL E3_MOL = VHL_E3_MOL WARHEAD_SMILES = KRAS_WARHEAD_SMILES E3_SMILES = VHL_E3_SMILES Builder = KRASVHL_PROTAC_Builder WH_REACTIVE_SMARTS = _NH_SMARTS E3_REACTIVE_SMARTS = _NH2_SMARTS WARHEAD_PAT = Chem.MolFromSmarts(_ISOXAZOLE_PYRIMIDINE_SMARTS) # KRAS warhead signature E3_PAT = Chem.MolFromSmarts("N1CC(O)CC1") # hydroxyproline (VHL) # --------------------------------------------------------------------------- # Self-test # --------------------------------------------------------------------------- if __name__ == "__main__": from rdkit import RDLogger RDLogger.DisableLog("rdApp.error") print("=" * 70) print("KRAS-VHL PROTAC Builder — Self-Test") print(f"Warhead : KRAS (ACBI3-diazepane, NH reactive)") print(f"E3 : VHL (VH032, NH2 reactive)") print(f"Chemistry: diacid linker + 2× amide bond formation") print("=" * 70) builder = KRASVHL_PROTAC_Builder() test_linkers = [ ("OC(=O)CCCCCCCCC(=O)O", "Sebacic acid (C10 diacid)"), ("OC(=O)CCOCCOCCOC(=O)O", "PEG-diacid (valid)"), ("NCCOCCOCCN", "Diamine — no COOH, should fail"), ("OC(=O)CCCC", "Single COOH — should fail"), ] 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 cooh_count = len(linker_mol.GetSubstructMatches(Chem.MolFromSmarts(_COOH_SMARTS))) 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']} " f"HeavyAtoms: {props['heavy_atoms']}") except Exception as e: print(f"Assembly: FAILED — {e}") print("-" * 70)