Skip to content

Commit 22e5a77

Browse files
committed
Add dataset-scale AlphaFold analytics
#1: DatasetView inspects many AlphaFold models at once (bounded concurrency), aggregates the confidence landscape — combined pLDDT distribution, mean confidence, % confident / disordered across the set — and shows a ranked, exportable per-protein table. Each row opens the full inspector. New "Dataset" nav + command. Turns the tool into a lightweight benchmarking platform. typecheck + build clean; 203 tests green. https://claude.ai/code/session_01DrYCeXtNJMVW4fkW1ywtgZ
1 parent 3777bd4 commit 22e5a77

3 files changed

Lines changed: 166 additions & 2 deletions

File tree

src/App.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { BatchView } from "./batch/BatchView.tsx";
3131
import { FoldView } from "./fold/FoldView.tsx";
3232
import { LearnView } from "./ui/LearnView.tsx";
3333
import { InspectView } from "./components/InspectView.tsx";
34+
import { DatasetView } from "./components/DatasetView.tsx";
3435
import { useWorkspace } from "./workspace/useWorkspace.ts";
3536
import type { StoredStructures, WorkspaceEntry } from "./workspace/types.ts";
3637
import { exportEntryXlsx, exportEntryCsv, exportEntryLog, exportEverything } from "./workspace/export.ts";
@@ -67,6 +68,7 @@ const BREADCRUMBS: Record<string, string> = {
6768
fold: "Fold sequences",
6869
learn: "Learn",
6970
inspect: "Inspect model",
71+
dataset: "Dataset analytics",
7072
};
7173

7274
/** Build the StoredStructures-shaped object the viewer uses from a pipeline result. */
@@ -90,7 +92,7 @@ const MolstarViewer = lazy(() =>
9092
);
9193

9294
type Status = "idle" | "loading" | "error" | "done";
93-
type View = "dashboard" | "compare" | "batch" | "compare2" | "fold" | "learn" | "inspect";
95+
type View = "dashboard" | "compare" | "batch" | "compare2" | "fold" | "learn" | "inspect" | "dataset";
9496

9597
interface Active {
9698
id: string;
@@ -307,6 +309,7 @@ export function App() {
307309
{ id: "new", label: "New comparison", hint: "from database", run: startNewComparison },
308310
{ id: "upload", label: "Upload your own files", hint: "compare local structures", run: () => { setCompareMode("upload"); setView("compare"); } },
309311
{ id: "inspect", label: "Inspect an AlphaFold model", hint: "any protein, no experimental needed", run: () => setView("inspect") },
312+
{ id: "dataset", label: "Dataset analytics", hint: "many models at once", run: () => setView("dataset") },
310313
{ id: "fold", label: "Fold a sequence", hint: "ESMFold", run: () => setView("fold") },
311314
{ id: "dashboard", label: "Go to Dashboard", run: () => setView("dashboard") },
312315
{ id: "batch", label: "Go to Batch", run: () => setView("batch") },
@@ -459,6 +462,10 @@ export function App() {
459462
/>
460463
)}
461464

465+
{view === "dataset" && (
466+
<DatasetView onOpen={(acc) => { setInspectQuery(acc); setView("inspect"); }} />
467+
)}
468+
462469
<footer className="app-footer">
463470
<span className="muted">
464471
OpenFoldUI · native TypeScript engine · client-only · metrics in-browser · saved locally (IndexedDB).

src/components/DatasetView.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Dataset-scale AlphaFold analytics — inspect many models at once and aggregate the
3+
* confidence science: a combined pLDDT distribution, mean confidence across the set,
4+
* and a ranked table (mean pLDDT, % disordered). Turns the single-protein tool into
5+
* a lightweight benchmarking platform.
6+
*/
7+
import { useMemo, useState } from "react";
8+
import { inspectModel } from "../api/inspect.ts";
9+
import { plddtSummary, disorderedRegions } from "../engine/disorder.ts";
10+
import { mapWithConcurrency } from "../batch/pool.ts";
11+
import { parseIdList } from "../batch/parseIds.ts";
12+
import { PlddtHistogram } from "../charts/PlddtHistogram.tsx";
13+
import { toCsv } from "../workspace/csv.ts";
14+
import { Icon } from "../ui/Icon.tsx";
15+
import { useToast } from "../ui/toast.tsx";
16+
17+
interface Row {
18+
accession: string;
19+
name: string;
20+
n: number;
21+
meanPlddt: number;
22+
fractionDisordered: number;
23+
fractionConfident: number;
24+
disorderRegions: number;
25+
plddts: number[];
26+
error?: string;
27+
}
28+
29+
const PLACEHOLDER = `Paste UniProt accessions (one per line or comma-separated):
30+
P04637
31+
P24941
32+
P38398
33+
P0DTD1`;
34+
35+
export function DatasetView({ onOpen }: { onOpen: (accession: string) => void }) {
36+
const { toast } = useToast();
37+
const [text, setText] = useState("");
38+
const [rows, setRows] = useState<Row[]>([]);
39+
const [running, setRunning] = useState(false);
40+
const [progress, setProgress] = useState(0);
41+
42+
const ids = useMemo(() => parseIdList(text), [text]);
43+
const ok = rows.filter((r) => !r.error);
44+
45+
const aggregate = useMemo(() => {
46+
const all = ok.flatMap((r) => r.plddts);
47+
return { all, summary: plddtSummary(all) };
48+
}, [ok]);
49+
50+
async function run() {
51+
if (ids.length === 0 || running) return;
52+
setRunning(true);
53+
setRows([]);
54+
setProgress(0);
55+
let done = 0;
56+
const results: Row[] = [];
57+
await mapWithConcurrency(ids, 3, async (id) => {
58+
try {
59+
const r = await inspectModel(id);
60+
const plddts = r.residues.map((x) => x.plddt);
61+
const s = plddtSummary(plddts);
62+
results.push({
63+
accession: r.accession,
64+
name: r.name,
65+
n: s.n,
66+
meanPlddt: s.mean,
67+
fractionDisordered: s.fractionDisordered,
68+
fractionConfident: s.fractionConfident,
69+
disorderRegions: disorderedRegions(r.residues).length,
70+
plddts,
71+
});
72+
} catch (e) {
73+
results.push({ accession: id, name: id, n: 0, meanPlddt: 0, fractionDisordered: 0, fractionConfident: 0, disorderRegions: 0, plddts: [], error: (e as Error).message });
74+
} finally {
75+
done++;
76+
setProgress(done);
77+
setRows([...results].sort((a, b) => b.meanPlddt - a.meanPlddt));
78+
}
79+
});
80+
setRunning(false);
81+
toast("Dataset analysis finished.", "success");
82+
}
83+
84+
function exportCsv() {
85+
const csv = toCsv(
86+
["accession", "protein", "residues", "mean_plddt", "fraction_disordered", "fraction_confident", "disorder_regions"],
87+
ok.map((r) => [r.accession, r.name, r.n, r.meanPlddt.toFixed(1), r.fractionDisordered.toFixed(3), r.fractionConfident.toFixed(3), r.disorderRegions]),
88+
);
89+
const blob = new Blob([csv], { type: "text/csv" });
90+
const url = URL.createObjectURL(blob);
91+
const a = document.createElement("a");
92+
a.href = url;
93+
a.download = "alphafold_dataset.csv";
94+
a.click();
95+
}
96+
97+
return (
98+
<section className="dataset-view">
99+
<div className="view-head">
100+
<div>
101+
<h2>Dataset analytics</h2>
102+
<p className="muted">Inspect many AlphaFold models at once and see the confidence landscape across the set.</p>
103+
</div>
104+
</div>
105+
106+
<textarea className="batch-input" rows={6} placeholder={PLACEHOLDER} value={text} onChange={(e) => setText(e.target.value)} disabled={running} />
107+
<div className="batch-controls">
108+
<button className="primary" onClick={() => void run()} disabled={running || ids.length === 0}>
109+
{running ? `Analyzing… ${progress}/${ids.length}` : `Analyze ${ids.length || ""} model${ids.length === 1 ? "" : "s"}`}
110+
</button>
111+
{ok.length > 0 && <button onClick={exportCsv}><Icon name="download" size={14} /> Export CSV</button>}
112+
</div>
113+
114+
{running && <div className="progress"><div className="progress-bar" style={{ width: `${(progress / ids.length) * 100}%` }} /></div>}
115+
116+
{ok.length > 0 && (
117+
<>
118+
<div className="metrics">
119+
<Stat label="Models" value={String(ok.length)} sub={`${aggregate.summary.n.toLocaleString()} residues`} />
120+
<Stat label="Mean pLDDT" value={aggregate.summary.mean.toFixed(1)} sub="across the set" />
121+
<Stat label="Confident" value={`${(aggregate.summary.fractionConfident * 100).toFixed(0)}%`} sub="pLDDT ≥ 70" />
122+
<Stat label="Disordered" value={`${(aggregate.summary.fractionDisordered * 100).toFixed(0)}%`} sub="pLDDT < 50" />
123+
</div>
124+
<div className="charts"><PlddtHistogram plddts={aggregate.all} /></div>
125+
<div className="dash-scroll">
126+
<table>
127+
<thead><tr><th>Protein</th><th>Residues</th><th>Mean pLDDT</th><th>Confident</th><th>Disordered</th><th>IDRs</th></tr></thead>
128+
<tbody>
129+
{rows.map((r) => (
130+
<tr key={r.accession} className={r.error ? "row-wrong" : ""}>
131+
<td>{r.error ? <span className="muted">{r.accession}{r.error}</span> : <button className="link strong" onClick={() => onOpen(r.accession)}>{r.name}</button>}</td>
132+
<td>{r.n || ""}</td>
133+
<td>{r.error ? "" : r.meanPlddt.toFixed(1)}</td>
134+
<td>{r.error ? "" : `${(r.fractionConfident * 100).toFixed(0)}%`}</td>
135+
<td>{r.error ? "" : `${(r.fractionDisordered * 100).toFixed(0)}%`}</td>
136+
<td>{r.error ? "" : r.disorderRegions}</td>
137+
</tr>
138+
))}
139+
</tbody>
140+
</table>
141+
</div>
142+
</>
143+
)}
144+
</section>
145+
);
146+
}
147+
148+
function Stat({ label, value, sub }: { label: string; value: string; sub: string }) {
149+
return (
150+
<div className="metric">
151+
<div className="metric-value">{value}</div>
152+
<div className="metric-label">{label}</div>
153+
<div className="metric-hint muted">{sub}</div>
154+
</div>
155+
);
156+
}

src/ui/Sidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { WorkspaceEntry } from "../workspace/types.ts";
66
import { allTags } from "../workspace/stats.ts";
77
import { Icon, type IconName } from "./Icon.tsx";
88

9-
export type View = "dashboard" | "compare" | "batch" | "compare2" | "fold" | "learn" | "inspect";
9+
export type View = "dashboard" | "compare" | "batch" | "compare2" | "fold" | "learn" | "inspect" | "dataset";
1010

1111
export function Sidebar({
1212
view,
@@ -54,6 +54,7 @@ export function Sidebar({
5454
<NavItem icon="grid" label="Dashboard" active={view === "dashboard"} onClick={() => onNavigate("dashboard")} />
5555
<NavItem icon="layers" label="Compare" active={view === "compare"} onClick={() => onNavigate("compare")} />
5656
<NavItem icon="grid" label="Inspect model" active={view === "inspect"} onClick={() => onNavigate("inspect")} />
57+
<NavItem icon="list" label="Dataset" active={view === "dataset"} onClick={() => onNavigate("dataset")} />
5758
<NavItem icon="list" label="Batch" active={view === "batch"} onClick={() => onNavigate("batch")} />
5859
<NavItem icon="beaker" label="Fold sequence" active={view === "fold"} onClick={() => onNavigate("fold")} />
5960
<NavItem icon="book" label="Learn" active={view === "learn"} onClick={() => onNavigate("learn")} />

0 commit comments

Comments
 (0)