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,67 @@
1
+ /* SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
2
+ * SPDX-License-Identifier: AGPL-3.0-or-later
3
+ *
4
+ * The report's own layer, on top of the shared design system.
5
+ *
6
+ * Everything a reader recognises -- statistics, badges, notes, tables, the
7
+ * absence dash -- comes from bindsight.css, so the report and the served
8
+ * interface are the same product. What is left here is what only a single
9
+ * self-contained file needs: a masthead, an embedded plot, and the row
10
+ * highlight that marks a candidate inside the top N.
11
+ *
12
+ * This file used to be 212 lines and carried a second copy of the tokens, a
13
+ * `.badge` that differed from the app's, and a `.kpi` component with no
14
+ * denominator slot -- which exempted the report's four headline numbers from
15
+ * the rule the rest of the project enforces.
16
+ */
17
+
18
+ main {
19
+ max-width: var(--measure-wide, 72rem);
20
+ margin: 0 auto;
21
+ padding: var(--s-8) var(--s-6) var(--s-16);
22
+ }
23
+
24
+ /* Masthead. The app has a navigation rail here; a file opened from an email
25
+ has nowhere to navigate to, so it gets the run's identity instead. */
26
+ header {
27
+ background: var(--navy-deep);
28
+ color: #eaf1fa;
29
+ padding: var(--s-8) var(--s-6);
30
+ }
31
+ header .inner { max-width: var(--measure-wide, 72rem); margin: 0 auto; }
32
+ header h1 { color: #fff; margin-bottom: var(--s-2); }
33
+ header .subtitle { color: #c7d8ee; margin: 0 0 var(--s-3); font-size: var(--t-sm); }
34
+ header .meta { color: #a8c0dd; font-size: var(--t-xs); }
35
+ header .meta code { background: rgba(255, 255, 255, 0.12); color: #eaf1fa; }
36
+ header a { color: #cfe0f5; }
37
+
38
+ section { margin-bottom: var(--s-12); }
39
+
40
+ /* The volcano plot is embedded as a data URI, so it must not be able to push
41
+ the page wider than the measure. */
42
+ img.plot {
43
+ display: block;
44
+ width: 100%;
45
+ height: auto;
46
+ border: 1px solid var(--rule);
47
+ border-radius: var(--radius);
48
+ background: var(--surface);
49
+ }
50
+
51
+ /* A candidate inside the top N. Backed by a badge in the same row, so the
52
+ tint is a second signal rather than the only one. */
53
+ tbody tr.top { background: #fffbe6; }
54
+
55
+ footer {
56
+ border-top: 1px solid var(--rule);
57
+ padding: var(--s-6);
58
+ color: var(--ink-soft);
59
+ font-size: var(--t-xs);
60
+ text-align: center;
61
+ }
62
+
63
+ @media print {
64
+ header { background: none; color: var(--ink); padding: 0 0 var(--s-4); }
65
+ header h1, header .subtitle, header .meta { color: var(--ink); }
66
+ main { padding: 0; }
67
+ }
@@ -0,0 +1,420 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>bindsight report — {{ run_name }}</title>
6
+ <style>{{ css|safe }}</style>
7
+ </head>
8
+ <body>
9
+ <header>
10
+ <div class="inner">
11
+ <h1>bindsight report</h1>
12
+ <p class="subtitle">RNA-seq → de novo protein binder candidates, with end-to-end provenance.</p>
13
+ <div class="meta">
14
+ run: <strong>{{ run_name }}</strong> &middot;
15
+ id: <code>{{ run_id }}</code> &middot;
16
+ created: {{ created_at }}
17
+ </div>
18
+ </div>
19
+ </header>
20
+
21
+ <main>
22
+
23
+ {% if failed_stages %}
24
+ <section id="incomplete">
25
+ <h2>Incomplete run &mdash; a stage failed</h2>
26
+ <div class="note note--err"><span class="note__glyph" aria-hidden="true">&#10007;</span><div class="note__body"><p style="margin:0">
27
+ <strong>These results are incomplete: a pipeline stage failed, so this is not a
28
+ real negative result.</strong>
29
+ The run stopped at
30
+ {% for f in failed_stages %}<code>{{ f.name }}</code>{% if not loop.last %}, {% endif %}{% endfor %},
31
+ which means the counts and tables below record a crash rather than a
32
+ measurement. Nothing here supports a conclusion &mdash; not even a negative one
33
+ &mdash; until the failure is fixed and the run repeated.
34
+ </p></div></div>
35
+ <div class="table-wrap">
36
+ <table>
37
+ <thead><tr><th>failed stage</th><th>error</th></tr></thead>
38
+ <tbody>
39
+ {% for f in failed_stages %}
40
+ <tr>
41
+ <td><strong>{{ f.name }}</strong></td>
42
+ <td><code>{{ f.error or 'no error detail recorded' }}</code></td>
43
+ </tr>
44
+ {% endfor %}
45
+ </tbody>
46
+ </table>
47
+ </div>
48
+ </section>
49
+ {% endif %}
50
+
51
+ <section>
52
+ <h2>Summary</h2>
53
+ <!-- `.stat`, not a private `.kpi`: the component carries the rule that a
54
+ figure appears with its denominator. Each of these is a survival count,
55
+ and the denominator is the stage before it — which is the shape of the
56
+ funnel and was already on disk. -->
57
+ <div class="row">
58
+ <div class="stat{% if not n_deg %} stat--absent{% endif %}">
59
+ <span class="stat__label">Genes tested</span>
60
+ <span class="stat__value">{{ n_deg or '&mdash;'|safe }}</span>
61
+ <span class="stat__of">rows in the differential-expression table</span>
62
+ </div>
63
+ <div class="stat{% if n_significant is none %} stat--absent{% endif %}">
64
+ <span class="stat__label">Significant DEGs</span>
65
+ <span class="stat__value">{{ n_significant }}</span>
66
+ <span class="stat__of">of {{ n_deg or '&mdash;'|safe }} tested</span>
67
+ </div>
68
+ <div class="stat{% if n_candidates is none %} stat--absent{% endif %}">
69
+ <span class="stat__label">Candidate targets</span>
70
+ <span class="stat__value">{{ n_candidates }}</span>
71
+ <span class="stat__of">of {{ n_significant or '&mdash;'|safe }} significant, after the surfaceome filter</span>
72
+ </div>
73
+ <div class="stat{% if n_epitopes is none %} stat--absent{% endif %}">
74
+ <span class="stat__label">Epitopes</span>
75
+ <span class="stat__value">{{ n_epitopes }}</span>
76
+ <span class="stat__of">of {{ n_candidates or '&mdash;'|safe }} candidates, top-N only</span>
77
+ </div>
78
+ </div>
79
+ {% if failed_stages %}
80
+ <div class="note note--err"><span class="note__glyph" aria-hidden="true">&#10007;</span><div class="note__body"><p style="margin:0">
81
+ The counts above are whatever the crashed run left on disk &mdash; they are not
82
+ measurements. See <a href="#incomplete">Incomplete run</a>.
83
+ </p></div></div>
84
+ {% endif %}
85
+ <div class="note note--info"><span class="note__glyph" aria-hidden="true">i</span><div class="note__body"><p style="margin:0">
86
+ Every number above is one click from its evidence in the
87
+ <a href="#provenance">provenance</a> section. The artifacts on disk
88
+ (Parquet + JSON-LD) are the canonical record &mdash; this HTML is a
89
+ rendered view.
90
+ </p></div></div>
91
+ </section>
92
+
93
+ {% if volcano_b64 %}
94
+ <section>
95
+ <h2>Differential expression</h2>
96
+ <p>
97
+ Volcano plot of the differential expression analysis.
98
+ {% if deg_fdr is not none and deg_log2fc is not none %}
99
+ Significant genes (padj&nbsp;&lt;&nbsp;{{ deg_fdr }} and
100
+ |log2FC|&nbsp;&ge;&nbsp;{{ deg_log2fc }}) are highlighted; the dashed lines
101
+ mark those cutoffs.
102
+ {% else %}
103
+ Significant genes are highlighted. This run did not record the cutoffs it
104
+ applied, so no threshold lines are drawn.
105
+ {% endif %}
106
+ </p>
107
+ <img class="plot" src="data:image/png;base64,{{ volcano_b64 }}" alt="Volcano plot">
108
+ </section>
109
+ {% endif %}
110
+
111
+ <section>
112
+ <h2>Candidate targets</h2>
113
+ <p>
114
+ Genes that survived all filters: significantly differentially expressed,
115
+ surface-exposed (SURFY), antibody-tractable (Open Targets, when enabled),
116
+ and below the safety-event threshold. Top-N rows highlighted.
117
+ </p>
118
+ {% if candidates_table %}
119
+ <div class="table-wrap">
120
+ <table>
121
+ <thead>
122
+ <tr>
123
+ <th>rank</th><th>symbol</th><th>UniProt</th>
124
+ <th>log2FC</th><th>padj</th>
125
+ <th>tractable modalities</th><th>safety events</th>
126
+ <th>has structure</th><th>top-N</th>
127
+ </tr>
128
+ </thead>
129
+ <tbody>
130
+ {% for r in candidates_table %}
131
+ <tr {% if r.rank_in_top_n %}class="top"{% endif %}>
132
+ <td class="num">{{ r.rank }}</td>
133
+ <td><strong>{{ r.symbol or '-' }}</strong></td>
134
+ <td><code>{{ r.uniprot_id or '-' }}</code></td>
135
+ <td class="num">{{ r.log2fc }}</td>
136
+ <td class="num">{{ r.padj }}</td>
137
+ <td>{{ r.tractable_modalities or '-' }}</td>
138
+ <td class="num">{{ r.n_safety_events }}</td>
139
+ <td>
140
+ {% if r.has_alphafold_structure %}<span class="badge badge--ok"><span class="badge__glyph" aria-hidden="true">&#10003;</span>yes</span>
141
+ {% else %}<span class="badge badge--warn"><span class="badge__glyph" aria-hidden="true">!</span>no</span>{% endif %}
142
+ </td>
143
+ <td>
144
+ {% if r.rank_in_top_n %}<span class="badge badge--ok"><span class="badge__glyph" aria-hidden="true">&#10003;</span>top</span>{% endif %}
145
+ </td>
146
+ </tr>
147
+ {% endfor %}
148
+ </tbody>
149
+ </table>
150
+ </div>
151
+ {% elif failed_stages %}
152
+ <div class="note note--err"><span class="note__glyph" aria-hidden="true">&#10007;</span><div class="note__body"><p style="margin:0">
153
+ No candidate table exists because the run did not finish (see
154
+ <a href="#incomplete">above</a>). The filters were never reached, so loosening
155
+ them would be answering a question that was never asked.
156
+ </p></div></div>
157
+ {% elif candidates_unreadable %}
158
+ <div class="note note--err"><span class="note__glyph" aria-hidden="true">&#10007;</span><div class="note__body"><p style="margin:0">
159
+ The candidate table could not be read, so how many candidates survived is
160
+ unknown. This is not the same as none surviving: nothing was measured.
161
+ </p></div></div>
162
+ {% else %}
163
+ <div class="note"><span class="note__glyph" aria-hidden="true">&#8212;</span><div class="note__body"><p style="margin:0">No candidates survived the filters. Loosen thresholds in the config and re-run.</p></div></div>
164
+ {% endif %}
165
+ </section>
166
+
167
+ <section>
168
+ <h2>Epitopes (top-N targets)</h2>
169
+ <p>
170
+ For each top-N target, the AlphaFoldDB structure path and SURFACE-Bind
171
+ epitope status. <code>surface_bind_site</code> = a vendored targetable site
172
+ (focused design); <code>no_surface_bind_site</code> /
173
+ <code>surface_bind_not_configured</code> = whole-surface design (the
174
+ SURFACE-Bind data tree had no site for this protein, or wasn't vendored
175
+ &mdash; see <code>data/surface_bind/</code> to populate it).
176
+ <code>surface_bind_lookup_failed</code> is different from both: the lookup
177
+ <em>errored</em>, so nothing is known about this protein either way. It is
178
+ not a measured absence of a site.
179
+ </p>
180
+ {% if epitopes_table %}
181
+ <div class="table-wrap">
182
+ <table>
183
+ <thead>
184
+ <tr><th>symbol</th><th>UniProt</th><th>structure</th><th>site id</th><th>status</th></tr>
185
+ </thead>
186
+ <tbody>
187
+ {% for e in epitopes_table %}
188
+ <tr>
189
+ <td><strong>{{ e.symbol or '-' }}</strong></td>
190
+ <td><code>{{ e.uniprot_id or '-' }}</code></td>
191
+ <td><code>{{ e.structure_path or '-' }}</code></td>
192
+ <td>{{ e.site_id or '-' }}</td>
193
+ <td>
194
+ {% if e.epitope_status == 'surface_bind_site' %}
195
+ <span class="badge badge--ok"><span class="badge__glyph" aria-hidden="true">&#10003;</span>{{ e.epitope_status }}</span>
196
+ {% else %}
197
+ <span class="badge badge--warn"><span class="badge__glyph" aria-hidden="true">!</span>{{ e.epitope_status }}</span>
198
+ {% endif %}
199
+ </td>
200
+ </tr>
201
+ {% endfor %}
202
+ </tbody>
203
+ </table>
204
+ </div>
205
+ {% else %}
206
+ <div class="note"><span class="note__glyph" aria-hidden="true">&#8212;</span><div class="note__body"><p style="margin:0">No top-N epitopes to display.</p></div></div>
207
+ {% endif %}
208
+ </section>
209
+
210
+ <section>
211
+ <h2>Designed binders</h2>
212
+ <p>
213
+ {% if n_binders > binders_table|length %}The {{ binders_table|length }}
214
+ best-scoring of the {{ n_binders }} designs the run produced{% else %}Every
215
+ design the run produced{% endif %}, ranked by the composite score. <code>ipTM</code>
216
+ is the validator's interface confidence (higher is better) and
217
+ <code>PAE-int</code> the predicted interface error in &Aring;ngstr&ouml;m
218
+ (lower is better). <code>passes</code> records each design against the
219
+ configured quality bars; nothing is dropped on their account unless
220
+ <code>apply_thresholds</code> is set.
221
+ </p>
222
+ {% if binders_table %}
223
+ <div class="table-wrap">
224
+ <table>
225
+ <thead>
226
+ <tr>
227
+ <th>rank</th><th>binder</th><th>target</th><th>ipTM</th>
228
+ <th>PAE-int</th><th>score</th><th>passes</th>
229
+ {% if include_binders %}<th>sequence</th>{% endif %}
230
+ </tr>
231
+ </thead>
232
+ <tbody>
233
+ {% for b in binders_table %}
234
+ <tr>
235
+ <td>{{ b.rank or '-' }}</td>
236
+ <td><code>{{ b.binder_id or '-' }}</code></td>
237
+ <td><strong>{{ b.symbol or b.target_uniprot or '-' }}</strong></td>
238
+ <td>{{ b.iptm or '-' }}</td>
239
+ <td>{{ b.pae_interaction or '-' }}</td>
240
+ <td>{{ b.score or '-' }}</td>
241
+ <td>
242
+ {% if b.passes_thresholds == 'pass' %}
243
+ <span class="badge badge--ok"><span class="badge__glyph" aria-hidden="true">&#10003;</span>pass</span>
244
+ {% elif b.passes_thresholds == 'fail' %}
245
+ <span class="badge badge--warn"><span class="badge__glyph" aria-hidden="true">!</span>fail</span>
246
+ {% else %}
247
+ <span class="badge badge--info">{{ b.passes_thresholds or '-' }}</span>
248
+ {% endif %}
249
+ </td>
250
+ {% if include_binders %}<td><code>{{ b.sequence or '-' }}</code></td>{% endif %}
251
+ </tr>
252
+ {% endfor %}
253
+ </tbody>
254
+ </table>
255
+ </div>
256
+ {% if structure_ids %}
257
+ <h3>The complexes themselves</h3>
258
+ <p class="small muted">
259
+ Drawn from the mmCIF carried inside this file &mdash; no server, no network
260
+ request, nothing to install.
261
+ {% if structures_omitted %}
262
+ The {{ structure_ids|length }} that fit are embedded, best-ranked first;
263
+ {{ structures_omitted }} more {{ 'are' if structures_omitted != 1 else 'is' }}
264
+ in <code>design/_targets/</code>, left out so this stays a file you can send.
265
+ {% endif %}
266
+ </p>
267
+ <div class="field viewer-picker" style="max-width:32rem">
268
+ <label for="binder-pick">Design</label>
269
+ <select id="binder-pick" data-binder-picker>
270
+ {% for bid in structure_ids %}
271
+ <option value="{{ bid }}">{{ bid }}</option>
272
+ {% endfor %}
273
+ </select>
274
+ </div>
275
+ <div id="viewer" class="viewer" role="img" data-binder-viewer
276
+ aria-label="Three-dimensional view of the predicted binder-target complex">
277
+ <p class="viewer__fallback small muted" role="alert">
278
+ The structure viewer needs JavaScript. The complexes are in this file
279
+ either way, as mmCIF.
280
+ </p>
281
+ </div>
282
+ <div class="viewer-legend">
283
+ <span><span class="swatch" style="background:#9aa5b1"></span>
284
+ chain&nbsp;<strong>T</strong> &mdash; the target</span>
285
+ <span><span class="swatch" style="background:#0f7d73"></span>
286
+ chain&nbsp;<strong>B</strong> &mdash; the designed binder</span>
287
+ </div>
288
+ {% elif structures_omitted %}
289
+ <h3>The complexes themselves</h3>
290
+ <p class="small muted">
291
+ None are embedded here. {{ structures_omitted }} predicted complex{{ 'es' if structures_omitted != 1 else '' }}
292
+ would have pushed this file past the size it will carry, so
293
+ {{ 'they are' if structures_omitted != 1 else 'it is' }} in
294
+ <code>design/_targets/</code> instead. Open
295
+ {{ 'them' if structures_omitted != 1 else 'it' }} in any structure viewer,
296
+ or re-render with a larger budget. Saying nothing here would have left this
297
+ section silently missing.
298
+ </p>
299
+ {% endif %}
300
+ {% if not include_binders %}
301
+ <div class="note"><span class="note__glyph" aria-hidden="true">&#8212;</span><div class="note__body"><p style="margin:0">
302
+ Re-render with <code>--include-binders</code> to embed each designed sequence.
303
+ </p></div></div>
304
+ {% endif %}
305
+ {% else %}
306
+ <div class="note"><span class="note__glyph" aria-hidden="true">&#8212;</span><div class="note__body"><p style="margin:0">
307
+ No designed binders in this run. Run <code>bindsight design</code> and
308
+ <code>bindsight rank</code> to produce them.
309
+ </p></div></div>
310
+ {% endif %}
311
+ </section>
312
+
313
+ {% if taxonomy_counts %}
314
+ <section>
315
+ <h2>Why candidates dropped (negative-result taxonomy)</h2>
316
+ <p>
317
+ Each of the {{ n_taxonomy }} differentially-expressed genes gets exactly one
318
+ disposition explaining why it did or didn't become a surfaced candidate &mdash;
319
+ so the failure modes are auditable rather than discarded. The counts sum to the
320
+ gene total.
321
+ </p>
322
+ <div class="table-wrap">
323
+ <table>
324
+ <thead><tr><th>disposition</th><th>genes</th><th>share</th></tr></thead>
325
+ <tbody>
326
+ {% for d in taxonomy_counts %}
327
+ <tr {% if d.disposition == 'surfaced' %}class="top"{% endif %}>
328
+ <td>
329
+ {% if d.disposition == 'surfaced' %}<span class="badge badge--ok"><span class="badge__glyph" aria-hidden="true">&#10003;</span>{{ d.disposition }}</span>
330
+ {% else %}<code>{{ d.disposition }}</code>{% endif %}
331
+ </td>
332
+ <td class="num">{{ d.count }}</td>
333
+ <td class="num">{{ d.pct }}</td>
334
+ </tr>
335
+ {% endfor %}
336
+ </tbody>
337
+ </table>
338
+ </div>
339
+ <div class="note"><span class="note__glyph" aria-hidden="true">&#8212;</span><div class="note__body"><p style="margin:0">Full per-gene table: <code>taxonomy/failure_taxonomy.parquet</code>.</p></div></div>
340
+ </section>
341
+ {% endif %}
342
+
343
+ <section id="provenance">
344
+ <h2>Provenance</h2>
345
+ <p>
346
+ Every stage's tool, version, license, inputs (with SHA-256), and outputs
347
+ (with SHA-256). The full PROV-O JSON-LD is in
348
+ <code>run_manifest.jsonld</code> at the run root.
349
+ </p>
350
+ {% if stages %}
351
+ <div class="table-wrap">
352
+ <table>
353
+ <thead>
354
+ <tr><th>stage</th><th>tool</th><th>version</th><th>license</th><th>status</th><th>notes</th></tr>
355
+ </thead>
356
+ <tbody>
357
+ {% for s in stages %}
358
+ <tr>
359
+ <td><strong>{{ s.name }}</strong></td>
360
+ <td><code>{{ s.tool.name }}</code></td>
361
+ <td>{{ s.tool.version }}</td>
362
+ <td>{{ s.tool.license }}</td>
363
+ <td>
364
+ {% if s.status == 'completed' %}<span class="badge badge--ok"><span class="badge__glyph" aria-hidden="true">&#10003;</span>{{ s.status }}</span>
365
+ {% elif s.status == 'failed' %}<span class="badge badge--err">{{ s.status }}</span>
366
+ {% else %}<span class="badge badge--info">{{ s.status }}</span>{% endif %}
367
+ </td>
368
+ <td>{{ s.notes or '' }}</td>
369
+ </tr>
370
+ {% endfor %}
371
+ </tbody>
372
+ </table>
373
+ </div>
374
+ {% else %}
375
+ <div class="note"><span class="note__glyph" aria-hidden="true">&#8212;</span><div class="note__body"><p style="margin:0">No manifest stages found.</p></div></div>
376
+ {% endif %}
377
+ </section>
378
+
379
+ <section id="limitations">
380
+ <h2>Limitations</h2>
381
+ <p>The honest scope of expression-based discovery — read before trusting a candidate:</p>
382
+ <ul>
383
+ {% for lim in limitations %}
384
+ <li><strong>{{ lim.title }}.</strong> {{ lim.body }}</li>
385
+ {% endfor %}
386
+ </ul>
387
+ </section>
388
+
389
+ <section>
390
+ <h2>How to read this report</h2>
391
+ <ul>
392
+ <li><strong>Candidate targets</strong> are ranked by the combined
393
+ differential-expression score &pi; = log2FC &times; &minus;log10(padj).
394
+ Structure availability does not affect that rank: it selects which of
395
+ the top-ranked candidates can be carried into the GPU half, because a
396
+ design needs a structure to design against.</li>
397
+ <li><strong>Epitopes</strong> become the input to <code>bindsight design</code>
398
+ once SURFACE-Bind data is populated.</li>
399
+ <li><strong>Provenance</strong> shows what ran. Every entry's
400
+ SHA-256 hashes are in <code>run_manifest.jsonld</code>.</li>
401
+ <li><strong>Limitations</strong> states what expression-based discovery can and
402
+ cannot establish — candidates are hypotheses, not validated targets.</li>
403
+ </ul>
404
+ </section>
405
+
406
+ <footer>
407
+ Generated by <a href="https://github.com/mikhaeelatefrizk/bindsight"><code>bindsight</code></a>.
408
+ Cite the upstream tools you used (see <code>LICENSING.md</code>).
409
+ </footer>
410
+
411
+ </main>
412
+ {% if structure_ids %}
413
+ {# Inlined, not linked: this file has to work with no server and no siblings.
414
+ The viewer is the same one bindsight ui and the documentation site load. #}
415
+ <script>{{ viewer_lib|safe }}</script>
416
+ <script>window.BINDSIGHT_STRUCTURES = {{ structures_json|safe }};</script>
417
+ <script>{{ viewer_js|safe }}</script>
418
+ {% endif %}
419
+ </body>
420
+ </html>