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,270 @@
|
|
|
1
|
+
# Clustering and embedding
|
|
2
|
+
|
|
3
|
+
Partition a cell graph with Leiden or Paris, choose a partition with evidence, build UMAP or t-SNE
|
|
4
|
+
layouts, and extract labels and coordinates as cell-aligned arrays. Docs:
|
|
5
|
+
<https://scarf.readthedocs.io/en/latest/tutorials/clustering.html>, <https://scarf.readthedocs.io/en/latest/tutorials/dimensionality_reduction.html>,
|
|
6
|
+
<https://scarf.readthedocs.io/en/latest/reference/api/datastore.html>, <https://scarf.readthedocs.io/en/latest/reference/api/integration.html>.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- A completed run or an explicit graph exists, and you need other resolutions, a hierarchy or a
|
|
11
|
+
new layout on that same graph.
|
|
12
|
+
- You must justify a partition (resolution, seed, representation) before naming clusters.
|
|
13
|
+
- You need cluster labels or layout coordinates as a NumPy array or a DataFrame indexed by cell ID.
|
|
14
|
+
|
|
15
|
+
## Key concepts
|
|
16
|
+
|
|
17
|
+
- Every producer takes an exact graph `ArtifactRef` (`run["connectivity_map"]` or an integrated
|
|
18
|
+
graph) and returns a new immutable `ArtifactRef`. None of them writes to `ds.cells`. Repeating a
|
|
19
|
+
call reuses the stored artifact; `invalidate_cache=True` creates another one.
|
|
20
|
+
- Payloads: Leiden `kind="cluster_labels"` stores `values` (int, numbered from 1); Paris
|
|
21
|
+
`kind="cluster_cut"` stores `labels`; UMAP and t-SNE `kind="embedding"` store `values` with shape
|
|
22
|
+
`(n_cells, dims)`.
|
|
23
|
+
- Payload rows follow the graph's frozen cell selection in stored-axis order, not every cell in
|
|
24
|
+
the store (3,847 of 5,025 cells in the 10x 5K PBMC docs dataset baseline). Recover that
|
|
25
|
+
selection with `ds.inspect_artifact(ref).input_ref("cell_selection")`.
|
|
26
|
+
- The default pipeline stores `leiden_0.5`, `leiden_0.75`, `leiden_1.0`, `leiden_1.25` and `paris`.
|
|
27
|
+
`run["clusters"]` is the Leiden candidate with the best PCA silhouette, recorded in
|
|
28
|
+
`run["cluster_selection"]`. Silhouette favours few compact clusters and can merge distinct
|
|
29
|
+
lineages or states. Treat that pick as a reproducible baseline only.
|
|
30
|
+
- Leiden is seeded (`random_seed=4444`), so a single run says nothing about stability. Agreement
|
|
31
|
+
across seeds, nested resolutions, markers and representations is the evidence.
|
|
32
|
+
- Leiden, Paris, layouts, membership strength and separability use only the graph or PCA and take
|
|
33
|
+
seconds. Marker search is the only step on this page that reads counts.
|
|
34
|
+
|
|
35
|
+
## Recipes
|
|
36
|
+
|
|
37
|
+
### Open the graph and baseline refs
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import numpy as np
|
|
41
|
+
import pandas as pd
|
|
42
|
+
import scarf
|
|
43
|
+
|
|
44
|
+
scarf.configure_output(level="WARNING", progress=False)
|
|
45
|
+
ds = scarf.DataStore("analysis.zarr", nthreads=4, min_features_per_cell=-1) # writable
|
|
46
|
+
run = ds.pipeline.open(label="baseline")
|
|
47
|
+
graph = run["connectivity_map"]
|
|
48
|
+
print([key for key in run if key.startswith(("leiden", "paris", "cluster"))])
|
|
49
|
+
|
|
50
|
+
def labels_of(ref):
|
|
51
|
+
"""Return a cluster payload as an array aligned to the ref's cell selection."""
|
|
52
|
+
key = "labels" if ref.kind == "cluster_cut" else "values"
|
|
53
|
+
return np.asarray(ds.load_artifact(ref)[key][:])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Build the partition evidence table
|
|
57
|
+
|
|
58
|
+
One row per candidate: stability over seeds, nesting in the next coarser candidate, separability,
|
|
59
|
+
graph support and marker support. Each marker search streams counts once.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from itertools import combinations
|
|
63
|
+
from sklearn.metrics import adjusted_rand_score
|
|
64
|
+
|
|
65
|
+
leiden, values, rows = {}, {}, []
|
|
66
|
+
for resolution in (0.5, 1.0, 1.5):
|
|
67
|
+
refs = [ds.run_leiden_clustering(graph, resolution=resolution, random_seed=seed)
|
|
68
|
+
for seed in (4444, 1, 2)]
|
|
69
|
+
name = f"leiden_{resolution}"
|
|
70
|
+
leiden[name], values[name] = refs[0], labels_of(refs[0]) # seed 4444 = the pipeline's
|
|
71
|
+
seeds = [labels_of(ref) for ref in refs]
|
|
72
|
+
rows.append({"candidate": name, "n_clusters": len(np.unique(seeds[0])),
|
|
73
|
+
"smallest": pd.Series(seeds[0]).value_counts().min(),
|
|
74
|
+
"min_seed_ari": min(adjusted_rand_score(a, b) for a, b in combinations(seeds, 2))})
|
|
75
|
+
evidence = pd.DataFrame(rows).set_index("candidate")
|
|
76
|
+
names = list(leiden)
|
|
77
|
+
evidence["nested_frac"] = [np.nan] + [ # share of cells inside one cluster of the coarser one
|
|
78
|
+
pd.crosstab(values[fine], values[coarse]).max(axis=1).sum() / len(values[fine])
|
|
79
|
+
for coarse, fine in zip(names, names[1:])]
|
|
80
|
+
sep = ds.metric_cluster_separability(run["pca"], leiden) # coordinates the graph was built on
|
|
81
|
+
evidence = evidence.join(sep.clustering_scores.set_index("clustering")[
|
|
82
|
+
["macro_f1_mean", "silhouette_score"]])
|
|
83
|
+
for name, ref in leiden.items():
|
|
84
|
+
strength = np.asarray(ds.load_artifact(ds.calc_membership_strength(ref, graph))["values"][:])
|
|
85
|
+
evidence.loc[name, "weak_cells"] = (strength < 0.5).mean()
|
|
86
|
+
per_cluster_strength = pd.Series(strength).groupby(values[name]).mean()
|
|
87
|
+
evidence.loc[name, "min_cluster_strength"] = per_cluster_strength.min()
|
|
88
|
+
found = ds.get_markers(marker=ds.run_marker_search(ref, features=run["feature_universe"]))
|
|
89
|
+
n_markers = found["group_id"].value_counts().reindex(
|
|
90
|
+
[str(g) for g in np.unique(values[name])], fill_value=0)
|
|
91
|
+
evidence.loc[name, "clusters_lt3_markers"] = int((n_markers < 3).sum())
|
|
92
|
+
print(evidence.round(3).to_string())
|
|
93
|
+
print(sep.cluster_scores.sort_values("f1_score").head(5)) # weakest individual clusters
|
|
94
|
+
nesting = ds.smart_label(leiden["leiden_1.0"], leiden["leiden_0.5"]) # "2a", "2b": splits of 2
|
|
95
|
+
print(pd.Series(labels_of(nesting)).value_counts().head(10))
|
|
96
|
+
selection = ds.load_artifact(run["cluster_selection"]) # the pipeline's silhouette choice
|
|
97
|
+
print(dict(zip(selection.attrs["candidateKeys"], np.asarray(selection["scores"][:]).round(3))))
|
|
98
|
+
print("selected:", selection.attrs["selectedKey"], "invalid:", selection.attrs["invalidReasons"])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Membership strength is the fraction of a cell's graph neighbours sharing its most common label.
|
|
102
|
+
Pass the Harmony ref instead of `run["pca"]` when the graph was built on Harmony coordinates.
|
|
103
|
+
|
|
104
|
+
### QC and doublet covariates per cluster
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
in_run = run.cells.fetch_all("I").astype(bool)
|
|
108
|
+
per_cell = pd.DataFrame({c: ds.cells.fetch_all(c)[in_run]
|
|
109
|
+
for c in ("RNA_nCounts", "RNA_nFeatures", "RNA_percentMito")})
|
|
110
|
+
per_cell["doublet_score"] = run.cells.fetch("doublet_score")
|
|
111
|
+
per_cluster = per_cell.groupby(values["leiden_1.0"]).median()
|
|
112
|
+
print(per_cluster.round(2), per_cell.median().round(2), sep="\n") # each cluster vs all cells
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A cluster that stands out only on counts, mito or doublet score is technical until markers say
|
|
116
|
+
otherwise. Doublet scores run high in high-RNA types (plasma cells), so compare within a lineage.
|
|
117
|
+
|
|
118
|
+
### Representation sensitivity
|
|
119
|
+
|
|
120
|
+
Repeat the evidence on one branch (more HVGs, other PCs or `k`; `features-and-graphs.md`). The
|
|
121
|
+
cells must match, so labels compare directly.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
alt = ds.pipeline.run(label="hvg2k_pc30_k21", hvg_count=2000, pca_dims=30, neighbors_k=21)
|
|
125
|
+
assert alt["analysis_cell_selection"] == run["analysis_cell_selection"]
|
|
126
|
+
alt_refs = [ds.run_leiden_clustering(alt["connectivity_map"], resolution=resolution)
|
|
127
|
+
for resolution in (0.5, 1.0, 1.5)]
|
|
128
|
+
for name, ref in leiden.items(): # best match over branch resolutions; k shifts the scale
|
|
129
|
+
evidence.loc[name, "ari_vs_branch"] = max(ds.metric_label_concordance(ref, other)
|
|
130
|
+
for other in alt_refs)
|
|
131
|
+
print(evidence.round(3).to_string())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Choose the partition
|
|
135
|
+
|
|
136
|
+
`run["clusters"]` (the silhouette pick) is a baseline candidate, never the answer by itself.
|
|
137
|
+
Combine the evidence; no single score decides.
|
|
138
|
+
|
|
139
|
+
1. Stability: keep candidates whose `min_seed_ari` stays near the coarsest one (about 0.9 or
|
|
140
|
+
more is a rule of thumb). Below that, the optimizer moves boundaries between seeds.
|
|
141
|
+
2. Nesting: prefer a candidate that refines the coarser one (`nested_frac` near 1, `smart_label`
|
|
142
|
+
shows "2a", "2b"). A non-nested jump, where many cells change parent, is a reason to reject it.
|
|
143
|
+
3. Markers: every cluster a finer candidate adds needs its own specific markers (about 3 or more
|
|
144
|
+
at default `get_markers` filters, named genes, not only ribosomal, mitochondrial or ambient).
|
|
145
|
+
4. Graph support: added clusters keep high mean membership strength and few weak cells.
|
|
146
|
+
5. QC and doublets: a split explained by counts, mito or doublet score is not a cell type. Keep it
|
|
147
|
+
as `unresolved` (`markers-and-annotation.md`) instead of naming it.
|
|
148
|
+
6. Representation: boundaries that survive an HVG, PC or `k` branch are robust; those that move
|
|
149
|
+
are provisional and need markers to stand.
|
|
150
|
+
|
|
151
|
+
Choose the finest candidate whose added splits all pass 1 to 5, and report 6. If the silhouette
|
|
152
|
+
pick merges populations that markers separate, a finer candidate that passes wins. Prefer a coarser
|
|
153
|
+
global partition plus targeted subclustering (`features-and-graphs.md`, "Subcluster one lineage")
|
|
154
|
+
when the lineage split is stable at a low resolution but finer global resolutions lose seed
|
|
155
|
+
stability, break nesting, or split only one lineage. The usual case is T and NK subsets in blood:
|
|
156
|
+
global HVGs are dominated by between-lineage genes, so a within-lineage graph resolves states
|
|
157
|
+
(naive, memory, regulatory, cytotoxic) that a finer global cut either misses or splits unstably.
|
|
158
|
+
|
|
159
|
+
(10x 5K PBMC docs dataset) Minimum seed ARI was 0.97, 0.82 and 0.60 at 0.5, 1.0 and 1.5; at 1.0,
|
|
160
|
+
four clusters had fewer than 3 markers; ARI with the branch fell from 0.86 to 0.61. The evidence
|
|
161
|
+
kept 0.5. A T-cell subgraph then gave a FOXP3/CTLA4 group (FOXP3 in 0.43 of cells); no global
|
|
162
|
+
cluster up to resolution 2.0 exceeded 0.22.
|
|
163
|
+
|
|
164
|
+
### Build a Paris hierarchy
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from dataclasses import asdict
|
|
168
|
+
|
|
169
|
+
paris = ds.run_paris_clustering(graph) # n_clusters="auto"; reuses run["paris"] when identical
|
|
170
|
+
result = ds.load_paris_clustering(paris)
|
|
171
|
+
diag = pd.DataFrame([asdict(item) for item in result.diagnostics])
|
|
172
|
+
print(result.n_clusters, diag[["label", "size", "persistence", "decision_margin", "forced"]])
|
|
173
|
+
paris_8 = ds.run_paris_clustering(graph, n_clusters=8)
|
|
174
|
+
print(ds.metric_label_concordance(paris, leiden["leiden_0.5"]))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
High `persistence` means a branch survives over a wide range of the hierarchy. `forced=True` groups
|
|
178
|
+
satisfy a structural constraint and are not, by themselves, biological evidence.
|
|
179
|
+
|
|
180
|
+
### Build UMAP and t-SNE layouts
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
init = run["embedding_initialization"] # K-means initialization built on run["pca"]
|
|
184
|
+
umap_tight = ds.run_umap(graph, init, min_dist=0.5)
|
|
185
|
+
tsne = ds.run_tsne(graph, init, verbose=False)
|
|
186
|
+
# For new coordinates, build a matching init first:
|
|
187
|
+
# init = ds.build_embedding_initialization(new_pca_ref)
|
|
188
|
+
print(ds.load_artifact(umap_tight)["values"].shape, ds.load_artifact(tsne)["values"].shape)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Get labels and coordinates from a pipeline run
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
cells = run.cells.to_pandas_dataframe(
|
|
195
|
+
["ids", "clusters", "leiden_1.0", "paris", "umap_1", "umap_2", "doublet_score"])
|
|
196
|
+
print(cells.head()) # one row per run cell, in stored-axis order
|
|
197
|
+
full = run.cells.fetch_all("clusters") # length ds.cells.N; -1 for cells outside the run
|
|
198
|
+
print(len(full), (full == -1).sum())
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`run.cells.columns` lists the frozen fields. `fetch(...)` returns run cells only. `fetch_all(...)`
|
|
202
|
+
fills cells outside the run with -1 (integers), NaN (floats) or "" (strings).
|
|
203
|
+
|
|
204
|
+
### Get labels and coordinates from explicit artifacts
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
def cell_ids_of(ref):
|
|
208
|
+
selection = ds.inspect_artifact(ref).input_ref("cell_selection")
|
|
209
|
+
mask = np.asarray(ds.load_artifact(selection)["values"][:], dtype=bool)
|
|
210
|
+
return ds.cells.fetch_all("ids")[mask]
|
|
211
|
+
|
|
212
|
+
ids = cell_ids_of(umap_tight)
|
|
213
|
+
assert np.array_equal(ids, cell_ids_of(leiden["leiden_1.5"])) # same frozen selection
|
|
214
|
+
xy = np.asarray(ds.load_artifact(umap_tight)["values"][:])
|
|
215
|
+
frame = pd.DataFrame({"leiden_1.5": labels_of(leiden["leiden_1.5"]),
|
|
216
|
+
"x": xy[:, 0], "y": xy[:, 1]},
|
|
217
|
+
index=pd.Index(ids, name="cell_id"))
|
|
218
|
+
print(frame.head())
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The same helper works for membership strength, doublet scores, cell cycle and enrichment scores.
|
|
222
|
+
|
|
223
|
+
## Parameters that matter
|
|
224
|
+
|
|
225
|
+
| Parameter | Default | Change when |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `resolution` (Leiden) | `1.0` (pipeline: 0.5, 0.75, 1.0, 1.25) | Raise for finer splits, lower to merge; always compare several |
|
|
228
|
+
| `random_seed` (Leiden) | `4444` | Rerun with 2 or more other seeds to test stability |
|
|
229
|
+
| `backend` (Leiden) | `"igraph"` | `"leidenalg"` only to match an external result |
|
|
230
|
+
| `n_clusters` (Paris) | `"auto"` | An integer to match another partition; it must be cuttable at one height, else `ValueError` |
|
|
231
|
+
| `min_cluster_size` (Paris) | `None` (resolved automatically) | Raise to suppress tiny branches in the auto cut |
|
|
232
|
+
| `min_dist` / `spread` (UMAP) | `1.0` / `2.0` | Lower `min_dist` (0.1 to 0.5) for tighter packing; appearance only |
|
|
233
|
+
| `use_density_map` (UMAP) | `False` | `True` for densMAP when relative density matters |
|
|
234
|
+
| `parallel` (UMAP, t-SNE) | `False` | `True` is faster but not reproducible |
|
|
235
|
+
|
|
236
|
+
## Check before moving on
|
|
237
|
+
|
|
238
|
+
- The evidence table exists for at least three candidates, and the chosen one is justified by
|
|
239
|
+
the six criteria above, not by silhouette alone. Record why coarser and finer were rejected.
|
|
240
|
+
- Every cluster under about 20 cells, or with mean membership strength well below the rest, has
|
|
241
|
+
been reviewed: doublet group, transition, contaminant or over-split.
|
|
242
|
+
- No cluster is named whose only distinction is a QC metric or doublet score.
|
|
243
|
+
- Lineages that need finer structure are listed for targeted subclustering, with the parent
|
|
244
|
+
partition kept for audit.
|
|
245
|
+
|
|
246
|
+
## Pitfalls
|
|
247
|
+
|
|
248
|
+
- Paris stores `labels`, not `values`. `ds.load_artifact(paris)["values"]` raises `KeyError`.
|
|
249
|
+
- Payload length is the selection size, not `ds.cells.N`. Align through the cell selection before
|
|
250
|
+
inserting into `ds.cells` or joining with other metadata.
|
|
251
|
+
- Cluster IDs are arbitrary per artifact. Never carry annotations from one partition to another by
|
|
252
|
+
ID; join on cell IDs or use `smart_label`.
|
|
253
|
+
- `metric_label_concordance` requires both artifacts to share the exact cell-selection ref. For
|
|
254
|
+
labels from different selections or from metadata, use sklearn on aligned arrays.
|
|
255
|
+
- Comparing branches at the same resolution only: `k` and HVG count shift the resolution scale.
|
|
256
|
+
- UMAP and t-SNE distances, empty space and island size are not biological measurements. Never
|
|
257
|
+
choose a resolution from a layout alone.
|
|
258
|
+
- A read-only store (`zarr_mode="r"`) raises `PermissionError` unless an identical artifact exists.
|
|
259
|
+
- `run_tsne` uses an `sgtsne` executable on `PATH` when present, otherwise the `sgtsnepi` Python
|
|
260
|
+
package. The docs state that t-SNE is unsupported on macOS and Windows.
|
|
261
|
+
|
|
262
|
+
## See also
|
|
263
|
+
|
|
264
|
+
- `features-and-graphs.md`: building graphs, HVG and `k` branches, subclustering one lineage.
|
|
265
|
+
- `markers-and-annotation.md`: marker tables and labels for the chosen partition.
|
|
266
|
+
- `quality-control.md`: doublet scores and QC covariates behind technical clusters.
|
|
267
|
+
- `plotting.md`: `ds.plots.embedding(layout=umap_ref, color_by=cluster_ref)`,
|
|
268
|
+
`cluster_connectivity(graph=, groups=, layout=)` and `cluster_tree(graph=, clusters=paris_ref)`.
|
|
269
|
+
- `pipeline-runs-and-artifacts.md`: run keys, `leiden=` pipeline settings, lineage.
|
|
270
|
+
- `integration-and-comparisons.md`: clustering on integrated graphs and batch metrics.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Data access
|
|
2
|
+
|
|
3
|
+
Open, inspect, import, and connect to Scarf DataStores, including Cytebase remote and mounted
|
|
4
|
+
stores. Docs: <https://scarf.readthedocs.io/en/latest/tutorials/data_organization.html>,
|
|
5
|
+
<https://scarf.readthedocs.io/en/latest/tutorials/import_and_export.html>, <https://scarf.readthedocs.io/en/latest/tutorials/cytebase.html>,
|
|
6
|
+
<https://scarf.readthedocs.io/en/latest/tutorials/remote_stores.html>, <https://scarf.readthedocs.io/en/latest/reference/api/datastore.html>, `import_export.html`, `cytebase.html` (same folder).
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- First contact with any dataset: choose read-only or writable, then inspect before computing.
|
|
11
|
+
- Converting 10x, Matrix Market, H5AD, or Seurat RDS input into a Scarf Zarr store.
|
|
12
|
+
- Finding a Cytebase dataset, exploring it remotely, or mounting it for analysis that saves results.
|
|
13
|
+
- Reopening a store or a mount from an earlier session.
|
|
14
|
+
|
|
15
|
+
## Key concepts
|
|
16
|
+
|
|
17
|
+
- A store is one Zarr directory: `cellData` (shared cell table, `ds.cells`), one group per assay
|
|
18
|
+
(`ds.RNA`; feature table `ds.RNA.feats`; counts; assay artifacts), datastore-scoped artifacts,
|
|
19
|
+
and `pipeline/runs`. Cell QC columns are assay-prefixed: `RNA_nCounts`, `RNA_nFeatures`,
|
|
20
|
+
`RNA_percentMito`, `RNA_percentRibo`. Tables are `MetaData` objects, not DataFrames.
|
|
21
|
+
- `I` is the live Boolean cell key (feature tables have one too). `fetch(col)` returns rows where
|
|
22
|
+
`I` is true; `fetch_all(col)` returns every row. Analytical filtering returns a cell-selection
|
|
23
|
+
artifact and never edits `I`.
|
|
24
|
+
- `zarr_mode="r+"` is the default. A writable open WRITES: first-open preparation of a freshly
|
|
25
|
+
written store (QC columns, feature `nCells`/`dropOuts`), the `min_features_per_cell` filter
|
|
26
|
+
applied to `I`, and the `defaultAssay`/`assayTypes` attributes. `zarr_mode="r"` writes nothing;
|
|
27
|
+
producers return a matching existing artifact but raise `PermissionError` instead of computing.
|
|
28
|
+
- RNA assays hold cell-major `counts` plus gene-major `countsT`; other assays hold `counts` only.
|
|
29
|
+
Stores from older releases (Zarr v2, no `countsT`, an `{assay}/state` group) fail to open:
|
|
30
|
+
re-import the source.
|
|
31
|
+
- A mount is a local writable target whose counts resolve from a separate source recorded in the
|
|
32
|
+
root `matrixSource` attribute (absolute path or URI). Metadata is copied once at mount time; new
|
|
33
|
+
artifacts are written only to the target.
|
|
34
|
+
- Cytebase has three layers: `Catalog` (verified local DuckDB copy of the catalog, no counts),
|
|
35
|
+
`open_datastore` (read-only, everything remote), `mount_datastore` (local writable target,
|
|
36
|
+
counts remote, plus a `<target>.cytebase.json` receipt beside it).
|
|
37
|
+
- `workspace` names a group inside one Zarr with its own `cellData`, assays, and artifacts (counts
|
|
38
|
+
under `matrices/`). Writers default to `None` (root layout); keep `None` unless the store was
|
|
39
|
+
written with a workspace. A mount records one source workspace and rejects another.
|
|
40
|
+
|
|
41
|
+
## Recipes
|
|
42
|
+
|
|
43
|
+
### Open and inspect a store
|
|
44
|
+
|
|
45
|
+
Design and annotation inference starts with `scripts/inspect_store.py STORE.zarr`, which ships with
|
|
46
|
+
the maintained copy of this skill in the Scarf repository (this catalogue copy does not include
|
|
47
|
+
it). Read-only, it flags annotation-like columns (values hidden unless
|
|
48
|
+
`--show-annotation-values`), design-like, author-derived and unflagged columns, and checks
|
|
49
|
+
whether counts look raw, corrected or pre-filtered. Flags are name-based hints: review every
|
|
50
|
+
column. By hand, open read-only until you need to write; `summary()` is metadata-only.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import scarf
|
|
54
|
+
|
|
55
|
+
scarf.configure_output(level="WARNING", progress=False)
|
|
56
|
+
ds = scarf.DataStore("analysis.zarr", zarr_mode="r")
|
|
57
|
+
print(ds) # "N_active (N_total) cells", assays, column names
|
|
58
|
+
snap = ds.summary().to_dict() # JSON-safe
|
|
59
|
+
print(snap["default_assay"], snap["total_cells"], snap["active_cells"], snap["resources"])
|
|
60
|
+
print(snap["pipeline_run_counts"], snap["labeled_pipeline_runs"])
|
|
61
|
+
print([(a["name"], a["assay_type"], a["total_features"], len(a["artifacts"])) for a in snap["assays"]])
|
|
62
|
+
print(ds.pipeline.list_runs()) # newest first
|
|
63
|
+
print(ds.list_artifacts(complete_only=True)) # default-assay artifacts
|
|
64
|
+
print(ds.list_artifacts(scope="datastore", complete_only=True)) # cell selections, snapshots
|
|
65
|
+
ds.show_zarr_tree(start="RNA", depth=1)
|
|
66
|
+
|
|
67
|
+
# Metadata: request only the columns you need
|
|
68
|
+
meta = ds.cells.to_pandas_dataframe(["ids", "RNA_nCounts", "RNA_nFeatures"], key="I").set_index("ids")
|
|
69
|
+
active_ids = ds.cells.fetch("ids") # rows where I is True
|
|
70
|
+
all_ids = ds.cells.fetch_all("ids") # every row
|
|
71
|
+
genes = ds.RNA.feats.to_pandas_dataframe(["ids", "names", "nCells"])
|
|
72
|
+
by_upper = {str(n).upper(): str(n) for n in ds.RNA.feats.fetch_all("names")} # case-safe lookup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Convert inputs to Zarr
|
|
76
|
+
|
|
77
|
+
Every writer follows reader then `*ToZarr(...).dump()`. Open each new store writable once.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# 10x HDF5: assays inferred from feature types (RNA, ADT, ATAC)
|
|
81
|
+
reader = scarf.CrH5Reader("filtered_feature_bc_matrix.h5")
|
|
82
|
+
print(reader.assayFeats) # assays and feature ranges that will be written
|
|
83
|
+
scarf.CrToZarr(reader, zarr_loc="pbmc.zarr", mem_budget="4G").dump()
|
|
84
|
+
# A default count layout that does not fit mem_budget raises MemoryError before writing and
|
|
85
|
+
# names a smaller policy=CountMatrixPolicy(...) (scarf.storage.count_matrix); raise
|
|
86
|
+
# mem_budget or pass that policy.
|
|
87
|
+
ds = scarf.DataStore("pbmc.zarr", default_assay="RNA") # required when there are 2+ assays
|
|
88
|
+
|
|
89
|
+
# 10x directory (matrix.mtx, genes/features.tsv, barcodes.tsv; .gz accepted)
|
|
90
|
+
scarf.CrToZarr(scarf.CrDirReader("filtered_feature_bc_matrix"), zarr_loc="dir.zarr").dump()
|
|
91
|
+
|
|
92
|
+
# Matrix Market: inspect, then pick one candidate explicitly
|
|
93
|
+
candidates = scarf.inspect_mtx("mtx_dir")
|
|
94
|
+
reader = scarf.MtxReader(candidates[0])
|
|
95
|
+
print(reader.assayFeats)
|
|
96
|
+
scarf.MtxToZarr(reader, zarr_loc="mtx.zarr").dump()
|
|
97
|
+
|
|
98
|
+
# H5AD: inspect first; selected obsm/obs values become artifacts, not live columns
|
|
99
|
+
import h5py
|
|
100
|
+
|
|
101
|
+
insp = scarf.inspect_h5ad("data.h5ad")
|
|
102
|
+
print(insp.matrixKey, insp.matrixCandidates, insp.integerLike, insp.layers, insp.suggestedAssays)
|
|
103
|
+
with h5py.File("data.h5ad", "r") as h5: # pass only keys the file has; a missing one is a KeyError
|
|
104
|
+
obsm = set(h5["obsm"]) if "obsm" in h5 else set()
|
|
105
|
+
obs = h5["obs"]
|
|
106
|
+
obs_cols = set(obs) if isinstance(obs, h5py.Group) else set(obs.dtype.names) # old files
|
|
107
|
+
print(sorted(obsm), sorted(obs_cols))
|
|
108
|
+
roles = {k: r for k, r in {"X_umap": "umap", "X_tsne": "tsne"}.items() if k in obsm}
|
|
109
|
+
kw = dict(embedding_roles=roles, cluster_keys=tuple(c for c in ("clusters",) if c in obs_cols))
|
|
110
|
+
reader = scarf.H5adReader.from_inspect(insp, **kw)
|
|
111
|
+
res = scarf.H5adToZarr(reader, zarr_loc="h5ad.zarr").dump()
|
|
112
|
+
print(dict(res.embeddingArtifacts), dict(res.clusterArtifacts))
|
|
113
|
+
|
|
114
|
+
# Seurat RDS (an on-disk .rds; .h5seurat is not read)
|
|
115
|
+
si = scarf.inspect_seurat("pbmc.rds")
|
|
116
|
+
reductions = [r.name for r in si.reductions if r.importable]
|
|
117
|
+
with scarf.SeuratReader("pbmc.rds", assays=[si.activeAssay], reductions=reductions) as sr:
|
|
118
|
+
out = scarf.SeuratToZarr(sr, zarr_loc="seurat.zarr").dump()
|
|
119
|
+
print(out.defaultAssay, dict(out.reductionArtifacts), out.activeIdentity, len(out.notices))
|
|
120
|
+
|
|
121
|
+
for path in ("dir.zarr", "mtx.zarr", "h5ad.zarr", "seurat.zarr"):
|
|
122
|
+
scarf.DataStore(path) # writable first open prepares the store
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`CSVReader`/`CSVtoZarr` (small dense CSV) and `SparseToZarr` (SciPy CSR plus IDs) also exist.
|
|
126
|
+
|
|
127
|
+
Every converter refuses this way from 1.0.0rc18 on (1.0.0rc17 shrinks the layout to fit). The
|
|
128
|
+
error is a `MemoryError`; releases after 1.0.0rc19 raise its subclass `CountLayoutMemoryError`, so
|
|
129
|
+
catch `MemoryError`. Prefer a larger `mem_budget` when the host has
|
|
130
|
+
the memory. Otherwise copy the policy numbers from the message exactly and record that choice:
|
|
131
|
+
smaller layouts make every later gene-major read slower.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from scarf.storage.count_matrix import CountMatrixPolicy
|
|
135
|
+
|
|
136
|
+
policy = CountMatrixPolicy(unitBytes=3906250, chunkBytes=390625) # copied from the message
|
|
137
|
+
scarf.CrToZarr(reader, zarr_loc="pbmc.zarr", mem_budget="128M", policy=policy).dump()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The need follows genes per cell relative to the gene count, not the number of cells. The 1K PBMC
|
|
141
|
+
CITE-seq file refuses at `512M` and imports at `1G`.
|
|
142
|
+
|
|
143
|
+
### Find a Cytebase dataset
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from scarf import cytebase
|
|
147
|
+
|
|
148
|
+
catalog = cytebase.Catalog() # public Nygen/cytebase, no credentials
|
|
149
|
+
hits = catalog.search("bladder immune", limit=5) # every word, case-insensitive
|
|
150
|
+
ids = [row["cytebase_id"] for row in hits] # take IDs from rows, not printed tables
|
|
151
|
+
blood = catalog.find_datasets(tissue="blood", organism="Homo sapiens") # exact labels
|
|
152
|
+
assay_labels = [row["label"] for row in catalog.list_terms("assay")] # exact facet labels
|
|
153
|
+
small = catalog.query(
|
|
154
|
+
"SELECT cytebase_id, cell_count FROM datasets "
|
|
155
|
+
"WHERE status = ? AND cell_count < ? ORDER BY cell_count LIMIT 3",
|
|
156
|
+
parameters=["ready", 5000],
|
|
157
|
+
)
|
|
158
|
+
entry = catalog.dataset(ids[0])
|
|
159
|
+
print(entry.describe()) # Markdown summary; does not open the store
|
|
160
|
+
print(entry.cell_count, entry.source_embeddings()) # source obsm keys, imported or not
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Explore a Cytebase dataset read-only
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
ds = catalog.open_datastore(entry.id) # zarr_mode="r", min_features_per_cell=-1
|
|
167
|
+
print(cytebase.embeddings(ds)) # {"X_umap": ArtifactRef}: keys actually imported
|
|
168
|
+
umap_ref = cytebase.embedding(ds, "X_umap")
|
|
169
|
+
coords = cytebase.embedding_coordinates(ds, umap_ref) # DataFrame indexed by cell id
|
|
170
|
+
meta = ds.cells.to_pandas_dataframe(["ids", "donor_id"], key="I").set_index("ids") # a design column
|
|
171
|
+
frame = coords.join(meta) # join on ids, never on row order
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Mount a Cytebase dataset for writable analysis
|
|
175
|
+
|
|
176
|
+
The target must be a new local path. Mounting copies metadata and verifies the source; expect a
|
|
177
|
+
few minutes for a few thousand cells.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
analysis = catalog.mount_datastore(entry.id, at="work/analysis.zarr")
|
|
181
|
+
# Writes work/analysis.zarr (copied metadata, future artifacts) and work/analysis.zarr.cytebase.json
|
|
182
|
+
print(analysis.zarr_mode, int(analysis.cells.fetch_all("I").sum()), analysis.cells.N)
|
|
183
|
+
print(cytebase.embeddings(analysis)) # {}: source artifacts are not copied into a mount
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Reopen a mount, or mount any store that owns its counts
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Verify against the current published build (needs the .cytebase.json sidecar beside it)
|
|
190
|
+
analysis = catalog.mount_datastore(entry.id, at="work/analysis.zarr")
|
|
191
|
+
# Or open it like any store: counts resolve from matrixSource; no catalog or sidecar needed
|
|
192
|
+
analysis = scarf.DataStore("work/analysis.zarr", min_features_per_cell=-1)
|
|
193
|
+
# Generic mount; for s3:// or gs:// sources also pass storage_options (not executed here)
|
|
194
|
+
mounted = scarf.mount_datastore("shared/data.zarr", at="my_analysis.zarr", default_assay="RNA")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Download a documentation dataset
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
repo = scarf.cytebase.connect("scarf_docs")
|
|
201
|
+
print(repo.list_datasets())
|
|
202
|
+
path = repo.download_dataset("tenx_5K_pbmc_rnaseq", destination="scarf_datasets", zarr=True)
|
|
203
|
+
ds = scarf.DataStore(f"{path}/data.zarr", zarr_mode="r") # prepared; run label "docs_default"
|
|
204
|
+
raw = repo.download_dataset("xin_1K_pancreas_rnaseq", destination="scarf_datasets") # source files
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Parameters that matter
|
|
208
|
+
|
|
209
|
+
| Parameter | Default | Change when |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `zarr_mode` | `"r+"` | `"r"` to inspect, share, or protect a store; a fresh store needs one `"r+"` open |
|
|
212
|
+
| `default_assay` | stored value, or the only assay | first open of a multi-assay store (otherwise `ValueError`) |
|
|
213
|
+
| `min_features_per_cell` | `10` | `-1` to leave `I` untouched on writable opens |
|
|
214
|
+
| `mito_pattern` / `ribo_pattern` | `None` (`^MT-`, `^RPS\|^RPL\|^MRPS\|^MRPL`) | non-human genes (`^mt-`), only on the FIRST writable open |
|
|
215
|
+
| `assay_types` | inferred from assay names | custom names, e.g. `{"GEX": "RNA"}` |
|
|
216
|
+
| `nthreads`, `mem_budget` | env `SCARF_WORKERS`, `SCARF_MEM_BUDGET`, else detected | see `performance-and-export.md` |
|
|
217
|
+
| `workspace` | `None` | store written into a named workspace |
|
|
218
|
+
| `storage_options` | `None` | object-store credentials or endpoints (read from env vars) |
|
|
219
|
+
| `zarrProfile` | from location (`fast_local`/`cloud`) | affects newly written arrays only |
|
|
220
|
+
| `Catalog(bucket=, token=)` | `Nygen/cytebase`, `None` (HF login if present) | private bucket; `token=False` forces anonymous |
|
|
221
|
+
| `search(limit=)`, `ready_only=` | `50`, `True` | `limit=None` for all; `ready_only=False` for unfinished |
|
|
222
|
+
|
|
223
|
+
## Check before moving on
|
|
224
|
+
|
|
225
|
+
- `print(ds)`: active versus total cells, expected assays, QC columns present.
|
|
226
|
+
- `snap["default_assay"]` is the assay you intend; `snap["labeled_pipeline_runs"]` and
|
|
227
|
+
`list_artifacts` show existing work to reuse (see `pipeline-runs-and-artifacts.md`).
|
|
228
|
+
- Imports: `reader.assayFeats` lists the assays to be written; afterwards check
|
|
229
|
+
`ds.RNA.rawData.shape` and dtype. For H5AD, `insp.integerLike` should be `True` for raw counts,
|
|
230
|
+
but SCT-corrected counts are integers too: run "Check the matrix first" in `quality-control.md`.
|
|
231
|
+
- Cytebase: `entry.row["status"] == "ready"`; check `cytebase.embeddings(ds)` before plotting an
|
|
232
|
+
imported layout.
|
|
233
|
+
- Mounts: `<target>.cytebase.json` exists beside the target; `ds.RNA.rawData.shape` resolves.
|
|
234
|
+
|
|
235
|
+
## Pitfalls
|
|
236
|
+
|
|
237
|
+
- A writable open with `min_features_per_cell=k` silently removes cells with at most `k` features
|
|
238
|
+
from `I` and persists it. Reopening with a lower value does not restore cells;
|
|
239
|
+
`ds.cells.reset_key("I")` does.
|
|
240
|
+
- Stores just written by a writer or `SubsetZarr` raise `Assay 'RNA' is not prepared. Rebuild ...`
|
|
241
|
+
when opened with `zarr_mode="r"`. Open once with `"r+"`; no rebuild is needed.
|
|
242
|
+
- Percent patterns are fixed at first preparation; a different pattern later raises `ValueError`.
|
|
243
|
+
Use `run_feature_percentage` (see `quality-control.md`) for another gene set.
|
|
244
|
+
- `open_datastore` rejects writes and any `min_features_per_cell` other than `-1`. Both mount
|
|
245
|
+
functions reject `zarr_mode="r"`; `scarf.mount_datastore` refuses an existing target, while
|
|
246
|
+
`catalog.mount_datastore` reopens one only when its matching sidecar is present.
|
|
247
|
+
- Catalog reopen needs the sidecar: copying a mount without `<name>.cytebase.json` makes
|
|
248
|
+
`catalog.mount_datastore` raise `FileExistsError`; plain `scarf.DataStore` still opens it.
|
|
249
|
+
- The mount source must stay at its recorded path or URI. Mounting a mount raises; repack first.
|
|
250
|
+
A changed Cytebase build requires a new mount directory.
|
|
251
|
+
- Printed `CatalogResults` tables HTML-escape text (`_` as `_`, `'` as `'`). Read IDs and
|
|
252
|
+
facet labels from the row dicts (`row["cytebase_id"]`, `row["label"]`), never from the table.
|
|
253
|
+
- A fresh mount has no imported source embeddings. Plot them from the read-only `ds` and join by id.
|
|
254
|
+
- `to_pandas_dataframe(columns)` defaults to `key=None` (all rows); pass `key="I"` for active cells.
|
|
255
|
+
- `to_mtx(..., compress=True)` writes the feature `feature_type` column as the 10x feature type.
|
|
256
|
+
CELLxGENE stores keep gene biotypes there, so re-import split one assay into thousands. Inspect
|
|
257
|
+
`reader.assayFeats` before `dump()` or export with `compress=False`.
|
|
258
|
+
- Source column names containing `/` or `\` are stored with `_`. Reserved columns `ids`, `names`,
|
|
259
|
+
`I` from a source are skipped. H5AD multi-assay import needs `assay_split_key` on `H5adToZarr`.
|
|
260
|
+
- `download_dataset(..., zarr=True)` keeps `data.zarr.tar.gz` beside `data.zarr` (double disk).
|
|
261
|
+
- Public atlas matrices (CELLxGENE, Seurat submissions) can hold SCT-corrected counts in `raw.X`,
|
|
262
|
+
with genes such as rRNA removed and cells already filtered. Check before QC or count models.
|
|
263
|
+
|
|
264
|
+
## See also
|
|
265
|
+
|
|
266
|
+
- `performance-and-export.md`: budgets, remote I/O cost, repacking a mount, exports.
|
|
267
|
+
- `pipeline-runs-and-artifacts.md`: runs, artifact inspection, lineage.
|
|
268
|
+
- `quality-control.md`: filtering cells without editing `I`.
|
|
269
|
+
- `plotting.md`: `ds.plots.embedding(layout=umap_ref, ...)` for imported layouts.
|