Spaces:
Running on Zero
Running on Zero
File size: 29,967 Bytes
9d20977 d728005 b90da4a 9d20977 62a01bd e4ecaa0 62a01bd 9d20977 b90da4a e4ecaa0 9d20977 62a01bd e4ecaa0 9d20977 62a01bd 9d20977 e15df09 9d20977 e15df09 9d20977 e15df09 9d20977 e15df09 9d20977 e15df09 9d20977 e15df09 9d20977 da6eaac 9d20977 62a01bd 9d20977 e15df09 9d20977 e15df09 9d20977 e15df09 9d20977 e15df09 9d20977 62a01bd 9d20977 62a01bd 9d20977 da6eaac 9d20977 e15df09 9d20977 e15df09 9d20977 62a01bd 9d20977 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 | """
LLPSense Gradio Demo
Condition-dependent protein LLPS prediction using ProtT5 + XGBoost
Pipeline:
(1) User inputs amino-acid sequence
(2) Click "Extract Feature" โ mean-pool ProtT5-XL embedding (1024-dim)
(3) Tab 1 โ Predict LLPS Probability: adjust temp / conc / pH sliders โ predict
(4) Tab 2 โ Condition Screening: pick one condition to vary, fix the rest โ plot
"""
import sys
import warnings
from pathlib import Path
from copy import deepcopy
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import joblib
import gradio as gr
import spaces
from huggingface_hub import snapshot_download
from examples import EXAMPLES, feature_path, find_example_by_seq
# Only surface examples that actually have a pre-computed T5 feature cached
# in assets/ โ an entry added via preprocess/add_example.py but not yet run
# through preprocess/extract_example_feat.py would otherwise show up in the
# picker and silently fall back to a slow/GPU extraction on first click.
AVAILABLE_EXAMPLES = [example for example in EXAMPLES if feature_path(example["id"]).exists()]
from t5_utils import (
T5_REPO_ID,
extract_t5_feature as _extract_t5_feature_core,
preload_t5_cpu,
read_feature_h5,
)
warnings.filterwarnings("ignore")
# โโ Paths โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
BASE_DIR = Path(__file__).parent
# โโ ProtT5 prefetch โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# This SDK-gradio Space builds from requirements.txt, not the repo's Dockerfile,
# so there is no build-time prefetch step. Download the weights here instead,
# at module import (app startup, plain CPU context) โ before any @spaces.GPU
# call โ so extract_t5_feature() never blocks on a network download while
# holding a ZeroGPU allocation (which has a short time budget).
snapshot_download(T5_REPO_ID)
# Also deserialize the weights into CPU memory here, still outside any
# @spaces.GPU context. Without this, T5EncoderModel.from_pretrained() (loading
# ~2.9GB from disk) would run lazily inside the first GPU-decorated call and
# could eat enough of the ZeroGPU time budget to abort the task. With this,
# the first GPU call only needs a fast .to("cuda") transfer.
preload_t5_cpu()
# โโ Physical constants (from preprocess/misc.py) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
MAX_TEMP = 60.0
MAX_CONC = 1000.0
MAX_PH = 14.0
MAX_MGCL2 = 50.0
MAX_NACL = 2000.0
MAX_KCL = 1000.0
MAX_CAGENT = 50.0
MAX_GLYC = 10.0
# โโ Screening ranges โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
SCREEN_RANGES = {
"Temperature": np.arange(0.0, 61.0, 1.0), # 0โ60 ยฐC
"Concentration": np.arange(0.0, 1010.0, 10.0), # 0โ1000 ยตM
"pH": np.arange(4.0, 12.1, 0.1), # 4.0โ12.0
}
SCREEN_KEYS = {
"Temperature": "temp",
"Concentration": "conc",
"pH": "pH",
}
SCREEN_LABELS = {
"Temperature": "Temperature (ยฐC)",
"Concentration": "Concentration (ยตM)",
"pH": "pH",
}
VALID_AA = set("ACDEFGHIKLMNPQRSTVWY")
ALLOW_AA = VALID_AA | set("XBJOUZ")
# โโ Lazy-loaded singletons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
_llps_model = None
def load_llps_model():
global _llps_model
if _llps_model is None:
model_path = BASE_DIR / "models" / "LLPSense.pkl"
if not model_path.exists():
raise FileNotFoundError(
f"Model file not found: {model_path}\n"
"Please place 'LLPSense.pkl' inside the 'models/' directory."
)
d = joblib.load(model_path)
mdl = d["model"]
# Force XGBoost to run on CPU to avoid device-mismatch warnings
mdl.set_params(device="cpu")
_llps_model = mdl
return _llps_model
# โโ Feature extraction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# The actual tokenize/forward/mean-pool logic lives in t5_utils.py, shared
# with preprocess/extract_example_feat.py so the offline-cached example
# features in assets/ always match what this would compute live. Only the
# @spaces.GPU wrapping (ZeroGPU allocation) is app-specific, and cb_extract()
# below skips calling this entirely when a cached feature is available.
@spaces.GPU(duration=180)
def extract_t5_feature(sequence: str) -> np.ndarray:
return _extract_t5_feature_core(sequence)
# โโ Condition vector builder โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_cond(temp, conc, pH,
nacl=160.0, mgcl2=0.0, kcl=0.0, glyc=0.0,
peg1=0.0, peg2=0.0, peg3=0.0,
ficoll=0.0, dext40=0.0, dext70=0.0) -> np.ndarray:
"""
Normalise environmental parameters and return a 13-dim condition vector.
Order: [temp, conc, pH, PEG300-1k, PEG3k-6k, PEG8k-20k,
Ficoll, Dextranโค40, Dextranโฅ70, MgCl2, NaCl, KCl, Glycerol]
"""
return np.array([
temp / MAX_TEMP,
conc / MAX_CONC,
pH / MAX_PH,
peg1 / MAX_CAGENT,
peg2 / MAX_CAGENT,
peg3 / MAX_CAGENT,
ficoll / MAX_CAGENT,
dext40 / MAX_CAGENT,
dext70 / MAX_CAGENT,
mgcl2 / MAX_MGCL2,
nacl / MAX_NACL,
kcl / MAX_KCL,
glyc / MAX_GLYC,
], dtype=np.float32)
def model_predict(feat: np.ndarray, cond: np.ndarray) -> float:
model = load_llps_model()
x = np.concatenate([feat, cond]).reshape(1, -1)
return float(model.predict_proba(x)[0, 1])
# โโ Smoothing (same as LLPSXG.py's moving_average) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def moving_average(y: np.ndarray, window_size: int) -> np.ndarray:
if window_size % 2 == 0:
raise ValueError("Window size should be odd to ensure symmetry.")
window = np.ones(int(window_size)) / float(window_size)
y_padded = np.pad(y, (window_size // 2, window_size // 2), mode="edge")
return np.convolve(y_padded, window, "valid")
# โโ Matplotlib helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def prob_gauge_figure(prob: float) -> plt.Figure:
"""Horizontal probability bar gauge."""
LLPS_COLOR = "#e74c3c"
NON_COLOR = "#2980b9"
color = LLPS_COLOR if prob >= 0.5 else NON_COLOR
label = "Phase Separating" if prob >= 0.5 else "Non-Phase Separating"
fig, ax = plt.subplots(figsize=(7, 2.8))
ax.barh([0], [prob], height=0.55, color=color, alpha=0.88, zorder=3)
ax.barh([0], [1 - prob], height=0.55, left=prob,
color="#ecf0f1", alpha=0.9, zorder=2)
ax.axvline(0.5, color="#2c3e50", lw=1.8, ls="--", label="Threshold 0.5", zorder=4)
ax.set_xlim(0, 1)
ax.set_ylim(-0.55, 0.55)
ax.set_yticks([])
ax.set_xlabel("LLPS Probability", fontsize=12)
ax.set_title(f"{label} | Probability: {prob:.4f}",
fontsize=14, fontweight="bold", color=color, pad=10)
ax.legend(fontsize=10, loc="lower right")
ax.spines[["top", "right", "left"]].set_visible(False)
plt.tight_layout()
return fig
def screening_figure(xvals: np.ndarray, probs: np.ndarray,
screen_name: str, probs_smooth: np.ndarray = None) -> plt.Figure:
"""Line graph for condition screening result.
If probs_smooth differs from probs (smoothing window > 1), the raw curve
is drawn as a faint dotted reference line and the smoothed curve becomes
the main plotted/filled line.
"""
xlabel = SCREEN_LABELS[screen_name]
LLPS_COLOR = "#e74c3c"
NON_COLOR = "#2980b9"
smoothed = probs_smooth is not None and not np.array_equal(probs, probs_smooth)
plot_probs = probs_smooth if smoothed else probs
fig, ax = plt.subplots(figsize=(9, 5))
if smoothed:
ax.plot(xvals, probs, lw=1.2, color=NON_COLOR, alpha=0.35, ls=":",
label="Raw", zorder=2)
ax.plot(xvals, plot_probs, lw=2.5, color=NON_COLOR,
label="Smoothed LLPS Probability" if smoothed else "LLPS Probability",
zorder=3)
ax.axhline(0.5, color=LLPS_COLOR, lw=1.8, ls="--",
label="Threshold 0.5", zorder=4)
ax.fill_between(xvals, plot_probs, 0.5,
where=(plot_probs >= 0.5), alpha=0.22,
color=LLPS_COLOR, label="LLPS region", zorder=2)
ax.fill_between(xvals, plot_probs, 0.5,
where=(plot_probs < 0.5), alpha=0.15,
color=NON_COLOR, label="Non-LLPS region", zorder=2)
ax.set_xlim(xvals[0], xvals[-1])
ax.set_ylim(0, 1)
ax.set_xlabel(xlabel, fontsize=13)
ax.set_ylabel("LLPS Probability", fontsize=13)
ax.set_title(f"Condition Screening โ {xlabel}", fontsize=14, fontweight="bold")
ax.legend(fontsize=11, loc="upper right")
ax.grid(True, alpha=0.3)
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
return fig
# โโ Status HTML templates โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
_SPINNER_HTML = """
<div style="display:flex;align-items:center;gap:14px;padding:10px 4px;">
<div style="
width:28px;height:28px;flex-shrink:0;
border:3px solid #fecaca;
border-top-color:#ef4444;
border-radius:50%;
animation:llps-spin 0.85s linear infinite;
"></div>
<span style="color:#555;font-size:14px;font-weight:500;line-height:1.5;">
ProtT5-XL feature extraction in progressโฆ<br>
<span style="font-size:12px;color:#999;font-weight:400;">
First run loads the model onto the GPU โ this may take a moment.
</span>
</span>
</div>
<style>@keyframes llps-spin{to{transform:rotate(360deg)}}</style>
"""
def _status_ok(seq_len: int, feat_dim: int) -> str:
pill = ("background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;"
"border-radius:9999px;padding:2px 10px;font-size:12px;font-weight:600;"
"display:inline-block;margin:0 4px 0 0;")
return (f'<div style="color:#16a34a;font-weight:600;padding:6px 0;display:flex;align-items:center;gap:6px;">'
f'<span>โ
Feature extracted</span>'
f'<span style="{pill}">Length: {seq_len} AA</span>'
f'</div>')
def _status_warn(msg: str) -> str:
return f'<div style="color:#d97706;padding:6px 0;">โ ๏ธ {msg}</div>'
def _status_err(msg: str) -> str:
return f'<div style="color:#dc2626;padding:6px 0;">โ {msg}</div>'
# โโ Gradio callback functions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def cb_extract(sequence: str):
"""Step 2: Extract ProtT5 feature from sequence (generator โ streams status).
Every yield also clears feat_state and the Step-3 result panels so a
stale feature/result from a previously extracted sequence can never
remain visible or be used once a new extraction starts.
"""
seq = sequence.strip().upper()
if not seq:
yield None, _status_warn("Please enter a protein sequence."), None, "", None, ""
return
invalid = set(seq) - ALLOW_AA
if invalid:
yield None, _status_warn(f"Invalid characters: <code>{''.join(sorted(invalid))}</code>"), None, "", None, ""
return
# โโ invalidate old feature/results immediately, then show spinner โโโโโโโโโ
yield None, _SPINNER_HTML, None, "", None, ""
try:
# Known example sequence with a pre-computed feature? Skip the T5
# model/GPU call entirely and load it straight from assets/.
example = find_example_by_seq(seq)
cached_path = feature_path(example["id"]) if example else None
if cached_path and cached_path.exists():
feat = read_feature_h5(cached_path)
else:
feat = extract_t5_feature(seq)
yield feat, _status_ok(len(seq), feat.shape[0]), None, "", None, ""
except Exception as e:
yield None, _status_err(str(e)), None, "", None, ""
def cb_predict(feat,
temp, conc, pH,
nacl, mgcl2, kcl, glyc,
peg1, peg2, peg3, ficoll, dext40, dext70):
"""Tab 1: Predict LLPS probability for a single condition point."""
if feat is None:
return None, "โ ๏ธ Please extract the T5 feature first (Step 2)."
try:
cond = build_cond(temp, conc, pH, nacl, mgcl2, kcl, glyc,
peg1, peg2, peg3, ficoll, dext40, dext70)
prob = model_predict(feat, cond)
fig = prob_gauge_figure(prob)
label = "**Phase Separating** ๐ด" if prob >= 0.5 else "**Non-Phase Separating** ๐ต"
txt = f"{label} \nLLPS Probability: **{prob:.4f}**"
return fig, txt
except Exception as e:
return None, f"โ Prediction failed: {e}"
def cb_screen(feat, screen_name,
fix_temp, fix_conc, fix_pH,
nacl, mgcl2, kcl, glyc,
peg1, peg2, peg3, ficoll, dext40, dext70,
smooth_window):
"""Tab 2: Screen LLPS across a range of one condition."""
if feat is None:
return None, "โ ๏ธ Please extract the T5 feature first (Step 2)."
if screen_name is None:
return None, "โ ๏ธ Please select a condition to screen."
try:
xvals = SCREEN_RANGES[screen_name]
probs = []
for v in xvals:
t = float(v) if screen_name == "Temperature" else fix_temp
c = float(v) if screen_name == "Concentration" else fix_conc
p = float(v) if screen_name == "pH" else fix_pH
cond = build_cond(t, c, p, nacl, mgcl2, kcl, glyc,
peg1, peg2, peg3, ficoll, dext40, dext70)
probs.append(model_predict(feat, cond))
probs = np.array(probs)
# Slider step keeps this odd (1, 3, 5, ...); window=1 is a no-op average.
window = int(smooth_window)
probs_plot = moving_average(probs, window) if window > 1 else probs
fig = screening_figure(xvals, probs, screen_name, probs_plot)
peak_idx = probs_plot.argmax()
xlabel = SCREEN_LABELS[screen_name]
txt = (f"Screening completed. \n"
f"Peak probability **{probs_plot[peak_idx]:.4f}** "
f"at {xlabel} = **{xvals[peak_idx]:.1f}** \n"
f"LLPS-positive range: "
f"**{(probs_plot >= 0.5).sum()}** / {len(probs_plot)} points โฅ 0.5")
if window > 1:
txt += f" \n*(Smoothed with moving-average window size {window})*"
return fig, txt
except Exception as e:
return None, f"โ Screening failed: {e}"
# โโ Gradio UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DESCRIPTION = """
# ๐งฌ LLPSense โ Condition-Dependent LLPS Prediction
**LLPSense** predicts whether a protein undergoes liquid-liquid phase separation (LLPS)
under user-defined environmental conditions.
> Baeโ , Kangโ , et al. *"A machine learning framework for predicting and modulating
> condition-dependent protein phase separation."* bioRxiv 2025.
---
## Workflow
1. **Paste your sequence** in the text box below.
2. Click **Extract Feature** to compute the ProtT5-XL embedding.
3. Use **Predict LLPS Probability** to query a specific condition point, or
**Condition Screening** to sweep one condition across its full range.
---
"""
CUSTOM_CSS = """
/* Import fonts from Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
/* Sequence textbox: monospace font for clean AA letter display */
#seq-input textarea,
#seq-input input {
font-family: 'JetBrains Mono', 'Source Code Pro', 'Courier New', monospace !important;
font-size: 14px !important;
line-height: 1.7 !important;
letter-spacing: 0.04em !important;
}
/* Slider thumb: orange */
input[type="range"]::-webkit-slider-thumb {
background: #ef4444 !important;
border-color: #ef4444 !important;
}
input[type="range"]::-moz-range-thumb {
background: #ef4444 !important;
border-color: #ef4444 !important;
}
/* Slider numeric value input */
input[type="number"] {
font-size: 15px !important;
font-weight: 600 !important;
}
/* Initial state: hide Temperature slider column (default screen condition) */
#s-temp-col { display: none; }
/* Tab buttons */
button[role="tab"] {
background: #ffffff !important;
border: 1.5px solid #d1d5db !important;
border-radius: 8px 8px 0 0 !important;
color: #4b5563 !important;
font-weight: 500 !important;
transition: background 0.15s, color 0.15s !important;
}
button[role="tab"]:hover {
background: #eff6ff !important;
color: #1d4ed8 !important;
border-color: #93c5fd !important;
}
button[role="tab"][aria-selected="true"] {
background: #2563eb !important;
color: #ffffff !important;
border-color: #2563eb !important;
font-weight: 600 !important;
}
"""
with gr.Blocks(
theme=gr.themes.Soft(
primary_hue="blue",
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
),
css=CUSTOM_CSS,
title="LLPSense Demo",
) as demo:
feat_state = gr.State(None)
gr.Markdown(DESCRIPTION)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Step 1 + 2: Sequence input & feature extraction
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
with gr.Group():
gr.Markdown("## Step 1 โ Enter Protein Sequence")
seq_box = gr.Textbox(
label="Amino Acid Sequence (1-letter code)",
placeholder="Paste your protein sequence here (e.g. MDVFMKGLSKโฆ)",
lines=5,
value=AVAILABLE_EXAMPLES[0]["seq"] if AVAILABLE_EXAMPLES else "",
elem_id="seq-input",
)
# Clicking an example fills seq_box; the matching Step-2 extraction
# is chained onto it further down (once the Step-3 output components
# exist) so selecting an example runs extraction automatically.
# Skipped entirely if no example has a cached feature yet.
example_picker = None
if AVAILABLE_EXAMPLES:
with gr.Accordion("๐งช Examples (We provide preprocessed T5 feature)", open=False):
example_picker = gr.Examples(
examples=[[example["seq"]] for example in AVAILABLE_EXAMPLES],
example_labels=[example["name"] for example in AVAILABLE_EXAMPLES],
inputs=[seq_box],
)
with gr.Group():
gr.Markdown("## Step 2 โ Extract ProtT5 Feature")
gr.Markdown(
"Runs the [**ProtT5-XL**](https://github.com/agemagician/ProtTrans) encoder to produce a 1024-dim mean-pool embedding. \n"
"โณ *First call loads the model onto the GPU and may take a moment.*"
)
extract_btn = gr.Button("๐ฌ Extract Feature", variant="primary", size="lg")
extract_status = gr.HTML("")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Step 3: Prediction & Screening
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
with gr.Group():
gr.Markdown("## Step 3 โ Run Demo")
gr.Markdown(
"Predict LLPS probability for a specific condition, "
"or sweep one condition across its full range."
)
with gr.Tabs():
# โโ Tab 1: Single-point prediction โโโโโโโโโโโโโโโโโโโโโโโโโโโ
with gr.Tab("๐ฎ Predict LLPS Probability"):
gr.Markdown(
"Set the environmental conditions with the sliders below, "
"then click **Predict** to obtain the LLPS probability."
)
# Primary conditions
with gr.Row():
p_temp = gr.Slider(0, 60, value=25.0, step=0.5,
label="Temperature (ยฐC)")
p_conc = gr.Slider(0, 1000, value=100.0, step=5.0,
label="Concentration (ยตM)")
p_pH = gr.Slider(0, 14, value=7.3, step=0.1,
label="pH")
# Advanced conditions
with gr.Accordion("โ๏ธ Advanced Conditions (Salts & Crowding Agents)", open=False):
gr.Markdown("Default values represent a common physiological buffer (160 mM NaCl).")
with gr.Row():
p_nacl = gr.Slider(0, 2000, value=160.0, step=10.0, label="NaCl (mM)")
p_mgcl2 = gr.Slider(0, 50, value=0.0, step=1.0, label="MgClโ (mM)")
p_kcl = gr.Slider(0, 1000, value=0.0, step=10.0, label="KCl (mM)")
p_glyc = gr.Slider(0, 10, value=0.0, step=0.5, label="Glycerol (%)")
with gr.Row():
p_peg1 = gr.Slider(0, 50, value=0, step=1, label="PEG 300โ1000 (%)")
p_peg2 = gr.Slider(0, 50, value=0, step=1, label="PEG 3kโ6k (%)")
p_peg3 = gr.Slider(0, 50, value=0, step=1, label="PEG 8kโ20k (%)")
with gr.Row():
p_ficoll = gr.Slider(0, 50, value=0, step=1, label="Ficoll (%)")
p_dext40 = gr.Slider(0, 50, value=0, step=1, label="Dextran โค40 kDa (%)")
p_dext70 = gr.Slider(0, 50, value=0, step=1, label="Dextran โฅ70 kDa (%)")
pred_btn = gr.Button("โก Predict LLPS Probability", variant="primary")
pred_plot = gr.Plot(label="Prediction Result")
pred_text = gr.Markdown("")
pred_btn.click(
fn=cb_predict,
inputs=[
feat_state,
p_temp, p_conc, p_pH,
p_nacl, p_mgcl2, p_kcl, p_glyc,
p_peg1, p_peg2, p_peg3, p_ficoll, p_dext40, p_dext70,
],
outputs=[pred_plot, pred_text],
)
# โโ Tab 2: Condition Screening โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
with gr.Tab("๐ Condition Screening"):
gr.Markdown(
"Select **one condition** to screen across its full physiological range. \n"
"The remaining conditions are held fixed at the values you specify below."
)
screen_radio = gr.Radio(
choices=["Temperature", "Concentration", "pH"],
value="Temperature",
label="Condition to Screen",
info="This condition will be swept across its full range; its slider value below is hidden.",
)
# Fixed-value sliders โ JS hides the swept condition's column (no Gradio re-render)
with gr.Row():
with gr.Column(elem_id="s-temp-col"):
s_temp = gr.Slider(0, 60, value=25.0, step=0.5,
label="Temperature (ยฐC) [fixed]")
with gr.Column(elem_id="s-conc-col"):
s_conc = gr.Slider(0, 1000, value=100.0, step=5.0,
label="Concentration (ยตM) [fixed]")
with gr.Column(elem_id="s-ph-col"):
s_pH = gr.Slider(0, 14, value=7.3, step=0.1,
label="pH [fixed]")
# Pure JS toggle โ bypasses Gradio server update, so slider fills are preserved
screen_radio.change(
fn=None,
inputs=[screen_radio],
outputs=[],
js="""(screen_name) => {
const map = {Temperature: 's-temp-col', Concentration: 's-conc-col', pH: 's-ph-col'};
for (const [cond, id] of Object.entries(map)) {
const el = document.getElementById(id);
if (el) el.style.display = (cond === screen_name) ? 'none' : 'flex';
}
}""",
)
# Advanced conditions (always fixed during screening)
with gr.Accordion("โ๏ธ Advanced Conditions (Salts & Crowding Agents)", open=False):
gr.Markdown("Default values represent a common physiological buffer (160 mM NaCl).")
with gr.Row():
s_nacl = gr.Slider(0, 2000, value=160.0, step=10.0, label="NaCl (mM)")
s_mgcl2 = gr.Slider(0, 50, value=0.0, step=1.0, label="MgClโ (mM)")
s_kcl = gr.Slider(0, 1000, value=0.0, step=10.0, label="KCl (mM)")
s_glyc = gr.Slider(0, 10, value=0.0, step=0.5, label="Glycerol (%)")
with gr.Row():
s_peg1 = gr.Slider(0, 50, value=0, step=1, label="PEG 300โ1000 (%)")
s_peg2 = gr.Slider(0, 50, value=0, step=1, label="PEG 3kโ6k (%)")
s_peg3 = gr.Slider(0, 50, value=0, step=1, label="PEG 8kโ20k (%)")
with gr.Row():
s_ficoll = gr.Slider(0, 50, value=0, step=1, label="Ficoll (%)")
s_dext40 = gr.Slider(0, 50, value=0, step=1, label="Dextran โค40 kDa (%)")
s_dext70 = gr.Slider(0, 50, value=0, step=1, label="Dextran โฅ70 kDa (%)")
s_smooth = gr.Slider(
1, 21, value=15, step=2,
label="Smoothing Window Size",
info="Odd window size for moving-average smoothing of the screening curve (1 = no smoothing).",
)
screen_btn = gr.Button("๐ Run Condition Screening", variant="primary")
screen_plot = gr.Plot(label="Screening Result")
screen_text = gr.Markdown("")
screen_btn.click(
fn=cb_screen,
inputs=[
feat_state, screen_radio,
s_temp, s_conc, s_pH,
s_nacl, s_mgcl2, s_kcl, s_glyc,
s_peg1, s_peg2, s_peg3, s_ficoll, s_dext40, s_dext70,
s_smooth,
],
outputs=[screen_plot, screen_text],
)
# Registered here (after Step 3 components exist) since cb_extract also
# clears the Predict/Screening panels so a re-extracted sequence can
# never leave a stale result from the previous sequence on screen.
extract_btn.click(
fn=cb_extract,
inputs=[seq_box],
outputs=[feat_state, extract_status, pred_plot, pred_text, screen_plot, screen_text],
)
# Selecting a built-in example runs extraction automatically (no manual
# Step-2 click needed) โ cache-hit examples resolve near-instantly since
# cb_extract reads their pre-computed feature from assets/ instead of
# calling the T5 model. example_picker is None when no example has a
# cached feature yet (see AVAILABLE_EXAMPLES above).
if example_picker is not None:
example_picker.load_input_event.then(
fn=cb_extract,
inputs=[seq_box],
outputs=[feat_state, extract_status, pred_plot, pred_text, screen_plot, screen_text],
)
# โโ Footer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
gr.Markdown("""
---
**Authors:** Jangwon Baeโ , Minjun Kangโ , Donghyuk Lee, Kuk-Jin Yoon*, Yongwon Jung*
**Paper:** [bioRxiv 2025.12.28.696755](https://doi.org/10.64898/2025.12.28.696755)
**GitHub:** [NearNiah/LLPSense](https://github.com/NearNiah/LLPSense)
""")
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)
|