opencode-skills-collection 4.1.7 → 4.1.8
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.
- package/bundled-skills/.antigravity-install-manifest.json +7 -1
- package/bundled-skills/court/SKILL.md +141 -0
- package/bundled-skills/court/references/jurors.json +14 -0
- package/bundled-skills/court/scripts/court.py +348 -0
- package/bundled-skills/darkmoon-pentest/SKILL.md +123 -0
- package/bundled-skills/lognorm/SKILL.md +86 -0
- package/bundled-skills/lognorm/references/playbooks.md +76 -0
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +6 -6
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +1 -1
- package/bundled-skills/parallel-search-mcp/SKILL.md +12 -0
- package/bundled-skills/scarf-single-cell/SKILL.md +326 -0
- package/bundled-skills/scarf-single-cell/references/LICENSE.md +29 -0
- package/bundled-skills/scarf-single-cell/references/clustering-and-embedding.md +270 -0
- package/bundled-skills/scarf-single-cell/references/data-access.md +269 -0
- package/bundled-skills/scarf-single-cell/references/features-and-graphs.md +279 -0
- package/bundled-skills/scarf-single-cell/references/gene-blacklists.md +182 -0
- package/bundled-skills/scarf-single-cell/references/integration-and-comparisons.md +295 -0
- package/bundled-skills/scarf-single-cell/references/markers-and-annotation.md +280 -0
- package/bundled-skills/scarf-single-cell/references/performance-and-export.md +291 -0
- package/bundled-skills/scarf-single-cell/references/pipeline-runs-and-artifacts.md +273 -0
- package/bundled-skills/scarf-single-cell/references/plotting.md +260 -0
- package/bundled-skills/scarf-single-cell/references/quality-control.md +290 -0
- package/bundled-skills/session-handoff/SKILL.md +112 -0
- package/bundled-skills/shark/SKILL.md +134 -0
- package/bundled-skills/shark/references/investors.json +12 -0
- package/bundled-skills/shark/scripts/shark.py +302 -0
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package-lock.json +6 -6
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package.json +1 -1
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package-lock.json +6 -6
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package.json +1 -1
- package/package.json +1 -1
- package/skills_index.json +201 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Performance and export
|
|
2
|
+
|
|
3
|
+
Resource budgets, out-of-core rules, remote I/O on mounts, and exporting counts, metadata,
|
|
4
|
+
markers, and pipeline-run results. Docs: <https://scarf.readthedocs.io/en/latest/concepts/memory_and_execution.html>,
|
|
5
|
+
<https://scarf.readthedocs.io/en/latest/tutorials/remote_stores.html>, <https://scarf.readthedocs.io/en/latest/tutorials/import_and_export.html>,
|
|
6
|
+
<https://scarf.readthedocs.io/en/latest/tutorials/custom_analyses.html>, <https://scarf.readthedocs.io/en/latest/concepts/benchmarks.html>.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- Before a large run, on a shared host, or in a batch job.
|
|
11
|
+
- When analysis on a mount is slow or will make many passes over counts.
|
|
12
|
+
- When handing results to Scanpy, Seurat, R, a count model, or a colleague.
|
|
13
|
+
- When a step may outlast the agent shell's foreground limit.
|
|
14
|
+
|
|
15
|
+
## Key concepts
|
|
16
|
+
|
|
17
|
+
- `mem_budget` is a planning budget (block size, write concurrency, feature batch size), not an
|
|
18
|
+
RSS cap. Graphs, marker batches, and allocator overhead come on top: in the benchmarks a 5M-cell
|
|
19
|
+
run peaked at 51.6 GiB on a 48 GiB budget, and marker search alone added about 32 GiB.
|
|
20
|
+
- Resolution order: explicit `mem_budget`/`nthreads` argument, then env `SCARF_MEM_BUDGET` /
|
|
21
|
+
`SCARF_WORKERS`, then detection (cgroup and CPU-affinity aware). Specs: bytes (int), `"8G"`,
|
|
22
|
+
`"512M"` (binary units, G = 1024^3 bytes), or a fraction of detected memory such as `"0.6"`.
|
|
23
|
+
A bare `"8"` is rejected as ambiguous.
|
|
24
|
+
- More workers mean more concurrent buffers and remote requests; pair them with a `mem_budget`.
|
|
25
|
+
- Converters need a `mem_budget` that fits the default count layout and refuse before writing
|
|
26
|
+
when it does not (`references/data-access.md`). Prefer more memory over the smaller layout the
|
|
27
|
+
refusal names: smaller layouts slow every later gene-major read.
|
|
28
|
+
- A `DataStore` is not thread-safe: call it from one thread at a time. Use separate stores or
|
|
29
|
+
separate mount targets for independent parallel analyses.
|
|
30
|
+
- Opt-in parallel UMAP, tSNE, and ANN builds record the resolved worker count in provenance.
|
|
31
|
+
Pass `nthreads` explicitly when such results must stay reusable across machines.
|
|
32
|
+
- Counts stream in blocks. `ds.RNA.rawData` is a lazy `ChunkedArray`; `.compute()` on a large
|
|
33
|
+
slice materializes it. `ds.to_anndata(...)` always builds `X` in memory; ordinary
|
|
34
|
+
`scarf.to_h5ad(assay, path)` and `scarf.to_mtx` stream.
|
|
35
|
+
- On a mount, every pass over counts is a network read; artifacts (normalized, PCA, graph) live
|
|
36
|
+
in the local target. `local_cache` (PCA) stages normalized data only when the datastore
|
|
37
|
+
location itself is remote, so it is skipped for a local mount target.
|
|
38
|
+
|
|
39
|
+
## Recipes
|
|
40
|
+
|
|
41
|
+
### Set budgets and batch-friendly output
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import scarf
|
|
45
|
+
|
|
46
|
+
scarf.configure_output(progress=False, timestamps=True)
|
|
47
|
+
scarf.set_verbosity(level="INFO", filepath="scarf-run.log") # logs go to this file, not stdout
|
|
48
|
+
ds = scarf.DataStore("analysis.zarr", mem_budget="16G", nthreads=8, min_features_per_cell=-1)
|
|
49
|
+
print(ds.summary().to_dict()["resources"]) # {'memory_bytes': ..., 'workers': 8, ...}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Environment equivalent: `SCARF_MEM_BUDGET=16G SCARF_WORKERS=8 python job.py`.
|
|
53
|
+
`configure_output(level=...)` does not hide progress bars; pass `progress=False` explicitly.
|
|
54
|
+
|
|
55
|
+
### Stream counts instead of materializing
|
|
56
|
+
|
|
57
|
+
Slice lazily, then iterate ordered row blocks; never `.compute()` the whole matrix.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import numpy as np
|
|
61
|
+
|
|
62
|
+
run = ds.pipeline.open(label="baseline")
|
|
63
|
+
cell_index = np.flatnonzero(run.cells.fetch_all("I"))
|
|
64
|
+
hvg = np.flatnonzero(np.asarray(run.features.fetch_all("highly_variable_features"), dtype=bool))
|
|
65
|
+
selected = ds.RNA.rawData[:, hvg][cell_index, :]
|
|
66
|
+
detected = np.concatenate(
|
|
67
|
+
[np.count_nonzero(block, axis=1) for block in selected.stream_blocks(nthreads=4, msg="HVGs")]
|
|
68
|
+
)
|
|
69
|
+
print(ds.RNA.rawData[:3, :5].compute()) # tiny slices only
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Make repeated passes over a mount cheap
|
|
73
|
+
|
|
74
|
+
Repack copies the remote counts once into a self-contained local store, keeping artifacts and
|
|
75
|
+
pipeline runs. It needs disk for the full counts plus `countsT`.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m scarf.tools.repack_zarr work/pbmc.zarr work/pbmc_local.zarr --mem-budget 4G
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For a mount of a few thousand cells, repack takes tens of seconds and reopening drops from 10 to
|
|
82
|
+
20 s to under 1 s. The output has no `matrixSource`, and `ds.pipeline.open(label=...)` still
|
|
83
|
+
works. `--data-only` drops analyses; `--nthreads`, `--profile {fast_local,cloud}` and
|
|
84
|
+
`--storage-options` (JSON, e.g. `'{"skip_signature": true}'` for public S3) also exist.
|
|
85
|
+
|
|
86
|
+
### Run long steps from an agent shell
|
|
87
|
+
|
|
88
|
+
Some agent shells stop a foreground command after about 10 minutes, and an unbounded wait loop can
|
|
89
|
+
hang for as long. Write the step as a script that logs to a file and ends with `DONE` or `FAILED`:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# step_baseline.py
|
|
93
|
+
import sys
|
|
94
|
+
import traceback
|
|
95
|
+
|
|
96
|
+
import scarf
|
|
97
|
+
|
|
98
|
+
scarf.configure_output(level="WARNING", progress=False)
|
|
99
|
+
try:
|
|
100
|
+
ds = scarf.DataStore("analysis.zarr", min_features_per_cell=-1)
|
|
101
|
+
run = ds.pipeline.run(label="baseline")
|
|
102
|
+
print("run", run.run_id, run.status, flush=True)
|
|
103
|
+
except Exception:
|
|
104
|
+
traceback.print_exc()
|
|
105
|
+
print("FAILED", flush=True)
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
print("DONE", flush=True)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Start it detached with a timeout and keep the PID in a file (shell variables do not survive between
|
|
111
|
+
agent tool calls). Make sure `timeout`'s SIGTERM reaches Python. Behind a wrapper such as `uv run`, put `timeout` inside it: Scarf then recorded
|
|
112
|
+
the run as `interrupted` in 6 of 7 tests; `timeout 1800 uv run ...` left it `running` both times.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
mkdir -p logs
|
|
116
|
+
nohup timeout 1800 python step_baseline.py > logs/baseline.log 2>&1 &
|
|
117
|
+
echo $! > logs/baseline.pid
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Wait in bounded slices that stay under the shell limit and fail fast:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
LOG=logs/baseline.log; PID=$(cat logs/baseline.pid); state=running
|
|
124
|
+
for i in $(seq 1 50); do # 50 x 10 s, under a 10-minute limit
|
|
125
|
+
alive=1; kill -0 "$PID" 2>/dev/null || alive=0 # check first: a dead process wrote its last line
|
|
126
|
+
if grep -qx DONE "$LOG"; then state=done; break; fi
|
|
127
|
+
if grep -qx FAILED "$LOG" || grep -q Traceback "$LOG"; then state=failed; break; fi
|
|
128
|
+
if [ "$alive" = 0 ]; then state=died; break; fi
|
|
129
|
+
sleep 10
|
|
130
|
+
done
|
|
131
|
+
echo "state=$state"; tail -n 5 "$LOG"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`running`: call the loop again. `failed`: read the traceback. `died`: the process ended without a
|
|
135
|
+
final line (timeout, out of memory, kill); `ds.pipeline.list_runs()` shows what was recorded. The
|
|
136
|
+
variable is `state` because zsh reserves `status`.
|
|
137
|
+
Check the saved PID with `kill -0`: `pgrep -f step_baseline.py` also matches the launching shell,
|
|
138
|
+
so it never reports the job as gone. On macOS, `timeout` is `gtimeout` from GNU coreutils.
|
|
139
|
+
|
|
140
|
+
### Export a completed run to AnnData or H5AD
|
|
141
|
+
|
|
142
|
+
Run export uses the frozen cells and feature universe and holds `X` in memory. `cell_type` below
|
|
143
|
+
and in later recipes is a placeholder for your own label column, not held-out author labels.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
adata = ds.to_anndata(run=run) # obs: frozen fields; obsm: X_umap; X: raw counts
|
|
147
|
+
live = ds.cells.to_pandas_dataframe(["ids", "cell_type", "RNA_nCounts"]).set_index("ids")
|
|
148
|
+
adata.obs = adata.obs.join(live) # live labels are not in the run: join on cell ids
|
|
149
|
+
adata.obsm["X_pca"] = np.asarray(ds.load_artifact(run["pca"])["data"][:]) # run analysis cells
|
|
150
|
+
graph = ds.load_graph(graph=run["connectivity_map"], symmetric=True, upper_only=False)
|
|
151
|
+
assert graph.shape[0] == adata.n_obs
|
|
152
|
+
adata.obsp["connectivities"] = graph
|
|
153
|
+
adata.write_h5ad("baseline_run.h5ad")
|
|
154
|
+
|
|
155
|
+
scarf.to_h5ad(ds.RNA, "baseline_run_plain.h5ad", run=run) # one call, no extras
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`adata.var_names` are feature IDs; symbols are in `adata.var["names"]`. Pass
|
|
159
|
+
`snapshot_columns=["cell_type"]` to `ds.pipeline.run` to freeze live labels into future run
|
|
160
|
+
exports (see `pipeline-runs-and-artifacts.md`).
|
|
161
|
+
|
|
162
|
+
### Export a complete assay (streaming)
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
scarf.to_h5ad(ds.RNA, "full_assay.h5ad") # every cell, including I False; live metadata
|
|
166
|
+
scarf.to_mtx(ds.RNA, "full_assay_mtx") # matrix.mtx, genes.tsv, barcodes.tsv
|
|
167
|
+
scarf.to_mtx(ds.RNA, "full_assay_mtx_gz", compress=True) # Cell Ranger 3 names, .gz
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Export a gene panel or normalized values
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
panel = ds.to_anndata(from_assay="RNA", feature_names=["CD3E", "MS4A1", "LYZ", "NKG7"])
|
|
174
|
+
normed = ds.to_anndata(run=run, matrix="normed") # library size scaled to 1000 per cell, no log
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`feature_names` (exact, unique, ordered) and `feature_indexes` are mutually exclusive and are
|
|
178
|
+
rejected together with `run=`. Without `run`, `cell_key` (default `"I"`) selects cells, and
|
|
179
|
+
`obs_names` are the cell `ids`: a panel export is the cheapest per-cell marker table.
|
|
180
|
+
|
|
181
|
+
### Export markers and metadata tables
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
ds.export_markers_to_csv(marker=run["markers"], csv_filename="markers_wide.csv") # names only
|
|
185
|
+
ds.get_markers(marker=run["markers"]).to_csv("markers_long.csv", index=False) # with stats
|
|
186
|
+
run.cells.to_pandas_dataframe(["ids", "clusters", "doublet_score"]).to_csv("run_cells.csv", index=False)
|
|
187
|
+
ds.cells.to_pandas_dataframe(["ids", "cell_type"], key="I").to_csv("cells.csv", index=False)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Export pseudobulk counts
|
|
191
|
+
|
|
192
|
+
Raw counts per population x sample plus a design table, for DESeq2, edgeR or another count model.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
bulk = ds.make_bulk(
|
|
196
|
+
"cell_type", secondary_groups="sample_id", # one column per population x sample
|
|
197
|
+
cell_selection=run["analysis_cell_selection"],
|
|
198
|
+
aggr_type="sum", feature_label="id", # raw counts; Ensembl ids are unique
|
|
199
|
+
)
|
|
200
|
+
cells = ds.cells.to_pandas_dataframe(["cell_type", "sample_id", "donor_id", "condition"])
|
|
201
|
+
cells = cells[np.asarray(run.cells.fetch_all("I"), dtype=bool)]
|
|
202
|
+
assert cells.groupby("sample_id")[["donor_id", "condition"]].nunique().eq(1).all().all()
|
|
203
|
+
cells["column"] = cells["cell_type"].astype(str) + "_" + cells["sample_id"].astype(str)
|
|
204
|
+
by_column = cells.groupby("column")
|
|
205
|
+
design = by_column.first().assign(n_cells=by_column.size()).reindex(bulk.columns) # empty -> NaN
|
|
206
|
+
design["n_cells"] = design["n_cells"].fillna(0).astype(int)
|
|
207
|
+
keep = design["n_cells"] >= 10 # choose a floor and report what you drop
|
|
208
|
+
assert np.allclose(bulk.to_numpy(), np.round(bulk.to_numpy()))
|
|
209
|
+
bulk.loc[:, keep].round().astype("int64").to_csv("pseudobulk_counts.csv")
|
|
210
|
+
design.loc[keep].to_csv("pseudobulk_design.csv")
|
|
211
|
+
print(design[keep].groupby(["cell_type", "condition"])["donor_id"].nunique().unstack(fill_value=0))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`make_bulk` emits every group x subgroup pair (all-zero when empty), named `f"{group}_{subgroup}"`;
|
|
215
|
+
a name collision raises. Sums keep the count dtype (often float32). Model one population at a time
|
|
216
|
+
with donor-level terms; with repeated samples per donor, include the donor (paired) or sum its
|
|
217
|
+
samples first, and avoid rank-deficient designs (integration-and-comparisons.md).
|
|
218
|
+
|
|
219
|
+
### Write a self-contained subset store
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
keep = np.flatnonzero(run.cells.fetch_all("I")) # or cell_key="my_bool_column"
|
|
223
|
+
scarf.SubsetZarr("run_cells.zarr", assays=[ds.RNA], cell_idx=keep).dump()
|
|
224
|
+
sub = scarf.DataStore("run_cells.zarr") # first open must be writable
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The subset copies counts locally (no `matrixSource`), keeps every feature and cell column, and
|
|
228
|
+
contains no artifacts or pipeline runs. Use it to hand off, or before `to_mtx` of run cells.
|
|
229
|
+
|
|
230
|
+
## What a run export contains
|
|
231
|
+
|
|
232
|
+
| Item | `to_anndata(run=run)` / `to_h5ad(assay, path, run=run)` | Otherwise |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| Cells | the run's analysis selection | `to_h5ad(ds.RNA, path)` exports all rows |
|
|
235
|
+
| Features | the full feature universe; HVG flag in `var["highly_variable_features"]` | `feature_names=` without `run` |
|
|
236
|
+
| `X` | raw counts; `matrix="normed"` (to_anndata only) for scaled values | |
|
|
237
|
+
| `obs` | frozen fields: cell-cycle, `leiden_*`, `paris`, `clusters`, `doublet_score`, `snapshot_columns` | join `ds.cells` by `ids` |
|
|
238
|
+
| `obsm` | `X_umap` | PCA: `ds.load_artifact(run["pca"])["data"]` |
|
|
239
|
+
| `obsp`, markers, run report | not exported | `ds.load_graph`, `get_markers`, `run.report(format="markdown")` |
|
|
240
|
+
| MTX | `to_mtx` has no `run=` | `SubsetZarr` with run cells, then `to_mtx` |
|
|
241
|
+
|
|
242
|
+
## Parameters that matter
|
|
243
|
+
|
|
244
|
+
| Parameter | Default | Change when |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `mem_budget` / `SCARF_MEM_BUDGET` | detected system or cgroup memory | shared host or batch job; leave headroom for graphs and markers |
|
|
247
|
+
| `nthreads` / `SCARF_WORKERS` | detected CPUs | shared host, remote stores, reproducible parallel UMAP/ANN |
|
|
248
|
+
| `configure_output(progress=)` | `True` | `False` in logs and batch jobs |
|
|
249
|
+
| `run_pca(local_cache=)` | `"auto"` | path string to keep scratch for a remote store; plan `cells x features x 4` bytes |
|
|
250
|
+
| `to_anndata(matrix=)` | `"raw"` | `"normed"` for scaled values |
|
|
251
|
+
| `to_h5ad(embeddings_cols=)` | `["UMAP", "tSNE"]` live column prefixes | legacy stores only; rejected with `run=` |
|
|
252
|
+
| `to_mtx(compress=)` | `False` | `True` for Cell Ranger 3 file names |
|
|
253
|
+
| `get_markers` / `export_markers_to_csv` `min_score`, `min_frac_exp` | `0.25`, `0.2` | widen or narrow the exported marker list |
|
|
254
|
+
| `make_bulk(aggr_type=, feature_label=, pseudo_reps=)` | `"mean"`, `"index"`, `1` | `"sum"` and `"id"` for count models; never `pseudo_reps` above 1 for inference |
|
|
255
|
+
| `SubsetZarr(reset_cell_filter=)` | `True` (new `I` all true) | `False` to carry `I` over |
|
|
256
|
+
|
|
257
|
+
## Check before moving on
|
|
258
|
+
|
|
259
|
+
- `ds.summary().to_dict()["resources"]` shows the budget actually in effect.
|
|
260
|
+
- At `INFO`, lines such as `execution countsTCellBand: ... fetch=...s` show remote fetch time per
|
|
261
|
+
stage; the run report records stage timing and sampled RSS.
|
|
262
|
+
- After an export: reload, compare shape to `run.cells.fetch("ids").shape` and feature count,
|
|
263
|
+
confirm `obsm["X_umap"]`, and check joined labels have no unexpected missing values.
|
|
264
|
+
- Pseudobulk: integer counts, `design` aligned to `bulk.columns`, empty and tiny columns dropped,
|
|
265
|
+
enough donors per condition in each population you model.
|
|
266
|
+
- A background step ended with `DONE`, and its run report says `completed`.
|
|
267
|
+
|
|
268
|
+
## Pitfalls
|
|
269
|
+
|
|
270
|
+
- Every `to_anndata` call (panel, run, or full) materializes `X`; size it before exporting a
|
|
271
|
+
large atlas. Ordinary `to_h5ad` streams but writes every cell, inactive ones included.
|
|
272
|
+
- Run export rejects `from_assay`, `cell_key`, `feature_names`, and `feature_indexes`.
|
|
273
|
+
`to_h5ad(run=)` also rejects `embeddings_cols`, `skip_recalc_nfeats=False`, and `nthreads`.
|
|
274
|
+
- Run `obs` lacks live columns (author labels, QC) unless captured with `snapshot_columns`.
|
|
275
|
+
- `export_markers_to_csv` writes names only, filtered by `min_score`/`min_frac_exp`; use
|
|
276
|
+
`get_markers(marker=ref)` for statistics. Pass the exact marker ref, never a guess.
|
|
277
|
+
- Remote costs for a few thousand cells on a Hugging Face mount: open 10 to 20 s, a new mount a
|
|
278
|
+
few minutes, the pipeline about a minute (cell cycle, HVG, normalization and markers each
|
|
279
|
+
re-read `countsT`), exports seconds to tens of seconds. A 4-gene panel export still reads every
|
|
280
|
+
cell's count rows.
|
|
281
|
+
- `SubsetZarr` keeps all features, drops artifacts and runs, refuses an existing destination
|
|
282
|
+
unless `overwrite_existing_file=True`, and refuses a destination overlapping the source.
|
|
283
|
+
- `to_mtx(compress=True)` on CELLxGENE stores writes gene biotypes as feature types; see
|
|
284
|
+
`data-access.md` before re-importing.
|
|
285
|
+
|
|
286
|
+
## See also
|
|
287
|
+
|
|
288
|
+
- `data-access.md`: opening stores, mounts, Cytebase, imports.
|
|
289
|
+
- `pipeline-runs-and-artifacts.md`: run reports, `snapshot_columns`, artifact loading.
|
|
290
|
+
- `markers-and-annotation.md`: marker statistics and thresholds.
|
|
291
|
+
- `integration-and-comparisons.md`: donors, two-arm donors and tests behind a pseudobulk design.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Pipeline runs and artifacts
|
|
2
|
+
|
|
3
|
+
Run the standard RNA recipe, reopen and audit runs, inspect and trace immutable artifacts, branch
|
|
4
|
+
from a run with exact refs, and record the analysis for handoff. Docs:
|
|
5
|
+
<https://scarf.readthedocs.io/en/latest/reference/api/pipeline.html>, <https://scarf.readthedocs.io/en/latest/reference/api/artifacts.html>,
|
|
6
|
+
<https://scarf.readthedocs.io/en/latest/concepts/provenance.html>, <https://scarf.readthedocs.io/en/latest/tutorials/reuse_and_tracing.html>,
|
|
7
|
+
<https://scarf.readthedocs.io/en/latest/analysis_with_agents.html>.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- First pass on any RNA store: one `ds.pipeline.run(label=...)` gives QC filtering, HVGs, PCA,
|
|
12
|
+
graph, UMAP, Leiden candidates, Paris, doublet scores and markers as one durable record.
|
|
13
|
+
- Resuming work: open a run by label or id; inspect any result before consuming it.
|
|
14
|
+
- Alternatives outside the fixed recipe: take refs from a completed run and pass them on.
|
|
15
|
+
|
|
16
|
+
## Key concepts
|
|
17
|
+
|
|
18
|
+
- Every result is an immutable artifact addressed by an `ArtifactRef` (frozen dataclass):
|
|
19
|
+
`scope` (`"assay"` or `"datastore"`), `kind`, `artifact_id` (64 hex chars; there is no `.id`),
|
|
20
|
+
`assay` (`None` for datastore scope). Cell selections and metadata snapshots are datastore-scoped.
|
|
21
|
+
- Reuse is content-addressed: same operation, parameters and exact input refs returns the existing
|
|
22
|
+
complete artifact. Changing anything creates a new artifact; upstream ones are reused.
|
|
23
|
+
`invalidate_cache=True` on a granular method forces a fresh one.
|
|
24
|
+
- `ds.pipeline.run` writes artifacts plus a run record only. It never edits live `I` or adds
|
|
25
|
+
metadata columns, and needs `zarr_mode="r+"` (the default); a read-only store raises
|
|
26
|
+
`PermissionError`, though `open`, reports and plots still work.
|
|
27
|
+
- `PipelineRun` is a read-only `Mapping[str, ArtifactRef]` (`keys`, `items`, `values`, `get`,
|
|
28
|
+
`run["pca"]`) plus `run_id`, `label`, `status`, `recipe` (`"basic_rna_analysis"`), `assay`,
|
|
29
|
+
`started_at_ns`, `finished_at_ns`, `report()`, `cells`, `features`. Outputs and views work only
|
|
30
|
+
on `status == "completed"`; otherwise they raise `RuntimeError`.
|
|
31
|
+
- Labels are immutable. Only a completed run acquires its label. A completed label cannot be
|
|
32
|
+
reused (`ValueError` before any work). Failed and interrupted runs reserve nothing.
|
|
33
|
+
- There is no resume. A new run reuses every complete artifact from earlier attempts.
|
|
34
|
+
- `run.cells` and `run.features` are frozen views of captured fields only. Live columns such as
|
|
35
|
+
author labels are absent unless listed in `snapshot_columns` on the run.
|
|
36
|
+
|
|
37
|
+
## Recipes
|
|
38
|
+
|
|
39
|
+
### Run the baseline
|
|
40
|
+
|
|
41
|
+
Default outputs and their kinds: `input_cell_selection`, `analysis_cell_selection`
|
|
42
|
+
(`cell_selection`), `feature_universe`, `highly_variable_features` (`feature_selection`),
|
|
43
|
+
`cell_cycle`, `normalized`, `pca` (`reduction`), `ann_index`, `neighbors`, `connectivity_map`,
|
|
44
|
+
`embedding_initialization`, `umap` (`embedding`), `leiden_0.5` ... `leiden_1.25`
|
|
45
|
+
(`cluster_labels`), `paris` (`cluster_cut`), `cluster_selection`, `clusters` (the selected Leiden
|
|
46
|
+
ref itself, not a copy), `doublets` (`doublet_score`), `markers` (`marker_table`). Harmony adds
|
|
47
|
+
`harmony` after `pca`; disabled stages omit their keys. A fresh baseline took 22 s on local
|
|
48
|
+
counts, 10 s of it one-time compilation in the graph stage; a second run in the same process took
|
|
49
|
+
11 s (10x 5K PBMC docs dataset). On remote counts expect about a minute for a few thousand cells.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import scarf
|
|
53
|
+
|
|
54
|
+
scarf.configure_output(level="WARNING", progress=False)
|
|
55
|
+
ds = scarf.DataStore("analysis.zarr", nthreads=4, min_features_per_cell=-1)
|
|
56
|
+
run = ds.pipeline.run(label="baseline")
|
|
57
|
+
list(run.keys())
|
|
58
|
+
run["clusters"] == run["leiden_0.5"] # True on the 10x 5K PBMC docs dataset
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Reopen, list and report runs
|
|
62
|
+
|
|
63
|
+
`open` needs exactly one of `label` or `run_id`; an unknown label raises `KeyError`. `list_runs`
|
|
64
|
+
is newest first, all statuses unless filtered, `limit=20` by default.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
run = ds.pipeline.open(label="baseline")
|
|
68
|
+
same = ds.pipeline.open(run_id=run.run_id)
|
|
69
|
+
ds.pipeline.list_runs(status="completed", limit=5)
|
|
70
|
+
report = run.report(format="dict") # keys: run, stages, summary
|
|
71
|
+
[(s["stage"], s["status"], s["metrics"]["wallSeconds"]) for s in report["stages"]]
|
|
72
|
+
report["run"]["config"] # resolved settings, including filtering method and params
|
|
73
|
+
len(report["summary"]["createdArtifacts"]), len(report["summary"]["reusedArtifacts"])
|
|
74
|
+
print(run.report(format="markdown")) # config, stage table, outputs, failure or interruption
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Read frozen fields
|
|
78
|
+
|
|
79
|
+
`run.cells.columns` here: `I, ids, names, s_score, g2m_score, cell_cycle_phase, umap_1, umap_2,
|
|
80
|
+
leiden_*, paris, clusters, doublet_score`, then any `snapshot_columns`; filter and Harmony columns
|
|
81
|
+
are not exposed. `run.features.columns`: `I, ids, names, highly_variable_features`. `fetch` returns
|
|
82
|
+
analysed rows; `fetch_all` aligns to every stored cell (3,847 and 5,025 on the 10x 5K PBMC docs
|
|
83
|
+
dataset, whose live `I` holds 3,948).
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
df = run.cells.to_pandas_dataframe(["umap_1", "umap_2", "clusters"])
|
|
87
|
+
clusters = run.cells.fetch("clusters") # int labels starting at 1
|
|
88
|
+
analysed = run.cells.fetch_all("I") # boolean mask over every stored cell
|
|
89
|
+
n_hvgs = int(run.features.fetch_all("highly_variable_features").sum())
|
|
90
|
+
mito = ds.cells.fetch_all("RNA_percentMito")[analysed] # a filter column, aligned to run cells
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Configure a run
|
|
94
|
+
|
|
95
|
+
Shortcuts and `params` sections are mutually exclusive for the same setting. Validation errors
|
|
96
|
+
(unknown section, column collision, reused label, `leiden={}`) raise before any run record exists.
|
|
97
|
+
`leiden.selected` replaces the silhouette choice and skips `cluster_selection`.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
custom = ds.pipeline.run(label="leiden_custom", leiden={"partitions": [0.4, 0.8]})
|
|
101
|
+
pinned = ds.pipeline.run(
|
|
102
|
+
label="leiden_pinned",
|
|
103
|
+
params={"leiden": {"partitions": [0.5, 1.0], "selected": 1.0}},
|
|
104
|
+
umap=False, doublets=False, markers=False, cell_cycle=False, paris=False,
|
|
105
|
+
)
|
|
106
|
+
pinned["clusters"] == pinned["leiden_1.0"] # True; no cluster_selection key
|
|
107
|
+
labelled = ds.pipeline.run(label="with_labels", snapshot_columns=["cell_type"]) # full recompute
|
|
108
|
+
"cell_type" in labelled.cells.columns # True
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`cell_type` stands for a label column you own; never snapshot held-out author labels.
|
|
112
|
+
|
|
113
|
+
### Handle failures and interruptions
|
|
114
|
+
|
|
115
|
+
A stage error raises `PipelineExecutionError` (`run_id`, `stage`, cause in `__cause__`). SIGTERM,
|
|
116
|
+
SIGINT, SIGHUP or `KeyboardInterrupt` stop at the next safe checkpoint (in testing, the running
|
|
117
|
+
stage finished and kept its artifact) and record `status="interrupted"`. Rerun with the same
|
|
118
|
+
label; completed artifacts are reused (interrupted during UMAP: rerun 10 s, 16 reused; 10x 5K
|
|
119
|
+
PBMC docs dataset). Run it as `timeout 1800 python job.py`; behind a wrapper such as `uv run`, put
|
|
120
|
+
`timeout` inside it (`uv run timeout 1800 python job.py`), because `timeout 1800 uv run ...` killed the run without the handler (performance-and-export.md).
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from scarf import PipelineExecutionError
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
ds.pipeline.run(label="bad", params={"paris": {"min_cluster_size": -5}})
|
|
127
|
+
except PipelineExecutionError as error:
|
|
128
|
+
failed = ds.pipeline.open(run_id=error.run_id)
|
|
129
|
+
error.stage, failed.status, failed.report(format="dict")["run"]["error"]
|
|
130
|
+
interrupted = ds.pipeline.list_runs(status="interrupted")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A process killed mid-stage (SIGKILL, OOM, or a `timeout` outside a `uv run` wrapper) leaves a record
|
|
134
|
+
stuck at `status="running"` with `summary["uncleanIncomplete"]` true; it claimed no label, so a
|
|
135
|
+
rerun with the label works. Check the process, not the status. Not executed (needs a hard-killed
|
|
136
|
+
finalizer): if a process died after claiming its label, the label stays blocked. After confirming
|
|
137
|
+
the process is gone, call `ds.pipeline.abandon_label_claim(label=..., run_id=..., reason=...)`,
|
|
138
|
+
then rerun. It refuses completed owners and has no timeout heuristic.
|
|
139
|
+
|
|
140
|
+
### Inspect, load and find artifacts
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
status = ds.inspect_artifact(run["pca"])
|
|
144
|
+
status.exists, status.complete, status.path, status.operation # ..., "run_pca"
|
|
145
|
+
status.parameters # {'dims': 21, 'feat_scaling': True}
|
|
146
|
+
status.inputs # serialized refs: normalized, feature_scaling, pca_cell_selection
|
|
147
|
+
status.input_ref("normalized") == run["normalized"] # True
|
|
148
|
+
status.execution_options, status.created_at_ns, status.scarf_version
|
|
149
|
+
group = ds.load_artifact(run["pca"]) # zarr group: data, loadings, center
|
|
150
|
+
group["data"].shape # (analysed cells, 21)
|
|
151
|
+
|
|
152
|
+
ds.list_artifacts(complete_only=True) # assay scope, default assay
|
|
153
|
+
ds.list_artifacts(kind="cell_selection", scope="datastore")
|
|
154
|
+
on_graph = ds.list_artifacts(
|
|
155
|
+
kind="cluster_labels",
|
|
156
|
+
operation="run_leiden_clustering",
|
|
157
|
+
inputs={"graph": run["connectivity_map"]},
|
|
158
|
+
)
|
|
159
|
+
[ds.inspect_artifact(ref).parameters["resolution"] for ref in on_graph]
|
|
160
|
+
|
|
161
|
+
decision = ds.load_artifact(run["cluster_selection"])
|
|
162
|
+
decision.attrs["selectedKey"], decision.attrs["candidateKeys"], decision["scores"][:]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Branch from a completed run and trace lineage
|
|
166
|
+
|
|
167
|
+
Pass exact run refs to granular methods. Same inputs return the run's own artifacts; new
|
|
168
|
+
parameters create a branch. Explicit normalization/PCA/graph chains: features-and-graphs.md.
|
|
169
|
+
Clustering choices: clustering-and-embedding.md.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
graph = run["connectivity_map"]
|
|
173
|
+
ds.run_leiden_clustering(graph, resolution=0.5) == run["leiden_0.5"] # reused, no recompute
|
|
174
|
+
fine_clusters = ds.run_leiden_clustering(graph, resolution=2.0) # new cluster_labels ref
|
|
175
|
+
fine_markers = ds.run_marker_search(fine_clusters, features=run["feature_universe"])
|
|
176
|
+
|
|
177
|
+
cells = run["analysis_cell_selection"]
|
|
178
|
+
hvgs = run["highly_variable_features"]
|
|
179
|
+
ds.run_normalization(cells, hvgs) == run["normalized"] # True: reused
|
|
180
|
+
ds.plots.embedding(layout=run["umap"], color_by=fine_clusters, show=False).close()
|
|
181
|
+
|
|
182
|
+
lineage = ds.lineage({"selected": run["clusters"], "fine": fine_clusters})
|
|
183
|
+
markdown = lineage.to_markdown() # Mermaid graph (shared upstream once) plus details
|
|
184
|
+
mermaid = lineage.to_mermaid()
|
|
185
|
+
snapshot = ds.summary().to_dict() # columns, artifact inventory, run counts, labels
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Record the analysis for handoff
|
|
189
|
+
|
|
190
|
+
Write the prospective record (question, unit of inference, input refs, planned writes,
|
|
191
|
+
alternatives, decision criteria) before the first mutating call, then extend it into the handoff:
|
|
192
|
+
add metadata roles and confounding, evidence per alternative, unsupported claims, required
|
|
193
|
+
validation and exported files. Refs round-trip with `to_dict()` and `ArtifactRef.from_dict()`.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
import json
|
|
197
|
+
from pathlib import Path
|
|
198
|
+
|
|
199
|
+
out = Path("handoff")
|
|
200
|
+
out.mkdir(exist_ok=True)
|
|
201
|
+
refs = {"cellSelection": "analysis_cell_selection", "featureSelection": "highly_variable_features",
|
|
202
|
+
"graph": "connectivity_map", "selectedClusters": "clusters"}
|
|
203
|
+
record = {
|
|
204
|
+
"question": "Which broad populations are present?",
|
|
205
|
+
"unitOfInference": "cells from one sample; no replicate-level claims",
|
|
206
|
+
"pipelineRun": {"runId": run.run_id, "label": run.label, "status": run.status},
|
|
207
|
+
"refs": {name: run[key].to_dict() for name, key in refs.items()}
|
|
208
|
+
| {"fineClusters": fine_clusters.to_dict()},
|
|
209
|
+
"alternatives": "Leiden 0.5 (pipeline choice) vs 2.0",
|
|
210
|
+
"decision": "provisional", "openQuestions": ["Does MAD filtering remove a real population?"],
|
|
211
|
+
}
|
|
212
|
+
(out / "handoff.json").write_text(json.dumps(record, indent=2))
|
|
213
|
+
(out / "run_report.md").write_text(run.report(format="markdown"))
|
|
214
|
+
(out / "lineage.md").write_text(lineage.to_markdown())
|
|
215
|
+
saved = json.loads((out / "handoff.json").read_text())
|
|
216
|
+
scarf.ArtifactRef.from_dict(saved["refs"]["fineClusters"]) == fine_clusters # True
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Parameters that matter
|
|
220
|
+
|
|
221
|
+
| Parameter | Default | Change when |
|
|
222
|
+
| --- | --- | --- |
|
|
223
|
+
| `label` | `None` | Always name runs you will reopen; pick a new name per variant. |
|
|
224
|
+
| `cell_key` | `"I"` | A boolean column already defines the cohort. |
|
|
225
|
+
| `filtering` | `True` (pooled MAD, 3 MADs, over `RNA_nCounts/nFeatures/percentMito/percentRibo`) | Data already filtered (`False`) or per-sample or manual bounds needed (mapping; see quality-control.md). |
|
|
226
|
+
| `harmony_batch_columns` | `None` | Real batch columns exist; see integration-and-comparisons.md. |
|
|
227
|
+
| `hvg_count` / `pca_dims` / `neighbors_k` | `1000` / `21` / `11` | A representation choice is under test. `pca_dims=0` skips PCA (output key `reduction`). |
|
|
228
|
+
| `umap` | `True` | `False` skips `embedding_initialization` and `umap`. |
|
|
229
|
+
| `leiden` | `True` = `[0.5, 0.75, 1.0, 1.25]` | Other candidates: `{"partitions": [...]}`; pin with `params["leiden"]["selected"]`. |
|
|
230
|
+
| `cell_cycle`, `paris` | `True` | Skip to save time (cell cycle is the slowest stage on remote counts). |
|
|
231
|
+
| `doublets`, `markers` | `True` | `False` to skip; both are required off when `leiden=False`. |
|
|
232
|
+
| `snapshot_columns` | `()` | Run-mode plots or `run.cells` need live columns. Choose on the first run (see Pitfalls). |
|
|
233
|
+
| `params` | `None` | Stage settings (`hvg`, `pca`, `umap`, `leiden`, `tsne`, `membership_strength`, ...). |
|
|
234
|
+
| `callback` | `None` | Progress events: `event.kind`, `event.stage`, `event.error`. |
|
|
235
|
+
|
|
236
|
+
## Check before moving on
|
|
237
|
+
|
|
238
|
+
- `run.status == "completed"` and the expected keys are present.
|
|
239
|
+
- `run.report(format="markdown")`: stage statuses, wall seconds, created versus reused counts,
|
|
240
|
+
resolved `filtering` config, and any failure or interruption section.
|
|
241
|
+
- Kept cells: `run.cells.fetch_all("I").sum()` versus the store total. Audit removed cells (by
|
|
242
|
+
your own clusters or a marker panel, not held-out labels) before trusting clusters.
|
|
243
|
+
- `decision["scores"]` from `cluster_selection`: silhouette is a provisional baseline, not ground
|
|
244
|
+
truth.
|
|
245
|
+
- For any ref you consume: `ds.inspect_artifact(ref).complete` is `True`, and `operation`,
|
|
246
|
+
`parameters` and `inputs` match what you intend.
|
|
247
|
+
|
|
248
|
+
## Pitfalls
|
|
249
|
+
|
|
250
|
+
- `snapshot_columns` (and Harmony columns) enter the cell snapshot that feeds filtering, so adding
|
|
251
|
+
one changes `analysis_cell_selection` and recomputes everything downstream. Same parameters under
|
|
252
|
+
a new label reused all 26 artifacts in 2 s; adding `["cell_type"]` created 24 of 27 again
|
|
253
|
+
(10x 5K PBMC docs dataset).
|
|
254
|
+
- `ds.list_artifacts(kind="cell_selection")` silently returns `[]`; pass `scope="datastore"`.
|
|
255
|
+
- Compare cell selections from different routes by mask, not by ref: equal values can have
|
|
256
|
+
different ids.
|
|
257
|
+
- A `PipelineRun` is bound to the `DataStore` object that opened it. After reopening the store,
|
|
258
|
+
reopen the run, or plotting raises `ValueError`.
|
|
259
|
+
- `run.get(key)` returns `None` for a disabled stage, but raises `RuntimeError` on a run that did
|
|
260
|
+
not complete.
|
|
261
|
+
- `run.cells.fetch("clusters")` is integer, while `get_markers(...)["group_id"]` is a string.
|
|
262
|
+
- In a backgrounded non-interactive shell, SIGINT is ignored; use SIGTERM to stop a run cleanly.
|
|
263
|
+
`SIGKILL` or OOM leaves an incomplete run with no cleanup.
|
|
264
|
+
- Never read private Zarr paths or mutate `ds.z`, `ds.zw` or run records; use the methods above.
|
|
265
|
+
|
|
266
|
+
## See also
|
|
267
|
+
|
|
268
|
+
- quality-control.md: filtering policies behind `analysis_cell_selection`.
|
|
269
|
+
- features-and-graphs.md: explicit normalization, PCA, ANN, neighbours and graph chains.
|
|
270
|
+
- clustering-and-embedding.md: re-clustering, UMAP/t-SNE branches, partition comparison.
|
|
271
|
+
- markers-and-annotation.md: `get_markers`, marker tables and labels.
|
|
272
|
+
- plotting.md: `ds.plots.*(run=run, ...)` versus explicit refs.
|
|
273
|
+
- performance-and-export.md: `ds.to_anndata(run=run)` and resource settings.
|