GPUburnout commited on
Commit
cbdebff
·
verified ·
1 Parent(s): fa075a3

Add fold confidence: color-by-predicted-error mode + readout (completes CHECK-VIABLE part 1)

Browse files
Files changed (1) hide show
  1. app.py +81 -8
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
- v.addStyle({chain:ch}, {cartoon:{color:"#cfd8dc"}});
440
- for (const c in RANGES){ const r=RANGES[c];
441
- v.addStyle({chain:ch, resi:rlist(r[0],r[1])}, {cartoon:{color:CDR[ch][c]}}); }
 
 
 
 
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 7 outputs (everything after `status`) for the early-return paths
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)'>■&nbsp;confident</span> → "
671
+ "<span style='color:rgb(240,200,50)'>■</span> → "
672
+ "<span style='color:rgb(215,48,39)'>■&nbsp;uncertain</span> "
673
+ "&nbsp;(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