bend2 1.0.0__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.
- bend2/__init__.py +12 -0
- bend2/__main__.py +3 -0
- bend2/_combine.py +191 -0
- bend2/_data/atlas-candidate-activities.parquet +0 -0
- bend2/_data/atlas-population.parquet +0 -0
- bend2/_data/atlas-programs.csv +17 -0
- bend2/_data/atlas-source-activities.parquet +0 -0
- bend2/_data/gtex-genes.tsv +19283 -0
- bend2/_data/gtex-targets.npz +0 -0
- bend2/_data/prior-panel.parquet +0 -0
- bend2/_data/prior-query-seeds.json +2063 -0
- bend2/_data/siraj-numeric-reference.npz +0 -0
- bend2/_data/siraj_selective.parquet +0 -0
- bend2/_examples/custom.py +26 -0
- bend2/_examples/dnabert2.py +10 -0
- bend2/_examples/huggingface.py +17 -0
- bend2/_examples/ntv3_prior.py +12 -0
- bend2/_linear.py +141 -0
- bend2/_rescoring/__init__.py +1 -0
- bend2/_rescoring/interventions.py +257 -0
- bend2/_rescoring/panels.py +272 -0
- bend2/_tasks/__init__.py +14 -0
- bend2/_tasks/_siraj.py +33 -0
- bend2/_tasks/atlas_profile.py +189 -0
- bend2/_tasks/background.py +56 -0
- bend2/_tasks/cell_selective.py +113 -0
- bend2/_tasks/context.py +28 -0
- bend2/_tasks/opensplice.py +33 -0
- bend2/_tasks/prior.py +90 -0
- bend2/_tasks/repair_percentile.py +72 -0
- bend2/_tasks/tissue_identification.py +182 -0
- bend2/assets.json +147 -0
- bend2/cache.py +212 -0
- bend2/catalog.py +52 -0
- bend2/cli.py +444 -0
- bend2/datasets.py +565 -0
- bend2/github.py +384 -0
- bend2/huggingface.py +183 -0
- bend2/model.py +215 -0
- bend2/planning.py +251 -0
- bend2/readouts.py +180 -0
- bend2/reference_models.py +433 -0
- bend2/results.py +186 -0
- bend2/reviews.py +365 -0
- bend2/runner.py +362 -0
- bend2/submissions.py +632 -0
- bend2-1.0.0.dist-info/METADATA +145 -0
- bend2-1.0.0.dist-info/RECORD +53 -0
- bend2-1.0.0.dist-info/WHEEL +5 -0
- bend2-1.0.0.dist-info/entry_points.txt +2 -0
- bend2-1.0.0.dist-info/licenses/LICENSE +21 -0
- bend2-1.0.0.dist-info/licenses/docs/data.md +107 -0
- bend2-1.0.0.dist-info/top_level.txt +1 -0
bend2/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""BEND 2.0: a benchmark for DNA models."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.0"
|
|
4
|
+
|
|
5
|
+
from bend2.catalog import tasks
|
|
6
|
+
from bend2.model import Model
|
|
7
|
+
from bend2.planning import plan
|
|
8
|
+
from bend2.results import Run
|
|
9
|
+
from bend2.runner import evaluate
|
|
10
|
+
from bend2._combine import combine
|
|
11
|
+
|
|
12
|
+
__all__ = ["Model", "Run", "evaluate", "plan", "combine", "tasks"]
|
bend2/__main__.py
ADDED
bend2/_combine.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Combine task results from separate evaluations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
from contextlib import ExitStack
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from filelock import FileLock
|
|
14
|
+
|
|
15
|
+
from bend2 import __version__
|
|
16
|
+
from bend2.catalog import CATALOG
|
|
17
|
+
from bend2.datasets import task_identity
|
|
18
|
+
from bend2.results import Run, digest, write_json
|
|
19
|
+
from bend2.submissions import _PROTOCOLS, _TABLES, _data_summary, _subset
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _hash(value):
|
|
23
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _merge_model(results):
|
|
27
|
+
models = [result["identity_spec"]["model"] for result in results]
|
|
28
|
+
merged = copy.deepcopy(models[0])
|
|
29
|
+
revision = merged.get("revision", "").strip()
|
|
30
|
+
if not revision or revision.lower() in {"main", "master", "latest", "head", "default"}:
|
|
31
|
+
raise ValueError("Declare a stable model revision before combining independent runs")
|
|
32
|
+
for model in models[1:]:
|
|
33
|
+
for field in merged:
|
|
34
|
+
if field != "adapter" and model[field] != merged[field]:
|
|
35
|
+
raise ValueError(f"Cannot combine runs with different model {field}")
|
|
36
|
+
for capability in merged["capabilities"]:
|
|
37
|
+
if (
|
|
38
|
+
merged["adapter"][capability]["inference_sha256"]
|
|
39
|
+
!= model["adapter"][capability]["inference_sha256"]
|
|
40
|
+
):
|
|
41
|
+
raise ValueError(f"Cannot combine different {capability} adapter implementations")
|
|
42
|
+
return merged
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check_task(task, row):
|
|
46
|
+
"""Check task protocols, data identities and evidence files."""
|
|
47
|
+
_subset(_PROTOCOLS[task], row["details"], f"{task}.protocol")
|
|
48
|
+
_subset(task_identity(task), _data_summary(row["data"]), f"{task}.data")
|
|
49
|
+
required = [f"{task}/{table}.parquet" for table in _TABLES[task]]
|
|
50
|
+
required.append(f"{task}/protocol.json")
|
|
51
|
+
if any(path not in row["files"] for path in required):
|
|
52
|
+
raise ValueError(f"Missing prediction/protocol evidence for {task}")
|
|
53
|
+
for relative in row["files"]:
|
|
54
|
+
if Path(relative).parts[0] != task or "\\" in relative:
|
|
55
|
+
raise ValueError(f"Unexpected evidence path for {task}: {relative}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _copy_verified(source, destination, expected):
|
|
59
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
with source.open("rb") as reader, destination.open("xb") as writer:
|
|
61
|
+
shutil.copyfileobj(reader, writer, length=1024 * 1024)
|
|
62
|
+
if digest(destination) != expected:
|
|
63
|
+
raise ValueError(f"Source artifact changed while combining: {source}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def combine(runs, output) -> Run:
|
|
67
|
+
"""Combine task results from the same model.
|
|
68
|
+
|
|
69
|
+
``runs`` is a sequence of :class:`Run` objects or result-directory paths.
|
|
70
|
+
``output`` must not exist. Duplicate tasks must have identical results.
|
|
71
|
+
The new directory includes the original environments, adapters and benchmark
|
|
72
|
+
source. To add tasks later, evaluate them separately and combine again.
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(runs, (str, Path, Run)):
|
|
75
|
+
raise TypeError("Pass a list of at least two result directories or Run objects")
|
|
76
|
+
sources = [run if isinstance(run, Run) else Run(run) for run in runs]
|
|
77
|
+
if len(sources) < 2:
|
|
78
|
+
raise ValueError("Choose at least two runs to combine")
|
|
79
|
+
if len({run.directory for run in sources}) != len(sources):
|
|
80
|
+
raise ValueError("The same result directory was supplied more than once")
|
|
81
|
+
destination = Path(output).expanduser().resolve()
|
|
82
|
+
if destination.exists():
|
|
83
|
+
raise FileExistsError(f"Combined output already exists: {destination}; choose a new directory")
|
|
84
|
+
with ExitStack() as stack:
|
|
85
|
+
for run in sorted(sources, key=lambda item: str(item.directory)):
|
|
86
|
+
if not (run.directory / "result.json").is_file():
|
|
87
|
+
raise ValueError(f"No BEND result.json in {run.directory}")
|
|
88
|
+
stack.enter_context(FileLock(run.directory / ".run.lock", timeout=0))
|
|
89
|
+
results = [run.validate() for run in sources]
|
|
90
|
+
merged_model = _merge_model(results)
|
|
91
|
+
rows, owners, origins = {}, {}, []
|
|
92
|
+
for run, result in zip(sources, results, strict=True):
|
|
93
|
+
source_result_hash = digest(run.directory / "result.json")
|
|
94
|
+
# The manifest hash distinguishes separate executions of the same model.
|
|
95
|
+
origin_key = source_result_hash
|
|
96
|
+
origin = {
|
|
97
|
+
"run_identity": result["identity"],
|
|
98
|
+
"result_sha256": source_result_hash,
|
|
99
|
+
"manifest": f"implementation/origins/{origin_key}/result.json",
|
|
100
|
+
"created_at": result["created_at"],
|
|
101
|
+
"status": result["status"],
|
|
102
|
+
"package_version": result["version"],
|
|
103
|
+
"environment": result.get("environment", {}),
|
|
104
|
+
"tasks": list(result["tasks"]),
|
|
105
|
+
}
|
|
106
|
+
origins.append(origin)
|
|
107
|
+
for task, row in result["tasks"].items():
|
|
108
|
+
if CATALOG[task][1] not in merged_model["capabilities"]:
|
|
109
|
+
raise ValueError(f"Model does not declare the capability required for {task}")
|
|
110
|
+
if task in merged_model.get("unsupported_tasks", {}):
|
|
111
|
+
raise ValueError(f"Model declares evaluated task {task} unsupported")
|
|
112
|
+
_check_task(task, row)
|
|
113
|
+
if task in rows:
|
|
114
|
+
keys = ("metric", "details", "data", "files")
|
|
115
|
+
if any(rows[task][key] != row[key] for key in keys):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Conflicting results for {task}; combine complementary runs or "
|
|
118
|
+
"identical evidence, never selected scores"
|
|
119
|
+
)
|
|
120
|
+
rows[task]["origins"].append(copy.deepcopy(origin))
|
|
121
|
+
else:
|
|
122
|
+
rows[task] = copy.deepcopy(row)
|
|
123
|
+
rows[task].pop("invocation", None) # Resolved in each original manifest below.
|
|
124
|
+
rows[task]["origins"] = [copy.deepcopy(origin)]
|
|
125
|
+
owners[task] = run
|
|
126
|
+
if not rows:
|
|
127
|
+
raise ValueError("No completed tasks are available to combine")
|
|
128
|
+
requested = list(
|
|
129
|
+
dict.fromkeys(task for result in results for task in result["requested_tasks"])
|
|
130
|
+
)
|
|
131
|
+
skipped = {}
|
|
132
|
+
for result in results:
|
|
133
|
+
for task, reason in result.get("skipped", {}).items():
|
|
134
|
+
if task not in rows:
|
|
135
|
+
if task in skipped and skipped[task] != reason:
|
|
136
|
+
raise ValueError(f"Conflicting skipped-task explanations for {task}")
|
|
137
|
+
skipped[task] = reason
|
|
138
|
+
pending = set(requested) - set(rows) - set(skipped)
|
|
139
|
+
identity_spec = {
|
|
140
|
+
"model": merged_model,
|
|
141
|
+
"combined_from": [
|
|
142
|
+
{key: item[key] for key in ("run_identity", "result_sha256")}
|
|
143
|
+
for item in origins
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
data = {
|
|
147
|
+
"schema_version": 1,
|
|
148
|
+
"benchmark": "BEND 2.0",
|
|
149
|
+
"version": __version__,
|
|
150
|
+
"identity": _hash(identity_spec),
|
|
151
|
+
"identity_spec": identity_spec,
|
|
152
|
+
"model": {key: value for key, value in merged_model.items() if key != "adapter"},
|
|
153
|
+
"created_at": datetime.now(UTC).isoformat(),
|
|
154
|
+
"status": "incomplete" if pending else "complete",
|
|
155
|
+
"combined_from": origins,
|
|
156
|
+
"requested_tasks": requested,
|
|
157
|
+
"skipped": skipped,
|
|
158
|
+
"tasks": {task: rows[task] for task in CATALOG if task in rows},
|
|
159
|
+
"implementation_files": {},
|
|
160
|
+
}
|
|
161
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
destination.mkdir()
|
|
163
|
+
try:
|
|
164
|
+
for task, row in data["tasks"].items():
|
|
165
|
+
for relative, checksum in row["files"].items():
|
|
166
|
+
_copy_verified(owners[task].directory / relative, destination / relative, checksum)
|
|
167
|
+
for run, result, origin in zip(sources, results, origins, strict=True):
|
|
168
|
+
prefix = Path(origin["manifest"]).parent
|
|
169
|
+
files = {"result.json": origin["result_sha256"]}
|
|
170
|
+
files.update(result.get("implementation_files", {}))
|
|
171
|
+
if result.get("adapter_file"):
|
|
172
|
+
files["model.py"] = result["adapter_file"]
|
|
173
|
+
for relative, checksum in files.items():
|
|
174
|
+
copied = (prefix / relative).as_posix()
|
|
175
|
+
if copied in data["implementation_files"]:
|
|
176
|
+
continue # Byte-identical source manifests share one snapshot.
|
|
177
|
+
_copy_verified(run.directory / relative, destination / copied, checksum)
|
|
178
|
+
data["implementation_files"][copied] = checksum
|
|
179
|
+
# Keep an adapter at the top level as well as in the source snapshots.
|
|
180
|
+
for run, result in zip(sources, results, strict=True):
|
|
181
|
+
if result.get("adapter_file"):
|
|
182
|
+
_copy_verified(
|
|
183
|
+
run.directory / "model.py", destination / "model.py", result["adapter_file"]
|
|
184
|
+
)
|
|
185
|
+
data["adapter_file"] = result["adapter_file"]
|
|
186
|
+
break
|
|
187
|
+
write_json(destination / "result.json", data)
|
|
188
|
+
return Run(destination)
|
|
189
|
+
except BaseException:
|
|
190
|
+
shutil.rmtree(destination)
|
|
191
|
+
raise
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
program,on_cells,off_cells
|
|
2
|
+
786_O|GM12878|SKNSH,786_O|GM12878|SKNSH,NT2_D1|WERI_Rb1|SJCRH30|HepG2|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
3
|
+
786_O|MCF7,786_O|MCF7,NT2_D1|GM12878|SKNSH|WERI_Rb1|SJCRH30|HepG2|K562|HeLaS3|HEK293|HMC3
|
|
4
|
+
GM12878|HepG2|NT2_D1,GM12878|HepG2|NT2_D1,786_O|SKNSH|WERI_Rb1|SJCRH30|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
5
|
+
GM12878|K562,GM12878|K562,NT2_D1|786_O|SKNSH|WERI_Rb1|SJCRH30|HepG2|MCF7|HeLaS3|HEK293|HMC3
|
|
6
|
+
GM12878|SJCRH30|WERI_Rb1,GM12878|SJCRH30|WERI_Rb1,NT2_D1|786_O|SKNSH|HepG2|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
7
|
+
GM12878|SKNSH,GM12878|SKNSH,NT2_D1|786_O|WERI_Rb1|SJCRH30|HepG2|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
8
|
+
HeLaS3|HepG2,HeLaS3|HepG2,NT2_D1|GM12878|786_O|SKNSH|WERI_Rb1|SJCRH30|K562|MCF7|HEK293|HMC3
|
|
9
|
+
HeLaS3|HepG2|MCF7,HeLaS3|HepG2|MCF7,NT2_D1|GM12878|786_O|SKNSH|WERI_Rb1|SJCRH30|K562|HEK293|HMC3
|
|
10
|
+
HeLaS3|K562|MCF7,HeLaS3|K562|MCF7,NT2_D1|GM12878|786_O|SKNSH|WERI_Rb1|SJCRH30|HepG2|HEK293|HMC3
|
|
11
|
+
HeLaS3|NT2_D1|WERI_Rb1,HeLaS3|NT2_D1|WERI_Rb1,GM12878|786_O|SKNSH|SJCRH30|HepG2|K562|MCF7|HEK293|HMC3
|
|
12
|
+
HeLaS3|SJCRH30|SKNSH,HeLaS3|SJCRH30|SKNSH,NT2_D1|GM12878|786_O|WERI_Rb1|HepG2|K562|MCF7|HEK293|HMC3
|
|
13
|
+
HepG2|K562,HepG2|K562,NT2_D1|GM12878|786_O|SKNSH|WERI_Rb1|SJCRH30|MCF7|HeLaS3|HEK293|HMC3
|
|
14
|
+
HepG2|K562|SKNSH,HepG2|K562|SKNSH,NT2_D1|GM12878|786_O|WERI_Rb1|SJCRH30|MCF7|HeLaS3|HEK293|HMC3
|
|
15
|
+
HepG2|WERI_Rb1,HepG2|WERI_Rb1,NT2_D1|GM12878|786_O|SKNSH|SJCRH30|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
16
|
+
NT2_D1|SKNSH,NT2_D1|SKNSH,GM12878|786_O|WERI_Rb1|SJCRH30|HepG2|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
17
|
+
SJCRH30|SKNSH,SJCRH30|SKNSH,NT2_D1|GM12878|786_O|WERI_Rb1|HepG2|K562|MCF7|HeLaS3|HEK293|HMC3
|
|
Binary file
|