Spaces:
Sleeping
Sleeping
Add fold confidence: color-by-predicted-error mode + readout (completes CHECK-VIABLE part 1)
Browse files
app.py
CHANGED
|
@@ -429,16 +429,31 @@ CONTROLLER_JS = """
|
|
| 429 |
const RANGES = __RANGES__;
|
| 430 |
const rlist = (lo,hi) => { let a=[]; for(let i=lo;i<=hi;i++) a.push(i); return a; };
|
| 431 |
const S = window.myabsState = {viewer:null, highlights:{H:[],L:[]}, picks:{H:[],L:[]},
|
| 432 |
-
focus:["H","L"], mode:"grey", spinning:false};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
window.myabsApply = () => {
|
| 435 |
const v = S.viewer; if(!v) return;
|
| 436 |
v.setStyle({}, {});
|
| 437 |
["H","L"].forEach(ch => {
|
| 438 |
if (S.focus.includes(ch)) {
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
const hl=S.highlights[ch]||[]; if(hl.length) v.addStyle({chain:ch, resi:hl},{stick:{color:"magenta",radius:0.3}});
|
| 443 |
const pk=S.picks[ch]||[]; if(pk.length) v.addStyle({chain:ch, resi:pk},{stick:{color:"lime",radius:0.3}});
|
| 444 |
} else if (S.mode === "grey") {
|
|
@@ -475,6 +490,9 @@ CONTROLLER_JS = """
|
|
| 475 |
window.myabsSetMode = (m) => {
|
| 476 |
S.mode = (m && m.indexOf("Hide")>=0) ? "hide" : "grey"; window.myabsApply();
|
| 477 |
};
|
|
|
|
|
|
|
|
|
|
| 478 |
window.myabsToggleResidue = (payload) => {
|
| 479 |
let p; try { p = JSON.parse(payload); } catch(e){ return; }
|
| 480 |
if (p.imgt == null || !p.chain) return;
|
|
@@ -504,6 +522,48 @@ CONTROLLER_JS = """
|
|
| 504 |
|
| 505 |
|
| 506 |
# ---------------------------------------------------------------------- fold
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
MAX_CHAIN_LEN = 250 # antibody variable domains are ~110-130 aa; a generous DoS cap
|
| 508 |
|
| 509 |
|
|
@@ -511,8 +571,8 @@ def fold(heavy: str, light: str):
|
|
| 511 |
"""Fold VH+VL, scan liabilities, and hand the structure to the client viewer."""
|
| 512 |
heavy, light = clean(heavy), clean(light)
|
| 513 |
reset_btn = gr.update(value="▶ Spin")
|
| 514 |
-
# trailing
|
| 515 |
-
tail = ("", None, "", [], [], {"H": [], "L": []}, reset_btn)
|
| 516 |
if not heavy or not light:
|
| 517 |
return ("Enter both a heavy and a light chain.",) + tail
|
| 518 |
if len(heavy) > MAX_CHAIN_LEN or len(light) > MAX_CHAIN_LEN:
|
|
@@ -534,13 +594,14 @@ def fold(heavy: str, light: str):
|
|
| 534 |
flags, highlights, h3_len, ok = developability(heavy, light, pdb)
|
| 535 |
status = (f"Folded in {dt:.1f} s · VH {len(heavy)} aa / VL {len(light)} aa · "
|
| 536 |
f"CDRs highlighted (H: yellow/orange/red, L: cyan/blue).")
|
|
|
|
| 537 |
flags_md = format_flags(flags, h3_len, ok)
|
| 538 |
|
| 539 |
h_tokens, h_idx = build_track("H", number_chain(heavy) or [], highlights.get("H", []))
|
| 540 |
l_tokens, l_idx = build_track("L", number_chain(light) or [], highlights.get("L", []))
|
| 541 |
idxmap = {"H": h_idx, "L": l_idx}
|
| 542 |
payload = build_payload(pdb, highlights) # -> client-side viewer via .change bridge
|
| 543 |
-
return (status, flags_md, out_path, payload,
|
| 544 |
h_tokens, l_tokens, idxmap, reset_btn)
|
| 545 |
|
| 546 |
|
|
@@ -586,6 +647,7 @@ with gr.Blocks(title="MyAbs") as demo: # theme moved to launch() in Gradio 6
|
|
| 586 |
go = gr.Button("Fold", variant="primary")
|
| 587 |
spin_btn = gr.Button("▶ Spin")
|
| 588 |
status = gr.Markdown()
|
|
|
|
| 589 |
pdb_file = gr.File(label="Download structure (.pdb)")
|
| 590 |
with gr.Column(scale=3):
|
| 591 |
with gr.Row():
|
|
@@ -593,6 +655,8 @@ with gr.Blocks(title="MyAbs") as demo: # theme moved to launch() in Gradio 6
|
|
| 593 |
label="Focus chain")
|
| 594 |
mode_dd = gr.Radio(choices=MODE_CHOICES, value="Grey out others",
|
| 595 |
label="The non-focused chain is…")
|
|
|
|
|
|
|
| 596 |
# Persistent 3Dmol viewer container. Updated in place by the client-side
|
| 597 |
# controller (CONTROLLER_JS) so interactions never reset the camera.
|
| 598 |
viewer = gr.HTML(
|
|
@@ -600,6 +664,14 @@ with gr.Blocks(title="MyAbs") as demo: # theme moved to launch() in Gradio 6
|
|
| 600 |
'border:1px solid #eee;border-radius:8px"></div>'
|
| 601 |
)
|
| 602 |
gr.HTML(LEGEND_HTML)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
gr.Markdown(
|
| 604 |
"**Rotate it yourself** — _Touchscreen:_ one-finger drag = rotate · "
|
| 605 |
"pinch = zoom · two-finger drag = pan. _Mouse:_ drag = rotate · "
|
|
@@ -624,7 +696,7 @@ with gr.Blocks(title="MyAbs") as demo: # theme moved to launch() in Gradio 6
|
|
| 624 |
load_box = gr.Textbox(visible=False) # fold -> myabsLoad
|
| 625 |
toggle_box = gr.Textbox(visible=False) # residue click -> myabsToggleResidue
|
| 626 |
|
| 627 |
-
fold_outputs = [status, flags_md, pdb_file, load_box, h_track, l_track,
|
| 628 |
idxmap_state, spin_btn]
|
| 629 |
|
| 630 |
go.click(fold, inputs=[h_in, l_in], outputs=fold_outputs)
|
|
@@ -639,6 +711,7 @@ with gr.Blocks(title="MyAbs") as demo: # theme moved to launch() in Gradio 6
|
|
| 639 |
toggle_box.change(None, inputs=[toggle_box], js="(p) => window.myabsToggleResidue(p)")
|
| 640 |
focus_dd.change(None, inputs=[focus_dd], js="(c) => window.myabsSetFocus(c)")
|
| 641 |
mode_dd.change(None, inputs=[mode_dd], js="(m) => window.myabsSetMode(m)")
|
|
|
|
| 642 |
spin_btn.click(None, outputs=[spin_btn], js="() => window.myabsToggleSpin()")
|
| 643 |
clear_btn.click(None, js="() => window.myabsClear()")
|
| 644 |
|
|
|
|
| 429 |
const RANGES = __RANGES__;
|
| 430 |
const rlist = (lo,hi) => { let a=[]; for(let i=lo;i<=hi;i++) a.push(i); return a; };
|
| 431 |
const S = window.myabsState = {viewer:null, highlights:{H:[],L:[]}, picks:{H:[],L:[]},
|
| 432 |
+
focus:["H","L"], mode:"grey", spinning:false, colorMode:"cdr"};
|
| 433 |
+
|
| 434 |
+
// Per-residue predicted error (PDB B-factor, Å) -> color. 0 = confident (blue),
|
| 435 |
+
// >= CONF_MAX = uncertain (red), through yellow. Matches AlphaFold-style intuition.
|
| 436 |
+
const CONF_MAX = 1.5;
|
| 437 |
+
const confColor = (atom) => {
|
| 438 |
+
const t = Math.max(0, Math.min(1, (atom.b || 0) / CONF_MAX));
|
| 439 |
+
let r,g,b;
|
| 440 |
+
if (t < 0.5){ const u=t/0.5; r=Math.round(43+(240-43)*u); g=Math.round(131+(200-131)*u); b=Math.round(186+(50-186)*u); }
|
| 441 |
+
else { const u=(t-0.5)/0.5; r=Math.round(240+(215-240)*u); g=Math.round(200+(48-200)*u); b=Math.round(50+(39-50)*u); }
|
| 442 |
+
return "rgb("+r+","+g+","+b+")";
|
| 443 |
+
};
|
| 444 |
|
| 445 |
window.myabsApply = () => {
|
| 446 |
const v = S.viewer; if(!v) return;
|
| 447 |
v.setStyle({}, {});
|
| 448 |
["H","L"].forEach(ch => {
|
| 449 |
if (S.focus.includes(ch)) {
|
| 450 |
+
if (S.colorMode === "confidence") {
|
| 451 |
+
v.addStyle({chain:ch}, {cartoon:{colorfunc: confColor}}); // color by predicted error
|
| 452 |
+
} else {
|
| 453 |
+
v.addStyle({chain:ch}, {cartoon:{color:"#cfd8dc"}});
|
| 454 |
+
for (const c in RANGES){ const r=RANGES[c];
|
| 455 |
+
v.addStyle({chain:ch, resi:rlist(r[0],r[1])}, {cartoon:{color:CDR[ch][c]}}); }
|
| 456 |
+
}
|
| 457 |
const hl=S.highlights[ch]||[]; if(hl.length) v.addStyle({chain:ch, resi:hl},{stick:{color:"magenta",radius:0.3}});
|
| 458 |
const pk=S.picks[ch]||[]; if(pk.length) v.addStyle({chain:ch, resi:pk},{stick:{color:"lime",radius:0.3}});
|
| 459 |
} else if (S.mode === "grey") {
|
|
|
|
| 490 |
window.myabsSetMode = (m) => {
|
| 491 |
S.mode = (m && m.indexOf("Hide")>=0) ? "hide" : "grey"; window.myabsApply();
|
| 492 |
};
|
| 493 |
+
window.myabsSetColorMode = (m) => {
|
| 494 |
+
S.colorMode = (m && m.indexOf("onfidence")>=0) ? "confidence" : "cdr"; window.myabsApply();
|
| 495 |
+
};
|
| 496 |
window.myabsToggleResidue = (payload) => {
|
| 497 |
let p; try { p = JSON.parse(payload); } catch(e){ return; }
|
| 498 |
if (p.imgt == null || !p.chain) return;
|
|
|
|
| 522 |
|
| 523 |
|
| 524 |
# ---------------------------------------------------------------------- fold
|
| 525 |
+
def fold_confidence(pdb: str) -> str:
|
| 526 |
+
"""Readout of ABodyBuilder2's per-residue predicted error (PDB B-factor = RMS
|
| 527 |
+
spread across the 4-model ensemble, Å; lower = models agree = more confident)."""
|
| 528 |
+
if not HAVE_SASA or not pdb:
|
| 529 |
+
return ""
|
| 530 |
+
try:
|
| 531 |
+
model = PDBParser(QUIET=True).get_structure("ab", io.StringIO(pdb))[0]
|
| 532 |
+
except Exception:
|
| 533 |
+
return ""
|
| 534 |
+
per, h3 = [], []
|
| 535 |
+
for chain in model:
|
| 536 |
+
for res in chain:
|
| 537 |
+
bs = [a.get_bfactor() for a in res]
|
| 538 |
+
if not bs:
|
| 539 |
+
continue
|
| 540 |
+
b = sum(bs) / len(bs)
|
| 541 |
+
per.append(b)
|
| 542 |
+
if chain.id == "H" and 105 <= res.id[1] <= 117:
|
| 543 |
+
h3.append(b)
|
| 544 |
+
if not per:
|
| 545 |
+
return ""
|
| 546 |
+
|
| 547 |
+
def band(x):
|
| 548 |
+
return "high" if x < 0.5 else ("moderate" if x < 1.5 else "low")
|
| 549 |
+
|
| 550 |
+
mean = sum(per) / len(per)
|
| 551 |
+
out = [f"**Fold confidence:** mean predicted error **{mean:.2f} Å** "
|
| 552 |
+
f"({band(mean)} confidence)."]
|
| 553 |
+
if h3:
|
| 554 |
+
h3m = sum(h3) / len(h3)
|
| 555 |
+
note = ""
|
| 556 |
+
if h3m >= 1.5:
|
| 557 |
+
note = " — the least certain region, so treat its liability flags with extra caution"
|
| 558 |
+
elif h3m >= 0.8:
|
| 559 |
+
note = " — moderate certainty"
|
| 560 |
+
out.append(f"CDR-H3 **{h3m:.2f} Å**{note}.")
|
| 561 |
+
out.append("_Predicted error = spread across the 4-model ensemble (lower = models agree). "
|
| 562 |
+
"Set the viewer's \"Color by\" to **Fold confidence** to see it on the structure "
|
| 563 |
+
"(blue = confident → red = uncertain)._")
|
| 564 |
+
return " ".join(out)
|
| 565 |
+
|
| 566 |
+
|
| 567 |
MAX_CHAIN_LEN = 250 # antibody variable domains are ~110-130 aa; a generous DoS cap
|
| 568 |
|
| 569 |
|
|
|
|
| 571 |
"""Fold VH+VL, scan liabilities, and hand the structure to the client viewer."""
|
| 572 |
heavy, light = clean(heavy), clean(light)
|
| 573 |
reset_btn = gr.update(value="▶ Spin")
|
| 574 |
+
# trailing 8 outputs (everything after `status`) for the early-return paths
|
| 575 |
+
tail = ("", "", None, "", [], [], {"H": [], "L": []}, reset_btn)
|
| 576 |
if not heavy or not light:
|
| 577 |
return ("Enter both a heavy and a light chain.",) + tail
|
| 578 |
if len(heavy) > MAX_CHAIN_LEN or len(light) > MAX_CHAIN_LEN:
|
|
|
|
| 594 |
flags, highlights, h3_len, ok = developability(heavy, light, pdb)
|
| 595 |
status = (f"Folded in {dt:.1f} s · VH {len(heavy)} aa / VL {len(light)} aa · "
|
| 596 |
f"CDRs highlighted (H: yellow/orange/red, L: cyan/blue).")
|
| 597 |
+
conf_md = fold_confidence(pdb)
|
| 598 |
flags_md = format_flags(flags, h3_len, ok)
|
| 599 |
|
| 600 |
h_tokens, h_idx = build_track("H", number_chain(heavy) or [], highlights.get("H", []))
|
| 601 |
l_tokens, l_idx = build_track("L", number_chain(light) or [], highlights.get("L", []))
|
| 602 |
idxmap = {"H": h_idx, "L": l_idx}
|
| 603 |
payload = build_payload(pdb, highlights) # -> client-side viewer via .change bridge
|
| 604 |
+
return (status, conf_md, flags_md, out_path, payload,
|
| 605 |
h_tokens, l_tokens, idxmap, reset_btn)
|
| 606 |
|
| 607 |
|
|
|
|
| 647 |
go = gr.Button("Fold", variant="primary")
|
| 648 |
spin_btn = gr.Button("▶ Spin")
|
| 649 |
status = gr.Markdown()
|
| 650 |
+
conf_md = gr.Markdown()
|
| 651 |
pdb_file = gr.File(label="Download structure (.pdb)")
|
| 652 |
with gr.Column(scale=3):
|
| 653 |
with gr.Row():
|
|
|
|
| 655 |
label="Focus chain")
|
| 656 |
mode_dd = gr.Radio(choices=MODE_CHOICES, value="Grey out others",
|
| 657 |
label="The non-focused chain is…")
|
| 658 |
+
color_dd = gr.Radio(choices=["CDR regions", "Fold confidence"],
|
| 659 |
+
value="CDR regions", label="Color by")
|
| 660 |
# Persistent 3Dmol viewer container. Updated in place by the client-side
|
| 661 |
# controller (CONTROLLER_JS) so interactions never reset the camera.
|
| 662 |
viewer = gr.HTML(
|
|
|
|
| 664 |
'border:1px solid #eee;border-radius:8px"></div>'
|
| 665 |
)
|
| 666 |
gr.HTML(LEGEND_HTML)
|
| 667 |
+
gr.HTML(
|
| 668 |
+
"<div style='font-size:13px;margin:2px 0'><b>Confidence scale</b> "
|
| 669 |
+
"(when Color by → Fold confidence): "
|
| 670 |
+
"<span style='color:rgb(43,131,186)'>■ confident</span> → "
|
| 671 |
+
"<span style='color:rgb(240,200,50)'>■</span> → "
|
| 672 |
+
"<span style='color:rgb(215,48,39)'>■ uncertain</span> "
|
| 673 |
+
" (predicted error, Å)</div>"
|
| 674 |
+
)
|
| 675 |
gr.Markdown(
|
| 676 |
"**Rotate it yourself** — _Touchscreen:_ one-finger drag = rotate · "
|
| 677 |
"pinch = zoom · two-finger drag = pan. _Mouse:_ drag = rotate · "
|
|
|
|
| 696 |
load_box = gr.Textbox(visible=False) # fold -> myabsLoad
|
| 697 |
toggle_box = gr.Textbox(visible=False) # residue click -> myabsToggleResidue
|
| 698 |
|
| 699 |
+
fold_outputs = [status, conf_md, flags_md, pdb_file, load_box, h_track, l_track,
|
| 700 |
idxmap_state, spin_btn]
|
| 701 |
|
| 702 |
go.click(fold, inputs=[h_in, l_in], outputs=fold_outputs)
|
|
|
|
| 711 |
toggle_box.change(None, inputs=[toggle_box], js="(p) => window.myabsToggleResidue(p)")
|
| 712 |
focus_dd.change(None, inputs=[focus_dd], js="(c) => window.myabsSetFocus(c)")
|
| 713 |
mode_dd.change(None, inputs=[mode_dd], js="(m) => window.myabsSetMode(m)")
|
| 714 |
+
color_dd.change(None, inputs=[color_dd], js="(m) => window.myabsSetColorMode(m)")
|
| 715 |
spin_btn.click(None, outputs=[spin_btn], js="() => window.myabsToggleSpin()")
|
| 716 |
clear_btn.click(None, js="() => window.myabsClear()")
|
| 717 |
|