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
bindsight/config.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""User-facing pipeline configuration.
|
|
4
|
+
|
|
5
|
+
The YAML configs under ``examples/`` (and any user-authored config) are
|
|
6
|
+
parsed and validated through these Pydantic v2 models. Validation runs at
|
|
7
|
+
load time so misconfigured runs fail loudly *before* any compute is spent.
|
|
8
|
+
|
|
9
|
+
Add new options here, not by inventing keys in the YAML. Schema additions are
|
|
10
|
+
backwards-compatible if every new field has a default; otherwise bump the
|
|
11
|
+
schema version (we don't track one yet — first add when we ship a breaking
|
|
12
|
+
change).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
# `ValidationInfo` comes from `pydantic`, not from `pydantic_core.core_schema`.
|
|
22
|
+
# The latter is pydantic's private internal surface: it is re-exported here
|
|
23
|
+
# only as an implementation detail, and importing it directly means a
|
|
24
|
+
# pydantic release that moves it breaks this package with no deprecation.
|
|
25
|
+
from pydantic import (
|
|
26
|
+
BaseModel,
|
|
27
|
+
ConfigDict,
|
|
28
|
+
Field,
|
|
29
|
+
field_validator,
|
|
30
|
+
model_validator,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Inputs
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
class GDCSource(BaseModel):
|
|
38
|
+
"""Auto-download spec for a real TCGA cohort from NIH/GDC.
|
|
39
|
+
|
|
40
|
+
When set on :class:`InputsConfig` and the ``counts``/``design`` files are
|
|
41
|
+
absent, ``bindsight discover`` fetches the cohort (STAR - Counts) from the
|
|
42
|
+
GDC open-access API and writes the files before running DESeq2.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(extra="forbid")
|
|
46
|
+
|
|
47
|
+
project: str = Field(..., description="GDC project id, e.g. 'TCGA-BRCA'.")
|
|
48
|
+
n_tumor: int = Field(20, ge=2, description="Number of Primary Tumor samples to fetch.")
|
|
49
|
+
n_normal: int = Field(20, ge=2, description="Number of Solid Tissue Normal samples to fetch.")
|
|
50
|
+
gene_types: list[str] = Field(
|
|
51
|
+
default_factory=lambda: ["protein_coding"],
|
|
52
|
+
description="Gene biotypes to keep ([] keeps all). Protein-coding covers the "
|
|
53
|
+
"surface-antigen targets and keeps DESeq2 tractable.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class InputsConfig(BaseModel):
|
|
58
|
+
"""Input file paths."""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="forbid")
|
|
61
|
+
|
|
62
|
+
counts: Path = Field(
|
|
63
|
+
...,
|
|
64
|
+
description="Path to the gene × sample counts matrix (TSV or TSV.gz). "
|
|
65
|
+
"Rows are gene IDs, columns are sample IDs, values are integer counts.",
|
|
66
|
+
)
|
|
67
|
+
design: Path = Field(
|
|
68
|
+
...,
|
|
69
|
+
description="Path to the sample design TSV. Rows are samples, columns include "
|
|
70
|
+
"at least the factors named in ``params.deg.design_formula``.",
|
|
71
|
+
)
|
|
72
|
+
download: GDCSource | None = Field(
|
|
73
|
+
None,
|
|
74
|
+
description="Optional: auto-download a real TCGA cohort from GDC when the "
|
|
75
|
+
"counts/design files are missing.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Open Targets' tractability vocabulary. The API reports modalities by these
|
|
80
|
+
# two-letter codes only, so any other spelling ("Antibody") intersects nothing and
|
|
81
|
+
# silently drops every candidate — the filter must reject it at load time instead.
|
|
82
|
+
TRACTABLE_MODALITIES: dict[str, str] = {
|
|
83
|
+
"AB": "antibody",
|
|
84
|
+
"OC": "other clinical modality",
|
|
85
|
+
"PR": "PROTAC",
|
|
86
|
+
"SM": "small molecule",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Per-stage parameters
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
class DEGParams(BaseModel):
|
|
94
|
+
"""Differential expression parameters."""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(extra="forbid")
|
|
97
|
+
|
|
98
|
+
design_formula: str = Field(..., description="Patsy formula, e.g. '~ condition'.")
|
|
99
|
+
contrast: list[str] = Field(
|
|
100
|
+
...,
|
|
101
|
+
min_length=3,
|
|
102
|
+
max_length=3,
|
|
103
|
+
description="Three-element list ``[factor, numerator_level, denominator_level]``.",
|
|
104
|
+
)
|
|
105
|
+
fdr_threshold: float = Field(0.05, ge=0.0, le=1.0)
|
|
106
|
+
log2fc_threshold: float = Field(1.0, ge=0.0)
|
|
107
|
+
min_replicates: int = Field(3, ge=2)
|
|
108
|
+
# The low-count filter: only genes with at least ``min_count`` counts in at
|
|
109
|
+
# least ``min_replicates`` samples are retained. 0 disables it. (This comment
|
|
110
|
+
# opened "When True" from when the field was a boolean.)
|
|
111
|
+
min_count: int = Field(10, ge=0)
|
|
112
|
+
# Worker processes pydeseq2 may use. None means "every core", which is the
|
|
113
|
+
# right default on a server and the wrong one on the laptop this pipeline is
|
|
114
|
+
# meant to run on: a multi-hour job that pegs every core makes the machine
|
|
115
|
+
# unusable. Set this to leave headroom.
|
|
116
|
+
n_cpus: int | None = Field(None, ge=1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TargetDiscoveryParams(BaseModel):
|
|
120
|
+
"""Target discovery / surfaceome / specificity parameters."""
|
|
121
|
+
|
|
122
|
+
model_config = ConfigDict(extra="forbid")
|
|
123
|
+
|
|
124
|
+
# Surfaceome filter
|
|
125
|
+
require_surfy: bool = True
|
|
126
|
+
surfy_allow_offline_fallback: bool = Field(
|
|
127
|
+
False,
|
|
128
|
+
description="Read ONLY when ``use_extended_surfaceome`` is False. If "
|
|
129
|
+
"True, fall back to the small bundled SURFY list when the user cache is "
|
|
130
|
+
"empty; production runs should leave it False so the pipeline fails fast "
|
|
131
|
+
"if the cache isn't populated. Under the default "
|
|
132
|
+
"``use_extended_surfaceome: true`` the extended list is loaded instead "
|
|
133
|
+
"and this field is not consulted at all, so setting it there has no "
|
|
134
|
+
"effect — see bindsight/pipelines/discover.py::_resolve_surfaceome.",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Tissue-specificity filter (low expression in vital tissues)
|
|
138
|
+
vital_tissues: list[str] = Field(
|
|
139
|
+
default_factory=lambda: [
|
|
140
|
+
"heart_left_ventricle",
|
|
141
|
+
"brain_cortex",
|
|
142
|
+
"liver",
|
|
143
|
+
"lung",
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
vital_tissue_max_tpm: float = Field(5.0, ge=0.0)
|
|
147
|
+
# A safety gate that cannot measure a gene has not cleared it. When GTEx has
|
|
148
|
+
# no median-TPM entry for a candidate, treat that as unassessed and withhold
|
|
149
|
+
# it from design carry-forward rather than passing it silently. This matches
|
|
150
|
+
# the contract documented on ``GTExTissueExpression.assess``: only a measured
|
|
151
|
+
# ``safe`` verdict may be presented as having cleared the gate. Set False to
|
|
152
|
+
# accept unmeasured candidates, which is a deliberate loosening.
|
|
153
|
+
gtex_require_measured: bool = True
|
|
154
|
+
|
|
155
|
+
# The same contract for Open Targets. ``n_safety_events`` defaults to zero
|
|
156
|
+
# when a lookup returns nothing, so an unreachable API would otherwise
|
|
157
|
+
# promote every affected gene to "no known safety events" and pass it
|
|
158
|
+
# through a filter built to exclude exactly that. Withhold instead, and
|
|
159
|
+
# record the disposition as ``safety_unassessed`` rather than
|
|
160
|
+
# ``fails_safety`` — a network outage is not a finding about the gene. Set
|
|
161
|
+
# False to accept unmeasured candidates, which is a deliberate loosening.
|
|
162
|
+
open_targets_require_measured: bool = True
|
|
163
|
+
|
|
164
|
+
# Structure-confidence (disorder) filter — AlphaFold pLDDT (0-100). pLDDT is
|
|
165
|
+
# always computed and surfaced (mean_plddt column); this only gates carry-
|
|
166
|
+
# forward. 0 disables the gate (default). A typical disorder threshold is
|
|
167
|
+
# ~50 (very low confidence / likely disordered) to ~70 (confident).
|
|
168
|
+
min_mean_plddt: float = Field(0.0, ge=0.0, le=100.0)
|
|
169
|
+
|
|
170
|
+
# Membrane-topology (extracellular-domain) awareness — UniProt. A binder can
|
|
171
|
+
# only reach the extracellular part of a surface protein. When enabled,
|
|
172
|
+
# discovery annotates each candidate's extracellular ranges and targets the
|
|
173
|
+
# ECD for whole-surface design. Off by default (requires UniProt network).
|
|
174
|
+
use_uniprot_topology: bool = False
|
|
175
|
+
# Gate: drop candidates with no annotated extracellular domain (only meaningful
|
|
176
|
+
# when use_uniprot_topology is True). Off by default.
|
|
177
|
+
require_extracellular_domain: bool = False
|
|
178
|
+
|
|
179
|
+
# Cap on how many significant DEGs are carried into target enrichment,
|
|
180
|
+
# ranked by the combined score pi = log2fc x -log10(padj). This is the most
|
|
181
|
+
# consequential filter in discovery: a gene outside the cut never becomes a
|
|
182
|
+
# candidate at all, no matter how surface-exposed or tractable it is. It is
|
|
183
|
+
# a declared parameter rather than a module constant precisely so it lands
|
|
184
|
+
# in the run manifest and can be reported as a gate, not disappear into the
|
|
185
|
+
# ranking.
|
|
186
|
+
enrich_top_k: int = Field(300, ge=1)
|
|
187
|
+
# Whether the surfaceome filter runs BEFORE the enrichment cut.
|
|
188
|
+
#
|
|
189
|
+
# It used to run after, which meant surface antigens competed against every
|
|
190
|
+
# gene in the genome for the enrichment slots. In a real cohort roughly four
|
|
191
|
+
# thousand genes are significant and only a few dozen of the top three
|
|
192
|
+
# hundred are surface proteins, so most of the surfaceome was discarded
|
|
193
|
+
# before it was ever looked at. Measured effect: NECTIN4 in bladder cancer,
|
|
194
|
+
# the target of an approved drug for that exact indication, ranks 259th of
|
|
195
|
+
# 2,104 surface proteins and was still excluded.
|
|
196
|
+
#
|
|
197
|
+
# The old ordering was forced rather than careless: the filter needs UniProt
|
|
198
|
+
# accessions, and those only existed after Open Targets enrichment. The
|
|
199
|
+
# vendored Ensembl-to-accession map removes that dependency, so the cut can
|
|
200
|
+
# now be spent on genes that could actually be targets. The number of Open
|
|
201
|
+
# Targets calls is unchanged.
|
|
202
|
+
surfaceome_prefilter: bool = True
|
|
203
|
+
# Whether to use the extended surfaceome (SURFY plus UniProt's curated
|
|
204
|
+
# cell-membrane annotations) rather than SURFY alone.
|
|
205
|
+
#
|
|
206
|
+
# SURFY is prediction-based and demonstrably incomplete. The rediscovery study
|
|
207
|
+
# measured CA9 at log2fc 9.58 in clear-cell kidney — the largest effect in the
|
|
208
|
+
# panel — and could not surface it at any expression level, because the
|
|
209
|
+
# accession is not in the list. STEAP1 is in the same position. The extension
|
|
210
|
+
# adds 1,915 accessions and makes both reachable.
|
|
211
|
+
#
|
|
212
|
+
# Set False to reproduce a SURFY-only run exactly; SURFY membership is
|
|
213
|
+
# preserved in the extension's source column either way.
|
|
214
|
+
use_extended_surfaceome: bool = True
|
|
215
|
+
|
|
216
|
+
# Open Targets enrichment
|
|
217
|
+
use_open_targets: bool = True
|
|
218
|
+
require_tractable_modality: list[str] = Field(
|
|
219
|
+
default_factory=lambda: ["AB"],
|
|
220
|
+
description="Open Targets tractability modality codes a candidate must have "
|
|
221
|
+
"(see TRACTABLE_MODALITIES). [] disables the filter.",
|
|
222
|
+
)
|
|
223
|
+
max_safety_events: int = Field(5, ge=0)
|
|
224
|
+
|
|
225
|
+
@field_validator("require_tractable_modality")
|
|
226
|
+
@classmethod
|
|
227
|
+
def _check_modalities(cls, v: list[str]) -> list[str]:
|
|
228
|
+
unknown = [m for m in v if m not in TRACTABLE_MODALITIES]
|
|
229
|
+
if unknown:
|
|
230
|
+
valid = ", ".join(f"{code} ({name})" for code, name in TRACTABLE_MODALITIES.items())
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"unknown tractability modality {unknown}: Open Targets reports "
|
|
233
|
+
f"modalities as {valid}. An unrecognised value matches nothing and "
|
|
234
|
+
"would drop every candidate."
|
|
235
|
+
)
|
|
236
|
+
return v
|
|
237
|
+
|
|
238
|
+
# Normal-tissue safety (GTEx) — on-target/off-tumor toxicity. When enabled,
|
|
239
|
+
# candidates whose median expression in any vital tissue exceeds
|
|
240
|
+
# ``vital_tissue_max_tpm`` are flagged ``high_normal_tissue_expression`` and
|
|
241
|
+
# dropped from design. Off by default (requires the GTEx download). Wires the
|
|
242
|
+
# ``vital_tissues`` / ``vital_tissue_max_tpm`` knobs above.
|
|
243
|
+
use_gtex_safety: bool = False
|
|
244
|
+
|
|
245
|
+
# SURFACE-Bind site lookup
|
|
246
|
+
require_surface_bind_site: bool = True
|
|
247
|
+
min_surface_bind_score: float = Field(0.5, ge=0.0, le=1.0)
|
|
248
|
+
|
|
249
|
+
# Pipeline cap
|
|
250
|
+
top_n: int = Field(5, ge=1)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _registered_or_raise(value: str, kind: str, group: str) -> str:
|
|
254
|
+
"""Accept a plugin name the registry can resolve; reject anything else.
|
|
255
|
+
|
|
256
|
+
These fields were `Literal`s, which rejected every plugin registered through
|
|
257
|
+
an entry point -- the mechanism `bindsight.plugins` exists for, and that
|
|
258
|
+
`docs/use-cases.md` documents with a worked example. Dropping the `Literal`
|
|
259
|
+
restored the capability and would have dropped the validation with it, so a
|
|
260
|
+
misspelled bundled name would have reached the loader and failed there with
|
|
261
|
+
a less useful message.
|
|
262
|
+
|
|
263
|
+
Imported inside the function because `bindsight.config` is imported by
|
|
264
|
+
nearly everything and the registry reads installed metadata.
|
|
265
|
+
"""
|
|
266
|
+
from bindsight.plugins import registered
|
|
267
|
+
|
|
268
|
+
known = registered(group)
|
|
269
|
+
if value in known:
|
|
270
|
+
return value
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"unknown {kind} {value!r}; this installation offers "
|
|
273
|
+
f"{', '.join(sorted(known))}. A third-party {kind} is selected by "
|
|
274
|
+
f"registering it in the {group!r} entry-point group."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class DesignParams(BaseModel):
|
|
279
|
+
"""De novo binder design parameters (consumed by the GPU half)."""
|
|
280
|
+
|
|
281
|
+
model_config = ConfigDict(extra="forbid")
|
|
282
|
+
|
|
283
|
+
# Validated against the plugin registry rather than a `Literal`, because a
|
|
284
|
+
# `Literal` rejected any designer registered through the
|
|
285
|
+
# `bindsight.designers` entry-point group -- the mechanism `bindsight.plugins`
|
|
286
|
+
# exists for and `docs/use-cases.md` documents step by step. The loader
|
|
287
|
+
# worked; nothing could reach it.
|
|
288
|
+
designer: str = "rfdiff_mpnn"
|
|
289
|
+
n_trajectories: int = Field(50, ge=1)
|
|
290
|
+
binder_length_min: int = Field(50, ge=20)
|
|
291
|
+
binder_length_max: int = Field(100, ge=20)
|
|
292
|
+
seed: int = 42
|
|
293
|
+
|
|
294
|
+
# GPU the run is costed against. None resolves to the backend's default in
|
|
295
|
+
# bindsight.cost, which is how every run behaved before this field existed;
|
|
296
|
+
# the --cheap profile sets it to "T4" so the estimate reflects the hardware
|
|
297
|
+
# the profile actually targets instead of quoting A100 prices.
|
|
298
|
+
gpu_type: str | None = None
|
|
299
|
+
|
|
300
|
+
# ESM-2 pre-screen: keep only this many designs for validation. None (the
|
|
301
|
+
# default) validates every design, so behaviour is unchanged unless asked
|
|
302
|
+
# for. See bindsight.design.prescreen.
|
|
303
|
+
prescreen_top_k: int | None = Field(None, ge=1)
|
|
304
|
+
|
|
305
|
+
# A field validator fires only when its own field is supplied. Pydantic does
|
|
306
|
+
# not validate defaults, so `DesignParams(binder_length_min=200)` left the
|
|
307
|
+
# max at its default of 100 and this check never ran: the object was built
|
|
308
|
+
# with min > max and the designer was handed an empty length range. An
|
|
309
|
+
# after-validator runs on the assembled model, so it sees the pair however
|
|
310
|
+
# either half arrived.
|
|
311
|
+
@field_validator("designer")
|
|
312
|
+
@classmethod
|
|
313
|
+
def _designer_is_registered(cls, v: str) -> str:
|
|
314
|
+
return _registered_or_raise(v, "designer", "bindsight.designers")
|
|
315
|
+
|
|
316
|
+
@model_validator(mode="after")
|
|
317
|
+
def _check_length_range(self) -> DesignParams:
|
|
318
|
+
if self.binder_length_max < self.binder_length_min:
|
|
319
|
+
raise ValueError(
|
|
320
|
+
f"binder_length_max ({self.binder_length_max}) must be >= "
|
|
321
|
+
f"binder_length_min ({self.binder_length_min})"
|
|
322
|
+
)
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ValidateParams(BaseModel):
|
|
327
|
+
"""Validator parameters."""
|
|
328
|
+
|
|
329
|
+
model_config = ConfigDict(extra="forbid")
|
|
330
|
+
|
|
331
|
+
validator: str = "boltz2"
|
|
332
|
+
|
|
333
|
+
@field_validator("validator")
|
|
334
|
+
@classmethod
|
|
335
|
+
def _validator_is_registered(cls, v: str) -> str:
|
|
336
|
+
return _registered_or_raise(v, "validator", "bindsight.validators")
|
|
337
|
+
|
|
338
|
+
# How many structures the validator draws per binder, and how many of those
|
|
339
|
+
# it diffuses at once.
|
|
340
|
+
#
|
|
341
|
+
# Boltz-2 builds structures by diffusion, so one draw is a sample rather
|
|
342
|
+
# than a measurement. The calibration measured what that costs: refolding
|
|
343
|
+
# twenty sequences moved ipTM by a median of 0.172 and flipped
|
|
344
|
+
# 4 of 20 verdicts at the threshold below. Averaging k draws cuts that spread
|
|
345
|
+
# by sqrt(k) and costs GPU time close to linearly, so the default stays 1
|
|
346
|
+
# and the choice is the user's — but it is now a choice rather than
|
|
347
|
+
# something only the executor could reach.
|
|
348
|
+
#
|
|
349
|
+
# The batch size is not a performance knob: the sampler draws noise shaped
|
|
350
|
+
# by the batch, so a seed consumes the RNG stream differently at batch 1 and
|
|
351
|
+
# batch 5. Sequential by default because the free-tier T4 this project
|
|
352
|
+
# targets peaked at 10,917 MiB of 15,360 on a single draw.
|
|
353
|
+
diffusion_samples: int = Field(1, ge=1)
|
|
354
|
+
max_parallel_samples: int = Field(1, ge=1)
|
|
355
|
+
# Quality bars applied to validated binders. Every row is annotated with
|
|
356
|
+
# ``passes_thresholds`` and a reason regardless of the flag — the bars are
|
|
357
|
+
# descriptive by default. ``apply_thresholds`` additionally moves failing
|
|
358
|
+
# designs out of ``validated.parquet`` and into
|
|
359
|
+
# ``excluded_by_thresholds.parquet`` beside it, reasons attached, so they are
|
|
360
|
+
# set aside rather than silently dropped.
|
|
361
|
+
iptm_threshold: float = Field(0.65, ge=0.0, le=1.0)
|
|
362
|
+
pae_interaction_threshold: float = Field(8.0, ge=0.0)
|
|
363
|
+
apply_thresholds: bool = False
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class RankWeights(BaseModel):
|
|
367
|
+
"""Composite-score weights.
|
|
368
|
+
|
|
369
|
+
The composite is a weighted average over the components *present* for a row
|
|
370
|
+
(missing metrics are excluded, not penalised), so the weights need not sum to
|
|
371
|
+
1.0 — they are renormalised per row. ``developability`` only contributes when
|
|
372
|
+
a binder sequence is available, so it is inert on sequence-less runs.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
model_config = ConfigDict(extra="forbid")
|
|
376
|
+
|
|
377
|
+
log2fc_specificity: float = Field(0.25, ge=0.0, le=1.0)
|
|
378
|
+
iptm: float = Field(0.30, ge=0.0, le=1.0)
|
|
379
|
+
affinity: float = Field(0.30, ge=0.0, le=1.0)
|
|
380
|
+
sequence_recovery: float = Field(0.15, ge=0.0, le=1.0)
|
|
381
|
+
developability: float = Field(0.15, ge=0.0, le=1.0)
|
|
382
|
+
|
|
383
|
+
@model_validator(mode="after")
|
|
384
|
+
def _at_least_one_weight_is_positive(self) -> RankWeights:
|
|
385
|
+
"""Reject an all-zero weighting instead of ranking by nothing.
|
|
386
|
+
|
|
387
|
+
Each weight is independently valid at 0.0 — turning a component off is a
|
|
388
|
+
real choice — but all of them at zero made every composite score NaN and
|
|
389
|
+
produced a "ranking" that was the input order with ranks stapled on. It
|
|
390
|
+
validated, ran, and published a shortlist.
|
|
391
|
+
"""
|
|
392
|
+
if not any(getattr(self, field) > 0.0 for field in type(self).model_fields):
|
|
393
|
+
raise ValueError(
|
|
394
|
+
"every rank weight is zero, so no candidate can score above any "
|
|
395
|
+
"other and the resulting order would be the input order. Set at "
|
|
396
|
+
"least one weight above zero."
|
|
397
|
+
)
|
|
398
|
+
return self
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class RankParams(BaseModel):
|
|
402
|
+
"""Ranking parameters."""
|
|
403
|
+
|
|
404
|
+
model_config = ConfigDict(extra="forbid")
|
|
405
|
+
|
|
406
|
+
weights: RankWeights = Field(default_factory=RankWeights)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# Top-level
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
class StageParams(BaseModel):
|
|
413
|
+
"""Container for per-stage parameter blocks."""
|
|
414
|
+
|
|
415
|
+
model_config = ConfigDict(extra="forbid")
|
|
416
|
+
|
|
417
|
+
deg: DEGParams
|
|
418
|
+
target_discovery: TargetDiscoveryParams = Field(default_factory=TargetDiscoveryParams)
|
|
419
|
+
design: DesignParams = Field(default_factory=DesignParams)
|
|
420
|
+
validate_: ValidateParams = Field(default_factory=ValidateParams, alias="validate")
|
|
421
|
+
rank: RankParams = Field(default_factory=RankParams)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class RunConfig(BaseModel):
|
|
425
|
+
"""Top-level pipeline configuration parsed from a YAML file."""
|
|
426
|
+
|
|
427
|
+
model_config = ConfigDict(extra="forbid")
|
|
428
|
+
|
|
429
|
+
name: str = Field(..., description="Human-readable run label, used in the manifest.")
|
|
430
|
+
out_dir: Path = Field(..., description="Output directory. Will be created.")
|
|
431
|
+
inputs: InputsConfig
|
|
432
|
+
params: StageParams
|
|
433
|
+
backend: str = "colab"
|
|
434
|
+
|
|
435
|
+
@field_validator("backend")
|
|
436
|
+
@classmethod
|
|
437
|
+
def _backend_is_registered(cls, v: str) -> str:
|
|
438
|
+
return _registered_or_raise(v, "backend", "bindsight.runners")
|
|
439
|
+
|
|
440
|
+
# Declared but not consumed by any stage. An example config described it as
|
|
441
|
+
# dropping to a T4 with fewer trajectories; the T4 is now the only GPU the
|
|
442
|
+
# Kaggle runner asks for, and nothing has ever read the flag. It is kept so
|
|
443
|
+
# existing run configs still load, and described accurately rather than
|
|
444
|
+
# advertising behaviour that does not exist.
|
|
445
|
+
cheap_profile: bool = Field(False, description="Reserved. No stage currently reads this.")
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def from_yaml(cls, path: Path | str) -> RunConfig:
|
|
449
|
+
"""Load and validate a config from a YAML file."""
|
|
450
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
451
|
+
return cls.model_validate(yaml.safe_load(text))
|