| """SugarKi public UI — sugar-chemistry Ki predictor (frontend, MWBC/sugarki). |
| |
| Pretty Gradio interface that calls the SugarKi backend Space (private, |
| ZeroGPU). No model weights here — just the UI. |
| """ |
| from __future__ import annotations |
|
|
| import os |
| from gradio_client import Client |
| import gradio as gr |
|
|
| BACKEND = os.environ.get("SUGARKI_BACKEND", "Umesh1608/sugarki-backend") |
| HF_TOKEN = os.environ.get("HF_TOKEN") |
|
|
| if not HF_TOKEN: |
| print("WARNING: HF_TOKEN not set; backend calls will fail if backend is private.") |
|
|
| |
| |
| |
|
|
| SUGAR_INHIBITORS = { |
| "D-mannitol": "OCC(O)C(O)C(O)C(O)CO", |
| "D-fructose": "OCC(=O)C(O)C(O)C(O)CO", |
| "D-sorbitol": "OCC(O)C(O)C(O)C(O)CO", |
| "D-glucose": "OCC(O)C(O)C(O)C(O)C=O", |
| "D-galactose": "OCC(O)C(O)C(O)C(O)C=O", |
| "D-mannose": "OCC(O)C(O)C(O)C(O)C=O", |
| "Xylitol": "OCC(O)C(O)C(O)CO", |
| "Glycerol": "OCC(O)CO", |
| "Glucose-6-phosphate": "OC(=O)CC(O)C(O)C(O)COP(O)(O)=O", |
| "Fructose-1,6-bisphos.": "O=P(O)(O)OCC(O)(C(O)C(O)COP(O)(O)=O)O", |
| "Sucrose": "OCC1OC(OC2(COC1O)OC(CO)C(O)C2O)C(O)C(O)C1O", |
| "Trehalose": "OCC1OC(OC2OC(CO)C(O)C(O)C2O)C(O)C(O)C1O", |
| "Custom (paste SMILES)": "", |
| } |
|
|
| EXAMPLE_MDH_WT = ( |
| "MGSSHHHHHHSSGLVPRGSHMVKLTLSALPALSPAVAVPAYDPRAQIPGIVHFGVGAFHRSHQAMYLDRL" |
| "LNSGRGAGWAICGVGVLPQDARMRDVLAEQDHLYTLVTRSPDGQAQARVIGAIVEFLFAPDDPERVLERL" |
| "ADPTTRIVSLTVTEGGYSVSNATGEFDPTPPDIAHDLTPGAVPRTFFGFLTEGLRRRRERGLPPFTVVSC" |
| "DNMPGNGEVTRRALTAFARLQDPELGDWIAHNVAFPNSMVDRITPATTEQDRQDIAAAYGIEDAWPVVAE" |
| "SFAQWVLEDRFTQGRPALETVGVQVVSDVEPYELMKLRLLNASHQALAYLGLLAGYRFVHEVCQDPLFAR" |
| "FLLDYMTQEATPTLRPVPGIDLGAYRRELIARFSNPAIRDPLTRLTVDSSERIPKFLLPVIRDQLARGGE" |
| "LARCALVIASWRAYLATVLEEGSASFPDQHAQALAEAVRRDAQQPGAFLDLEAVFGELGRNARFRTAYLS" |
| "AWESLRRQGPLGAMRALMGEESSPSNVTSLSGR" |
| ) |
|
|
| EXAMPLES = [ |
| [EXAMPLE_MDH_WT, "D-mannitol", "substrate", "OCC(=O)C(O)C(O)C(O)CO"], |
| ] |
|
|
| |
| |
| |
|
|
| _client = None |
| def _backend(): |
| global _client |
| if _client is None: |
| kwargs = {} |
| if HF_TOKEN: |
| kwargs["hf_token"] = HF_TOKEN |
| _client = Client(BACKEND, **kwargs) |
| return _client |
|
|
|
|
| def predict(sequence, inhibitor_choice, inhibitor_custom, inh_type, substrate_smiles): |
| if not sequence or not sequence.strip(): |
| return "### ⚠️ Please paste an enzyme sequence", None, "", None |
|
|
| smiles = ( |
| inhibitor_custom.strip() |
| if inhibitor_choice == "Custom (paste SMILES)" |
| else SUGAR_INHIBITORS.get(inhibitor_choice, "") |
| ) |
| if not smiles: |
| return "### ⚠️ Inhibitor SMILES is empty", None, "", None |
|
|
| try: |
| result = _backend().predict( |
| sequence.strip(), smiles, inh_type, |
| substrate_smiles.strip() if substrate_smiles else "", |
| api_name="/predict_ki", |
| ) |
| except Exception as e: |
| return f"### ❌ Backend error\n```\n{e}\n```", None, "", None |
|
|
| if isinstance(result, dict) and result.get("error"): |
| msg = result.get("error_message", str(result)) |
| return f"### ❌ Prediction error\n```\n{msg}\n```", None, "", result |
|
|
| |
| ki_mm = result.get("Ki_mM") |
| ki_um = result.get("Ki_uM") |
| log_ki = result.get("log10_Ki_mM") |
| inh_used = result.get("inh_type_used", "?") |
| his_stripped = "✓ stripped" if result.get("his_tag_stripped") else "—" |
| sigma = result.get("ensemble_std_log10", "?") |
|
|
| |
| if ki_mm is not None: |
| if ki_mm < 0.001: |
| ki_display = f"**{ki_mm * 1e6:.2f} nM**" |
| elif ki_mm < 1: |
| ki_display = f"**{ki_um:.1f} µM**" |
| else: |
| ki_display = f"**{ki_mm:.3f} mM**" |
| else: |
| ki_display = "—" |
|
|
| summary_md = f""" |
| ### 🧪 Predicted Ki: {ki_display} |
| |
| | | | |
| |---|---| |
| | **Ki (mM)** | {ki_mm:.4f} | |
| | **Ki (µM)** | {ki_um:,.1f} | |
| | **log₁₀(Ki / mM)** | {log_ki:+.3f} | |
| | **Inhibition mode** | `{inh_used}` | |
| | **His-tag prefix** | {his_stripped} | |
| | **Ensemble σ across 5 modes** | {sigma} | |
| |
| > **Model:** {result.get('model_version', 'SugarKi')} |
| """ |
|
|
| |
| mode_mm = result.get("mode_predictions_mM", {}) |
| mode_log = result.get("mode_predictions_log10", {}) |
|
|
| mode_rows = [] |
| for mode in ["external", "product", "substrate", "product_or_substrate", "unknown"]: |
| if mode in mode_mm: |
| mm = mode_mm[mode] |
| um = mm * 1000 |
| log = mode_log.get(mode, 0) |
| highlight = " ⭐" if mode == inh_used else "" |
| mode_rows.append([ |
| f"{mode.replace('_', ' ').title()}{highlight}", |
| f"{log:+.3f}", |
| f"{mm:.3f}", |
| f"{um:,.1f}", |
| ]) |
|
|
| notes = result.get("notes", "") |
| notes_md = f"\n\n**ℹ️ Notes:** _{notes}_\n" if notes else "" |
|
|
| return summary_md, mode_rows, notes_md, result |
|
|
|
|
| |
| |
| |
|
|
| CSS = """ |
| /* Force a consistent DARK theme regardless of system preference. |
| Dark backgrounds with light text — both set together so contrast is guaranteed. */ |
| |
| html, body, .gradio-container, .gradio-container .main { |
| background: #0b1220 !important; |
| color: #f3f4f6 !important; |
| } |
| |
| /* All Gradio block-level containers — force dark backgrounds */ |
| .gradio-container .block, |
| .gradio-container .form, |
| .gradio-container .panel, |
| .gradio-container .gr-box, |
| .gradio-container .gr-block, |
| .gradio-container .gr-form, |
| .gradio-container .gr-panel, |
| .gradio-container fieldset, |
| .gradio-container details, |
| .gradio-container summary, |
| .gradio-container .label-wrap, |
| .gradio-container .wrap, |
| .gradio-container .accordion, |
| .gradio-container .gr-accordion, |
| .gradio-container .prose, |
| .gradio-container .markdown, |
| .gradio-container .markdown-body { |
| background-color: #111827 !important; |
| color: #f3f4f6 !important; |
| border-color: #374151 !important; |
| } |
| |
| /* Inputs — slightly lighter slate so they stand out from the page */ |
| .gradio-container input, |
| .gradio-container textarea, |
| .gradio-container select, |
| .gradio-container .gr-input, |
| .gradio-container .gr-textarea { |
| background-color: #1f2937 !important; |
| color: #f9fafb !important; |
| border-color: #374151 !important; |
| } |
| .gradio-container input::placeholder, |
| .gradio-container textarea::placeholder { |
| color: #9ca3af !important; |
| } |
| |
| /* All text */ |
| .gradio-container, .gradio-container * { |
| color: #f3f4f6 !important; |
| } |
| .gradio-container h1, |
| .gradio-container h2, |
| .gradio-container h3, |
| .gradio-container h4, |
| .gradio-container h5, |
| .gradio-container h6, |
| .gradio-container strong, |
| .gradio-container b { |
| color: #ffffff !important; |
| } |
| |
| /* Tables */ |
| .gradio-container table { |
| background-color: #111827 !important; |
| border-color: #374151 !important; |
| } |
| .gradio-container table th, |
| .gradio-container table td { |
| color: #e5e7eb !important; |
| background-color: #111827 !important; |
| border-color: #374151 !important; |
| } |
| .gradio-container table th { |
| background-color: #1f2937 !important; |
| color: #ffffff !important; |
| } |
| |
| /* Links and inline code — bright accent colors that pop on dark */ |
| .gradio-container a { color: #93c5fd !important; } |
| .gradio-container code { |
| background-color: #1f2937 !important; |
| color: #fbcfe8 !important; |
| } |
| |
| /* Primary buttons — keep vivid blue with white text */ |
| .gradio-container button.primary, |
| .gradio-container button.lg.primary, |
| .gradio-container .primary, |
| .gradio-container .primary * { |
| background: #2563eb !important; |
| color: #ffffff !important; |
| border-color: #1d4ed8 !important; |
| } |
| |
| /* Title block layout */ |
| .title-block { text-align: center; padding: 1em 0; margin-bottom: 0.5em; } |
| |
| /* Ki result card */ |
| .ki-card { |
| background: #111827 !important; |
| border: 1px solid #374151 !important; |
| border-radius: 8px !important; |
| padding: 1em 1.5em !important; |
| } |
| """ |
|
|
| with gr.Blocks(title="SugarKi — Ki prediction", theme=gr.themes.Default(), css=CSS) as demo: |
|
|
| |
| gr.Markdown( |
| """ |
| <div align='center'> |
| |
| # 🧪 SugarKi |
| |
| ##### Family-specialized Ki prediction for sugar-chemistry enzymes |
| ##### polyol DHs · sugar kinases · glycosidases · phosphatases · aldolases · isomerases · phosphomutases |
| |
| </div> |
| """ |
| ) |
|
|
| |
| with gr.Accordion("📐 How does SugarKi work?", open=True): |
| gr.Markdown( |
| """ |
| SugarKi takes an **enzyme sequence** and an **inhibitor SMILES** and predicts the |
| inhibition constant **Ki** specifically for sugar-chemistry enzymes — where current |
| SOTA models like CatPred fail catastrophically (R² = −0.95 on monosaccharide |
| inhibitors). |
| |
| **Inputs** |
| |
| - `Enzyme sequence` — paste any sugar-family enzyme (His-tag auto-stripped). Best for EC 1.1.1.x, 2.7.1.x, 3.1.3.x, 3.2.1.x, 4.1.2.x, 5.3.1.x, 5.4.2.x. |
| - `Inhibitor SMILES` — SMILES of the small molecule whose Ki you want. |
| - `Inhibition type` — choose `product` for product inhibition (e.g., mannitol on mannitol DH), `substrate` for substrate-mode at high [S], or `auto` for an ensemble across all 5 modes. |
| - `Substrate SMILES` (optional) — native substrate, helps disambiguate product-inhibition cases. |
| |
| ### Validation |
| |
| - WT MDH-006 (mannitol DH) + D-mannitol → predicted **11.78 mM** in substrate mode vs literature **12 mM** (1.8% error). |
| - Sugar-Ki test set R² = **0.702** vs CatPred 0.243 / SELFprot 0.623. |
| - Typical MAE ≈ 0.6 log units; relative ranking is more reliable than absolute Ki values. |
| """ |
| ) |
|
|
| |
| with gr.Row(): |
| with gr.Column(scale=3): |
| seq_in = gr.Textbox( |
| label="Enzyme amino acid sequence", |
| placeholder="MGSSHHHHHH... or just the catalytic domain (30–1000 aa)", |
| lines=10, |
| value=EXAMPLE_MDH_WT, |
| show_copy_button=True, |
| ) |
| with gr.Row(): |
| inh_choice = gr.Dropdown( |
| choices=list(SUGAR_INHIBITORS.keys()), |
| value="D-mannitol", |
| label="Inhibitor", |
| ) |
| inh_type = gr.Dropdown( |
| ["auto", "external", "product", "substrate", |
| "product_or_substrate", "unknown"], |
| value="auto", |
| label="Inhibition type", |
| info="`product` for product inhibition; `substrate` for substrate inhibition at high [S]", |
| ) |
| inh_custom = gr.Textbox( |
| label="Custom inhibitor SMILES (only used if inhibitor = 'Custom (paste SMILES)')", |
| value="", |
| ) |
| sub_smiles = gr.Textbox( |
| label="Native substrate SMILES (optional, helps product-inhibition prediction)", |
| value="", |
| ) |
| btn = gr.Button("🧬 Predict Ki", variant="primary", size="lg") |
|
|
| with gr.Column(scale=2): |
| summary_out = gr.Markdown(elem_classes=["ki-card"]) |
| gr.Markdown( |
| """ |
| **What each inhibition mode means** |
| |
| - **External** — small-molecule inhibitor unrelated to substrate or product (classical competitive/non-competitive ligand). |
| - **Product** — the inhibitor IS the reaction product (product inhibition; e.g., mannitol on mannitol dehydrogenase). |
| - **Substrate** — the inhibitor IS the native substrate; binding becomes inhibitory at high [S] (substrate inhibition). |
| - **Product or Substrate** — chemically ambiguous: the molecule could play either role for this enzyme. |
| - **Unknown** — mode not annotated; treat as a generic ensemble estimate. |
| |
| The ⭐ row is the mode you selected (or the auto-resolved default). |
| """ |
| ) |
| mode_table = gr.Dataframe( |
| headers=["Inhibition mode", "log₁₀(Ki/mM)", "Ki (mM)", "Ki (µM)"], |
| label="Per-mode predictions (across 5 inhibition assumptions)", |
| wrap=True, |
| interactive=False, |
| ) |
| notes_out = gr.Markdown() |
| with gr.Accordion("Raw JSON response", open=False): |
| raw_out = gr.JSON() |
|
|
| btn.click( |
| predict, |
| inputs=[seq_in, inh_choice, inh_custom, inh_type, sub_smiles], |
| outputs=[summary_out, mode_table, notes_out, raw_out], |
| ) |
|
|
| gr.Examples( |
| examples=EXAMPLES, |
| inputs=[seq_in, inh_choice, inh_type, sub_smiles], |
| label="Example: MDH-006 WT + D-mannitol (substrate-mode)", |
| ) |
|
|
| |
| gr.Markdown( |
| """ |
| --- |
| |
| ### How SugarKi compares to existing Ki predictors |
| |
| | Model | Sugar-chemistry Ki | General Ki | |
| |---|---|---| |
| | CatPred zero-shot | R² = 0.243 (catastrophic on monosaccharides/polyols) | R² = 0.578 | |
| | SELFprot zero-shot | R² = 0.623 | R² = 0.314 | |
| | **SugarKi specialist** | **R² = 0.702** | (router falls back to CatPred) | |
| |
| ### Limitations |
| |
| - Test MAE ~0.6 log units; **rankings** more reliable than **absolute** values. |
| - Best on EC families 1.1.1.x / 2.7.1.x / 3.2.1.x / 3.1.3.x / 4.1.2.x / 5.3.1.x / 5.4.2.x. |
| - Allosteric inhibition not separately modeled. |
| - First call cold-starts ~30 s while ESMFold loads; subsequent calls re-use cached structures. |
| |
| ### Citation |
| |
| Paper in preparation. Hosted by [MWBC](https://huggingface.co/MWBC), backed by |
| a private SugarKi inference Space ([Umesh1608](https://huggingface.co/Umesh1608)). |
| """ |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|