Skill Tracer
Skills › Scientific Agent Skills › geniml › scembed.md

scembed.md

367 lines · skills/geniml/references/scembed.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# scEmbed
2
3Verified against `geniml==0.8.4` release source, current Gtars
4`0.10.0`, and official BEDbase documentation on 2026-10-01.
5

scEmbed

Verified against geniml==0.8.4 release source, current Gtars 0.10.0, and official BEDbase documentation on 2026-10-01.

Raw lines 1 to 5
6## Scope and evidence
7
8scEmbed learns region embeddings from scATAC-seq accessibility and pools them
9to represent cells. The primary paper reports that pre-trained region
10embeddings can support clustering and transfer to unseen datasets. Do not turn
11that result into a universal accuracy claim; performance depends on assay,
12reference corpus, universe, filtering, cell types, and split design.
14Primary source: LeRoy et al. (2024), *Fast clustering and cell-type annotation
15of scATAC data with pre-trained embeddings*,
16doi:[10.1093/nargab/lqae073](https://doi.org/10.1093/nargab/lqae073).

Scope and evidence

scEmbed learns region embeddings from scATAC-seq accessibility and pools them to represent cells. The primary paper reports that pre-trained region embeddings can support clustering and transfer to unseen datasets. Do not turn that result into a universal accuracy claim; performance depends on assay, reference corpus, universe, filtering, cell types, and split design.

Primary source: LeRoy et al. (2024), Fast clustering and cell-type annotation of scATAC data with pre-trained embeddings, doi:10.1093/nargab/lqae073.

Raw lines 6 to 17
18## Stable API and known drift
20Use:
22```python
23from geniml.scembed.main import ScEmbed
24from geniml.region2vec.utils import Region2VecDataset
25from gtars.tokenizers import Tokenizer
26```
28Do not use `from geniml.scembed import ScEmbed`: the 0.8.4 package
29`__init__` does not export it. The installed `geniml scembed` command parses
30legacy MatrixMarket options but its 0.8.4 command body does no training or
31encoding.
33With Geniml 0.8.4 / Gtars 0.10.0, both `tokenize_anndata` and
34`ScEmbed.encode` fail: Geniml omits the required fourth `Region(..., rest)`
35argument. Even after that is fixed, `ScEmbed.encode` assumes an extra nesting
36level that the tokenizer does not return. Use the explicit recipe below; it
37was tested on small synthetic cells, including cell-order and pooling checks.
38The environment must keep AnnData 0.12.19 and Zarr 2.18.7; AnnData 0.13's
39Zarr >=3 requirement conflicts with Geniml.

Stable API and known drift

Use:

from geniml.scembed.main import ScEmbed
from geniml.region2vec.utils import Region2VecDataset
from gtars.tokenizers import Tokenizer

Do not use from geniml.scembed import ScEmbed: the 0.8.4 package __init__ does not export it. The installed geniml scembed command parses legacy MatrixMarket options but its 0.8.4 command body does no training or encoding.

With Geniml 0.8.4 / Gtars 0.10.0, both tokenize_anndata and ScEmbed.encode fail: Geniml omits the required fourth Region(..., rest) argument. Even after that is fixed, ScEmbed.encode assumes an extra nesting level that the tokenizer does not return. Use the explicit recipe below; it was tested on small synthetic cells, including cell-order and pooling checks. The environment must keep AnnData 0.12.19 and Zarr 2.18.7; AnnData 0.13's Zarr >=3 requirement conflicts with Geniml.

Raw lines 18 to 40
41## AnnData contract
43The AnnData object must satisfy:
45- rows (`obs`) are cells;
46- columns (`var`) are accessible regions/features;
47- `var["chr"]`, `var["start"]`, and `var["end"]` describe each feature;
48- coordinates are validated 0-based half-open BED coordinates;
49- all features use one declared assembly and contig convention;
50- `X` is sparse CSR for bounded tokenization performance;
51- duplicate feature coordinates and duplicate barcodes have an explicit policy.
53Confirm matrix orientation. A 10x peak-by-barcode MatrixMarket file is often
54transposed when constructing AnnData; inspect dimensions instead of copying a
55blind `.T`.
57Do not expose barcodes, patient IDs, phenotypes, rare cell labels, or raw
58intervals in logs. An `.h5ad` may contain identifying metadata in `obs`,
59`uns`, embeddings, and file provenance. Output only bounded aggregate counts
60unless the user explicitly approves disclosure.

AnnData contract

The AnnData object must satisfy:

  • rows (obs) are cells;
  • columns (var) are accessible regions/features;
  • var["chr"], var["start"], and var["end"] describe each feature;
  • coordinates are validated 0-based half-open BED coordinates;
  • all features use one declared assembly and contig convention;
  • X is sparse CSR for bounded tokenization performance;
  • duplicate feature coordinates and duplicate barcodes have an explicit policy.

Confirm matrix orientation. A 10x peak-by-barcode MatrixMarket file is often transposed when constructing AnnData; inspect dimensions instead of copying a blind .T.

Do not expose barcodes, patient IDs, phenotypes, rare cell labels, or raw intervals in logs. An .h5ad may contain identifying metadata in obs, uns, embeddings, and file provenance. Output only bounded aggregate counts unless the user explicitly approves disclosure.

Raw lines 41 to 61
62## Leakage-safe split order
64Split before fitting or selecting anything:
661. Group cells by patient/donor and biological replicate.
672. Assign complete groups to train/validation/test.
683. Fit QC thresholds, feature/universe selection, token vocabulary, model,
69 annotation references, and hyperparameters on training data only.
704. Apply the frozen universe/tokenizer/model to validation and test.
715. Keep technical replicates and multiple samples from one patient together.
73Randomly splitting cells from the same donor leaks donor- and batch-specific
74accessibility. Building a consensus universe from all patients can also leak
75test-set feature prevalence even when labels are hidden.
77Audit a local manifest without printing metadata values:
79```bash
80python skills/geniml/scripts/corpus_auditor.py \
81 --manifest data/cells.tsv \
82 --group-column patient_id \
83 --split-column split \
84 --assembly-column assembly
85```

Leakage-safe split order

Split before fitting or selecting anything:

  1. Group cells by patient/donor and biological replicate.
  2. Assign complete groups to train/validation/test.
  3. Fit QC thresholds, feature/universe selection, token vocabulary, model, annotation references, and hyperparameters on training data only.
  4. Apply the frozen universe/tokenizer/model to validation and test.
  5. Keep technical replicates and multiple samples from one patient together.

Randomly splitting cells from the same donor leaks donor- and batch-specific accessibility. Building a consensus universe from all patients can also leak test-set feature prevalence even when labels are hidden.

Audit a local manifest without printing metadata values:

python skills/geniml/scripts/corpus_auditor.py \
  --manifest data/cells.tsv \
  --group-column patient_id \
  --split-column split \
  --assembly-column assembly
Raw lines 62 to 86
Runs code python skills/geniml/scripts/corpus_auditor.py :80
87## Build and validate the tokenizer
89Use a local, checksummed universe from the training partition:
91```python
92from gtars.tokenizers import Tokenizer
94tokenizer = Tokenizer.from_bed("refs/training_universe.bed")
95```
97Record:
99- source cohort and split;
100- assembly, chromosome sizes, coordinate/contig/strand policy;
101- universe SHA-256, row order, and row count;
102- Gtars version and special-token map/IDs;
103- `len(tokenizer)`.
105Gtars 0.10.0 `Tokenizer.from_pretrained(path)` accepts only a path argument;
106it provides no revision/cache kwargs. For an authorized Hub download, fetch
107the exact universe through the Hub client at a reviewed revision and build
108`Tokenizer.from_bed` from those verified local bytes. A model and tokenizer
109are compatible only when the exact universe,
110special-token IDs, and model vocabulary size agree.

Build and validate the tokenizer

Use a local, checksummed universe from the training partition:

from gtars.tokenizers import Tokenizer

tokenizer = Tokenizer.from_bed("refs/training_universe.bed")

Record:

  • source cohort and split;
  • assembly, chromosome sizes, coordinate/contig/strand policy;
  • universe SHA-256, row order, and row count;
  • Gtars version and special-token map/IDs;
  • len(tokenizer).

Gtars 0.10.0 Tokenizer.from_pretrained(path) accepts only a path argument; it provides no revision/cache kwargs. For an authorized Hub download, fetch the exact universe through the Hub client at a reviewed revision and build Tokenizer.from_bed from those verified local bytes. A model and tokenizer are compatible only when the exact universe, special-token IDs, and model vocabulary size agree.

Raw lines 87 to 111
112## Pre-tokenize to one bounded Parquet file
114```python
115import numpy as np
116import pyarrow as pa
117import pyarrow.parquet as pq
118import scanpy as sc
119from scipy.sparse import csr_matrix
120from gtars.models import Region
122adata = sc.read_h5ad("data/train.h5ad")
123# Validate coordinates/assembly first; do not truncate noninteger coordinates.
124adata.X = csr_matrix(adata.X)
125adata.X.sum_duplicates()
126adata.X.eliminate_zeros()
127if not np.isfinite(adata.X.data).all() or (adata.X.data < 0).any():
128 raise ValueError("accessibility must be finite and nonnegative")
129features = [
130 Region(chrom, int(start), int(end), None)
131 for chrom, start, end in zip(adata.var["chr"], adata.var["start"], adata.var["end"])
133cells = []
134for row in range(adata.n_obs):
135 indices = adata.X.indices[adata.X.indptr[row]:adata.X.indptr[row + 1]]
136 ids = tokenizer([features[i] for i in indices])["input_ids"] if len(indices) else []
137 cells.append(ids)
138special_ids = {getattr(tokenizer, key + "_id") for key in tokenizer.special_tokens_map}
139if any(not ids or special_ids.intersection(ids) for ids in cells):
140 raise ValueError("empty or unmatched cells require an explicit QC policy")
142table = pa.table({"tokens": pa.array(cells, type=pa.list_(pa.int32()))})
143pq.write_table(table, "work/train_tokens.parquet")
144```
146Before writing:
148- verify `len(cells) == adata.n_obs`;
149- check each token list is bounded and contains IDs in
150 `[0, len(tokenizer))`;
151- quantify empty cells and out-of-vocabulary/unmatched features;
152- preserve row correspondence in a separate protected manifest;
153- do not include barcodes or labels in the training Parquet unless required.
155The upstream issue
156[`databio/geniml#14`](https://github.com/databio/geniml/issues/14)
157(opened 2025-09-05) proposes moving away from one `.gtok` file per cell.
158Prefer the single Parquet corpus for current work; treat `.gtok` as legacy.

Pre-tokenize to one bounded Parquet file

import numpy as np
import pyarrow as pa
import pyarrow.parquet as pq
import scanpy as sc
from scipy.sparse import csr_matrix
from gtars.models import Region

adata = sc.read_h5ad("data/train.h5ad")
# Validate coordinates/assembly first; do not truncate noninteger coordinates.
adata.X = csr_matrix(adata.X)
adata.X.sum_duplicates()
adata.X.eliminate_zeros()
if not np.isfinite(adata.X.data).all() or (adata.X.data < 0).any():
    raise ValueError("accessibility must be finite and nonnegative")
features = [
    Region(chrom, int(start), int(end), None)
    for chrom, start, end in zip(adata.var["chr"], adata.var["start"], adata.var["end"])
]
cells = []
for row in range(adata.n_obs):
    indices = adata.X.indices[adata.X.indptr[row]:adata.X.indptr[row + 1]]
    ids = tokenizer([features[i] for i in indices])["input_ids"] if len(indices) else []
    cells.append(ids)
special_ids = {getattr(tokenizer, key + "_id") for key in tokenizer.special_tokens_map}
if any(not ids or special_ids.intersection(ids) for ids in cells):
    raise ValueError("empty or unmatched cells require an explicit QC policy")

table = pa.table({"tokens": pa.array(cells, type=pa.list_(pa.int32()))})
pq.write_table(table, "work/train_tokens.parquet")

Before writing:

  • verify len(cells) == adata.n_obs;
  • check each token list is bounded and contains IDs in [0, len(tokenizer));
  • quantify empty cells and out-of-vocabulary/unmatched features;
  • preserve row correspondence in a separate protected manifest;
  • do not include barcodes or labels in the training Parquet unless required.

The upstream issue [databio/geniml#14](https://github.com/databio/geniml/issues/14) (opened 2025-09-05) proposes moving away from one .gtok file per cell. Prefer the single Parquet corpus for current work; treat .gtok as legacy.

Raw lines 112 to 159
160## Train
162```python
163from geniml.region2vec.utils import Region2VecDataset
164from geniml.scembed.main import ScEmbed
166dataset = Region2VecDataset(
167 "work/train_tokens.parquet",
168 shuffle=True,
169 convert_to_str=True,
171model = ScEmbed(
172 tokenizer=tokenizer,
173 embedding_dim=100,
174 pooling_method="mean",
175 device="cpu",
177model.train(
178 dataset,
179 window_size=5,
180 epochs=10,
181 min_count=10,
182 num_cpus=4,
183 seed=42,
185```
187Seed Python `random` before token shuffling and Torch before construction;
188Gensim's `seed` alone does not set those RNGs. Derive and retain trained-token
189IDs from the training corpus counts and the actual `min_count`; untouched
190Torch rows remain random even after `model.trained` becomes true.
192Bound cells, nonzeros, tokens per cell, workers, epochs, checkpoint frequency,
193RAM, and disk. `Region2VecDataset` loads the full Parquet token column into
194memory. Training uses Gensim and Torch; Gensim checkpoint loading is unsafe for
195untrusted `.model` files.
197Generate a run plan first:
199```bash
200python skills/geniml/scripts/embedding_plan.py \
201 --mode scembed \
202 --data work/train_tokens.parquet \
203 --universe refs/training_universe.bed \
204 --output-dir work/scembed \
205 --assembly GRCh38 \
206 --embedding-dim 100 --epochs 10 --workers 4 --seed 42
207```

Train

from geniml.region2vec.utils import Region2VecDataset
from geniml.scembed.main import ScEmbed

dataset = Region2VecDataset(
    "work/train_tokens.parquet",
    shuffle=True,
    convert_to_str=True,
)
model = ScEmbed(
    tokenizer=tokenizer,
    embedding_dim=100,
    pooling_method="mean",
    device="cpu",
)
model.train(
    dataset,
    window_size=5,
    epochs=10,
    min_count=10,
    num_cpus=4,
    seed=42,
)

Seed Python random before token shuffling and Torch before construction; Gensim's seed alone does not set those RNGs. Derive and retain trained-token IDs from the training corpus counts and the actual min_count; untouched Torch rows remain random even after model.trained becomes true.

Bound cells, nonzeros, tokens per cell, workers, epochs, checkpoint frequency, RAM, and disk. Region2VecDataset loads the full Parquet token column into memory. Training uses Gensim and Torch; Gensim checkpoint loading is unsafe for untrusted .model files.

Generate a run plan first:

python skills/geniml/scripts/embedding_plan.py \
  --mode scembed \
  --data work/train_tokens.parquet \
  --universe refs/training_universe.bed \
  --output-dir work/scembed \
  --assembly GRCh38 \
  --embedding-dim 100 --epochs 10 --workers 4 --seed 42
Raw lines 160 to 208
Runs code python skills/geniml/scripts/embedding_plan.py :200
209## Export and local loading
211```python
212from pathlib import Path
213import shutil
214import yaml
216bundle = Path("models/scembed")
217model.export(str(bundle))
218shutil.copyfile(
219 "refs/training_universe.bed",
220 bundle / "universe.bed",
222config_path = bundle / "config.yaml"
223config = yaml.safe_load(config_path.read_text())
224config["pooling_method"] = model.pooling_method
225config_path.write_text(yaml.safe_dump(config))
226```
228As in Region2Vec, the 0.8.4 export utility writes `checkpoint.pt` and
229`config.yaml` but does not write the tokenizer universe or preserve
230`pooling_method`. Add the exact validated `universe.bed` and record pooling
231before generating checksums.
233Inspect before loading:
235```bash
236python skills/geniml/scripts/model_artifact_inspector.py \
237 --model-dir models/scembed
239python skills/geniml/scripts/tokenizer_compatibility.py \
240 --model-dir models/scembed \
241 --universe refs/training_universe.bed \
242 --assembly GRCh38
243```
245Then, for a trusted local bundle:
247```python
248from geniml.scembed.main import ScEmbed
250model = ScEmbed.from_pretrained("models/scembed")
251```
253This classmethod is local. In contrast,
254`ScEmbed(model_path="organization/model")` downloads three files through
255Hugging Face Hub. Constructor kwargs for revision/cache/offline behavior are
256silently discarded in 0.8.4. For authorized downloads, call `hf_hub_download`
257directly at a reviewed immutable revision, verify each file, then load the
258local bundle. No public weights were downloaded in this review.
260`checkpoint.pt` is loaded with Torch `weights_only=True`. Continue to treat it
261as untrusted until verified and load in an isolated, resource-bounded
262environment. Never inspect it using pickle.

Export and local loading

from pathlib import Path
import shutil
import yaml

bundle = Path("models/scembed")
model.export(str(bundle))
shutil.copyfile(
    "refs/training_universe.bed",
    bundle / "universe.bed",
)
config_path = bundle / "config.yaml"
config = yaml.safe_load(config_path.read_text())
config["pooling_method"] = model.pooling_method
config_path.write_text(yaml.safe_dump(config))

As in Region2Vec, the 0.8.4 export utility writes checkpoint.pt and config.yaml but does not write the tokenizer universe or preserve pooling_method. Add the exact validated universe.bed and record pooling before generating checksums.

Inspect before loading:

python skills/geniml/scripts/model_artifact_inspector.py \
  --model-dir models/scembed

python skills/geniml/scripts/tokenizer_compatibility.py \
  --model-dir models/scembed \
  --universe refs/training_universe.bed \
  --assembly GRCh38

Then, for a trusted local bundle:

from geniml.scembed.main import ScEmbed

model = ScEmbed.from_pretrained("models/scembed")

This classmethod is local. In contrast, ScEmbed(model_path="organization/model") downloads three files through Hugging Face Hub. Constructor kwargs for revision/cache/offline behavior are silently discarded in 0.8.4. For authorized downloads, call hf_hub_download directly at a reviewed immutable revision, verify each file, then load the local bundle. No public weights were downloaded in this review.

checkpoint.pt is loaded with Torch weights_only=True. Continue to treat it as untrusted until verified and load in an isolated, resource-bounded environment. Never inspect it using pickle.

Raw lines 209 to 263
Runs code python skills/geniml/scripts/model_artifact_inspector.py :236
Runs code python skills/geniml/scripts/tokenizer_compatibility.py :239
264## Generate and attach cell embeddings
266Use the token lists above, in exactly the AnnData cell order. This explicit
267mean pooling rejects unsupported cells rather than assigning random unknown
268or untrained vectors. For held-out data, tokenize its own features with the
269same frozen tokenizer; keep `trained_ids` from the training corpus.
271```python
272from collections import Counter
273import numpy as np
274import torch
276# Set to the actual min_count passed to model.train, using training cells only.
277min_count = 10
278counts = Counter(token for ids in cells for token in ids)
279trained_ids = {token for token, count in counts.items() if count >= min_count}
281def pool_cells(model, cell_tokens, trained_ids):
282 projection = model.model.projection
283 rows = []
284 with torch.inference_mode():
285 for ids in cell_tokens:
286 if not ids or any(token not in trained_ids for token in ids):
287 raise ValueError("cell has empty or untrained token set")
288 tensor = torch.tensor(ids, dtype=torch.long, device=projection.weight.device)
289 rows.append(projection(tensor).mean(dim=0).cpu().numpy())
290 if not rows:
291 raise ValueError("no cells to embed")
292 return np.vstack(rows)
294embeddings = pool_cells(model, cells, trained_ids)
295assert embeddings.shape == (adata.n_obs, model.model.embedding_dim)
296assert np.isfinite(embeddings).all()
297adata.obsm["X_scembed"] = embeddings
298```
300This recipe explicitly chooses mean pooling. Record a different pooling policy
301if needed and test its effect. Passing `device="cuda"` to the wrapper alone
302does not move its model; CPU training/projection is the tested configuration.
304Illustrative downstream analysis (requires the Scanpy Leiden/igraph extras):
306```python
307import scanpy as sc
309sc.pp.neighbors(adata, use_rep="X_scembed")
310sc.tl.leiden(adata, resolution=0.5, random_state=42)
311sc.tl.umap(adata, random_state=42)
312```
314UMAP and Leiden are exploratory unless validated on held-out donors. Store
315software versions, seeds, neighborhood parameters, and the embedding checksum.

Generate and attach cell embeddings

Use the token lists above, in exactly the AnnData cell order. This explicit mean pooling rejects unsupported cells rather than assigning random unknown or untrained vectors. For held-out data, tokenize its own features with the same frozen tokenizer; keep trained_ids from the training corpus.

from collections import Counter
import numpy as np
import torch

# Set to the actual min_count passed to model.train, using training cells only.
min_count = 10
counts = Counter(token for ids in cells for token in ids)
trained_ids = {token for token, count in counts.items() if count >= min_count}

def pool_cells(model, cell_tokens, trained_ids):
    projection = model.model.projection
    rows = []
    with torch.inference_mode():
        for ids in cell_tokens:
            if not ids or any(token not in trained_ids for token in ids):
                raise ValueError("cell has empty or untrained token set")
            tensor = torch.tensor(ids, dtype=torch.long, device=projection.weight.device)
            rows.append(projection(tensor).mean(dim=0).cpu().numpy())
    if not rows:
        raise ValueError("no cells to embed")
    return np.vstack(rows)

embeddings = pool_cells(model, cells, trained_ids)
assert embeddings.shape == (adata.n_obs, model.model.embedding_dim)
assert np.isfinite(embeddings).all()
adata.obsm["X_scembed"] = embeddings

This recipe explicitly chooses mean pooling. Record a different pooling policy if needed and test its effect. Passing device="cuda" to the wrapper alone does not move its model; CPU training/projection is the tested configuration.

Illustrative downstream analysis (requires the Scanpy Leiden/igraph extras):

import scanpy as sc

sc.pp.neighbors(adata, use_rep="X_scembed")
sc.tl.leiden(adata, resolution=0.5, random_state=42)
sc.tl.umap(adata, random_state=42)

UMAP and Leiden are exploratory unless validated on held-out donors. Store software versions, seeds, neighborhood parameters, and the embedding checksum.

Raw lines 264 to 316
317## Cell-type annotation
319The release contains `geniml.scembed.annotation.Annotator`, but it is not a
320working current-client recipe: it calls removed `QdrantClient.search`, and
321internally hard-codes `obsm["embedding"]` and `obs["leiden"]` despite exposing
322custom key arguments. Its public constructor also does not accept an API key.
323A modern Qdrant integration must use `query_points(...).points`, preserve the
324requested keys, and handle authentication explicitly; this is source/runtime
325API verification, not a validated annotation workflow. That is a separate
326network/data-disclosure decision: embeddings and metadata can be sensitive.
327Do not create or contact an annotation server without explicit approval.
329For any KNN annotation:
331- reference and query embeddings must use the same model/tokenizer/universe;
332- fit the reference index using training donors only;
333- tune `k` and score thresholds on validation donors;
334- include unknown/reject behavior;
335- report per-class metrics and calibration on held-out donors;
336- avoid claiming labels for absent reference cell types.
338Never send raw barcodes, patient metadata, or interval lists to a hosted vector
339store by default.

Cell-type annotation

The release contains geniml.scembed.annotation.Annotator, but it is not a working current-client recipe: it calls removed QdrantClient.search, and internally hard-codes obsm["embedding"] and obs["leiden"] despite exposing custom key arguments. Its public constructor also does not accept an API key. A modern Qdrant integration must use query_points(...).points, preserve the requested keys, and handle authentication explicitly; this is source/runtime API verification, not a validated annotation workflow. That is a separate network/data-disclosure decision: embeddings and metadata can be sensitive. Do not create or contact an annotation server without explicit approval.

For any KNN annotation:

  • reference and query embeddings must use the same model/tokenizer/universe;
  • fit the reference index using training donors only;
  • tune k and score thresholds on validation donors;
  • include unknown/reject behavior;
  • report per-class metrics and calibration on held-out donors;
  • avoid claiming labels for absent reference cell types.

Never send raw barcodes, patient metadata, or interval lists to a hosted vector store by default.

Raw lines 317 to 340
Involves a person without explicit approval :327
341## Evaluation
343Report:
345- donor-grouped clustering metrics with confidence intervals;
346- annotation macro/micro F1 and per-class support;
347- unknown/reject rate;
348- batch/donor association;
349- runtime and peak memory;
350- baselines fitted on the same training split.
352Do not select clusters, labels, or universe parameters by inspecting the test
353UMAP. If pre-trained public models were trained on overlapping donors or
354datasets, document that possible leakage.

Evaluation

Report:

  • donor-grouped clustering metrics with confidence intervals;
  • annotation macro/micro F1 and per-class support;
  • unknown/reject rate;
  • batch/donor association;
  • runtime and peak memory;
  • baselines fitted on the same training split.

Do not select clusters, labels, or universe parameters by inspecting the test UMAP. If pre-trained public models were trained on overlapping donors or datasets, document that possible leakage.

Raw lines 341 to 355
356## Official sources
358- [scEmbed training tutorial](https://docs.bedbase.org/geniml/tutorials/train-scembed-model)
359 (undated; accessed 2026-10-01)
360- [scEmbed API page](https://docs.bedbase.org/geniml/api-reference/scembed/)
361 (undated; accessed 2026-10-01)
362- [Geniml v0.8.4 source](https://github.com/databio/geniml/tree/v0.8.4/geniml/scembed)
363 (released 2026-01-14; accessed 2026-10-01)
364- [Gtars tokenizer documentation](https://docs.bedbase.org/gtars/tokenizers)
365 (undated; accessed 2026-10-01)
366- [Primary scEmbed paper](https://doi.org/10.1093/nargab/lqae073)
367 (2024)

Official sources

Raw lines 356 to 367