multibench-sc 0.2.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.
- multibench/__init__.py +46 -0
- multibench/__main__.py +7 -0
- multibench/cli.py +230 -0
- multibench/config.py +98 -0
- multibench/data/__init__.py +2 -0
- multibench/data/catalog.py +132 -0
- multibench/data/fetch.py +40 -0
- multibench/data/results.py +128 -0
- multibench/discover.py +173 -0
- multibench/engine/__init__.py +0 -0
- multibench/engine/builder.py +52 -0
- multibench/engine/drivers/run_matilda.py +84 -0
- multibench/engine/drivers/run_stabmap.R +71 -0
- multibench/engine/env_groups.yaml +283 -0
- multibench/engine/env_locks/MOFA2_env.yml +418 -0
- multibench/engine/env_locks/env_moETM.yml +189 -0
- multibench/engine/env_locks/env_sciPENN.yml +79 -0
- multibench/engine/env_locks/glue.yml +146 -0
- multibench/engine/env_locks/matilda.post.sh +18 -0
- multibench/engine/env_locks/matilda.yml +117 -0
- multibench/engine/env_locks/paste_envi.yml +210 -0
- multibench/engine/env_locks/scmb_cobolt3.post.sh +9 -0
- multibench/engine/env_locks/scmb_cobolt3.yml +96 -0
- multibench/engine/env_locks/scmb_concerto.yml +114 -0
- multibench/engine/env_locks/scmb_gpsa2.yml +493 -0
- multibench/engine/env_locks/scmb_ipolng2.yml +76 -0
- multibench/engine/env_locks/scmb_mira.yml +131 -0
- multibench/engine/env_locks/scmb_multigrate2.yml +164 -0
- multibench/engine/env_locks/scmb_multivi.yml +141 -0
- multibench/engine/env_locks/scmb_r.post.sh +21 -0
- multibench/engine/env_locks/scmb_r.yml +396 -0
- multibench/engine/env_locks/scmb_scican.yml +125 -0
- multibench/engine/env_locks/scmb_scjoint.yml +93 -0
- multibench/engine/env_locks/scmb_scmdc.yml +94 -0
- multibench/engine/env_locks/scmb_scmm2.yml +60 -0
- multibench/engine/env_locks/scmb_scmsi.yml +75 -0
- multibench/engine/env_locks/scmb_scmvp4.post.sh +13 -0
- multibench/engine/env_locks/scmb_scmvp4.yml +71 -0
- multibench/engine/env_locks/scmb_scvi.yml +144 -0
- multibench/engine/env_locks/scmb_seurat4b.yml +305 -0
- multibench/engine/env_locks/scmb_seurat5.yml +160 -0
- multibench/engine/env_locks/scmb_torch.post.sh +17 -0
- multibench/engine/env_locks/scmb_torch.yml +135 -0
- multibench/engine/env_locks/scmb_torch_v2.post.sh +17 -0
- multibench/engine/env_locks/scmb_torch_v2.yml +288 -0
- multibench/engine/env_locks/scmb_vimcca.yml +121 -0
- multibench/engine/env_locks/scmb_vipcca27.yml +115 -0
- multibench/engine/env_locks/spiral_environment.post.sh +17 -0
- multibench/engine/env_locks/spiral_environment.yml +644 -0
- multibench/engine/env_locks/unitednet.yml +342 -0
- multibench/engine/env_specs.yaml +851 -0
- multibench/engine/envs.py +669 -0
- multibench/engine/ingest.py +123 -0
- multibench/engine/io.py +20 -0
- multibench/engine/methods.yaml +955 -0
- multibench/engine/registry.py +107 -0
- multibench/engine/resolve.py +246 -0
- multibench/engine/runner.py +183 -0
- multibench/engine/schema.py +81 -0
- multibench/eval/__init__.py +4 -0
- multibench/eval/io.py +42 -0
- multibench/eval/pipeline.py +81 -0
- multibench/eval/scib.py +236 -0
- multibench/files/README_PROVENANCE.txt +5 -0
- multibench/files/dataset.csv +72 -0
- multibench/files/method.csv +41 -0
- multibench/files/metric_full.csv +11 -0
- multibench/plot/__init__.py +22 -0
- multibench/plot/bar.py +125 -0
- multibench/plot/bubble.py +328 -0
- multibench/plot/style.py +28 -0
- multibench/workflow.py +1008 -0
- multibench_sc-0.2.0.dist-info/METADATA +80 -0
- multibench_sc-0.2.0.dist-info/RECORD +78 -0
- multibench_sc-0.2.0.dist-info/WHEEL +5 -0
- multibench_sc-0.2.0.dist-info/entry_points.txt +2 -0
- multibench_sc-0.2.0.dist-info/licenses/LICENSE +202 -0
- multibench_sc-0.2.0.dist-info/top_level.txt +1 -0
multibench/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""multibench - run and compare single-cell multimodal integration methods.
|
|
2
|
+
|
|
3
|
+
Quickstart
|
|
4
|
+
----------
|
|
5
|
+
::
|
|
6
|
+
|
|
7
|
+
import multibench as mtb
|
|
8
|
+
|
|
9
|
+
mtb.list_categories() # the four integration scenarios
|
|
10
|
+
print(mtb.describe_layout()) # how to lay out YOUR dataset
|
|
11
|
+
|
|
12
|
+
mtb.scan("MYDATA") # which methods can run on it (and why not the rest)
|
|
13
|
+
|
|
14
|
+
res = mtb.run_all("MYDATA", "vertical", out_dir="out/")
|
|
15
|
+
print(res.summary) # per-method status + metrics
|
|
16
|
+
print(res.failures) # what did not finish, and why
|
|
17
|
+
res.plot().savefig("compare.png")
|
|
18
|
+
|
|
19
|
+
The four ``category`` values are ``vertical``, ``diagonal``, ``mosaic`` and
|
|
20
|
+
``cross`` - see :func:`list_categories` for what each one means, and
|
|
21
|
+
:func:`describe_layout` for the filenames each expects.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from . import config
|
|
25
|
+
from . import plot
|
|
26
|
+
from . import eval
|
|
27
|
+
from .data import catalog
|
|
28
|
+
from .data.results import load_results, available_datasets
|
|
29
|
+
from .eval.pipeline import evaluate, to_long
|
|
30
|
+
from .engine.runner import run
|
|
31
|
+
from .engine.registry import list_methods, list_tasks
|
|
32
|
+
from .engine.resolve import inputs_for, labels_for
|
|
33
|
+
from .engine import ingest as io
|
|
34
|
+
from .engine import envs as env
|
|
35
|
+
from .discover import find_methods, method_info, params_for
|
|
36
|
+
from .workflow import (scan, run_all, BatchResult, list_categories,
|
|
37
|
+
describe_layout, load_batch, runtime_hint, sweep)
|
|
38
|
+
|
|
39
|
+
__version__ = "0.2.0"
|
|
40
|
+
|
|
41
|
+
__all__ = ["config", "plot", "eval", "catalog", "load_results", "available_datasets",
|
|
42
|
+
"evaluate", "to_long", "run", "list_methods", "list_tasks", "inputs_for",
|
|
43
|
+
"labels_for", "io", "env", "method_info", "find_methods", "params_for",
|
|
44
|
+
"scan", "run_all", "BatchResult",
|
|
45
|
+
"list_categories", "describe_layout", "load_batch",
|
|
46
|
+
"runtime_hint", "sweep", "__version__"]
|
multibench/__main__.py
ADDED
multibench/cli.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Thin CLI over the multibench Python API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from . import discover, load_results
|
|
7
|
+
from . import plot as plot_ns
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _cmd_list(args) -> int:
|
|
11
|
+
from .engine import registry
|
|
12
|
+
for m in registry.list_methods(category=args.category, task=args.task,
|
|
13
|
+
runnable=args.runnable or None):
|
|
14
|
+
print(m)
|
|
15
|
+
return 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cmd_find(args) -> int:
|
|
19
|
+
needs = True if args.needs_labels else None
|
|
20
|
+
for m in discover.find_methods(category=args.category, task=args.task,
|
|
21
|
+
needs_labels=needs, atac=args.atac,
|
|
22
|
+
runnable=args.runnable or None):
|
|
23
|
+
print(m)
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cmd_plot(args) -> int:
|
|
28
|
+
if args.kind != "bubble":
|
|
29
|
+
raise SystemExit(f"unknown plot kind {args.kind!r}")
|
|
30
|
+
df = load_results(category=args.category, dataset=args.dataset)
|
|
31
|
+
metrics = args.metrics.split(",") if args.metrics else None
|
|
32
|
+
plot_ns.bubble(df, metrics=metrics, aggregate=args.aggregate, save=args.out)
|
|
33
|
+
print(f"wrote {args.out}")
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_inputs(pairs) -> dict:
|
|
38
|
+
out = {}
|
|
39
|
+
for p in pairs or []:
|
|
40
|
+
if "=" not in p:
|
|
41
|
+
raise SystemExit(f"--input must be role=path, got {p!r}")
|
|
42
|
+
role, path = p.split("=", 1)
|
|
43
|
+
if role.strip() == "" or path == "":
|
|
44
|
+
raise SystemExit(f"--input must be role=path with non-empty parts, got {p!r}")
|
|
45
|
+
out[role] = path
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_run(args) -> int:
|
|
50
|
+
import multibench
|
|
51
|
+
res = multibench.run(method=args.method, category=args.category, task=args.task,
|
|
52
|
+
inputs=_parse_inputs(args.input), out_dir=args.out,
|
|
53
|
+
cmd_template=args.runner)
|
|
54
|
+
print(f"ran {args.method} -> {res.out_dir}")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _cmd_evaluate(args) -> int:
|
|
59
|
+
import multibench
|
|
60
|
+
df = multibench.evaluate(output=args.output, category=args.category, task=args.task,
|
|
61
|
+
labels=args.labels, clustering=args.cluster)
|
|
62
|
+
if args.out:
|
|
63
|
+
df.to_csv(args.out)
|
|
64
|
+
print(f"wrote {args.out}")
|
|
65
|
+
else:
|
|
66
|
+
print(df.to_string())
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _cmd_env(args) -> int:
|
|
71
|
+
from .engine import envs
|
|
72
|
+
cmd = args.env_cmd
|
|
73
|
+
if cmd == "status":
|
|
74
|
+
for r in envs.status():
|
|
75
|
+
mark = "x" if r["exists"] else " "
|
|
76
|
+
tag = r["difficulty"] + ("*" if r["verified_working"] else "")
|
|
77
|
+
print(f"[{mark}] {r['method']:16} {r['group']:16} {tag}")
|
|
78
|
+
return 0
|
|
79
|
+
if cmd == "groups":
|
|
80
|
+
for name, spec in envs.groups().items():
|
|
81
|
+
if spec.get("shared"):
|
|
82
|
+
print(f"{name:16} ({len(spec['members']):2}): {', '.join(spec['members'])}")
|
|
83
|
+
return 0
|
|
84
|
+
if cmd == "plan":
|
|
85
|
+
_mlist = getattr(args, "methods", None)
|
|
86
|
+
_mlist = [m.strip() for m in _mlist.split(",")] if _mlist else None
|
|
87
|
+
for p in envs.plan(category=getattr(args, "category", None), methods=_mlist):
|
|
88
|
+
tag = "shared" if p["shared"] else "own"
|
|
89
|
+
print(f"{p['env']:16} [{tag:6}] <- {', '.join(p['methods'])}")
|
|
90
|
+
return 0
|
|
91
|
+
if cmd == "doctor":
|
|
92
|
+
_mlist = getattr(args, "methods", None)
|
|
93
|
+
_mlist = [m.strip() for m in _mlist.split(",")] if _mlist else None
|
|
94
|
+
rows = envs.doctor(category=getattr(args, "category", None), methods=_mlist)
|
|
95
|
+
for r in rows:
|
|
96
|
+
mark = "x" if r["exists"] else ("L" if r["has_lock"] else "!")
|
|
97
|
+
print(f"[{mark}] {r['env']:18} ({len(r['methods']):2}) <- {', '.join(r['methods'])}")
|
|
98
|
+
missing = [r for r in rows if not r["exists"]]
|
|
99
|
+
nolock = [r["env"] for r in missing if not r["has_lock"]]
|
|
100
|
+
print(f"# {len(rows)} envs needed, {len(missing)} missing"
|
|
101
|
+
+ (f"; NO lockfile for: {', '.join(nolock)}" if nolock else ""))
|
|
102
|
+
print("# legend: [x]=installed [L]=missing, lockfile ready (run `multibench env install --run`) [!]=missing, no lockfile")
|
|
103
|
+
return 0
|
|
104
|
+
if cmd == "install":
|
|
105
|
+
_mlist = getattr(args, "methods", None)
|
|
106
|
+
_mlist = [m.strip() for m in _mlist.split(",")] if _mlist else None
|
|
107
|
+
try:
|
|
108
|
+
rows = envs.create_all(category=getattr(args, "category", None),
|
|
109
|
+
methods=_mlist,
|
|
110
|
+
dry_run=not getattr(args, "run", False))
|
|
111
|
+
except RuntimeError as e:
|
|
112
|
+
print(f"error: {e}")
|
|
113
|
+
return 1
|
|
114
|
+
for r in rows:
|
|
115
|
+
state = ("have" if r["exists"]
|
|
116
|
+
else ("BUILD" if r["has_lock"] and getattr(args, "run", False)
|
|
117
|
+
else "build(dry-run)" if r["has_lock"] else "NO-LOCK"))
|
|
118
|
+
print(f"{r['env']:18} [{state:14}] <- {', '.join(r['methods'])}")
|
|
119
|
+
if not getattr(args, "run", False):
|
|
120
|
+
print("# dry-run — add --run to create the missing envs from their lockfiles")
|
|
121
|
+
return 0
|
|
122
|
+
if cmd == "freeze":
|
|
123
|
+
if getattr(args, "all", False):
|
|
124
|
+
for env in envs.required_envs(category=getattr(args, "category", None)):
|
|
125
|
+
try:
|
|
126
|
+
print(f"froze {env} -> {envs.freeze(env)}")
|
|
127
|
+
except Exception as e: # noqa: BLE001 - report per-env, keep going
|
|
128
|
+
print(f"SKIP {env}: {str(e)[:120]}")
|
|
129
|
+
else:
|
|
130
|
+
print(f"froze {args.env} -> {envs.freeze(args.env)}")
|
|
131
|
+
return 0
|
|
132
|
+
if cmd == "create-group":
|
|
133
|
+
cmds = envs.create_group(args.group, dry_run=not getattr(args, "run", False))
|
|
134
|
+
if not getattr(args, "run", False):
|
|
135
|
+
print("# dry-run — add --run to execute:")
|
|
136
|
+
for c in cmds:
|
|
137
|
+
print(" ".join(c))
|
|
138
|
+
else:
|
|
139
|
+
print(f"created group env {args.group}")
|
|
140
|
+
return 0
|
|
141
|
+
method = getattr(args, "method", None)
|
|
142
|
+
name = getattr(args, "name", None)
|
|
143
|
+
if cmd == "recipe":
|
|
144
|
+
for c in envs.create_commands(method, env_name=name):
|
|
145
|
+
print(" ".join(c))
|
|
146
|
+
return 0
|
|
147
|
+
if cmd == "yml":
|
|
148
|
+
y = envs.environment_yml(method, env_name=name)
|
|
149
|
+
out = getattr(args, "out", None)
|
|
150
|
+
if out:
|
|
151
|
+
with open(out, "w") as f:
|
|
152
|
+
f.write(y)
|
|
153
|
+
print(f"wrote {out}")
|
|
154
|
+
else:
|
|
155
|
+
print(y, end="")
|
|
156
|
+
return 0
|
|
157
|
+
if cmd == "create":
|
|
158
|
+
cmds = envs.create(method, env_name=name, dry_run=not getattr(args, "run", False))
|
|
159
|
+
if not getattr(args, "run", False):
|
|
160
|
+
print("# dry-run — add --run to execute:")
|
|
161
|
+
for c in cmds:
|
|
162
|
+
print(" ".join(c))
|
|
163
|
+
else:
|
|
164
|
+
print(f"created environment for {method}")
|
|
165
|
+
return 0
|
|
166
|
+
raise SystemExit(
|
|
167
|
+
"usage: multibench env {status|groups|plan|doctor|install|freeze|recipe|yml|create|create-group}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
171
|
+
p = argparse.ArgumentParser(prog="multibench")
|
|
172
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
173
|
+
|
|
174
|
+
pl = sub.add_parser("list"); pl.add_argument("--category"); pl.add_argument("--task")
|
|
175
|
+
pl.add_argument("--runnable", action="store_true",
|
|
176
|
+
help="only methods with a declared variant (usable by run)")
|
|
177
|
+
pl.set_defaults(func=_cmd_list)
|
|
178
|
+
|
|
179
|
+
pf = sub.add_parser("find")
|
|
180
|
+
pf.add_argument("--category"); pf.add_argument("--task")
|
|
181
|
+
pf.add_argument("--needs-labels", action="store_true")
|
|
182
|
+
pf.add_argument("--atac", choices=["peak", "gene_activity"],
|
|
183
|
+
help="filter by ATAC representation the method consumes")
|
|
184
|
+
pf.add_argument("--runnable", action="store_true",
|
|
185
|
+
help="only methods with a declared variant (usable by run)")
|
|
186
|
+
pf.set_defaults(func=_cmd_find)
|
|
187
|
+
|
|
188
|
+
pp = sub.add_parser("plot")
|
|
189
|
+
pp.add_argument("kind")
|
|
190
|
+
pp.add_argument("--category", required=True); pp.add_argument("--dataset")
|
|
191
|
+
pp.add_argument("--metrics"); pp.add_argument("--aggregate", default="dataset")
|
|
192
|
+
pp.add_argument("--out", required=True)
|
|
193
|
+
pp.set_defaults(func=_cmd_plot)
|
|
194
|
+
|
|
195
|
+
pr = sub.add_parser("run")
|
|
196
|
+
pr.add_argument("--method", required=True); pr.add_argument("--category", required=True)
|
|
197
|
+
pr.add_argument("--task", default="clustering")
|
|
198
|
+
pr.add_argument("--input", action="append", help="role=path (repeatable)")
|
|
199
|
+
pr.add_argument("--out", required=True); pr.add_argument("--runner")
|
|
200
|
+
pr.set_defaults(func=_cmd_run)
|
|
201
|
+
|
|
202
|
+
pe = sub.add_parser("evaluate")
|
|
203
|
+
pe.add_argument("--output", required=True)
|
|
204
|
+
pe.add_argument("--category", help="reserved; not used by v1 metrics")
|
|
205
|
+
pe.add_argument("--task", default="clustering")
|
|
206
|
+
pe.add_argument("--labels"); pe.add_argument("--cluster"); pe.add_argument("--out")
|
|
207
|
+
pe.set_defaults(func=_cmd_evaluate)
|
|
208
|
+
|
|
209
|
+
pv = sub.add_parser("env", help="per-method environment recipes")
|
|
210
|
+
ev = pv.add_subparsers(dest="env_cmd", required=True)
|
|
211
|
+
ev.add_parser("status").set_defaults(func=_cmd_env)
|
|
212
|
+
ev.add_parser("groups").set_defaults(func=_cmd_env)
|
|
213
|
+
er = ev.add_parser("recipe"); er.add_argument("method"); er.add_argument("--name"); er.set_defaults(func=_cmd_env)
|
|
214
|
+
ey = ev.add_parser("yml"); ey.add_argument("method"); ey.add_argument("--name"); ey.add_argument("--out"); ey.set_defaults(func=_cmd_env)
|
|
215
|
+
ec = ev.add_parser("create"); ec.add_argument("method"); ec.add_argument("--name"); ec.add_argument("--run", action="store_true"); ec.set_defaults(func=_cmd_env)
|
|
216
|
+
ep = ev.add_parser("plan"); ep.add_argument("--category"); ep.add_argument("--methods", help="comma-separated method names; only their envs"); ep.set_defaults(func=_cmd_env)
|
|
217
|
+
eg = ev.add_parser("create-group"); eg.add_argument("group"); eg.add_argument("--run", action="store_true"); eg.set_defaults(func=_cmd_env)
|
|
218
|
+
edoc = ev.add_parser("doctor", help="preflight: which envs are present / need building"); edoc.add_argument("--category"); edoc.add_argument("--methods", help="comma-separated method names; only their envs"); edoc.set_defaults(func=_cmd_env)
|
|
219
|
+
ei = ev.add_parser("install", help="build every needed env from its lockfile"); ei.add_argument("--category"); ei.add_argument("--methods", help="comma-separated method names; only their envs"); ei.add_argument("--run", action="store_true"); ei.set_defaults(func=_cmd_env)
|
|
220
|
+
ef = ev.add_parser("freeze", help="capture an env (or --all) to a committed lockfile"); ef.add_argument("env", nargs="?"); ef.add_argument("--all", action="store_true"); ef.add_argument("--category"); ef.set_defaults(func=_cmd_env)
|
|
221
|
+
return p
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def main(argv=None) -> int:
|
|
225
|
+
args = build_parser().parse_args(argv)
|
|
226
|
+
return args.func(args)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
raise SystemExit(main())
|
multibench/config.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Filesystem paths and on-disk token maps for multibench.
|
|
2
|
+
|
|
3
|
+
The benchmark's result tree uses space-named category folders and a singular
|
|
4
|
+
`scib_metric` top-level dir. Callers use clean tokens (e.g. "vertical"); this
|
|
5
|
+
module translates them to the real folder names.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
__all__ = ["Config", "DEFAULT", "category_folder", "metric_set_dir"]
|
|
13
|
+
|
|
14
|
+
# token -> on-disk space-named folder
|
|
15
|
+
_CATEGORY_FOLDERS = {
|
|
16
|
+
"vertical": "vertical integration",
|
|
17
|
+
"diagonal": "diagonal integration",
|
|
18
|
+
"mosaic": "mosaic integration",
|
|
19
|
+
"cross": "cross integration",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# metric-set token -> top-level result dir. Only "scib" is wired in v1
|
|
23
|
+
# (load_results raises NotImplementedError otherwise); other metric sets are not
|
|
24
|
+
# yet exposed here rather than advertising tokens with no working loader.
|
|
25
|
+
_METRIC_SET_DIRS = {
|
|
26
|
+
"scib": "scib_metric",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_ROOT = Path(__file__).resolve().parent.parent # <ROOT>
|
|
30
|
+
# Where the package lives decides where large client-side artefacts go. In a
|
|
31
|
+
# repository checkout (source tree / editable install) they sit next to the
|
|
32
|
+
# package, as always; installed as a wheel, _ROOT lands inside site-packages,
|
|
33
|
+
# which must not accumulate datasets or clones - use a per-user cache dir.
|
|
34
|
+
import os as _os
|
|
35
|
+
_IN_REPO = (_ROOT / "pyproject.toml").is_file()
|
|
36
|
+
_BASE = _ROOT if _IN_REPO else (
|
|
37
|
+
Path(_os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache")))
|
|
38
|
+
/ "multibench")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def category_folder(token: str) -> str:
|
|
42
|
+
"""Map a category token to its space-named result folder."""
|
|
43
|
+
try:
|
|
44
|
+
return _CATEGORY_FOLDERS[token]
|
|
45
|
+
except KeyError:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"unknown category {token!r}; valid: {sorted(_CATEGORY_FOLDERS)}"
|
|
48
|
+
) from None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def metric_set_dir(token: str) -> str:
|
|
52
|
+
"""Map a metric-set token to its top-level result dir name."""
|
|
53
|
+
try:
|
|
54
|
+
return _METRIC_SET_DIRS[token]
|
|
55
|
+
except KeyError:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"unknown metric_set {token!r}; valid: {sorted(_METRIC_SET_DIRS)}"
|
|
58
|
+
) from None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Config:
|
|
63
|
+
"""Resolved paths. Override fields to point at custom data locations."""
|
|
64
|
+
|
|
65
|
+
result_path: Path = field(default_factory=lambda: _ROOT / "multibench" / "result")
|
|
66
|
+
files_path: Path = field(default_factory=lambda: _ROOT / "multibench" / "files")
|
|
67
|
+
repo_path: Path = field(default_factory=lambda: _BASE / "scMultiBench_ref")
|
|
68
|
+
data_path: Path = field(default_factory=lambda: _BASE / "data")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# module-level default instance; callers may replace its fields
|
|
72
|
+
DEFAULT = Config()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def ensure_repo(path=None):
|
|
76
|
+
"""Return a directory that contains ``tools_scripts/``, provisioning it if needed.
|
|
77
|
+
|
|
78
|
+
Resolution order: the given (or configured) ``repo_path``; the package root
|
|
79
|
+
itself (the merged-repository layout, where ``tools_scripts/`` sits next to
|
|
80
|
+
``multibench/``); otherwise a one-time shallow clone of the public
|
|
81
|
+
scMultiBench repository into the configured location - which is what makes
|
|
82
|
+
method execution work on a fresh machine or Colab, where the wrapper's
|
|
83
|
+
clone does not carry the 3 GB of upstream method scripts.
|
|
84
|
+
"""
|
|
85
|
+
import subprocess
|
|
86
|
+
from pathlib import Path as _P
|
|
87
|
+
|
|
88
|
+
p = _P(path) if path else DEFAULT.repo_path
|
|
89
|
+
if (p / "tools_scripts").is_dir():
|
|
90
|
+
return p
|
|
91
|
+
if (_ROOT / "tools_scripts").is_dir():
|
|
92
|
+
return _ROOT
|
|
93
|
+
print(f"method scripts not found - fetching PYangLab/scMultiBench (once) into {p} ...",
|
|
94
|
+
flush=True)
|
|
95
|
+
subprocess.run(["git", "clone", "--depth", "1",
|
|
96
|
+
"https://github.com/PYangLab/scMultiBench.git", str(p)],
|
|
97
|
+
check=True)
|
|
98
|
+
return p
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Catalog: parse files/*.csv into typed tables with canonical names."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .. import config
|
|
10
|
+
|
|
11
|
+
__all__ = ["methods", "datasets", "metrics", "canonical_id", "canonical_metric"]
|
|
12
|
+
|
|
13
|
+
# --- canonical method id + aliases -----------------------------------------
|
|
14
|
+
# Canonical id = the registry token. Map known display / result-dir spellings.
|
|
15
|
+
_ALIASES = {
|
|
16
|
+
"seurat v3": "Seurat_v3",
|
|
17
|
+
"seurat_v3": "Seurat_v3",
|
|
18
|
+
"seurat.v3": "Seurat_v3",
|
|
19
|
+
"seurat v4": "Seurat_v4",
|
|
20
|
+
"seurat_v4": "Seurat_v4",
|
|
21
|
+
"seurat.v4": "Seurat_v4",
|
|
22
|
+
"seurat v5": "Seurat_v5",
|
|
23
|
+
"seurat_v5": "Seurat_v5",
|
|
24
|
+
"seurat.v5": "Seurat_v5",
|
|
25
|
+
"seurat(wnn)": "Seurat_WNN",
|
|
26
|
+
"seurat_wnn": "Seurat_WNN",
|
|
27
|
+
"seurat.wnn": "Seurat_WNN",
|
|
28
|
+
"mofa+": "MOFA2",
|
|
29
|
+
"mofa2": "MOFA2",
|
|
30
|
+
"online inmf": "online_iNMF",
|
|
31
|
+
"online_inmf": "online_iNMF",
|
|
32
|
+
"online.inmf": "online_iNMF",
|
|
33
|
+
"ipolng": "iPOLNG",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def canonical_id(name: str) -> str:
|
|
38
|
+
"""Return the canonical method id for any known spelling."""
|
|
39
|
+
key = name.strip().lower()
|
|
40
|
+
if key in _ALIASES:
|
|
41
|
+
return _ALIASES[key]
|
|
42
|
+
# default: collapse separators to underscore, keep original casing token
|
|
43
|
+
return re.sub(r"[ .]+", "_", name.strip())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- metric code canonicalization ------------------------------------------
|
|
47
|
+
_METRIC_CANON = {
|
|
48
|
+
"kbet": "kBET",
|
|
49
|
+
"ifi": "iF1",
|
|
50
|
+
"if1": "iF1",
|
|
51
|
+
"ari": "ARI",
|
|
52
|
+
"nmi": "NMI",
|
|
53
|
+
"asw": "ASW",
|
|
54
|
+
"iasw": "iASW",
|
|
55
|
+
"clisi": "cLISI",
|
|
56
|
+
"ilisi": "iLISI",
|
|
57
|
+
"gc": "GC",
|
|
58
|
+
"asw_batch": "ASW_batch",
|
|
59
|
+
"pcr": "PCR",
|
|
60
|
+
# raw scIB long-names -> canonical codes (some metric.csv use these)
|
|
61
|
+
"ari_cluster/label": "ARI",
|
|
62
|
+
"nmi_cluster/label": "NMI",
|
|
63
|
+
"asw_label": "ASW",
|
|
64
|
+
"isolated_label_f1": "iF1",
|
|
65
|
+
"isolated_label_silhouette": "iASW",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def canonical_metric(code: str) -> str | None:
|
|
70
|
+
"""Canonicalize a raw metric short-code; None for blank/unknown-empty."""
|
|
71
|
+
if code is None:
|
|
72
|
+
return None
|
|
73
|
+
key = str(code).strip().lower()
|
|
74
|
+
if key == "" or key == "nan":
|
|
75
|
+
return None
|
|
76
|
+
return _METRIC_CANON.get(key, str(code).strip())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --- tables ----------------------------------------------------------------
|
|
80
|
+
def _split_multivalue(cell: object) -> list[str]:
|
|
81
|
+
if pd.isna(cell):
|
|
82
|
+
return []
|
|
83
|
+
parts = re.split(r"[\n;,]+", str(cell))
|
|
84
|
+
out = []
|
|
85
|
+
for p in parts:
|
|
86
|
+
t = p.strip().lower().replace(" integration", "").replace("integration", "").strip()
|
|
87
|
+
if t:
|
|
88
|
+
out.append(t)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def methods(files_dir: Path | str | None = None) -> pd.DataFrame:
|
|
93
|
+
"""Return the methods table with normalized columns and list-valued cats/tasks."""
|
|
94
|
+
if files_dir is None:
|
|
95
|
+
files_dir = config.DEFAULT.files_path
|
|
96
|
+
raw = pd.read_csv(Path(files_dir) / "method.csv")
|
|
97
|
+
# normalize column access by stripping whitespace/newlines
|
|
98
|
+
cols = {c: c.strip().replace("\n", " ").strip() for c in raw.columns}
|
|
99
|
+
raw = raw.rename(columns=cols)
|
|
100
|
+
out = pd.DataFrame()
|
|
101
|
+
out["method"] = raw["Methods"].astype(str).str.strip()
|
|
102
|
+
out["canonical_id"] = out["method"].map(canonical_id)
|
|
103
|
+
out["language"] = raw["Programming Language"].astype(str).str.strip().str.lower()
|
|
104
|
+
out["deep_learning"] = raw["Deep Learning"].astype(str).str.strip()
|
|
105
|
+
out["atac"] = raw["Peak/Gene Activity"].astype(str).str.strip()
|
|
106
|
+
out["output"] = raw["Output"].astype(str).str.strip()
|
|
107
|
+
out["needs_labels"] = (
|
|
108
|
+
raw["CellType Information Required"].astype(str).str.strip().str.lower().isin(["yes", "y", "true"])
|
|
109
|
+
)
|
|
110
|
+
out["categories"] = raw["Integration Categories"].map(_split_multivalue)
|
|
111
|
+
out["tasks"] = raw["Task Categories"].map(_split_multivalue)
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def datasets(files_dir: Path | str | None = None) -> pd.DataFrame:
|
|
116
|
+
"""Return the datasets table with a derived `simulated` flag."""
|
|
117
|
+
if files_dir is None:
|
|
118
|
+
files_dir = config.DEFAULT.files_path
|
|
119
|
+
raw = pd.read_csv(Path(files_dir) / "dataset.csv")
|
|
120
|
+
raw = raw.rename(columns={c: c.strip() for c in raw.columns})
|
|
121
|
+
raw = raw.dropna(how="all", axis=1).dropna(how="all", axis=0)
|
|
122
|
+
name_col = "dataset name"
|
|
123
|
+
raw["simulated"] = raw[name_col].astype(str).str.strip().str.upper().str.startswith("SD")
|
|
124
|
+
return raw
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def metrics(files_dir: Path | str | None = None) -> pd.DataFrame:
|
|
128
|
+
"""Return the metric details table."""
|
|
129
|
+
if files_dir is None:
|
|
130
|
+
files_dir = config.DEFAULT.files_path
|
|
131
|
+
raw = pd.read_csv(Path(files_dir) / "metric_full.csv")
|
|
132
|
+
return raw.rename(columns={c: c.strip() for c in raw.columns})
|
multibench/data/fetch.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Download reference datasets from the repository's release assets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tarfile
|
|
5
|
+
import urllib.request
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .. import config
|
|
9
|
+
|
|
10
|
+
RELEASE_URL = "https://github.com/DSichang/scMultiBench/releases/download/data-v1"
|
|
11
|
+
|
|
12
|
+
# datasets published as release assets, with approximate download sizes
|
|
13
|
+
AVAILABLE = {"D11": "11 MB", "D28": "137 MB", "D45": "290 MB",
|
|
14
|
+
"D46": "97 MB", "D52": "179 MB"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch(*datasets: str, data_path=None, quiet: bool = False) -> Path:
|
|
18
|
+
"""Ensure the named reference datasets exist locally, downloading if needed.
|
|
19
|
+
|
|
20
|
+
Idempotent: datasets already present under the data root are left alone.
|
|
21
|
+
Returns the data root. The full 65-dataset collection is linked from the
|
|
22
|
+
scMultiBench README; this helper covers the tutorial reference sets.
|
|
23
|
+
"""
|
|
24
|
+
root = Path(data_path) if data_path is not None else config.DEFAULT.data_path
|
|
25
|
+
for ds in datasets:
|
|
26
|
+
if (root / ds).is_dir():
|
|
27
|
+
continue
|
|
28
|
+
if ds not in AVAILABLE:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"{ds!r} is not in the release assets ({', '.join(sorted(AVAILABLE))}); "
|
|
31
|
+
"see 'Get the data' in the installation guide for the full collection")
|
|
32
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
if not quiet:
|
|
34
|
+
print(f"downloading {ds} ({AVAILABLE[ds]}) ...", flush=True)
|
|
35
|
+
tgz, _ = urllib.request.urlretrieve(f"{RELEASE_URL}/{ds}.tar.gz")
|
|
36
|
+
with tarfile.open(tgz) as t:
|
|
37
|
+
t.extractall(root)
|
|
38
|
+
if not (root / ds).is_dir():
|
|
39
|
+
raise RuntimeError(f"downloaded archive did not contain {ds}/")
|
|
40
|
+
return root
|