Tengo Gzirishvili Claude Opus 4.8 commited on
Commit
8d1e644
·
1 Parent(s): ed32186

Receipts + seed: validation harness, DMS→commons seeding, evidence UI

Browse files

Turns the glass-box thesis from claims into proof — and gives the empty
commons a path to real data. Honest by construction: no benchmark number is
ever hand-entered; the app serves only what a real run produced, and ships an
explicit "pending" state until then (this sandbox has no torch/ESM, so the
numbers get made on a box with the weights).

- dee/core/benchmark.py — validation harness: average-rank Spearman ρ +
top-decile precision ("of your top picks, how many are genuinely high-
fitness?") + cross-dataset medians. Pure numpy, predictions injected →
unit-testable without ESM.
- dee/core/dms_seed.py — turns published DMS assays into de-identified
substitution-prior rows via the SAME k-anonymized aggregate machinery user
data uses (each assay = one contributor; a substitution needs ≥ MIN_USERS
independent studies to survive). ESM-free, so it can seed the commons with
real, citable data on day one; user results refine it after.
- scripts/run_benchmarks.py + scripts/seed_commons_from_dms.py — the
reproducible runners (the honesty artifacts: exactly how numbers/seed get
made) against ProteinGym-format DMS.
- /api/benchmarks — public read of dee/data/benchmarks.json (ships empty).
- Field Atlas modal gains a "Validated on public DMS" evidence strip
(median ρ + top-pick precision + per-dataset rows), honest pending state —
the receipts paired with the commons grid.

17 new tests (Spearman/ties/top-decile math, seed k-anonymity + ProteinGym
CSV parsing, /api/benchmarks public+empty). 451 green. Evidence strip verified
in the preview: honest pending state on the live empty endpoint, and the
populated render (headline ρ + per-dataset rows above the atlas grid). No
console errors; classic UI untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

dee/core/benchmark.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validation harness — does the engine actually predict? Receipts, not claims.
2
+
3
+ The glass-box thesis lives or dies on this: every score we surface is framed
4
+ as a prediction to verify, so we owe the user proof the predictions track
5
+ reality. This harness correlates the engine's zero-shot ESM-2 ΔLL ranking
6
+ against measured deep-mutational-scanning (DMS) fitness from published studies
7
+ (e.g. ProteinGym) and reports the two numbers a protein engineer actually
8
+ cares about:
9
+
10
+ * Spearman ρ — does the ranking order match measured fitness order?
11
+ * top-decile precision — of the variants we rank in the top 10%, what
12
+ fraction are genuinely high-fitness (top quartile measured)? i.e. "if I
13
+ only make the picks you put on top, how often are they real?"
14
+
15
+ Pure numpy, predictions injected — so it's unit-testable without ESM. The
16
+ reproducible CLI (scripts/run_benchmarks.py) runs it against real DMS with the
17
+ live model where the weights exist; the app only ever serves results a real
18
+ run produced. No number is fabricated here.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+ from typing import Dict, List, Optional, Sequence
24
+
25
+ import numpy as np
26
+
27
+
28
+ def _average_ranks(x: np.ndarray) -> np.ndarray:
29
+ """Ranks with ties resolved to the average rank (proper Spearman ties)."""
30
+ order = np.argsort(x, kind="mergesort")
31
+ ranks = np.empty(len(x), dtype=np.float64)
32
+ sx = x[order]
33
+ i = 0
34
+ n = len(x)
35
+ while i < n:
36
+ j = i
37
+ while j + 1 < n and sx[j + 1] == sx[i]:
38
+ j += 1
39
+ avg = (i + j) / 2.0 + 1.0 # 1-indexed average rank over the tie block
40
+ ranks[order[i:j + 1]] = avg
41
+ i = j + 1
42
+ return ranks
43
+
44
+
45
+ def spearman(a: Sequence[float], b: Sequence[float]) -> Optional[float]:
46
+ """Spearman rank correlation. None if <3 points or no variance."""
47
+ a = np.asarray(a, dtype=np.float64)
48
+ b = np.asarray(b, dtype=np.float64)
49
+ mask = np.isfinite(a) & np.isfinite(b)
50
+ a, b = a[mask], b[mask]
51
+ if len(a) < 3:
52
+ return None
53
+ ra, rb = _average_ranks(a), _average_ranks(b)
54
+ if ra.std() < 1e-9 or rb.std() < 1e-9:
55
+ return None
56
+ return float(np.corrcoef(ra, rb)[0, 1])
57
+
58
+
59
+ def top_decile_precision(
60
+ predicted: Sequence[float], measured: Sequence[float],
61
+ *, pred_frac: float = 0.10, true_frac: float = 0.25,
62
+ ) -> Optional[float]:
63
+ """Of the top ``pred_frac`` by prediction, the fraction that land in the
64
+ top ``true_frac`` by measured fitness. The 'are your top picks real?' number.
65
+ None if too few points to form a meaningful top set."""
66
+ p = np.asarray(predicted, dtype=np.float64)
67
+ m = np.asarray(measured, dtype=np.float64)
68
+ mask = np.isfinite(p) & np.isfinite(m)
69
+ p, m = p[mask], m[mask]
70
+ n = len(p)
71
+ k = int(round(n * pred_frac))
72
+ if n < 10 or k < 1:
73
+ return None
74
+ top_pred_idx = np.argsort(-p)[:k]
75
+ true_cut = np.quantile(m, 1.0 - true_frac)
76
+ hits = int(np.sum(m[top_pred_idx] >= true_cut))
77
+ return float(hits / k)
78
+
79
+
80
+ @dataclass
81
+ class DatasetResult:
82
+ """One DMS assay's validation result."""
83
+ name: str
84
+ protein: str # e.g. UniProt/DMS id
85
+ n: int # variants scored
86
+ spearman: Optional[float]
87
+ top_decile_precision: Optional[float]
88
+ source: str = "" # citation / DOI / dataset id (provenance)
89
+
90
+ def as_dict(self) -> dict:
91
+ return {
92
+ "name": self.name, "protein": self.protein, "n": self.n,
93
+ "spearman": None if self.spearman is None else round(self.spearman, 4),
94
+ "top_decile_precision": None if self.top_decile_precision is None
95
+ else round(self.top_decile_precision, 4),
96
+ "source": self.source,
97
+ }
98
+
99
+
100
+ def evaluate_dataset(
101
+ name: str, protein: str,
102
+ predicted: Sequence[float], measured: Sequence[float],
103
+ *, source: str = "",
104
+ ) -> DatasetResult:
105
+ """Score one aligned (predicted, measured) DMS assay."""
106
+ p = np.asarray(predicted, dtype=np.float64)
107
+ m = np.asarray(measured, dtype=np.float64)
108
+ mask = np.isfinite(p) & np.isfinite(m)
109
+ return DatasetResult(
110
+ name=name, protein=protein, n=int(mask.sum()),
111
+ spearman=spearman(p, m),
112
+ top_decile_precision=top_decile_precision(p, m),
113
+ source=source,
114
+ )
115
+
116
+
117
+ def summarize(results: Sequence[DatasetResult]) -> dict:
118
+ """Headline across datasets: median Spearman, median top-decile precision,
119
+ dataset + variant counts. Medians (robust to a couple of hard assays)."""
120
+ rhos = [r.spearman for r in results if r.spearman is not None]
121
+ precs = [r.top_decile_precision for r in results if r.top_decile_precision is not None]
122
+ return {
123
+ "n_datasets": len(results),
124
+ "n_variants": int(sum(r.n for r in results)),
125
+ "median_spearman": None if not rhos else round(float(np.median(rhos)), 4),
126
+ "median_top_decile_precision": None if not precs
127
+ else round(float(np.median(precs)), 4),
128
+ }
dee/core/dms_seed.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Seed the commons (public.mutation_priors) from published DMS studies.
2
+
3
+ The Field Atlas / cross-user prior pools MEASURED substitution effects across
4
+ labs (dee.core.aggregate). Public deep-mutational-scanning (DMS) datasets are
5
+ exactly that — real, measured effects from independent published assays. So we
6
+ can treat each assay as one contributor and pre-populate the commons with real,
7
+ citable data: the atlas has value on day one, and every user result afterward
8
+ refines it.
9
+
10
+ ESM-free — this only aggregates measured values, no model. It reuses the SAME
11
+ de-identification as user data (dee.core.aggregate.build_priors): z-score
12
+ within each assay, pool by substitution TYPE only, and keep a substitution
13
+ only when ≥ MIN_USERS *independent assays* measured it. So the seed carries no
14
+ per-variant, per-position, or per-study raw value — only 'the field finds W>L
15
+ favorable', backed by ≥3 studies. That k-anonymity bar makes the seed both
16
+ private-by-construction and defensibly strong.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import datetime as _dt
21
+ from typing import List, Optional, Sequence, Tuple
22
+
23
+ from dee.core import aggregate as _agg
24
+
25
+
26
+ def _normalize_label(label: str) -> str:
27
+ """ProteinGym multi-mutants join with ':' ('A1C:D5E'); aggregate splits on
28
+ ',/;/whitespace'. Normalize so multi-mutant assays still contribute (the
29
+ aggregate ridge decomposes them into single-site effects)."""
30
+ return str(label).strip().replace(":", ",")
31
+
32
+
33
+ def seed_rows(
34
+ assays: Sequence[Tuple[str, Sequence[Tuple[str, object]]]],
35
+ *,
36
+ now: Optional[_dt.date] = None,
37
+ enforce_gate: bool = True,
38
+ ) -> List[dict]:
39
+ """Turn published DMS assays into de-identified substitution-prior rows.
40
+
41
+ ``assays``: list of ``(assay_id, [(mutant_label, score), ...])`` — one entry
42
+ per independent published assay. Returns rows shaped for
43
+ ``auth.replace_mutation_priors`` ({substitution, n_users, n_obs,
44
+ mean_effect}); ``n_users`` here is the number of independent assays, so the
45
+ ≥ MIN_USERS k-anonymity floor means ≥ that many studies agree.
46
+ """
47
+ grouped: List[Tuple[str, List[Tuple[str, float]]]] = []
48
+ for assay_id, records in assays:
49
+ meas: List[Tuple[str, float]] = []
50
+ for label, score in records:
51
+ try:
52
+ v = float(score)
53
+ except (TypeError, ValueError):
54
+ continue
55
+ lab = _normalize_label(label)
56
+ if lab:
57
+ meas.append((lab, v))
58
+ if meas:
59
+ grouped.append((str(assay_id), meas))
60
+ prior = _agg.build_priors(grouped, now=now, enforce_gate=enforce_gate)
61
+ return prior.to_rows()
62
+
63
+
64
+ def parse_proteingym_csv(text: str) -> List[Tuple[str, float]]:
65
+ """Parse a ProteinGym-style substitution CSV (header with a 'mutant' /
66
+ 'mutation' column and a 'DMS_score' / 'score' column) into
67
+ [(mutant_label, score)]. Tolerant of column-name variants; skips rows it
68
+ can't read rather than guessing."""
69
+ import csv
70
+ import io
71
+
72
+ out: List[Tuple[str, float]] = []
73
+ reader = csv.DictReader(io.StringIO(text))
74
+ if not reader.fieldnames:
75
+ return out
76
+ lower = {f.lower(): f for f in reader.fieldnames}
77
+ mut_col = next((lower[k] for k in ("mutant", "mutation", "variant", "mutations") if k in lower), None)
78
+ score_col = next((lower[k] for k in ("dms_score", "score", "fitness", "value", "measured") if k in lower), None)
79
+ if not mut_col or not score_col:
80
+ return out
81
+ for row in reader:
82
+ try:
83
+ out.append((str(row[mut_col]).strip(), float(row[score_col])))
84
+ except (TypeError, ValueError, KeyError):
85
+ continue
86
+ return out
dee/data/benchmarks.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "generated_at": null,
3
+ "model": null,
4
+ "note": "Validation results are produced by scripts/run_benchmarks.py against published DMS data with the live ESM-2 model (where the weights exist), never hand-entered. Until a real run populates this file, the app shows an honest 'pending' state — no fabricated numbers.",
5
+ "summary": {
6
+ "n_datasets": 0,
7
+ "n_variants": 0,
8
+ "median_spearman": null,
9
+ "median_top_decile_precision": null
10
+ },
11
+ "datasets": []
12
+ }
dee/server.py CHANGED
@@ -2719,6 +2719,26 @@ def create_app() -> Flask:
2719
  "effective_date": _agg.EFFECTIVE_DATE.isoformat(),
2720
  })
2721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2722
  @app.get("/api/admin/mutation-priors")
2723
  def admin_mutation_priors() -> Response:
2724
  """View the stored cross-user aggregate (de-identified). Admin-token gated."""
 
2719
  "effective_date": _agg.EFFECTIVE_DATE.isoformat(),
2720
  })
2721
 
2722
+ @app.get("/api/benchmarks")
2723
+ def benchmarks() -> Response:
2724
+ """The receipts — how well the engine's zero-shot ranking predicts
2725
+ measured DMS fitness (Spearman ρ + top-decile precision), per published
2726
+ dataset. PUBLIC read. Served from dee/data/benchmarks.json, which is
2727
+ produced ONLY by scripts/run_benchmarks.py against real data with the
2728
+ live model — never hand-entered. Ships empty (honest 'pending' state)
2729
+ until a real run populates it; no fabricated numbers ever reach here."""
2730
+ import json as _json
2731
+ path = Path(__file__).resolve().parent / "data" / "benchmarks.json"
2732
+ try:
2733
+ data = _json.loads(path.read_text(encoding="utf-8"))
2734
+ except Exception: # noqa: BLE001
2735
+ data = {"generated_at": None, "model": None,
2736
+ "summary": {"n_datasets": 0, "n_variants": 0,
2737
+ "median_spearman": None, "median_top_decile_precision": None},
2738
+ "datasets": []}
2739
+ data["ok"] = True
2740
+ return jsonify(data)
2741
+
2742
  @app.get("/api/admin/mutation-priors")
2743
  def admin_mutation_priors() -> Response:
2744
  """View the stored cross-user aggregate (de-identified). Admin-token gated."""
dee/static/app.css CHANGED
@@ -6498,6 +6498,27 @@ h3, h4 {
6498
  .atlas-empty { padding: 20px; border: 1px dashed var(--line-strong); text-align: center; }
6499
  .atlas-empty h3 { margin: 0 0 8px; font-size: 16px; font-weight: 600; color: var(--ink-strong); }
6500
  .atlas-empty .muted { max-width: 52ch; margin: 0 auto 16px; line-height: 1.55; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6501
  @media (max-width: 560px) {
6502
  .ag-head { font-size: 7px; }
6503
  .atlas-legend .al-note { margin-left: 0; width: 100%; }
 
6498
  .atlas-empty { padding: 20px; border: 1px dashed var(--line-strong); text-align: center; }
6499
  .atlas-empty h3 { margin: 0 0 8px; font-size: 16px; font-weight: 600; color: var(--ink-strong); }
6500
  .atlas-empty .muted { max-width: 52ch; margin: 0 auto 16px; line-height: 1.55; }
6501
+
6502
+ /* Evidence strip — the receipts (Spearman ρ vs published DMS). */
6503
+ .atlas-evidence:empty { display: none; }
6504
+ .atlas-evidence-pending { padding: 12px 14px; border: 1px solid var(--line);
6505
+ color: var(--ink-faint); line-height: 1.55; }
6506
+ .atlas-evidence-pending b { color: var(--ink); }
6507
+ .atlas-ev-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 14px;
6508
+ padding: 14px; border: 1px solid var(--line-strong); }
6509
+ .atlas-ev-k { width: 100%; color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.14em; }
6510
+ .atlas-ev-big { font-family: var(--font-display); font-size: 26px; font-weight: 600;
6511
+ color: var(--ink-strong); line-height: 1; }
6512
+ .atlas-ev-lbl { color: var(--ink-faint); margin-right: 8px; }
6513
+ .atlas-ev-meta { width: 100%; color: var(--ink-disabled); }
6514
+ .atlas-ev-list { display: flex; flex-wrap: wrap; gap: 1px; margin-top: 1px;
6515
+ background: var(--line); border: 1px solid var(--line); }
6516
+ .atlas-ev-row { flex: 1 1 180px; display: flex; align-items: baseline; gap: 8px;
6517
+ padding: 8px 12px; background: var(--bg-card); }
6518
+ .aer-n { font-size: 12.5px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6519
+ .aer-rho { font-family: var(--font-mono); font-size: 12px; color: var(--ink-strong); margin-left: auto; }
6520
+ .aer-var { color: var(--ink-faint); }
6521
+
6522
  @media (max-width: 560px) {
6523
  .ag-head { font-size: 7px; }
6524
  .atlas-legend .al-note { margin-left: 0; width: 100%; }
dee/static/app.js CHANGED
@@ -9077,11 +9077,47 @@ function runOracle(opts){
9077
  fetch('/api/atlas').then(function (r) { return r.json(); }).then(function (j) {
9078
  if (!j || !j.ok) { body.innerHTML = '<p class="muted">The atlas is unavailable right now — try again shortly.</p>'; return; }
9079
  render(j);
 
 
 
9080
  }).catch(function () {
9081
  body.innerHTML = '<p class="muted">Couldn’t reach the atlas — check your connection.</p>';
9082
  });
9083
  }
9084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9085
  // Collapse ESM-bin-split rows ("W>L@hi") back to the base substitution,
9086
  // pooling by observation count so the grid shows one cell per (from,to).
9087
  function collapse(rows) {
@@ -9120,6 +9156,9 @@ function runOracle(opts){
9120
  var cells = collapse(j.substitutions);
9121
  var count = j.count || 0;
9122
 
 
 
 
9123
  var stats = '<div class="atlas-stats micro">'
9124
  + '<span><b>' + count + '</b> substitution type' + (count === 1 ? '' : 's') + '</span>'
9125
  + '<span><b>' + (j.total_observations || 0).toLocaleString() + '</b> pooled measurements</span>'
@@ -9127,7 +9166,7 @@ function runOracle(opts){
9127
  + '</div>';
9128
 
9129
  if (!count) {
9130
- body.innerHTML = stats
9131
  + '<div class="atlas-empty">'
9132
  + '<h3>The commons is still filling in.</h3>'
9133
  + '<p class="muted">A substitution appears here only once at least <b>' + (j.min_users || 3)
@@ -9143,7 +9182,7 @@ function runOracle(opts){
9143
  }
9144
 
9145
  // Grid: corner + 20 "to" headers, then 20 rows (from-header + 20 cells).
9146
- var html = stats
9147
  + '<div class="atlas-legend micro"><span class="al-swatch al-pos"></span> field found favorable'
9148
  + '<span class="al-swatch al-neg"></span> unfavorable'
9149
  + '<span class="al-note">rows = original residue · columns = substituted residue</span></div>';
 
9077
  fetch('/api/atlas').then(function (r) { return r.json(); }).then(function (j) {
9078
  if (!j || !j.ok) { body.innerHTML = '<p class="muted">The atlas is unavailable right now — try again shortly.</p>'; return; }
9079
  render(j);
9080
+ // Second half of the "receipts" pair: how well the engine predicts.
9081
+ fetch('/api/benchmarks').then(function (r) { return r.json(); })
9082
+ .then(renderEvidence).catch(function () {});
9083
  }).catch(function () {
9084
  body.innerHTML = '<p class="muted">Couldn’t reach the atlas — check your connection.</p>';
9085
  });
9086
  }
9087
 
9088
+ // Validation strip — Spearman ρ + top-decile precision vs published DMS.
9089
+ // Fills the placeholder render() leaves at the top of the modal. Honest
9090
+ // 'pending' state until a real benchmark run populates the numbers.
9091
+ function renderEvidence(b) {
9092
+ var host = el('atlasEvidence'); if (!host) return;
9093
+ var s = (b && b.summary) || {};
9094
+ if (!b || !b.ok || !s.n_datasets) {
9095
+ host.innerHTML = '<div class="atlas-evidence-pending micro">'
9096
+ + '<b>Independent validation.</b> How well the engine&rsquo;s ranking predicts measured '
9097
+ + 'fitness is benchmarked against published deep-mutational-scanning datasets, computed on '
9098
+ + 'the full model — results appear here once a run completes. We never hand-enter numbers.'
9099
+ + '</div>';
9100
+ return;
9101
+ }
9102
+ var rho = s.median_spearman == null ? '—' : s.median_spearman.toFixed(2);
9103
+ var prec = s.median_top_decile_precision == null ? '—'
9104
+ : Math.round(s.median_top_decile_precision * 100) + '%';
9105
+ var head = '<div class="atlas-ev-head">'
9106
+ + '<span class="atlas-ev-k micro">Validated on public DMS</span>'
9107
+ + '<span class="atlas-ev-big">' + rho + '</span><span class="atlas-ev-lbl micro">median Spearman &rho;</span>'
9108
+ + '<span class="atlas-ev-big">' + prec + '</span><span class="atlas-ev-lbl micro">top-pick precision</span>'
9109
+ + '<span class="atlas-ev-meta micro">' + (s.n_datasets) + ' datasets · '
9110
+ + (s.n_variants || 0).toLocaleString() + ' variants'
9111
+ + (b.model ? ' · ESM-2 ' + esc(b.model) : '') + '</span>'
9112
+ + '</div>';
9113
+ var rows = (b.datasets || []).slice(0, 8).map(function (d) {
9114
+ return '<div class="atlas-ev-row"><span class="aer-n">' + esc(d.name) + '</span>'
9115
+ + '<span class="aer-rho">&rho; ' + (d.spearman == null ? '—' : d.spearman.toFixed(2)) + '</span>'
9116
+ + '<span class="aer-var micro">' + (d.n || 0).toLocaleString() + ' var</span></div>';
9117
+ }).join('');
9118
+ host.innerHTML = head + '<div class="atlas-ev-list">' + rows + '</div>';
9119
+ }
9120
+
9121
  // Collapse ESM-bin-split rows ("W>L@hi") back to the base substitution,
9122
  // pooling by observation count so the grid shows one cell per (from,to).
9123
  function collapse(rows) {
 
9156
  var cells = collapse(j.substitutions);
9157
  var count = j.count || 0;
9158
 
9159
+ // Evidence strip (filled by renderEvidence once /api/benchmarks returns)
9160
+ // sits at the very top — the receipts before the atlas grid.
9161
+ var ev = '<div class="atlas-evidence" id="atlasEvidence"></div>';
9162
  var stats = '<div class="atlas-stats micro">'
9163
  + '<span><b>' + count + '</b> substitution type' + (count === 1 ? '' : 's') + '</span>'
9164
  + '<span><b>' + (j.total_observations || 0).toLocaleString() + '</b> pooled measurements</span>'
 
9166
  + '</div>';
9167
 
9168
  if (!count) {
9169
+ body.innerHTML = ev + stats
9170
  + '<div class="atlas-empty">'
9171
  + '<h3>The commons is still filling in.</h3>'
9172
  + '<p class="muted">A substitution appears here only once at least <b>' + (j.min_users || 3)
 
9182
  }
9183
 
9184
  // Grid: corner + 20 "to" headers, then 20 rows (from-header + 20 cells).
9185
+ var html = ev + stats
9186
  + '<div class="atlas-legend micro"><span class="al-swatch al-pos"></span> field found favorable'
9187
  + '<span class="al-swatch al-neg"></span> unfavorable'
9188
  + '<span class="al-note">rows = original residue · columns = substituted residue</span></div>';
dee/static/index.html CHANGED
@@ -112,7 +112,7 @@
112
  <!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
113
  app.js change. Bump these numbers whenever you ship a frontend update —
114
  without them, users keep getting the stale file for up to a week. -->
115
- <link rel="stylesheet" href="/static/app.css?v=20260714-glassbox" />
116
  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=2" />
117
  <link rel="apple-touch-icon" href="/static/favicon.svg?v=2" />
118
  <!-- Mol* (PDBe) 3-D viewer is ~4.9 MB. We do NOT eager-load it on every
@@ -2259,7 +2259,7 @@
2259
  <!-- Cloning reference data must load before app.js so the Designer
2260
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2261
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2262
- <script src="/static/app.js?v=20260714-glassbox" defer></script>
2263
  <!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
2264
  through the JWT-attaching fetch wrapper (signed-in attribution). -->
2265
  <script src="/static/telemetry.js?v=20260622-analytics" defer></script>
 
112
  <!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
113
  app.js change. Bump these numbers whenever you ship a frontend update —
114
  without them, users keep getting the stale file for up to a week. -->
115
+ <link rel="stylesheet" href="/static/app.css?v=20260715-evidence" />
116
  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=2" />
117
  <link rel="apple-touch-icon" href="/static/favicon.svg?v=2" />
118
  <!-- Mol* (PDBe) 3-D viewer is ~4.9 MB. We do NOT eager-load it on every
 
2259
  <!-- Cloning reference data must load before app.js so the Designer
2260
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2261
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2262
+ <script src="/static/app.js?v=20260715-evidence" defer></script>
2263
  <!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
2264
  through the JWT-attaching fetch wrapper (signed-in attribution). -->
2265
  <script src="/static/telemetry.js?v=20260622-analytics" defer></script>
scripts/run_benchmarks.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Produce the REAL validation numbers — the receipts behind the glass-box claim.
3
+
4
+ Runs the engine's zero-shot ESM-2 ranking against published deep-mutational-
5
+ scanning (DMS) datasets and writes dee/data/benchmarks.json (Spearman ρ +
6
+ top-decile precision per dataset). Run this where torch + the ESM weights
7
+ exist (the HF Space, or any box with the model); it is the ONLY thing that
8
+ should ever populate benchmarks.json — the app never hand-enters numbers.
9
+
10
+ Input: a manifest JSON, a list of assays:
11
+ [
12
+ {"name": "...", "protein": "GENE_ORG", "sequence": "MSK...",
13
+ "csv": "path/to/dms.csv", "source": "doi:..."},
14
+ ...
15
+ ]
16
+ The CSV is ProteinGym-style (a 'mutant'/'mutation' column, a 'DMS_score'/
17
+ 'score' column). Multi-mutants ('A1C:D5E') are scored as the sum of their
18
+ single-site ΔLLs (the same additive assumption the design engine makes — so
19
+ this validates exactly what we ship).
20
+
21
+ Usage:
22
+ python scripts/run_benchmarks.py manifest.json [--model small] [--out dee/data/benchmarks.json]
23
+ """
24
+ import argparse
25
+ import datetime as dt
26
+ import json
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ # Make `dee` importable when run from the repo root.
31
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
32
+
33
+ from dee.core import benchmark as bm # noqa: E402
34
+ from dee.core.dms_seed import parse_proteingym_csv # noqa: E402
35
+
36
+
37
+ def _predict(sequence, labels, scorer):
38
+ """Sum-of-single-site ESM-2 ΔLL for each (possibly multi-site) label."""
39
+ df = scorer.score_all_substitutions(sequence)
40
+ lut = {(int(r.position), str(r.mut_aa)): float(r.delta_ll)
41
+ for r in df.itertuples(index=False)}
42
+ import re
43
+ rx = re.compile(r"^([A-Za-z])(\d+)([A-Za-z*])$")
44
+ preds = []
45
+ for lab in labels:
46
+ total, ok = 0.0, True
47
+ for tok in str(lab).replace(":", ",").split(","):
48
+ m = rx.match(tok.strip())
49
+ if not m:
50
+ ok = False
51
+ break
52
+ pos, mut = int(m.group(2)) - 1, m.group(3).upper()
53
+ if (pos, mut) not in lut:
54
+ ok = False
55
+ break
56
+ total += lut[(pos, mut)]
57
+ preds.append(total if ok else float("nan"))
58
+ return preds
59
+
60
+
61
+ def main():
62
+ ap = argparse.ArgumentParser()
63
+ ap.add_argument("manifest")
64
+ ap.add_argument("--model", default="small")
65
+ ap.add_argument("--out", default=str(Path(__file__).resolve().parent.parent / "dee" / "data" / "benchmarks.json"))
66
+ args = ap.parse_args()
67
+
68
+ from dee.core import scoring
69
+ scorer = scoring.get_scorer(args.model)
70
+
71
+ assays = json.loads(Path(args.manifest).read_text(encoding="utf-8"))
72
+ results = []
73
+ for a in assays:
74
+ recs = parse_proteingym_csv(Path(a["csv"]).read_text(encoding="utf-8"))
75
+ if not recs:
76
+ print(f" skip {a.get('name')}: no parseable records")
77
+ continue
78
+ labels = [lab for (lab, _v) in recs]
79
+ measured = [v for (_lab, v) in recs]
80
+ predicted = _predict(a["sequence"], labels, scorer)
81
+ r = bm.evaluate_dataset(a.get("name", a.get("protein", "?")),
82
+ a.get("protein", ""), predicted, measured,
83
+ source=a.get("source", ""))
84
+ results.append(r)
85
+ print(f" {r.name:24s} n={r.n:6d} rho={r.spearman} top10p={r.top_decile_precision}")
86
+
87
+ out = {
88
+ "generated_at": dt.datetime.utcnow().isoformat() + "Z",
89
+ "model": args.model,
90
+ "summary": bm.summarize(results),
91
+ "datasets": [r.as_dict() for r in results],
92
+ }
93
+ Path(args.out).write_text(json.dumps(out, indent=2), encoding="utf-8")
94
+ print(f"\nWrote {len(results)} dataset result(s) -> {args.out}")
95
+ print(f"Summary: {out['summary']}")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
scripts/seed_commons_from_dms.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Seed the Field Atlas / commons from published DMS studies (ESM-free).
3
+
4
+ Turns a set of public DMS assays into de-identified substitution-effect rows
5
+ (dee.core.dms_seed → the same k-anonymized aggregation user data goes through)
6
+ so the commons has real, citable value on day one. No model needed — this only
7
+ pools measured values by substitution TYPE, keeping a substitution only when
8
+ ≥ MIN_USERS independent studies measured it.
9
+
10
+ Input: a manifest JSON, a list of assays:
11
+ [ {"name": "...", "csv": "path/to/dms.csv"}, ... ]
12
+ (ProteinGym-style CSVs: a 'mutant' column + a 'DMS_score' column.)
13
+
14
+ By default it WRITES the rows to a JSON file for review. Pass --push to upload
15
+ them to public.mutation_priors via dee.auth (requires SUPABASE creds in the
16
+ environment — run it where the service key lives, e.g. the deploy box).
17
+
18
+ Usage:
19
+ python scripts/seed_commons_from_dms.py manifest.json [--out seed_rows.json] [--push]
20
+ """
21
+ import argparse
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
27
+
28
+ from dee.core.dms_seed import parse_proteingym_csv, seed_rows # noqa: E402
29
+
30
+
31
+ def main():
32
+ ap = argparse.ArgumentParser()
33
+ ap.add_argument("manifest")
34
+ ap.add_argument("--out", default="seed_rows.json")
35
+ ap.add_argument("--push", action="store_true",
36
+ help="upload to public.mutation_priors (needs SUPABASE env)")
37
+ args = ap.parse_args()
38
+
39
+ assays = []
40
+ for a in json.loads(Path(args.manifest).read_text(encoding="utf-8")):
41
+ recs = parse_proteingym_csv(Path(a["csv"]).read_text(encoding="utf-8"))
42
+ if recs:
43
+ assays.append((a.get("name", a["csv"]), recs))
44
+ print(f" {a.get('name', a['csv']):28s} {len(recs):7d} records")
45
+
46
+ rows = seed_rows(assays) # enforces the effective-date gate + k-anonymity
47
+ Path(args.out).write_text(json.dumps(rows, indent=2), encoding="utf-8")
48
+ print(f"\n{len(rows)} de-identified substitution row(s) (>= MIN_USERS studies each) -> {args.out}")
49
+
50
+ if args.push:
51
+ from dee import auth
52
+ result = auth.replace_mutation_priors(rows)
53
+ print(f"push -> {result}")
54
+ else:
55
+ print("(dry run — pass --push to upload to the live commons)")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
tests/test_benchmark.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the validation harness (dee.core.benchmark) — the receipts engine.
2
+
3
+ Predictions are injected, so we can assert the exact correlation / precision
4
+ behaviour without ESM. These pin the numbers we'd publish as our benchmark.
5
+ """
6
+ import numpy as np
7
+ import pytest
8
+
9
+ from dee.core.benchmark import (
10
+ DatasetResult,
11
+ evaluate_dataset,
12
+ spearman,
13
+ summarize,
14
+ top_decile_precision,
15
+ )
16
+
17
+
18
+ def test_spearman_perfect_and_anti():
19
+ a = list(range(20))
20
+ assert spearman(a, a) == pytest.approx(1.0)
21
+ assert spearman(a, list(reversed(a))) == pytest.approx(-1.0)
22
+
23
+
24
+ def test_spearman_monotonic_nonlinear_is_one():
25
+ a = list(range(1, 11))
26
+ b = [x ** 3 for x in a] # monotonic → rank corr = 1 even if nonlinear
27
+ assert spearman(a, b) == pytest.approx(1.0)
28
+
29
+
30
+ def test_spearman_handles_ties():
31
+ a = [1, 1, 2, 2, 3, 3]
32
+ b = [1, 1, 2, 2, 3, 3]
33
+ assert spearman(a, b) == pytest.approx(1.0)
34
+
35
+
36
+ def test_spearman_none_when_degenerate():
37
+ assert spearman([1, 2], [3, 4]) is None # < 3 points
38
+ assert spearman([5, 5, 5, 5], [1, 2, 3, 4]) is None # no variance in a
39
+
40
+
41
+ def test_top_decile_precision_perfect_alignment():
42
+ rng = np.random.default_rng(0)
43
+ measured = rng.normal(size=100)
44
+ predicted = measured.copy() # perfect ranking
45
+ # top 10% predicted are exactly the top 10% measured, all within top 25%.
46
+ assert top_decile_precision(predicted, measured) == pytest.approx(1.0)
47
+
48
+
49
+ def test_top_decile_precision_anti_alignment_is_low():
50
+ measured = np.linspace(0, 1, 100)
51
+ predicted = -measured # worst possible ranking
52
+ assert top_decile_precision(predicted, measured) == pytest.approx(0.0)
53
+
54
+
55
+ def test_top_decile_precision_none_when_too_small():
56
+ assert top_decile_precision([1, 2, 3], [3, 2, 1]) is None
57
+
58
+
59
+ def test_evaluate_dataset_and_summarize():
60
+ rng = np.random.default_rng(1)
61
+ m1 = rng.normal(size=60)
62
+ p1 = m1 + rng.normal(scale=0.3, size=60) # good but noisy predictor
63
+ m2 = rng.normal(size=40)
64
+ p2 = -m2 # a hard/anti assay
65
+ r1 = evaluate_dataset("assayA", "P1", p1, m1, source="doi:1")
66
+ r2 = evaluate_dataset("assayB", "P2", p2, m2, source="doi:2")
67
+ assert r1.spearman > 0.6
68
+ assert r2.spearman < 0
69
+ s = summarize([r1, r2])
70
+ assert s["n_datasets"] == 2
71
+ assert s["n_variants"] == 100
72
+ assert s["median_spearman"] is not None
73
+ # as_dict rounds + is JSON-safe
74
+ d = r1.as_dict()
75
+ assert set(d) == {"name", "protein", "n", "spearman", "top_decile_precision", "source"}
76
+
77
+
78
+ def test_summarize_empty():
79
+ s = summarize([])
80
+ assert s == {"n_datasets": 0, "n_variants": 0,
81
+ "median_spearman": None, "median_top_decile_precision": None}
82
+
83
+
84
+ def test_benchmarks_route_public_and_honest_empty():
85
+ from dee import server
86
+ app = server.create_app()
87
+ app.config.update(TESTING=True)
88
+ body = app.test_client().get("/api/benchmarks").get_json() # no auth — public
89
+ assert body["ok"] is True
90
+ assert "summary" in body and "datasets" in body
91
+ # Ships empty (no fabricated numbers) until a real run populates it.
92
+ assert body["summary"]["n_datasets"] == 0
93
+ assert body["datasets"] == []
tests/test_dms_seed.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for seeding the commons from public DMS (dee.core.dms_seed).
2
+
3
+ ESM-free: pure aggregation. We construct synthetic 'assays' where a
4
+ substitution's effect is controlled across studies, and check the k-anonymity
5
+ floor (≥ MIN_USERS independent assays) and the de-identified output shape.
6
+ """
7
+ import pytest
8
+
9
+ from dee.core import aggregate as _agg
10
+ from dee.core.dms_seed import parse_proteingym_csv, seed_rows
11
+
12
+
13
+ def _assay(aid, effects):
14
+ """Build one assay's [(label, score)] where each single-site 'X{i}Y' gets a
15
+ base score = its intended effect (plus a couple of WT-ish low rows so the
16
+ within-assay z-score has spread)."""
17
+ recs = [(lab, val) for lab, val in effects]
18
+ return (aid, recs)
19
+
20
+
21
+ def test_seed_keeps_substitution_seen_in_enough_assays():
22
+ # 'W>L' favorable, measured in 3 independent assays → survives k-anon (=3).
23
+ assays = []
24
+ for i in range(_agg.MIN_USERS):
25
+ assays.append(_assay(f"study{i}", [
26
+ (f"W{10 + i}L", 2.0), # the favorable W>L
27
+ (f"K{20 + i}D", -1.0), # spread so z-score is defined
28
+ (f"A{30 + i}G", 0.0),
29
+ ]))
30
+ rows = seed_rows(assays, enforce_gate=False)
31
+ subs = {r["substitution"] for r in rows}
32
+ assert "W>L" in subs
33
+ wl = next(r for r in rows if r["substitution"] == "W>L")
34
+ assert wl["n_users"] >= _agg.MIN_USERS # backed by ≥ 3 studies
35
+ assert wl["mean_effect"] > 0 # favorable, correct sign
36
+
37
+
38
+ def test_seed_drops_substitution_below_k_anon():
39
+ # 'C>Y' appears in only ONE assay → dropped (privacy floor).
40
+ assays = [
41
+ _assay("only_study", [("C5Y", 3.0), ("K6D", -1.0), ("A7G", 0.0)]),
42
+ _assay("study2", [("K6D", -1.0), ("A7G", 0.5), ("M8I", 0.2)]),
43
+ _assay("study3", [("K6D", -0.8), ("A7G", 0.3), ("M8I", 0.1)]),
44
+ ]
45
+ rows = seed_rows(assays, enforce_gate=False)
46
+ assert "C>Y" not in {r["substitution"] for r in rows}
47
+
48
+
49
+ def test_seed_handles_multi_mutant_colon_labels():
50
+ # ProteinGym multi-mutants ('A1C:D5E') must not crash and should contribute.
51
+ assays = [
52
+ _assay(f"s{i}", [("A1C:D5E", 1.5), ("K2D", -1.0), ("M3I", 0.0)])
53
+ for i in range(_agg.MIN_USERS)
54
+ ]
55
+ rows = seed_rows(assays, enforce_gate=False)
56
+ assert isinstance(rows, list) # decomposed by the aggregate ridge, no error
57
+
58
+
59
+ def test_seed_empty_when_no_assays():
60
+ assert seed_rows([], enforce_gate=False) == []
61
+
62
+
63
+ def test_parse_proteingym_csv():
64
+ csv_text = "mutant,DMS_score\nA1C,1.2\nD5E,-0.4\nbad,notanumber\n"
65
+ recs = parse_proteingym_csv(csv_text)
66
+ assert recs == [("A1C", 1.2), ("D5E", -0.4)] # bad row skipped
67
+
68
+
69
+ def test_parse_proteingym_csv_alt_columns():
70
+ csv_text = "mutation,fitness\nW10L,0.9\n"
71
+ assert parse_proteingym_csv(csv_text) == [("W10L", 0.9)]
72
+
73
+
74
+ def test_parse_proteingym_csv_missing_columns():
75
+ assert parse_proteingym_csv("foo,bar\n1,2\n") == []