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,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
|
+
)
|