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.
Files changed (106) hide show
  1. bindsight/README.md +44 -0
  2. bindsight/__init__.py +15 -0
  3. bindsight/benchmark/__init__.py +71 -0
  4. bindsight/benchmark/core.py +497 -0
  5. bindsight/benchmark/defaults.py +28 -0
  6. bindsight/benchmark/designer_bench.py +899 -0
  7. bindsight/benchmark/outcomes.py +508 -0
  8. bindsight/benchmark/panel.py +621 -0
  9. bindsight/benchmark/rediscovery.py +1153 -0
  10. bindsight/benchmark/statistics.py +622 -0
  11. bindsight/benchmark/study.py +1153 -0
  12. bindsight/benchmark/study_figures.py +164 -0
  13. bindsight/benchmark/study_report.py +607 -0
  14. bindsight/cli.py +2429 -0
  15. bindsight/config.py +451 -0
  16. bindsight/cost.py +258 -0
  17. bindsight/deg/__init__.py +7 -0
  18. bindsight/deg/pydeseq2_runner.py +285 -0
  19. bindsight/design/__init__.py +23 -0
  20. bindsight/design/_common.py +501 -0
  21. bindsight/design/bindcraft.py +78 -0
  22. bindsight/design/boltzgen.py +79 -0
  23. bindsight/design/developability.py +140 -0
  24. bindsight/design/embeddings.py +165 -0
  25. bindsight/design/prescreen.py +169 -0
  26. bindsight/design/protocol.py +111 -0
  27. bindsight/design/rfdiff_mpnn.py +93 -0
  28. bindsight/epitopes/__init__.py +7 -0
  29. bindsight/epitopes/surface_bind.py +134 -0
  30. bindsight/export/__init__.py +7 -0
  31. bindsight/export/ro_crate.py +595 -0
  32. bindsight/io/__init__.py +19 -0
  33. bindsight/io/cbioportal.py +118 -0
  34. bindsight/io/gdc.py +343 -0
  35. bindsight/io/paths.py +124 -0
  36. bindsight/pipelines/__init__.py +9 -0
  37. bindsight/pipelines/caveats.py +60 -0
  38. bindsight/pipelines/discover.py +1443 -0
  39. bindsight/pipelines/full_run.py +410 -0
  40. bindsight/plugins.py +335 -0
  41. bindsight/provenance/__init__.py +34 -0
  42. bindsight/provenance/append.py +199 -0
  43. bindsight/provenance/fragments.py +216 -0
  44. bindsight/provenance/manifest.py +506 -0
  45. bindsight/rank/__init__.py +7 -0
  46. bindsight/rank/scoring.py +355 -0
  47. bindsight/report/__init__.py +39 -0
  48. bindsight/report/format.py +100 -0
  49. bindsight/report/html.py +645 -0
  50. bindsight/report/showcase.py +653 -0
  51. bindsight/report/templates/report.css +67 -0
  52. bindsight/report/templates/report.html.j2 +420 -0
  53. bindsight/report/theme.py +231 -0
  54. bindsight/report/web/__init__.py +35 -0
  55. bindsight/report/web/app.py +624 -0
  56. bindsight/report/web/static/binder_viewer.js +245 -0
  57. bindsight/report/web/static/bindsight.css +455 -0
  58. bindsight/report/web/static/charts.js +425 -0
  59. bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
  60. bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
  61. bindsight/report/web/static/your_data_check.js +370 -0
  62. bindsight/report/web/templates/base.html.j2 +48 -0
  63. bindsight/report/web/templates/evidence.html.j2 +341 -0
  64. bindsight/report/web/templates/overview.html.j2 +140 -0
  65. bindsight/report/web/templates/runs.html.j2 +125 -0
  66. bindsight/report/web/templates/try.html.j2 +155 -0
  67. bindsight/report/web/templates/your_data.html.j2 +46 -0
  68. bindsight/runners/__init__.py +7 -0
  69. bindsight/runners/colab.py +158 -0
  70. bindsight/runners/job_exec.py +1110 -0
  71. bindsight/runners/kaggle.py +425 -0
  72. bindsight/runners/kaggle_kernel.py +489 -0
  73. bindsight/runners/local_docker.py +154 -0
  74. bindsight/runners/mock.py +167 -0
  75. bindsight/runners/modal_runner.py +199 -0
  76. bindsight/runners/notebook.py +116 -0
  77. bindsight/runners/notebook_content.py +217 -0
  78. bindsight/runners/protocol.py +81 -0
  79. bindsight/runners/source_wheel.py +104 -0
  80. bindsight/runners/tools.py +997 -0
  81. bindsight/structures/__init__.py +7 -0
  82. bindsight/structures/alphafolddb.py +93 -0
  83. bindsight/structures/plddt.py +92 -0
  84. bindsight/structures/topology.py +227 -0
  85. bindsight/surfaceome/__init__.py +42 -0
  86. bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
  87. bindsight/surfaceome/data/surfy_offline.txt +17 -0
  88. bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
  89. bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
  90. bindsight/surfaceome/surfy.py +441 -0
  91. bindsight/targets/__init__.py +7 -0
  92. bindsight/targets/ensembl_uniprot.py +56 -0
  93. bindsight/targets/gtex.py +250 -0
  94. bindsight/targets/open_targets.py +239 -0
  95. bindsight/validate/__init__.py +7 -0
  96. bindsight/validate/af2_ig.py +49 -0
  97. bindsight/validate/boltz2.py +256 -0
  98. bindsight/validate/chai1r.py +42 -0
  99. bindsight/validate/protocol.py +188 -0
  100. bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
  101. bindsight-0.3.5.dist-info/METADATA +495 -0
  102. bindsight-0.3.5.dist-info/RECORD +106 -0
  103. bindsight-0.3.5.dist-info/WHEEL +4 -0
  104. bindsight-0.3.5.dist-info/entry_points.txt +20 -0
  105. bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
  106. bindsight-0.3.5.dist-info/licenses/LICENSE +681 -0
@@ -0,0 +1,621 @@
1
+ # SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+ """The rediscovery evaluation panel: which antigens, in which cohorts, and why.
4
+
5
+ This module is data, deliberately separated from the code that runs the study so
6
+ the panel can be read, cited and criticised on its own.
7
+
8
+ Three design rules govern what is in here, and each replaces something the
9
+ earlier six-cohort study did wrong.
10
+
11
+ **1. Cohorts are never defined by the answer.** The previous study selected
12
+ TCGA-BRCA tumours by PAM50 HER2-enriched subtype and then reported discovering
13
+ ERBB2. ERBB2 is one of the fifty genes the PAM50 centroid classifier is built
14
+ on, so the tumour arm was chosen partly by high ERBB2 expression. EGFR is also a
15
+ PAM50 gene. No cohort here is stratified by any biomarker: the arms are
16
+ **every patient in the project who contributed both a primary tumour and a
17
+ solid-tissue normal**, and nothing about the antigen under test enters that
18
+ choice.
19
+
20
+ That is a subset of the project, and saying so matters. This used to read
21
+ "all primary tumour against all solid-tissue normal", which is not what runs:
22
+ ``study.prepare_cohort`` calls ``matched_pair_cases``, so a tumour with no
23
+ matched normal is not in the contrast. In TCGA-UCEC that leaves 23 pairs. A
24
+ reader who takes the older sentence literally is wrong about the denominator
25
+ of every number downstream of it. The pairing is a design choice about the
26
+ contrast, not a selection on the answer, so the non-circularity argument below
27
+ is unaffected -- but the two are different claims and only one of them was true.
28
+
29
+ Two properties have to hold and they are not the same thing:
30
+
31
+ - *Non-circular* — the stratifier is not derived from the run's own counts.
32
+ - *Not enriched for the answer* — the stratifier is not a measurement of the
33
+ antigen under test on any analyte.
34
+
35
+ Clinical HER2 immunohistochemistry passes the first and fails the second: it is
36
+ a protein assay, so it is not circular, but it still selects for the very
37
+ antigen being sought. Such an arm may only ever appear as a labelled sensitivity
38
+ analysis answering "can the pipeline find HER2 in a HER2-enriched population",
39
+ and it must never produce a recall number. See
40
+ :data:`ADMISSIBLE_STRATIFIER_RULE`.
41
+
42
+ **2. Reachability is decided before the run, not inferred afterwards.** An
43
+ antigen absent from the surfaceome reference can never be surfaced however
44
+ over-expressed it is, and reporting that as a ranking miss would be false. Two
45
+ antigens in this panel are in exactly that position — see
46
+ :data:`UNREACHABLE_NOTE`.
47
+
48
+ **3. Regulatory status is graded, not asserted.** "Clinically validated" is
49
+ doing a lot of work in most benchmark papers. Each pair here carries a
50
+ :attr:`AntigenCohort.tier` so the primary denominator can be pre-registered on
51
+ approved agents alone and the wider sets reported as sensitivity analyses.
52
+
53
+ Every sample count below was verified live against the GDC files API for open
54
+ ``Gene Expression Quantification`` / ``STAR - Counts`` files, and every Ensembl
55
+ gene id was resolved from the UniProt cross-reference for its accession.
56
+ """
57
+
58
+ from __future__ import annotations
59
+
60
+ from dataclasses import dataclass
61
+ from typing import Literal
62
+
63
+ #: The rule governing whether a cohort may be stratified at all. Stated once,
64
+ #: applied identically to every cohort, and never tuned per antigen.
65
+ ADMISSIBLE_STRATIFIER_RULE = (
66
+ "A stratifying variable is admissible only if BOTH hold: (a) it is computable "
67
+ "without the run's own counts matrix, and (b) it is not a measurement of the "
68
+ "antigen under test on any analyte. PAM50 and every other mRNA-cluster subtype "
69
+ "fails (a). Clinical HER2 IHC/FISH passes (a) but fails (b), so it may appear "
70
+ "only as a labelled sensitivity analysis and never in a recall denominator. "
71
+ "Receptor status for a different receptor, histology and stage pass both."
72
+ )
73
+
74
+ #: The instrument-coverage gap this study found, and what closing it took.
75
+ UNREACHABLE_NOTE = (
76
+ "CA9 (Q16790) and STEAP1 (Q9UHE8) are absent from the SURFY surfaceome list — "
77
+ "verified against both the vendored file and the upstream one, so a genuine "
78
+ "reference gap rather than a build error. Under SURFY alone neither could "
79
+ "enter the candidate table at any expression level, and CA9 measures a log2 "
80
+ "fold change of 9.58 in clear-cell kidney, the largest effect anywhere in this "
81
+ "panel. That is an instrument-coverage failure, not a ranking failure, and the "
82
+ "fix was a better instrument: the extended reference adds UniProt's curated "
83
+ "cell-membrane annotations, 1,915 further accessions, and makes both reachable. "
84
+ "Runs with `use_extended_surfaceome=False` reproduce the SURFY-only behaviour, "
85
+ "under which these two are still reported as unreachable rather than as misses."
86
+ )
87
+
88
+ #: Minimum solid-tissue normals for a cohort to enter a recall denominator.
89
+ #: Cohorts below this are still run and published, but excluded from every rate.
90
+ MIN_NORMALS_FOR_POWER = 10
91
+
92
+ #: Solid-tissue-normal STAR-Counts files per TCGA project, verified against the
93
+ #: GDC files API. Projects absent from this mapping have none.
94
+ PROJECT_NORMALS: dict[str, int] = {
95
+ "TCGA-BRCA": 113,
96
+ "TCGA-KIRC": 72,
97
+ "TCGA-LUAD": 59,
98
+ "TCGA-THCA": 59,
99
+ "TCGA-PRAD": 52,
100
+ "TCGA-LUSC": 51,
101
+ "TCGA-LIHC": 50,
102
+ "TCGA-HNSC": 44,
103
+ "TCGA-COAD": 41,
104
+ "TCGA-STAD": 36,
105
+ "TCGA-UCEC": 35,
106
+ "TCGA-KIRP": 32,
107
+ "TCGA-KICH": 25,
108
+ "TCGA-BLCA": 19,
109
+ "TCGA-ESCA": 13,
110
+ "TCGA-READ": 10,
111
+ "TCGA-CHOL": 9,
112
+ "TCGA-GBM": 5,
113
+ "TCGA-PAAD": 4,
114
+ "TCGA-CESC": 3,
115
+ "TCGA-PCPG": 3,
116
+ "TCGA-SARC": 2,
117
+ "TCGA-THYM": 2,
118
+ "TCGA-SKCM": 1,
119
+ }
120
+
121
+ #: Patients contributing BOTH a tumour and a normal sample, per project. This is
122
+ #: what the paired design can actually use, and it is nearly all of the normals —
123
+ #: which is why pairing is the largest free power gain available.
124
+ PROJECT_MATCHED_PAIRS: dict[str, int] = {
125
+ "TCGA-BRCA": 113,
126
+ "TCGA-KIRC": 72,
127
+ "TCGA-THCA": 59,
128
+ "TCGA-LUAD": 58,
129
+ "TCGA-PRAD": 52,
130
+ "TCGA-LUSC": 51,
131
+ "TCGA-LIHC": 50,
132
+ "TCGA-HNSC": 43,
133
+ "TCGA-COAD": 41,
134
+ "TCGA-STAD": 33,
135
+ "TCGA-KIRP": 32,
136
+ "TCGA-KICH": 25,
137
+ "TCGA-UCEC": 23,
138
+ "TCGA-BLCA": 19,
139
+ "TCGA-ESCA": 13,
140
+ "TCGA-READ": 9,
141
+ "TCGA-PAAD": 4,
142
+ }
143
+
144
+ #: Regulatory tier of the targeting agent. The primary denominator is
145
+ #: pre-registered on ``approved`` alone; the wider tiers are sensitivity
146
+ #: analyses, reported with their own denominators.
147
+ Tier = Literal["approved", "late_clinical", "clinical_stage"]
148
+
149
+ #: Whether the cohort can carry a tumour-vs-normal contrast at all.
150
+ Usability = Literal["scored", "underpowered", "no_normals"]
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class AntigenCohort:
155
+ """One antigen evaluated in one indication cohort.
156
+
157
+ Attributes:
158
+ project: TCGA project code; the cohort is the whole project, unstratified.
159
+ symbol: HGNC gene symbol.
160
+ uniprot: UniProt accession, the key bindsight's surfaceome filter uses.
161
+ ensembl: Ensembl gene id, resolved from the UniProt cross-reference.
162
+ agent: The targeting agent that makes this a surface antigen of interest.
163
+ tier: Regulatory standing of that agent.
164
+ usable: Whether this pair enters the scored panel.
165
+ note: What a reader needs in order to interpret the result, including any
166
+ reason to expect a null result a priori. Writing that expectation down
167
+ before the run is what stops a miss being explained away afterwards.
168
+ """
169
+
170
+ project: str
171
+ symbol: str
172
+ uniprot: str
173
+ ensembl: str
174
+ agent: str
175
+ tier: Tier
176
+ usable: Usability
177
+ note: str
178
+
179
+ @property
180
+ def key(self) -> str:
181
+ """Stable identifier for this antigen-cohort pair."""
182
+ return f"{self.project.removeprefix('TCGA-').lower()}_{self.symbol.lower()}"
183
+
184
+ @property
185
+ def n_normals(self) -> int:
186
+ """Solid-tissue normals available in this project."""
187
+ return PROJECT_NORMALS.get(self.project, 0)
188
+
189
+ @property
190
+ def n_matched_pairs(self) -> int:
191
+ """Patients contributing both arms, which the paired design uses."""
192
+ return matched_pairs_for(self.project)
193
+
194
+
195
+ def matched_pairs_for(project: str) -> int:
196
+ """Patients contributing both a tumour and a normal arm in ``project``.
197
+
198
+ One reader for the table. ``AntigenCohort.n_matched_pairs`` wrapped it and
199
+ was never called, while the two places that actually need the number indexed
200
+ ``PROJECT_MATCHED_PAIRS`` directly -- so the accessor could have changed its
201
+ default or its lookup without either caller noticing.
202
+ """
203
+ return PROJECT_MATCHED_PAIRS.get(project, 0)
204
+
205
+
206
+ PANEL: list[AntigenCohort] = [
207
+ # -- Breast ------------------------------------------------------------
208
+ AntigenCohort(
209
+ project="TCGA-BRCA",
210
+ symbol="ERBB2",
211
+ uniprot="P04626",
212
+ ensembl="ENSG00000141736",
213
+ agent="trastuzumab, pertuzumab, T-DXd",
214
+ tier="approved",
215
+ usable="scored",
216
+ note="The unstratified BRCA cohort is the honest contrast. HER2-positive "
217
+ "disease is roughly a fifth of breast cancer, so averaging across the whole "
218
+ "project dilutes the signal; that dilution is the finding, not a flaw to be "
219
+ "engineered away by selecting HER2-enriched tumours.",
220
+ ),
221
+ AntigenCohort(
222
+ project="TCGA-BRCA",
223
+ symbol="TACSTD2",
224
+ uniprot="P09758",
225
+ ensembl="ENSG00000184292",
226
+ agent="sacituzumab govitecan, datopotamab deruxtecan",
227
+ tier="approved",
228
+ usable="scored",
229
+ note="TROP2. Approved in metastatic triple-negative and HR-positive breast "
230
+ "cancer. Broadly expressed across epithelia, so normal breast expression is "
231
+ "not negligible.",
232
+ ),
233
+ # -- Kidney ------------------------------------------------------------
234
+ AntigenCohort(
235
+ project="TCGA-KIRC",
236
+ symbol="CA9",
237
+ uniprot="Q16790",
238
+ ensembl="ENSG00000107159",
239
+ agent="[89Zr]Zr-girentuximab (imaging, under FDA review)",
240
+ tier="clinical_stage",
241
+ usable="scored",
242
+ note="Carbonic anhydrase IX is the defining clear-cell RCC surface antigen. "
243
+ "Measured here at log2fc 9.58 with an adjusted p below floating-point "
244
+ "resolution, the largest effect in the panel. Two notes: the girentuximab "
245
+ "ADJUVANT phase 3 (ARISER) FAILED its primary endpoint, so the live asset is "
246
+ "the diagnostic imaging agent rather than a therapeutic; and CA9 is absent "
247
+ "from the SURFY list, so under that reference alone it was unreachable at any "
248
+ "expression level. It is reachable through the extended reference, and this "
249
+ "pair is why that extension exists.",
250
+ ),
251
+ AntigenCohort(
252
+ project="TCGA-KIRP",
253
+ symbol="MET",
254
+ uniprot="P08581",
255
+ ensembl="ENSG00000105976",
256
+ agent="telisotuzumab vedotin",
257
+ tier="approved",
258
+ usable="scored",
259
+ note="MET is the defining driver of type 1 papillary RCC. A second MET "
260
+ "indication in the panel, giving a within-antigen control against LUAD.",
261
+ ),
262
+ # -- Lung --------------------------------------------------------------
263
+ AntigenCohort(
264
+ project="TCGA-LUAD",
265
+ symbol="MET",
266
+ uniprot="P08581",
267
+ ensembl="ENSG00000105976",
268
+ agent="telisotuzumab vedotin",
269
+ tier="approved",
270
+ usable="scored",
271
+ note="Approved May 2025 for c-Met-overexpressing non-squamous NSCLC with a "
272
+ "companion diagnostic, which is what qualifies MET as a validated surface "
273
+ "antigen rather than only a kinase target.",
274
+ ),
275
+ AntigenCohort(
276
+ project="TCGA-LUAD",
277
+ symbol="TACSTD2",
278
+ uniprot="P09758",
279
+ ensembl="ENSG00000184292",
280
+ agent="datopotamab deruxtecan",
281
+ tier="approved",
282
+ usable="scored",
283
+ note="TROP2 in lung adenocarcinoma. Second indication for TROP2 in the panel.",
284
+ ),
285
+ AntigenCohort(
286
+ project="TCGA-LUAD",
287
+ symbol="ERBB2",
288
+ uniprot="P04626",
289
+ ensembl="ENSG00000141736",
290
+ agent="T-DXd (tumour-agnostic, HER2 IHC3+)",
291
+ tier="approved",
292
+ usable="scored",
293
+ note="Third ERBB2 indication. HER2 in lung adenocarcinoma is usually mutation-"
294
+ "driven rather than over-expressed, so a null here is the expected result and "
295
+ "is recorded as such in advance.",
296
+ ),
297
+ AntigenCohort(
298
+ project="TCGA-LUAD",
299
+ symbol="EGFR",
300
+ uniprot="P00533",
301
+ ensembl="ENSG00000146648",
302
+ agent="cetuximab, necitumumab",
303
+ tier="approved",
304
+ usable="scored",
305
+ note="EGFR drives lung adenocarcinoma through mutation and amplification, not "
306
+ "bulk mRNA over-expression. The published run measured log2fc 0.06 (padj 0.75, "
307
+ "not significant), so a null is the a-priori expectation and is a statement "
308
+ "about the biology, not about the ranker. This note quoted 0.42 until a check "
309
+ "against the artifact caught it -- 0.41 is ERBB2's log2fc in the same cohort, "
310
+ "not EGFR's.",
311
+ ),
312
+ AntigenCohort(
313
+ project="TCGA-LUSC",
314
+ symbol="EGFR",
315
+ uniprot="P00533",
316
+ ensembl="ENSG00000146648",
317
+ agent="necitumumab",
318
+ tier="approved",
319
+ usable="scored",
320
+ note="Necitumumab remains FDA-approved for squamous NSCLC but was rejected by "
321
+ "NICE and has had minimal uptake, so this is an approved agent that is not a "
322
+ "current standard of care. EGFR is more genuinely over-expressed in squamous "
323
+ "than in adenocarcinoma.",
324
+ ),
325
+ # -- Prostate ----------------------------------------------------------
326
+ AntigenCohort(
327
+ project="TCGA-PRAD",
328
+ symbol="FOLH1",
329
+ uniprot="Q04609",
330
+ ensembl="ENSG00000086205",
331
+ agent="[177Lu]Lu-PSMA-617 (Pluvicto)",
332
+ tier="approved",
333
+ usable="scored",
334
+ note="PSMA is highly expressed in prostate cancer but is also abundant in "
335
+ "normal prostate, so the tumour-vs-adjacent-normal fold change is modest. A "
336
+ "designed test of the lineage-antigen blind spot, not a hoped-for hit.",
337
+ ),
338
+ AntigenCohort(
339
+ project="TCGA-PRAD",
340
+ symbol="STEAP1",
341
+ uniprot="Q9UHE8",
342
+ ensembl="ENSG00000164647",
343
+ agent="xaluritamig (phase 3)",
344
+ tier="late_clinical",
345
+ usable="scored",
346
+ note="STEAP1 is prostate-restricted and a strong a-priori positive. Like CA9 it "
347
+ "is absent from the SURFY list, so under that reference alone it was "
348
+ "unreachable by construction. Reachable through the extended reference.",
349
+ ),
350
+ # -- Stomach and oesophagus -------------------------------------------
351
+ AntigenCohort(
352
+ project="TCGA-STAD",
353
+ symbol="CLDN18",
354
+ uniprot="P56856",
355
+ ensembl="ENSG00000066405",
356
+ agent="zolbetuximab (Vyloy)",
357
+ tier="approved",
358
+ usable="scored",
359
+ note="Claudin-18.2, approved October 2024 for CLDN18.2-positive gastric and GEJ "
360
+ "adenocarcinoma. Stated up front: gene-level RNA-seq cannot separate the "
361
+ "CLDN18.2 isoform from CLDN18.1, and normal gastric mucosa expresses CLDN18.2 "
362
+ "highly, so a near-zero bulk fold change is the honest expectation. This pair "
363
+ "is a designed test of the isoform blind spot.",
364
+ ),
365
+ AntigenCohort(
366
+ project="TCGA-STAD",
367
+ symbol="ERBB2",
368
+ uniprot="P04626",
369
+ ensembl="ENSG00000141736",
370
+ agent="trastuzumab, T-DXd",
371
+ tier="approved",
372
+ usable="scored",
373
+ note="Second, independent, non-breast test of ERBB2. This is what makes an "
374
+ "ERBB2 result generalisable rather than a HER2 demonstration.",
375
+ ),
376
+ AntigenCohort(
377
+ project="TCGA-STAD",
378
+ symbol="FGFR2",
379
+ uniprot="P21802",
380
+ ensembl="ENSG00000066468",
381
+ agent="bemarituzumab (phase 3)",
382
+ tier="late_clinical",
383
+ usable="scored",
384
+ note="FORTITUDE-101 met its interim overall-survival endpoint in FGFR2b-"
385
+ "overexpressing first-line gastric cancer, but the benefit attenuated with "
386
+ "longer follow-up. Reported as equivocal late-stage rather than validated.",
387
+ ),
388
+ AntigenCohort(
389
+ project="TCGA-ESCA",
390
+ symbol="CLDN18",
391
+ uniprot="P56856",
392
+ ensembl="ENSG00000066405",
393
+ agent="zolbetuximab (Vyloy)",
394
+ tier="approved",
395
+ usable="scored",
396
+ note="Zolbetuximab's label covers gastro-oesophageal junction adenocarcinoma, "
397
+ "which TCGA-ESCA contains. Only 13 normals, just above the power floor, so its "
398
+ "per-cohort power must be published alongside the result. ESCA mixes squamous "
399
+ "and adenocarcinoma and CLDN18.2 biology is adenocarcinoma-specific, so the "
400
+ "histology split is a pre-registered stratum.",
401
+ ),
402
+ # -- Colorectal --------------------------------------------------------
403
+ AntigenCohort(
404
+ project="TCGA-COAD",
405
+ symbol="EGFR",
406
+ uniprot="P00533",
407
+ ensembl="ENSG00000146648",
408
+ agent="cetuximab, panitumumab",
409
+ tier="approved",
410
+ usable="scored",
411
+ note="Third EGFR indication, enabling a within-antigen comparison across four cancers.",
412
+ ),
413
+ AntigenCohort(
414
+ project="TCGA-COAD",
415
+ symbol="CEACAM5",
416
+ uniprot="P06731",
417
+ ensembl="ENSG00000105388",
418
+ agent="tusamitamab ravtansine, labetuzumab govitecan (phase 2/3)",
419
+ tier="late_clinical",
420
+ usable="scored",
421
+ note="CEA is abundantly expressed in normal colonic epithelium, and the "
422
+ "published run measured log2fc -0.48 (padj 0.045, not significant: the rule requires both an adjusted p below the FDR threshold and an absolute log2 fold change at or above the floor, and this clears only the first). Together with FOLH1 "
423
+ "and CLDN18 this makes three pre-registered lineage-antigen nulls, which is "
424
+ "what turns an anecdote into a measured statement about the blind spot.",
425
+ ),
426
+ # -- Bladder -----------------------------------------------------------
427
+ AntigenCohort(
428
+ project="TCGA-BLCA",
429
+ symbol="NECTIN4",
430
+ uniprot="Q96NY8",
431
+ ensembl="ENSG00000143217",
432
+ agent="enfortumab vedotin (Padcev)",
433
+ tier="approved",
434
+ usable="scored",
435
+ note="Nectin-4 is expressed in the large majority of urothelial carcinomas. "
436
+ "Only 19 normals — the second-weakest retained cohort, after TCGA-ESCA at 13 "
437
+ "— so its power estimate must be "
438
+ "published. Note the TROP2 alternative for this indication is unusable: "
439
+ "sacituzumab govitecan's urothelial approval was withdrawn in October 2024.",
440
+ ),
441
+ # -- Head and neck -----------------------------------------------------
442
+ AntigenCohort(
443
+ project="TCGA-HNSC",
444
+ symbol="EGFR",
445
+ uniprot="P00533",
446
+ ensembl="ENSG00000146648",
447
+ agent="cetuximab",
448
+ tier="approved",
449
+ usable="scored",
450
+ note="The EGFR indication where a positive expectation is most defensible: "
451
+ "EGFR is genuinely over-expressed in head and neck squamous carcinoma.",
452
+ ),
453
+ # -- Liver -------------------------------------------------------------
454
+ AntigenCohort(
455
+ project="TCGA-LIHC",
456
+ symbol="GPC3",
457
+ uniprot="P51654",
458
+ ensembl="ENSG00000147257",
459
+ agent="GPC3 CAR-T and bispecifics (phase 1/2)",
460
+ tier="clinical_stage",
461
+ usable="scored",
462
+ note="Glypican-3 is oncofetal and near-absent in adult liver, making it the "
463
+ "panel's strongest a-priori positive outside ERBB2. No approved GPC3 agent "
464
+ "exists, so it is labelled clinical-stage and excluded from the approved-only "
465
+ "primary denominator.",
466
+ ),
467
+ # -- Endometrium -------------------------------------------------------
468
+ AntigenCohort(
469
+ project="TCGA-UCEC",
470
+ symbol="ERBB2",
471
+ uniprot="P04626",
472
+ ensembl="ENSG00000141736",
473
+ agent="T-DXd (tumour-agnostic accelerated approval)",
474
+ tier="approved",
475
+ usable="scored",
476
+ note="Fourth ERBB2 indication. 35 normals but only 23 matched pairs, the "
477
+ "weakest pairing in the panel, so the paired primary analysis is underpowered "
478
+ "here and this cohort carries the least weight of any in the study. There is "
479
+ "no unpaired secondary: study.prepare_cohort refuses to fall back to an "
480
+ "unpaired design rather than substitute a different contrast, so an "
481
+ "underpowered pair is reported as underpowered instead of being rescued.",
482
+ ),
483
+ AntigenCohort(
484
+ project="TCGA-UCEC",
485
+ symbol="FOLR1",
486
+ uniprot="P15328",
487
+ ensembl="ENSG00000110195",
488
+ agent="mirvetuximab soravtansine (Elahere)",
489
+ tier="approved",
490
+ usable="scored",
491
+ note="Folate receptor alpha has full approval in ovarian cancer with a "
492
+ "companion diagnostic, and endometrial carcinoma is a phase 2 indication. This "
493
+ "is the only way to test FOLR1 at all, because its on-label indication has no "
494
+ "TCGA normals.",
495
+ ),
496
+ ]
497
+
498
+ #: Pairs published for transparency but excluded from every recall denominator.
499
+ #: They are run where a run is possible, and their numbers are reported, but a
500
+ #: rate computed over them would be meaningless.
501
+ EXCLUDED: list[AntigenCohort] = [
502
+ AntigenCohort(
503
+ project="TCGA-PAAD",
504
+ symbol="MSLN",
505
+ uniprot="Q13421",
506
+ ensembl="ENSG00000102854",
507
+ agent="anetumab ravtansine, MSLN CAR-T (phase 1/2)",
508
+ tier="clinical_stage",
509
+ usable="underpowered",
510
+ note="Mesothelin is genuinely over-expressed in pancreatic adenocarcinoma, but "
511
+ "TCGA-PAAD ships only 4 solid-tissue normals. The published run measured "
512
+ "log2fc 2.31 at padj 0.14 — not significant purely for lack of normals, which "
513
+ "is a statement about the cohort and not about the antigen.",
514
+ ),
515
+ AntigenCohort(
516
+ project="TCGA-OV",
517
+ symbol="FOLR1",
518
+ uniprot="P15328",
519
+ ensembl="ENSG00000110195",
520
+ agent="mirvetuximab soravtansine (Elahere)",
521
+ tier="approved",
522
+ usable="no_normals",
523
+ note="The strongest ovarian surface-antigen evidence of any target, and "
524
+ "untestable here: TCGA-OV ships zero solid-tissue normals. Substituting GTEx "
525
+ "ovary would confound the contrast with a cross-study batch effect and must "
526
+ "not be done silently.",
527
+ ),
528
+ AntigenCohort(
529
+ project="TCGA-LAML",
530
+ symbol="CD33",
531
+ uniprot="P20138",
532
+ ensembl="ENSG00000105383",
533
+ agent="gemtuzumab ozogamicin (Mylotarg)",
534
+ tier="approved",
535
+ usable="no_normals",
536
+ note="TCGA-LAML ships zero solid-tissue normals; its normals are blood-derived, "
537
+ "which is not a tissue-of-origin comparator for a leukaemia. A case-level GDC "
538
+ "query appears to return LAML cases with a normal, which is an artefact of "
539
+ "filter combination; the file-level facet returns none.",
540
+ ),
541
+ AntigenCohort(
542
+ project="TCGA-LAML",
543
+ symbol="IL3RA",
544
+ uniprot="P26951",
545
+ ensembl="ENSG00000185291",
546
+ agent="tagraxofusp",
547
+ tier="approved",
548
+ usable="no_normals",
549
+ note="CD123. Same limitation as CD33: no solid-tissue normal reference exists "
550
+ "in TCGA-LAML.",
551
+ ),
552
+ AntigenCohort(
553
+ project="TCGA-OV",
554
+ symbol="CLDN6",
555
+ uniprot="P56747",
556
+ ensembl="ENSG00000184697",
557
+ agent="BNT211 CLDN6 CAR-T (phase 1/2)",
558
+ tier="clinical_stage",
559
+ usable="no_normals",
560
+ note="Oncofetal claudin-6. Same zero-normal limitation as FOLR1 in TCGA-OV. "
561
+ "Note the canonical accession is P56747, not Q14953, which is a different gene.",
562
+ ),
563
+ ]
564
+
565
+ #: Projects run with no antigen attached, purely to calibrate the null models and
566
+ #: audit shortlist composition. Both have ample normals and neither has a surface
567
+ #: antigen with an approved or phase-3 agent, so including them as scored pairs
568
+ #: would be inventing an expectation.
569
+ NULL_CALIBRATION_PROJECTS: tuple[str, ...] = ("TCGA-THCA", "TCGA-KICH")
570
+
571
+
572
+ def scored_panel(*, tiers: tuple[Tier, ...] = ("approved",)) -> list[AntigenCohort]:
573
+ """Pairs entering a recall denominator at the given regulatory tiers.
574
+
575
+ The primary analysis is pre-registered on ``("approved",)``. Wider tiers are
576
+ sensitivity analyses and must be reported with their own denominators rather
577
+ than silently merged.
578
+ """
579
+ return [
580
+ c
581
+ for c in PANEL
582
+ if c.usable == "scored" and c.tier in tiers and c.n_normals >= MIN_NORMALS_FOR_POWER
583
+ ]
584
+
585
+
586
+ def deduplicated_panel(*, tiers: tuple[Tier, ...] = ("approved",)) -> list[AntigenCohort]:
587
+ """One cohort per antigen, so the confidence interval sees independent trials.
588
+
589
+ ERBB2 appears in four indications, EGFR in four and MET and TACSTD2 in two, so
590
+ a plain binomial interval over all pairs would understate uncertainty by
591
+ treating repeats of one antigen as independent evidence. The retained cohort
592
+ for each antigen is the one with the most solid-tissue normals, chosen by that
593
+ rule alone rather than by outcome.
594
+ """
595
+ best: dict[str, AntigenCohort] = {}
596
+ for c in scored_panel(tiers=tiers):
597
+ current = best.get(c.uniprot)
598
+ if current is None or c.n_normals > current.n_normals:
599
+ best[c.uniprot] = c
600
+ return sorted(best.values(), key=lambda c: (c.symbol, c.project))
601
+
602
+
603
+ def projects_in_panel() -> list[str]:
604
+ """Distinct TCGA projects the scored panel touches, plus null-calibration ones."""
605
+ return sorted(
606
+ {c.project for c in PANEL if c.usable == "scored"} | set(NULL_CALIBRATION_PROJECTS)
607
+ )
608
+
609
+
610
+ def reachability(uniprot: str, surfaceome: frozenset[str] | set[str]) -> dict[str, bool]:
611
+ """Whether an antigen could be surfaced at all, decided before the run.
612
+
613
+ Returns the two properties that make a pair reachable by bindsight's
614
+ instrument: a resolvable UniProt accession and membership of the surfaceome
615
+ reference. A pair failing either is an instrument-coverage failure, and
616
+ reporting it as a ranking miss would be false.
617
+ """
618
+ return {
619
+ "uniprot_resolved": bool(uniprot),
620
+ "in_surfaceome": bool(uniprot) and uniprot in surfaceome,
621
+ }