bindsight 0.3.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bindsight/README.md +44 -0
- bindsight/__init__.py +15 -0
- bindsight/benchmark/__init__.py +71 -0
- bindsight/benchmark/core.py +497 -0
- bindsight/benchmark/defaults.py +28 -0
- bindsight/benchmark/designer_bench.py +899 -0
- bindsight/benchmark/outcomes.py +508 -0
- bindsight/benchmark/panel.py +621 -0
- bindsight/benchmark/rediscovery.py +1153 -0
- bindsight/benchmark/statistics.py +622 -0
- bindsight/benchmark/study.py +1153 -0
- bindsight/benchmark/study_figures.py +164 -0
- bindsight/benchmark/study_report.py +607 -0
- bindsight/cli.py +2429 -0
- bindsight/config.py +451 -0
- bindsight/cost.py +258 -0
- bindsight/deg/__init__.py +7 -0
- bindsight/deg/pydeseq2_runner.py +285 -0
- bindsight/design/__init__.py +23 -0
- bindsight/design/_common.py +501 -0
- bindsight/design/bindcraft.py +78 -0
- bindsight/design/boltzgen.py +79 -0
- bindsight/design/developability.py +140 -0
- bindsight/design/embeddings.py +165 -0
- bindsight/design/prescreen.py +169 -0
- bindsight/design/protocol.py +111 -0
- bindsight/design/rfdiff_mpnn.py +93 -0
- bindsight/epitopes/__init__.py +7 -0
- bindsight/epitopes/surface_bind.py +134 -0
- bindsight/export/__init__.py +7 -0
- bindsight/export/ro_crate.py +595 -0
- bindsight/io/__init__.py +19 -0
- bindsight/io/cbioportal.py +118 -0
- bindsight/io/gdc.py +343 -0
- bindsight/io/paths.py +124 -0
- bindsight/pipelines/__init__.py +9 -0
- bindsight/pipelines/caveats.py +60 -0
- bindsight/pipelines/discover.py +1443 -0
- bindsight/pipelines/full_run.py +410 -0
- bindsight/plugins.py +335 -0
- bindsight/provenance/__init__.py +34 -0
- bindsight/provenance/append.py +199 -0
- bindsight/provenance/fragments.py +216 -0
- bindsight/provenance/manifest.py +506 -0
- bindsight/rank/__init__.py +7 -0
- bindsight/rank/scoring.py +355 -0
- bindsight/report/__init__.py +39 -0
- bindsight/report/format.py +100 -0
- bindsight/report/html.py +645 -0
- bindsight/report/showcase.py +653 -0
- bindsight/report/templates/report.css +67 -0
- bindsight/report/templates/report.html.j2 +420 -0
- bindsight/report/theme.py +231 -0
- bindsight/report/web/__init__.py +35 -0
- bindsight/report/web/app.py +624 -0
- bindsight/report/web/static/binder_viewer.js +245 -0
- bindsight/report/web/static/bindsight.css +455 -0
- bindsight/report/web/static/charts.js +425 -0
- bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
- bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
- bindsight/report/web/static/your_data_check.js +370 -0
- bindsight/report/web/templates/base.html.j2 +48 -0
- bindsight/report/web/templates/evidence.html.j2 +341 -0
- bindsight/report/web/templates/overview.html.j2 +140 -0
- bindsight/report/web/templates/runs.html.j2 +125 -0
- bindsight/report/web/templates/try.html.j2 +155 -0
- bindsight/report/web/templates/your_data.html.j2 +46 -0
- bindsight/runners/__init__.py +7 -0
- bindsight/runners/colab.py +158 -0
- bindsight/runners/job_exec.py +1110 -0
- bindsight/runners/kaggle.py +425 -0
- bindsight/runners/kaggle_kernel.py +489 -0
- bindsight/runners/local_docker.py +154 -0
- bindsight/runners/mock.py +167 -0
- bindsight/runners/modal_runner.py +199 -0
- bindsight/runners/notebook.py +116 -0
- bindsight/runners/notebook_content.py +217 -0
- bindsight/runners/protocol.py +81 -0
- bindsight/runners/source_wheel.py +104 -0
- bindsight/runners/tools.py +997 -0
- bindsight/structures/__init__.py +7 -0
- bindsight/structures/alphafolddb.py +93 -0
- bindsight/structures/plddt.py +92 -0
- bindsight/structures/topology.py +227 -0
- bindsight/surfaceome/__init__.py +42 -0
- bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
- bindsight/surfaceome/data/surfy_offline.txt +17 -0
- bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
- bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
- bindsight/surfaceome/surfy.py +441 -0
- bindsight/targets/__init__.py +7 -0
- bindsight/targets/ensembl_uniprot.py +56 -0
- bindsight/targets/gtex.py +250 -0
- bindsight/targets/open_targets.py +239 -0
- bindsight/validate/__init__.py +7 -0
- bindsight/validate/af2_ig.py +49 -0
- bindsight/validate/boltz2.py +256 -0
- bindsight/validate/chai1r.py +42 -0
- bindsight/validate/protocol.py +188 -0
- bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
- bindsight-0.3.5.dist-info/METADATA +495 -0
- bindsight-0.3.5.dist-info/RECORD +106 -0
- bindsight-0.3.5.dist-info/WHEEL +4 -0
- bindsight-0.3.5.dist-info/entry_points.txt +20 -0
- bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
- bindsight-0.3.5.dist-info/licenses/LICENSE +681 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""Read-only loaders for the real, committed benchmark results.
|
|
4
|
+
|
|
5
|
+
``benchmarks/`` already holds the strongest evidence this project has:
|
|
6
|
+
|
|
7
|
+
- ``benchmarks/study/`` — the rediscovery study over fifteen whole, unstratified
|
|
8
|
+
TCGA projects (CA9 surfaced at rank 1 of 291), with its rank and outcome-class
|
|
9
|
+
figures.
|
|
10
|
+
- ``benchmarks/designer_benchmark/`` — 20 real ERBB2 binders designed on a free
|
|
11
|
+
Kaggle T4, each with the actual Boltz-2 predicted complex ``.cif``, per-design
|
|
12
|
+
metrics, developability descriptors, and ESM-2 embedding coords. The run used
|
|
13
|
+
the corrected ProteinMPNN protocol, and every design carries a target chain
|
|
14
|
+
byte-identical to the native domain IV.
|
|
15
|
+
|
|
16
|
+
Until now none of it was reachable from the web app, so a visitor saw a Demo
|
|
17
|
+
button and had to take the science on faith. This module is the single source
|
|
18
|
+
of truth both the web interface and the documentation site read from, so the
|
|
19
|
+
numbers on screen can never drift from the numbers in ``benchmarks/``.
|
|
20
|
+
|
|
21
|
+
Everything here is read-only, network-free, and degrades to ``None`` rather
|
|
22
|
+
than raising: ``benchmarks/`` is not packaged into the wheel
|
|
23
|
+
(``pyproject.toml`` ships only the ``bindsight`` package), so a user who
|
|
24
|
+
installed from a wheel has no such tree and sees the page degrade.
|
|
25
|
+
|
|
26
|
+
There was a second deployment once, built from an image that copied a named
|
|
27
|
+
set of paths and did not include ``benchmarks/`` -- so ``benchmarks_root()``
|
|
28
|
+
returned ``None`` there and the Real results page rendered nothing while the
|
|
29
|
+
landing page promised twenty binders in 3-D. A named subset is how that
|
|
30
|
+
happens. The container this project publishes copies the working tree whole,
|
|
31
|
+
and the deployment that did not is retired.
|
|
32
|
+
|
|
33
|
+
Only the standard library is imported at module scope, so this stays importable
|
|
34
|
+
without pandas -- which is what lets the CLI read a committed result on a machine
|
|
35
|
+
that never installed the discovery extra.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import csv
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
ENV_BENCHMARKS_DIR = "BINDSIGHT_BENCHMARKS_DIR"
|
|
48
|
+
|
|
49
|
+
_STUDY_SUBDIR = "study"
|
|
50
|
+
_DESIGNER_SUBDIR = "designer_benchmark"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Location
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
def benchmarks_root() -> Path | None:
|
|
57
|
+
"""Locate the repository's ``benchmarks/`` tree.
|
|
58
|
+
|
|
59
|
+
Honours the ``BINDSIGHT_BENCHMARKS_DIR`` environment variable first — the
|
|
60
|
+
same override style as ``BINDSIGHT_SURFACE_BIND_DATA`` — then walks up from
|
|
61
|
+
this file looking for the directory.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The ``benchmarks/`` directory, or ``None`` when running from a wheel
|
|
65
|
+
install where it was never shipped.
|
|
66
|
+
"""
|
|
67
|
+
override = os.environ.get(ENV_BENCHMARKS_DIR)
|
|
68
|
+
if override:
|
|
69
|
+
p = Path(override).expanduser()
|
|
70
|
+
return p if p.is_dir() else None
|
|
71
|
+
|
|
72
|
+
here = Path(__file__).resolve()
|
|
73
|
+
for parent in here.parents:
|
|
74
|
+
candidate = parent / "benchmarks"
|
|
75
|
+
if (candidate / _STUDY_SUBDIR).is_dir() or (candidate / _DESIGNER_SUBDIR).is_dir():
|
|
76
|
+
return candidate
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
81
|
+
"""Parse ``path`` as JSON, returning ``None`` if absent or malformed."""
|
|
82
|
+
try:
|
|
83
|
+
with path.open(encoding="utf-8") as fh:
|
|
84
|
+
data = json.load(fh)
|
|
85
|
+
except (OSError, json.JSONDecodeError):
|
|
86
|
+
return None
|
|
87
|
+
return data if isinstance(data, dict) else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_tsv(path: Path) -> list[dict[str, str]]:
|
|
91
|
+
"""Parse a TSV into a list of row dicts, returning ``[]`` on any failure."""
|
|
92
|
+
try:
|
|
93
|
+
with path.open(encoding="utf-8", newline="") as fh:
|
|
94
|
+
return list(csv.DictReader(fh, delimiter="\t"))
|
|
95
|
+
except OSError:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
100
|
+
"""Parse a JSON-lines file, skipping unparseable lines."""
|
|
101
|
+
rows: list[dict[str, Any]] = []
|
|
102
|
+
try:
|
|
103
|
+
with path.open(encoding="utf-8") as fh:
|
|
104
|
+
for line in fh:
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if not line:
|
|
107
|
+
continue
|
|
108
|
+
try:
|
|
109
|
+
obj = json.loads(line)
|
|
110
|
+
except json.JSONDecodeError:
|
|
111
|
+
continue
|
|
112
|
+
if isinstance(obj, dict):
|
|
113
|
+
rows.append(obj)
|
|
114
|
+
except OSError:
|
|
115
|
+
return []
|
|
116
|
+
return rows
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _as_float(value: object) -> float | None:
|
|
120
|
+
"""Coerce to float, returning ``None`` for missing or non-numeric input."""
|
|
121
|
+
if value is None or value == "":
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
return float(value) # type: ignore[arg-type]
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _as_optional_bool(value: object) -> bool | None:
|
|
130
|
+
"""Coerce a JSON boolean, returning ``None`` when it is absent or unreadable.
|
|
131
|
+
|
|
132
|
+
Provenance that cannot be read must never collapse into ``False``: an
|
|
133
|
+
artifact with no flag has *unknown* provenance, and a caller has to be able
|
|
134
|
+
to tell that apart from an affirmative "this was recorded as real".
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
value: The raw value read from the artifact, or ``None`` when absent.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The boolean, or ``None`` when the artifact did not state one.
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(value, bool):
|
|
143
|
+
return value
|
|
144
|
+
if isinstance(value, str):
|
|
145
|
+
lowered = value.strip().lower()
|
|
146
|
+
if lowered in {"true", "yes", "1"}:
|
|
147
|
+
return True
|
|
148
|
+
if lowered in {"false", "no", "0"}:
|
|
149
|
+
return False
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Rediscovery study
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class StudyShowcase:
|
|
158
|
+
"""The rediscovery study, as committed in ``benchmarks/study/``.
|
|
159
|
+
|
|
160
|
+
Replaces the earlier six-cohort validation, whose single positive result came
|
|
161
|
+
from a cohort selected by a classifier keyed on the antigen being sought, and
|
|
162
|
+
whose denominator was chosen after seeing which antigens turned out to be
|
|
163
|
+
over-expressed. Neither survives here: cohorts are whole unstratified TCGA
|
|
164
|
+
projects, and the denominators are pre-registered.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
design: dict[str, Any]
|
|
168
|
+
recall_cascade: dict[str, Any]
|
|
169
|
+
tier_sensitivity: dict[str, Any]
|
|
170
|
+
primary_interval: dict[str, Any]
|
|
171
|
+
#: Outcome counts over the **pre-registered denominator only** (the tiers in
|
|
172
|
+
#: ``design.tiers_in_primary_denominator``), not over every scored pair. Use
|
|
173
|
+
#: :attr:`outcome_counts_every_tier` for the whole panel, and never show
|
|
174
|
+
#: either without saying which one it is.
|
|
175
|
+
outcome_counts: dict[str, int]
|
|
176
|
+
pairs: list[dict[str, Any]]
|
|
177
|
+
set_sizes: dict[str, dict[str, int]]
|
|
178
|
+
surfaceome_size: int
|
|
179
|
+
figures: dict[str, Path]
|
|
180
|
+
#: The indication-specificity permutation null. The strongest claim the
|
|
181
|
+
#: study supports, and until this field existed no generated surface could
|
|
182
|
+
#: render it: it lived only in ``benchmarks/study/RESULTS.md``, absent from
|
|
183
|
+
#: every README, docs page and manuscript.
|
|
184
|
+
specificity_null: dict[str, Any] = field(default_factory=dict)
|
|
185
|
+
#: The abundance- and dispersion-matched decoy null. Its headline is a
|
|
186
|
+
#: negative, which is exactly why it has to travel with the positive one.
|
|
187
|
+
decoy_null: dict[str, Any] = field(default_factory=dict)
|
|
188
|
+
#: Cohorts carrying no panel antigen, run to show what no signal looks like.
|
|
189
|
+
null_calibration: dict[str, Any] = field(default_factory=dict)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def specificity_is_at_its_floor(self) -> bool:
|
|
193
|
+
"""Whether the permutation p is the smallest this design can express.
|
|
194
|
+
|
|
195
|
+
Seven antigens give 5,040 orderings, and two of them are prostate, so
|
|
196
|
+
every distinct assignment is enumerated twice: the floor is 2/5040 and
|
|
197
|
+
the observed p sits on it. A surface that prints the p without saying so
|
|
198
|
+
implies a precision the design does not have.
|
|
199
|
+
"""
|
|
200
|
+
spec = self.specificity_null
|
|
201
|
+
p_value, floor = spec.get("p_value"), spec.get("p_value_floor")
|
|
202
|
+
if p_value is None or floor is None:
|
|
203
|
+
return False
|
|
204
|
+
return bool(p_value <= floor * 1.000001)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def indication_gap(self) -> dict[str, Any]:
|
|
208
|
+
"""The within-antigen difference between own and off indication.
|
|
209
|
+
|
|
210
|
+
The paired contrast, not the two means side by side: each antigen is its
|
|
211
|
+
own control, which is what makes the interval meaningful at this panel
|
|
212
|
+
size.
|
|
213
|
+
"""
|
|
214
|
+
return dict(self.null_calibration.get("paired_difference") or {})
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def decoy_rows(self) -> list[dict[str, Any]]:
|
|
218
|
+
"""Pairs carrying a decoy-null p, best first. The null lives per pair."""
|
|
219
|
+
rows = [p for p in self.pairs if p.get("p_decoy") is not None]
|
|
220
|
+
return sorted(rows, key=lambda r: r["p_decoy"])
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def decoy_nominal(self) -> int:
|
|
224
|
+
"""Pairs nominally significant against abundance-matched decoys."""
|
|
225
|
+
return sum(1 for r in self.decoy_rows if r["p_decoy"] < 0.05)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def decoy_surviving_correction(self) -> int:
|
|
229
|
+
"""Pairs surviving Benjamini-Hochberg across the panel."""
|
|
230
|
+
return sum(
|
|
231
|
+
1 for r in self.decoy_rows if r.get("p_decoy_bh") is not None and r["p_decoy_bh"] < 0.05
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def smallest_attainable_decoy_bh(self) -> float | None:
|
|
236
|
+
"""The smallest BH-adjusted p any pair in this panel could have produced.
|
|
237
|
+
|
|
238
|
+
Each pair's decoy p is bounded below by its own stratum size, so the
|
|
239
|
+
panel's best possible adjusted value follows from the design rather than
|
|
240
|
+
from the data. When that bound already exceeds 0.05, "none survives
|
|
241
|
+
correction" was settled before a single number was computed, and
|
|
242
|
+
reporting it as a finding overstates what the experiment could ever have
|
|
243
|
+
shown. The study's headline negative is exactly this case.
|
|
244
|
+
"""
|
|
245
|
+
rows = self.decoy_rows
|
|
246
|
+
if not rows:
|
|
247
|
+
return None
|
|
248
|
+
floors = sorted(r["p_decoy_floor"] for r in rows if r.get("p_decoy_floor") is not None)
|
|
249
|
+
if not floors:
|
|
250
|
+
return None
|
|
251
|
+
n = len(rows)
|
|
252
|
+
# Benjamini-Hochberg at its most generous: every pair sits on its floor.
|
|
253
|
+
return min(float(floors[i] * n) / (i + 1) for i in range(len(floors)))
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def decoy_negative_was_forced_by_design(self) -> bool:
|
|
257
|
+
"""True when no pair could have survived correction whatever the data."""
|
|
258
|
+
bound = self.smallest_attainable_decoy_bh
|
|
259
|
+
return bound is not None and bound >= 0.05
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def surfaced(self) -> list[dict[str, Any]]:
|
|
263
|
+
"""Pairs that reached the candidate shortlist, best rank first."""
|
|
264
|
+
ranked = [p for p in self.pairs if isinstance(p.get("rank"), int)]
|
|
265
|
+
return sorted(ranked, key=lambda p: p["rank"])
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def headline(self) -> dict[str, Any] | None:
|
|
269
|
+
"""The best-ranked surfaced antigen."""
|
|
270
|
+
surfaced = self.surfaced
|
|
271
|
+
return surfaced[0] if surfaced else None
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def primary_tiers(self) -> tuple[str, ...]:
|
|
275
|
+
"""The regulatory tiers the headline denominator was pre-registered on."""
|
|
276
|
+
return tuple(self.design.get("tiers_in_primary_denominator") or ())
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def outcome_counts_every_tier(self) -> dict[str, int]:
|
|
280
|
+
"""The same four outcomes over every scored pair.
|
|
281
|
+
|
|
282
|
+
Counted from :attr:`pairs` rather than read from the artifact, because
|
|
283
|
+
the artifact records only the primary-denominator counts and a reader
|
|
284
|
+
comparing them against the pair table needs the other frame too.
|
|
285
|
+
"""
|
|
286
|
+
counts: dict[str, int] = {}
|
|
287
|
+
for pair in self.pairs:
|
|
288
|
+
outcome = str(pair.get("outcome_class") or "")
|
|
289
|
+
if outcome:
|
|
290
|
+
counts[outcome] = counts.get(outcome, 0) + 1
|
|
291
|
+
return counts
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def n_scored(self) -> int:
|
|
295
|
+
"""Antigen-cohort pairs in the scored panel."""
|
|
296
|
+
return len(self.pairs)
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def n_projects(self) -> int:
|
|
300
|
+
"""TCGA projects the study covers."""
|
|
301
|
+
return len(self.set_sizes)
|
|
302
|
+
|
|
303
|
+
def recall_row(self, denominator: str = "all") -> dict[str, Any]:
|
|
304
|
+
"""One denominator's rates across every reported cutoff."""
|
|
305
|
+
block = self.recall_cascade.get(denominator) or {}
|
|
306
|
+
return {
|
|
307
|
+
"median_shortlist": block.get("median_shortlist_size"),
|
|
308
|
+
"at_k": {k: v.get("wilson", {}) for k, v in (block.get("at_k") or {}).items()},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
def rows(self) -> list[dict[str, Any]]:
|
|
312
|
+
"""One display-ready row per antigen-cohort pair.
|
|
313
|
+
|
|
314
|
+
Every row carries the size of the set its rank was taken within, because
|
|
315
|
+
a rank without it cannot be interpreted, and the outcome class, because
|
|
316
|
+
an antigen the instrument could not see and one the ranking placed low
|
|
317
|
+
are different findings.
|
|
318
|
+
"""
|
|
319
|
+
out: list[dict[str, Any]] = []
|
|
320
|
+
for pair in self.pairs:
|
|
321
|
+
out.append(
|
|
322
|
+
{
|
|
323
|
+
"antigen": pair.get("symbol", "—"),
|
|
324
|
+
"cohort": str(pair.get("project", "")).removeprefix("TCGA-"),
|
|
325
|
+
"agent": pair.get("agent", ""),
|
|
326
|
+
"tier": pair.get("tier", ""),
|
|
327
|
+
"outcome": pair.get("outcome_class", ""),
|
|
328
|
+
"log2fc": _as_float(pair.get("log2fc")),
|
|
329
|
+
"padj": _as_float(pair.get("padj")),
|
|
330
|
+
"rank": pair.get("rank"),
|
|
331
|
+
"shortlist": pair.get("shortlist_size"),
|
|
332
|
+
"counterfactual_rank": pair.get("counterfactual_rank"),
|
|
333
|
+
"eligible": pair.get("n_eligible"),
|
|
334
|
+
"why": pair.get("reason", ""),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
return sorted(out, key=lambda r: (r["rank"] is None, r["rank"] or 0, r["antigen"]))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def load_study(root: Path | None = None) -> StudyShowcase | None:
|
|
341
|
+
"""Load the rediscovery study results.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
root: Optional explicit ``benchmarks/`` directory; discovered when omitted.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
A :class:`StudyShowcase`, or ``None`` when the results are absent.
|
|
348
|
+
"""
|
|
349
|
+
base = (root or benchmarks_root() or Path()) / _STUDY_SUBDIR
|
|
350
|
+
data = _read_json(base / "results.json")
|
|
351
|
+
if data is None:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
figures: dict[str, Path] = {}
|
|
355
|
+
fig_dir = base / "figures"
|
|
356
|
+
if fig_dir.is_dir():
|
|
357
|
+
figures = {p.stem: p for p in sorted(fig_dir.glob("*.png"))}
|
|
358
|
+
|
|
359
|
+
return StudyShowcase(
|
|
360
|
+
design=dict(data.get("design") or {}),
|
|
361
|
+
recall_cascade=dict(data.get("recall_cascade") or {}),
|
|
362
|
+
tier_sensitivity=dict(data.get("tier_sensitivity") or {}),
|
|
363
|
+
primary_interval=dict(data.get("primary_interval") or {}),
|
|
364
|
+
outcome_counts=dict(data.get("outcome_class_counts") or {}),
|
|
365
|
+
pairs=list(data.get("pairs") or []),
|
|
366
|
+
set_sizes=dict(data.get("set_sizes_by_cohort") or {}),
|
|
367
|
+
surfaceome_size=int(data.get("surfaceome_size") or 0),
|
|
368
|
+
figures=figures,
|
|
369
|
+
specificity_null=dict(data.get("specificity_null") or {}),
|
|
370
|
+
decoy_null=dict(data.get("decoy_null") or {}),
|
|
371
|
+
null_calibration=dict(data.get("null_calibration") or {}),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
# Designer benchmark
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
@dataclass(frozen=True)
|
|
379
|
+
class BinderDesign:
|
|
380
|
+
"""One designed binder, joined across metrics, developability and embedding."""
|
|
381
|
+
|
|
382
|
+
binder_id: str
|
|
383
|
+
iptm: float | None = None
|
|
384
|
+
pae_interaction: float | None = None
|
|
385
|
+
target_uniprot: str = ""
|
|
386
|
+
validator_name: str = ""
|
|
387
|
+
validator_version: str = ""
|
|
388
|
+
complex_cif: Path | None = None
|
|
389
|
+
fasta: Path | None = None
|
|
390
|
+
developability: dict[str, float] = field(default_factory=dict)
|
|
391
|
+
pc1: float | None = None
|
|
392
|
+
pc2: float | None = None
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def sequence(self) -> str | None:
|
|
396
|
+
"""Read the designed sequence from the committed FASTA, if present."""
|
|
397
|
+
if self.fasta is None or not self.fasta.is_file():
|
|
398
|
+
return None
|
|
399
|
+
try:
|
|
400
|
+
lines = self.fasta.read_text(encoding="utf-8").splitlines()
|
|
401
|
+
except OSError:
|
|
402
|
+
return None
|
|
403
|
+
return "".join(ln.strip() for ln in lines if ln and not ln.startswith(">")) or None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@dataclass(frozen=True)
|
|
407
|
+
class DesignerShowcase:
|
|
408
|
+
"""The designer benchmark, as committed in ``benchmarks/designer_benchmark/``."""
|
|
409
|
+
|
|
410
|
+
generated_utc: str
|
|
411
|
+
bindsight_version: str
|
|
412
|
+
backend: str
|
|
413
|
+
gpu: str
|
|
414
|
+
validator: str
|
|
415
|
+
n_trajectories: int
|
|
416
|
+
#: ``True`` mock backend, ``False`` real GPU run, ``None`` when the artifact
|
|
417
|
+
#: never stated it. Unknown is not "real": a benchmark that does not record
|
|
418
|
+
#: its own provenance cannot be advertised as a genuine GPU result.
|
|
419
|
+
is_mock: bool | None
|
|
420
|
+
targets: list[str]
|
|
421
|
+
designers: list[dict[str, Any]]
|
|
422
|
+
binders: list[BinderDesign]
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def scored(self) -> list[BinderDesign]:
|
|
426
|
+
"""Binders that carry a validator ipTM, best first."""
|
|
427
|
+
have = [b for b in self.binders if b.iptm is not None]
|
|
428
|
+
return sorted(have, key=lambda b: b.iptm or 0.0, reverse=True)
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def best(self) -> BinderDesign | None:
|
|
432
|
+
"""The highest-ipTM design."""
|
|
433
|
+
ranked = self.scored
|
|
434
|
+
return ranked[0] if ranked else None
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def success_rate(self) -> float | None:
|
|
438
|
+
"""Fraction of designs at or above the ipTM 0.65 criterion, as recorded.
|
|
439
|
+
|
|
440
|
+
**Withdrawn as a measure of design quality.** The rate is a real count
|
|
441
|
+
of a real artifact, and it is reported for comparability with published
|
|
442
|
+
de novo work — but a paired control folded each design beside a shuffle
|
|
443
|
+
of its own sequence, and the shuffles cleared the same bar more often
|
|
444
|
+
(50% against 30%; paired difference -0.043, 95% CI -0.142 to +0.054).
|
|
445
|
+
|
|
446
|
+
Any surface rendering this must carry that with it, adjacently, and
|
|
447
|
+
``bindsight.report.web`` does. It is deliberately not a headline
|
|
448
|
+
statistic anywhere: a caveat underneath a number does not travel with
|
|
449
|
+
the number.
|
|
450
|
+
|
|
451
|
+
See ``benchmarks/calibration/``.
|
|
452
|
+
"""
|
|
453
|
+
for d in self.designers:
|
|
454
|
+
rate = _as_float(d.get("success_rate"))
|
|
455
|
+
if rate is not None:
|
|
456
|
+
return rate
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def n_success(self) -> int | None:
|
|
461
|
+
"""Designs at or above the criterion, as the artifact counts them."""
|
|
462
|
+
for d in self.designers:
|
|
463
|
+
value = d.get("n_success")
|
|
464
|
+
if value is not None:
|
|
465
|
+
return int(value)
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def success_interval(self) -> tuple[float, float] | None:
|
|
470
|
+
"""The reported interval around the success rate, if the artifact has one.
|
|
471
|
+
|
|
472
|
+
Clustered over backbones, because designs sharing an RFdiffusion
|
|
473
|
+
trajectory are not independent trials. A page that prints the rate
|
|
474
|
+
without it claims a precision twenty designs from ten backbones do not
|
|
475
|
+
carry.
|
|
476
|
+
"""
|
|
477
|
+
for d in self.designers:
|
|
478
|
+
low = _as_float(d.get("success_ci_low"))
|
|
479
|
+
high = _as_float(d.get("success_ci_high"))
|
|
480
|
+
if low is not None and high is not None:
|
|
481
|
+
return (low, high)
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def mean_iptm(self) -> float | None:
|
|
486
|
+
"""Mean ipTM as the benchmark artifact reports it."""
|
|
487
|
+
for d in self.designers:
|
|
488
|
+
value = _as_float(d.get("mean_iptm"))
|
|
489
|
+
if value is not None:
|
|
490
|
+
return value
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def n_designs(self) -> int:
|
|
495
|
+
"""Total number of designs in the benchmark."""
|
|
496
|
+
return len(self.binders)
|
|
497
|
+
|
|
498
|
+
def with_structures(self) -> list[BinderDesign]:
|
|
499
|
+
"""Designs whose real predicted complex structure is available."""
|
|
500
|
+
return [b for b in self.scored if b.complex_cif is not None]
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def load_designer_benchmark(root: Path | None = None) -> DesignerShowcase | None:
|
|
504
|
+
"""Load the designer benchmark and join the per-binder artifacts.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
root: Optional explicit ``benchmarks/`` directory; discovered when omitted.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
A :class:`DesignerShowcase`, or ``None`` when the results are absent.
|
|
511
|
+
"""
|
|
512
|
+
base = (root or benchmarks_root() or Path()) / _DESIGNER_SUBDIR
|
|
513
|
+
data = _read_json(base / "results.json")
|
|
514
|
+
if data is None:
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
binder_dir = base / "binders"
|
|
518
|
+
dev_by_id: dict[str, dict[str, float]] = {}
|
|
519
|
+
for row in _read_tsv(binder_dir / "developability.tsv"):
|
|
520
|
+
bid = row.get("binder_id")
|
|
521
|
+
if not bid:
|
|
522
|
+
continue
|
|
523
|
+
dev_by_id[bid] = {
|
|
524
|
+
k: v
|
|
525
|
+
for k, v in ((k, _as_float(v)) for k, v in row.items() if k != "binder_id")
|
|
526
|
+
if v is not None
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
coords: dict[str, tuple[float | None, float | None]] = {}
|
|
530
|
+
for row in _read_tsv(binder_dir / "embedding_coords.tsv"):
|
|
531
|
+
bid = row.get("binder_id")
|
|
532
|
+
if bid:
|
|
533
|
+
coords[bid] = (_as_float(row.get("pc1")), _as_float(row.get("pc2")))
|
|
534
|
+
|
|
535
|
+
binders: list[BinderDesign] = []
|
|
536
|
+
for row in _read_jsonl(binder_dir / "metrics.jsonl"):
|
|
537
|
+
bid = str(row.get("binder_id") or "")
|
|
538
|
+
if not bid:
|
|
539
|
+
continue
|
|
540
|
+
cif = binder_dir / f"{bid}_complex.cif"
|
|
541
|
+
fasta = binder_dir / f"{bid}.fasta"
|
|
542
|
+
pc1, pc2 = coords.get(bid, (None, None))
|
|
543
|
+
binders.append(
|
|
544
|
+
BinderDesign(
|
|
545
|
+
binder_id=bid,
|
|
546
|
+
iptm=_as_float(row.get("iptm")),
|
|
547
|
+
pae_interaction=_as_float(row.get("pae_interaction")),
|
|
548
|
+
target_uniprot=str(row.get("target_uniprot") or ""),
|
|
549
|
+
validator_name=str(row.get("validator_name") or ""),
|
|
550
|
+
validator_version=str(row.get("validator_version") or ""),
|
|
551
|
+
complex_cif=cif if cif.is_file() else None,
|
|
552
|
+
fasta=fasta if fasta.is_file() else None,
|
|
553
|
+
developability=dev_by_id.get(bid, {}),
|
|
554
|
+
pc1=pc1,
|
|
555
|
+
pc2=pc2,
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
return DesignerShowcase(
|
|
560
|
+
generated_utc=str(data.get("generated_utc", "")),
|
|
561
|
+
bindsight_version=str(data.get("bindsight_version", "")),
|
|
562
|
+
backend=str(data.get("backend", "")),
|
|
563
|
+
gpu=str(data.get("gpu", "")),
|
|
564
|
+
validator=str(data.get("validator", "")),
|
|
565
|
+
n_trajectories=int(data.get("n_trajectories") or 0),
|
|
566
|
+
is_mock=_as_optional_bool(data.get("is_mock")),
|
|
567
|
+
targets=[str(t) for t in (data.get("targets") or [])],
|
|
568
|
+
designers=list(data.get("designers") or []),
|
|
569
|
+
binders=binders,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# ---------------------------------------------------------------------------
|
|
574
|
+
# Headline figures shared by the app and the docs site
|
|
575
|
+
# ---------------------------------------------------------------------------
|
|
576
|
+
@dataclass(frozen=True)
|
|
577
|
+
class Headline:
|
|
578
|
+
"""One published number, with the label and context it must be shown with."""
|
|
579
|
+
|
|
580
|
+
value: str
|
|
581
|
+
label: str
|
|
582
|
+
detail: str
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def headline_stats() -> list[Headline]:
|
|
586
|
+
"""Derive the landing-page numbers from the committed results.
|
|
587
|
+
|
|
588
|
+
Every value is read from ``benchmarks/`` rather than written by hand, so the
|
|
589
|
+
marketing surface cannot overstate what the benchmarks actually show.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
Zero to four :class:`Headline` entries, depending on what is available.
|
|
593
|
+
"""
|
|
594
|
+
stats: list[Headline] = []
|
|
595
|
+
|
|
596
|
+
study = load_study()
|
|
597
|
+
if study is not None:
|
|
598
|
+
top = study.headline
|
|
599
|
+
if top is not None:
|
|
600
|
+
stats.append(
|
|
601
|
+
Headline(
|
|
602
|
+
value=f"rank {top['rank']}",
|
|
603
|
+
label=f"{top['symbol']} surfaced",
|
|
604
|
+
detail=(
|
|
605
|
+
f"{str(top['project']).removeprefix('TCGA-')} · "
|
|
606
|
+
f"of {top['shortlist_size']} candidates · "
|
|
607
|
+
f"log2fc {top['log2fc']:.2f}"
|
|
608
|
+
),
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
if study.n_projects:
|
|
612
|
+
stats.append(
|
|
613
|
+
Headline(
|
|
614
|
+
value=f"{study.n_scored} pairs",
|
|
615
|
+
label=f"across {study.n_projects} TCGA cohorts",
|
|
616
|
+
detail="whole unstratified projects, paired on patient",
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# The fourth card used to be `success@0.65`, with the withdrawal riding in
|
|
621
|
+
# its caption. That is the same shape as a retracted headline on a front
|
|
622
|
+
# page: the number is what a reader takes away, and a caveat underneath does
|
|
623
|
+
# not travel with it. The rate is still reported in full on the Real results
|
|
624
|
+
# page, beside the control that withdrew it.
|
|
625
|
+
designer = load_designer_benchmark()
|
|
626
|
+
if designer is not None and designer.n_designs:
|
|
627
|
+
stats.append(
|
|
628
|
+
Headline(
|
|
629
|
+
value=f"{designer.n_designs} binders",
|
|
630
|
+
label="designed and folded",
|
|
631
|
+
detail=(
|
|
632
|
+
f"on {designer.gpu or 'a free GPU'}, structures committed; "
|
|
633
|
+
"interface confidence is a triage order, not binding evidence"
|
|
634
|
+
),
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# The study's one positive result, which no surface carried until now.
|
|
639
|
+
if study is not None:
|
|
640
|
+
gap = study.indication_gap
|
|
641
|
+
if gap.get("point") is not None and gap.get("low") is not None:
|
|
642
|
+
stats.append(
|
|
643
|
+
Headline(
|
|
644
|
+
value=f"{gap['point']:.2f}",
|
|
645
|
+
label="indication-specific gap",
|
|
646
|
+
detail=(
|
|
647
|
+
f"95% CI {gap['low']:.2f}–{gap['high']:.2f}, excludes zero — "
|
|
648
|
+
"antigens rank higher in the cancer they are actually used in"
|
|
649
|
+
),
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return stats
|