"""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.") # ---------------------------------------------------------------------- # Curated example library # ---------------------------------------------------------------------- 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"], ] # ---------------------------------------------------------------------- # Backend client # ---------------------------------------------------------------------- _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 # Headline card 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", "?") # Format Ki nicely with appropriate units 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')} """ # Per-mode table 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 # ---------------------------------------------------------------------- # UI — uses theme-aware colors so text stays readable on any background # ---------------------------------------------------------------------- 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: # ----- header ----- gr.Markdown( """