File size: 6,588 Bytes
8d1e644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598a072
8d1e644
6c0aef4
8d1e644
 
 
598a072
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d1e644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Validation harness — does the engine actually predict? Receipts, not claims.

The glass-box thesis lives or dies on this: every score we surface is framed
as a prediction to verify, so we owe the user proof the predictions track
reality. This harness correlates the engine's zero-shot ESM-2 ΔLL ranking
against measured deep-mutational-scanning (DMS) fitness from published studies
(e.g. ProteinGym) and reports the two numbers a protein engineer actually
cares about:

  * Spearman ρ — does the ranking order match measured fitness order?
  * top-decile precision — of the variants we rank in the top 10%, what
    fraction are genuinely high-fitness (top quartile measured)? i.e. "if I
    only make the picks you put on top, how often are they real?"

Pure numpy, predictions injected — so it's unit-testable without ESM. The
reproducible CLI (scripts/run_benchmarks.py) runs it against real DMS with the
live model where the weights exist; the app only ever serves results a real
run produced. No number is fabricated here.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import List, Optional, Sequence

import numpy as np

_LABEL_RE = re.compile(r"^([A-Za-z])(\d+)([A-Za-z*])$")


def predict_additive(scores_df, labels: Sequence[str]) -> List[float]:
    """Sum-of-single-site ESM-2 ΔLL for each (possibly multi-site) DMS label
    ('A1C', 'A1C:D5E', or 'A1C,D5E') — the SAME additive scoring the design
    engine itself uses (dee.optimizer.search), so this validates exactly what
    we ship rather than a different metric. ``scores_df`` is the long-format
    output of ``ESM2Scorer.score_all_substitutions`` (columns: position
    0-indexed, mut_aa, delta_ll).

    Single source of truth for both the reproducible CLI
    (scripts/run_benchmarks.py) and the admin in-Space endpoint — never
    duplicated, so they can't silently drift apart.

    A label with any unparseable or out-of-table site becomes NaN (never
    silently coerced to 0) — spearman()/top_decile_precision() mask NaNs out
    via ``np.isfinite``, so a bad label reduces the effective N rather than
    corrupting the correlation."""
    lut = {(int(r.position), str(r.mut_aa)): float(r.delta_ll)
           for r in scores_df.itertuples(index=False)}
    out: List[float] = []
    for lab in labels:
        total, ok = 0.0, True
        for tok in re.split(r"[:,]", str(lab).strip()):
            m = _LABEL_RE.match(tok.strip())
            if not m:
                ok = False
                break
            pos, mut = int(m.group(2)) - 1, m.group(3).upper()
            key = (pos, mut)
            if key not in lut:
                ok = False
                break
            total += lut[key]
        out.append(total if ok else float("nan"))
    return out


def _average_ranks(x: np.ndarray) -> np.ndarray:
    """Ranks with ties resolved to the average rank (proper Spearman ties)."""
    order = np.argsort(x, kind="mergesort")
    ranks = np.empty(len(x), dtype=np.float64)
    sx = x[order]
    i = 0
    n = len(x)
    while i < n:
        j = i
        while j + 1 < n and sx[j + 1] == sx[i]:
            j += 1
        avg = (i + j) / 2.0 + 1.0          # 1-indexed average rank over the tie block
        ranks[order[i:j + 1]] = avg
        i = j + 1
    return ranks


def spearman(a: Sequence[float], b: Sequence[float]) -> Optional[float]:
    """Spearman rank correlation. None if <3 points or no variance."""
    a = np.asarray(a, dtype=np.float64)
    b = np.asarray(b, dtype=np.float64)
    mask = np.isfinite(a) & np.isfinite(b)
    a, b = a[mask], b[mask]
    if len(a) < 3:
        return None
    ra, rb = _average_ranks(a), _average_ranks(b)
    if ra.std() < 1e-9 or rb.std() < 1e-9:
        return None
    return float(np.corrcoef(ra, rb)[0, 1])


def top_decile_precision(
    predicted: Sequence[float], measured: Sequence[float],
    *, pred_frac: float = 0.10, true_frac: float = 0.25,
) -> Optional[float]:
    """Of the top ``pred_frac`` by prediction, the fraction that land in the
    top ``true_frac`` by measured fitness. The 'are your top picks real?' number.
    None if too few points to form a meaningful top set."""
    p = np.asarray(predicted, dtype=np.float64)
    m = np.asarray(measured, dtype=np.float64)
    mask = np.isfinite(p) & np.isfinite(m)
    p, m = p[mask], m[mask]
    n = len(p)
    k = int(round(n * pred_frac))
    if n < 10 or k < 1:
        return None
    top_pred_idx = np.argsort(-p)[:k]
    true_cut = np.quantile(m, 1.0 - true_frac)
    hits = int(np.sum(m[top_pred_idx] >= true_cut))
    return float(hits / k)


@dataclass
class DatasetResult:
    """One DMS assay's validation result."""
    name: str
    protein: str            # e.g. UniProt/DMS id
    n: int                  # variants scored
    spearman: Optional[float]
    top_decile_precision: Optional[float]
    source: str = ""        # citation / DOI / dataset id (provenance)

    def as_dict(self) -> dict:
        return {
            "name": self.name, "protein": self.protein, "n": self.n,
            "spearman": None if self.spearman is None else round(self.spearman, 4),
            "top_decile_precision": None if self.top_decile_precision is None
                else round(self.top_decile_precision, 4),
            "source": self.source,
        }


def evaluate_dataset(
    name: str, protein: str,
    predicted: Sequence[float], measured: Sequence[float],
    *, source: str = "",
) -> DatasetResult:
    """Score one aligned (predicted, measured) DMS assay."""
    p = np.asarray(predicted, dtype=np.float64)
    m = np.asarray(measured, dtype=np.float64)
    mask = np.isfinite(p) & np.isfinite(m)
    return DatasetResult(
        name=name, protein=protein, n=int(mask.sum()),
        spearman=spearman(p, m),
        top_decile_precision=top_decile_precision(p, m),
        source=source,
    )


def summarize(results: Sequence[DatasetResult]) -> dict:
    """Headline across datasets: median Spearman, median top-decile precision,
    dataset + variant counts. Medians (robust to a couple of hard assays)."""
    rhos = [r.spearman for r in results if r.spearman is not None]
    precs = [r.top_decile_precision for r in results if r.top_decile_precision is not None]
    return {
        "n_datasets": len(results),
        "n_variants": int(sum(r.n for r in results)),
        "median_spearman": None if not rhos else round(float(np.median(rhos)), 4),
        "median_top_decile_precision": None if not precs
            else round(float(np.median(precs)), 4),
    }