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,410 @@
1
+ # SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+ """Full-pipeline orchestrator for ``bindsight run``.
4
+
5
+ Drives the complete chain: discover → design → validate → rank → report → export.
6
+
7
+ Each stage is opt-out through this module's ``skip_*`` parameters. There is
8
+ no CLI flag for them. This docstring used to offer a no-design flag in the
9
+ option syntax, which no command exposes, so a reader following it got an
10
+ unknown-option error.
11
+ The orchestrator emits one combined manifest; failures in any stage are
12
+ recorded but downstream stages still attempt to run on whatever upstream
13
+ artifacts are available, so partial successes still produce a useful report.
14
+
15
+ Every stage — including the ones it delegates to ``bindsight.cli`` — appends a
16
+ :class:`~bindsight.provenance.StageRecord`, so the manifest of a full run
17
+ accounts for all seven stages: what ran, what was skipped and why, and the
18
+ digest of everything each stage consumed and produced.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import subprocess
25
+ from collections.abc import Iterable
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from bindsight import __version__
31
+ from bindsight.config import RunConfig
32
+ from bindsight.pipelines import discover as discover_pipeline
33
+ from bindsight.provenance import (
34
+ ContainerRef,
35
+ InputRef,
36
+ Manifest,
37
+ OutputRef,
38
+ StageRecord,
39
+ ToolRef,
40
+ )
41
+ from bindsight.provenance.fragments import artifact_ref
42
+
43
+ LOG = logging.getLogger(__name__)
44
+
45
+ # Backends that execute the design+validation job headlessly, derived from the
46
+ # runner registry rather than copied from it. `colab` only generates a notebook
47
+ # a human runs by hand, so it is the exception and is named as one: a newly
48
+ # registered runner is headless unless it says otherwise.
49
+ #
50
+ # Copied, these silently stopped covering anything new. A backend absent from
51
+ # the set was treated as non-headless and, worse, `_remote_container_backends()`
52
+ # governs whether a local `docker image inspect` is attempted — so a new remote
53
+ # backend had an inspect run against an image that is not on this machine, took
54
+ # the warning path, and completed green with **no container digest in the
55
+ # manifest**.
56
+ _INTERACTIVE_BACKENDS = frozenset({"colab"})
57
+
58
+
59
+ def _all_backends() -> frozenset[str]:
60
+ from bindsight import plugins
61
+
62
+ return frozenset(plugins._FALLBACK["bindsight.runners"])
63
+
64
+
65
+ def _headless_backends() -> frozenset[str]:
66
+ return _all_backends() - _INTERACTIVE_BACKENDS
67
+
68
+
69
+ #: Backends whose GPU half runs in a container this machine cannot inspect: the
70
+ #: image lives on the provider, so no digest can be read locally. Derived as
71
+ #: "remote" — anything that is neither interactive nor executed on this host.
72
+ _LOCAL_BACKENDS = frozenset({"mock", "local_docker"})
73
+
74
+
75
+ def _remote_container_backends() -> frozenset[str]:
76
+ return _headless_backends() - _LOCAL_BACKENDS
77
+
78
+
79
+ _REPO_URL = "https://github.com/mikhaeelatefrizk/bindsight"
80
+
81
+ # How long to wait on `docker image inspect` before giving up on a digest.
82
+ _DOCKER_INSPECT_TIMEOUT_S = 20
83
+
84
+
85
+ @dataclass
86
+ class FullRunResult:
87
+ """Summary of a full ``bindsight run`` invocation."""
88
+
89
+ manifest: Manifest
90
+ discover_ok: bool
91
+ design_ok: bool
92
+ validate_ok: bool
93
+ rank_ok: bool
94
+ report_path: Path | None
95
+ crate_path: Path | None
96
+
97
+
98
+ def _stage_tool(name: str) -> ToolRef:
99
+ """Tool reference for a stage bindsight itself performs."""
100
+ return ToolRef(
101
+ name=name,
102
+ version=__version__,
103
+ license="AGPL-3.0-or-later",
104
+ repo_url=_REPO_URL,
105
+ )
106
+
107
+
108
+ def _inputs(root: Path, refs: Iterable[tuple[Path, str]]) -> list[InputRef]:
109
+ """Digest every artifact that exists; the absent ones are simply not claimed."""
110
+ out: list[InputRef] = []
111
+ for path, role in refs:
112
+ ref: dict[str, Any] | None = artifact_ref(path, role=role, run_root=root)
113
+ if ref is not None:
114
+ out.append(InputRef(**ref))
115
+ return out
116
+
117
+
118
+ def _parquet_has_rows(path: Path) -> bool:
119
+ """True when ``path`` is a parquet file holding at least one row.
120
+
121
+ Existence and size do not answer this: an empty table is a valid parquet
122
+ file of ~1.9 kB, all of it schema and footer. Reading the metadata rather
123
+ than the table keeps this cheap on a large result.
124
+ """
125
+ if not path.exists():
126
+ return False
127
+ try:
128
+ import pyarrow.parquet as pq
129
+
130
+ # ``ParquetFile`` is untyped upstream, so the comparison's result is
131
+ # ``Any``; bool() makes the declared return type true rather than
132
+ # asserted. This is the one mypy error that predates this branch.
133
+ return bool(pq.ParquetFile(path).metadata.num_rows > 0) # type: ignore[no-untyped-call]
134
+ except Exception: # unreadable or not parquet: not a completed stage
135
+ return False
136
+
137
+
138
+ def _outputs(root: Path, refs: Iterable[tuple[Path, str]]) -> list[OutputRef]:
139
+ """Digest every produced artifact that exists (see :func:`_inputs`)."""
140
+ out: list[OutputRef] = []
141
+ for path, role in refs:
142
+ ref: dict[str, Any] | None = artifact_ref(path, role=role, run_root=root)
143
+ if ref is not None:
144
+ out.append(OutputRef(**ref))
145
+ return out
146
+
147
+
148
+ def _container_ref(backend: str) -> ContainerRef | None:
149
+ """Resolve the image digest of the container a backend runs the GPU half in.
150
+
151
+ Returns ``None`` — and says why in the log — whenever the digest cannot be
152
+ read: a backend that is not containerised, a local runner in native mode,
153
+ no Docker on PATH, or an image that was built locally and never pushed (so
154
+ it carries no repository digest). A manifest must not claim an image
155
+ identity it could not verify, and a tag is not one.
156
+ """
157
+ if backend in _remote_container_backends():
158
+ LOG.info("backend %s runs its container remotely; no local digest to record", backend)
159
+ return None
160
+ if backend != "local_docker":
161
+ return None
162
+
163
+ from bindsight.runners.local_docker import LocalDockerRunner
164
+
165
+ runner = LocalDockerRunner()
166
+ if runner.native:
167
+ return None
168
+ image = runner.image
169
+ try:
170
+ # Fixed argv (no shell); the image name comes from our own runner.
171
+ proc = subprocess.run(
172
+ ["docker", "image", "inspect", "--format", "{{index .RepoDigests 0}}", image],
173
+ capture_output=True,
174
+ text=True,
175
+ timeout=_DOCKER_INSPECT_TIMEOUT_S,
176
+ check=True,
177
+ )
178
+ except (OSError, subprocess.SubprocessError) as e:
179
+ LOG.warning("could not read the image digest for %s (%s); not recording one", image, e)
180
+ return None
181
+
182
+ repo_digest = proc.stdout.strip()
183
+ name, _, digest = repo_digest.partition("@")
184
+ if not digest.startswith("sha256:"):
185
+ LOG.warning("image %s has no repository digest (never pushed?); not recording one", image)
186
+ return None
187
+ _, _, tag = image.partition(":")
188
+ return ContainerRef(image=name, tag=tag or None, digest=digest, runtime="docker")
189
+
190
+
191
+ def run(
192
+ config: RunConfig,
193
+ *,
194
+ out_dir: Path | None = None,
195
+ skip_design: bool = False,
196
+ skip_validate: bool = False,
197
+ skip_rank: bool = False,
198
+ skip_report: bool = False,
199
+ skip_export: bool = False,
200
+ ) -> FullRunResult:
201
+ """Run the full pipeline.
202
+
203
+ CPU-only stages always execute; GPU stages run only if the corresponding
204
+ artifacts are already present (i.e. the user has run the GPU half on
205
+ Colab/Modal and pulled results back).
206
+
207
+ Returns a :class:`FullRunResult` describing what completed.
208
+ """
209
+ out = Path(out_dir) if out_dir else Path(config.out_dir)
210
+ manifest_path = out / "run_manifest.jsonld"
211
+
212
+ # ---- 1. Discover (CPU) ----
213
+ LOG.info("== full run: discover ==")
214
+ manifest = discover_pipeline.run(config, out_dir=out)
215
+ discover_ok = all(s.status == "completed" for s in manifest.stages)
216
+
217
+ candidates_path = out / "targets" / "candidates.parquet"
218
+ epitopes_path = out / "epitopes" / "epitopes.parquet"
219
+
220
+ # ---- 2. Design (GPU). Headless backends (mock/local_docker/modal/kaggle)
221
+ # launch the executor now; `colab` just generates notebooks, so we
222
+ # fall back to checking for an already-produced tarball. ----
223
+ design_tarball = out / "design" / "results.tar.gz"
224
+ design_stage = StageRecord(
225
+ name="design",
226
+ tool=_stage_tool(f"bindsight.design.{config.params.design.designer}"),
227
+ container=_container_ref(config.backend),
228
+ inputs=_inputs(out, [(epitopes_path, "epitopes"), (candidates_path, "candidates")]),
229
+ params=config.params.design.model_dump() | {"backend": config.backend},
230
+ )
231
+ if skip_design:
232
+ design_stage.mark_skipped("design skipped by request")
233
+ elif not discover_ok:
234
+ design_stage.mark_skipped("discovery did not complete; nothing to design against")
235
+ elif config.backend not in _headless_backends():
236
+ design_stage.mark_skipped(
237
+ f"backend {config.backend!r} is not headless; run the GPU half by hand"
238
+ )
239
+ else:
240
+ try:
241
+ from bindsight.cli import _launch_design
242
+
243
+ _launch_design(
244
+ out,
245
+ backend=config.backend,
246
+ designer=config.params.design.designer,
247
+ validator=config.params.validate_.validator,
248
+ trajectories=config.params.design.n_trajectories,
249
+ prescreen_top_k=config.params.design.prescreen_top_k,
250
+ )
251
+ except Exception as e:
252
+ LOG.exception("design stage failed")
253
+ design_stage.mark_failed(repr(e))
254
+ if design_stage.status == "running":
255
+ if design_tarball.exists():
256
+ design_stage.mark_completed(
257
+ outputs=_outputs(
258
+ out,
259
+ [
260
+ (design_tarball, "design_results"),
261
+ (out / "design" / "metrics.jsonl", "metrics"),
262
+ ],
263
+ )
264
+ )
265
+ else:
266
+ design_stage.mark_failed("design produced no design/results.tar.gz")
267
+ manifest.append(design_stage)
268
+ manifest.write(manifest_path)
269
+ design_ok = design_stage.status == "completed"
270
+
271
+ # ---- 3. Validate (GPU): materialise validated.parquet from design output. ----
272
+ validated_path = out / "validate" / "validated.parquet"
273
+ validate_stage = StageRecord(
274
+ name="validate",
275
+ tool=_stage_tool(f"bindsight.validate.{config.params.validate_.validator}"),
276
+ inputs=_inputs(out, [(design_tarball, "design_results")]),
277
+ params=config.params.validate_.model_dump(),
278
+ )
279
+ if skip_validate:
280
+ validate_stage.mark_skipped("validation skipped by request")
281
+ else:
282
+ if design_ok:
283
+ try:
284
+ from bindsight.cli import _finalize_validate
285
+
286
+ _finalize_validate(out)
287
+ except Exception as e:
288
+ LOG.exception("validate stage failed")
289
+ validate_stage.mark_failed(repr(e))
290
+ if validate_stage.status == "running":
291
+ # Rows, not bytes. A zero-row parquet still carries its schema
292
+ # footer — 1,878 bytes for this table — so `st_size > 0` passed on a
293
+ # validation that produced nothing, and the manifest recorded a
294
+ # completed stage while the report rendered an empty table as a
295
+ # finished result.
296
+ if _parquet_has_rows(validated_path):
297
+ if not design_ok:
298
+ # The GPU half can run elsewhere (Colab/Modal) with the
299
+ # results dropped back into the run directory; the digest
300
+ # still pins what was ranked, but this run did not make it.
301
+ validate_stage.notes = (
302
+ "adopted validate/validated.parquet produced outside this run"
303
+ )
304
+ validate_stage.mark_completed(
305
+ outputs=_outputs(out, [(validated_path, "validated")])
306
+ )
307
+ elif design_ok:
308
+ validate_stage.mark_failed("validation produced no validate/validated.parquet")
309
+ else:
310
+ validate_stage.mark_skipped("no design output in this run and none supplied")
311
+ manifest.append(validate_stage)
312
+ manifest.write(manifest_path)
313
+ validate_ok = validate_stage.status == "completed"
314
+
315
+ # ---- 4. Rank (CPU; runs only if validate produced output) ----
316
+ ranking_path = out / "rank" / "ranking.parquet"
317
+ rank_stage = StageRecord(
318
+ name="rank",
319
+ tool=_stage_tool("bindsight.rank"),
320
+ inputs=_inputs(out, [(validated_path, "validated"), (candidates_path, "candidates")]),
321
+ params=config.params.rank.model_dump(),
322
+ )
323
+ if skip_rank:
324
+ rank_stage.mark_skipped("ranking skipped by request")
325
+ elif not validate_ok:
326
+ rank_stage.mark_skipped("no validation output to rank")
327
+ else:
328
+ try:
329
+ from bindsight.rank import rank_run
330
+
331
+ rank_run(out, weights=config.params.rank.weights)
332
+ rank_stage.mark_completed(outputs=_outputs(out, [(ranking_path, "ranking")]))
333
+ except Exception as e:
334
+ LOG.exception("rank stage failed")
335
+ rank_stage.mark_failed(repr(e))
336
+ manifest.append(rank_stage)
337
+ manifest.write(manifest_path)
338
+ rank_ok = rank_stage.status == "completed"
339
+
340
+ # ---- 5. Report (CPU; works on whatever artifacts are present) ----
341
+ report_path: Path | None = None
342
+ report_stage = StageRecord(
343
+ name="report",
344
+ tool=_stage_tool("bindsight.report"),
345
+ inputs=_inputs(
346
+ out,
347
+ [
348
+ (out / "deg" / "results.parquet", "deg_table"),
349
+ (candidates_path, "candidates"),
350
+ (epitopes_path, "epitopes"),
351
+ (out / "taxonomy" / "failure_taxonomy.parquet", "failure_taxonomy"),
352
+ (ranking_path, "ranking"),
353
+ ],
354
+ ),
355
+ )
356
+ if skip_report:
357
+ report_stage.mark_skipped("report skipped by request")
358
+ else:
359
+ try:
360
+ from bindsight.report import render_run
361
+
362
+ report_path = render_run(out)
363
+ report_stage.mark_completed(outputs=_outputs(out, [(report_path, "report")]))
364
+ except Exception as e:
365
+ LOG.exception("report stage failed")
366
+ report_stage.mark_failed(repr(e))
367
+ manifest.append(report_stage)
368
+ manifest.write(manifest_path)
369
+
370
+ # ---- 6. Export (CPU; bundles everything) ----
371
+ crate_path: Path | None = None
372
+ # The crate bundles the whole run directory, this manifest included, so the
373
+ # manifest is deliberately not listed as an input: its digest changes the
374
+ # moment this stage is recorded, and a digest that never matched the file
375
+ # is worse than none. The crate's own metadata carries one per packaged file.
376
+ export_inputs: list[tuple[Path, str]] = []
377
+ if report_path is not None:
378
+ export_inputs.append((report_path, "report"))
379
+ export_stage = StageRecord(
380
+ name="export",
381
+ tool=_stage_tool("bindsight.export"),
382
+ inputs=_inputs(out, export_inputs),
383
+ )
384
+ # The crate packages the manifest, so the copy inside it necessarily
385
+ # predates this stage's own completion: the in-flight record is written
386
+ # first, so the crate carries every stage up to the export itself.
387
+ manifest.append(export_stage)
388
+ manifest.write(manifest_path)
389
+ if skip_export:
390
+ export_stage.mark_skipped("export skipped by request")
391
+ else:
392
+ try:
393
+ from bindsight.export import export_ro_crate
394
+
395
+ crate_path = export_ro_crate(out)
396
+ export_stage.mark_completed(outputs=_outputs(out, [(crate_path, "ro_crate")]))
397
+ except Exception as e:
398
+ LOG.exception("export stage failed")
399
+ export_stage.mark_failed(repr(e))
400
+ manifest.write(manifest_path)
401
+
402
+ return FullRunResult(
403
+ manifest=manifest,
404
+ discover_ok=discover_ok,
405
+ design_ok=design_ok,
406
+ validate_ok=validate_ok,
407
+ rank_ok=rank_ok,
408
+ report_path=report_path,
409
+ crate_path=crate_path,
410
+ )