Skip to content

Detect cell barcodes from read counts; add --split_molecules - #420

Open
andrewprzh wants to merge 18 commits into
masterfrom
badger
Open

Detect cell barcodes from read counts; add --split_molecules#420
andrewprzh wants to merge 18 commits into
masterfrom
badger

Conversation

@andrewprzh

Copy link
Copy Markdown
Collaborator

Why

With a stock 10x whitelist (737k for v2, ~3M for v3), per-read barcode matching collapses
into exact matching
min_score equals the barcode length, so any read with a sequencing
error in the barcode is lost. Three mechanisms compound it: the score threshold, k growing
with whitelist size until a mid-barcode error destroys every k-mer, and hits_delta pruning
the true barcode before alignment.

A run has a few thousand cells, not millions. Counting extracted barcodes first turns the
whitelist into a filter over a few thousand candidates instead of a per-read search space.

What changed

Cell barcode detection. --n_cells now decides what --barcode_whitelist means: unset it
is the cell list (one pass, matched as today); set to a number or auto it is a pool, so a
first pass extracts barcode windows and counts them, the counts pick the cell barcodes, and an
ordinary second pass matches reads against those. --barcode_whitelist auto detects with no
pool at all. Correction is always the existing SSW matcher — only the list it matches against
varies.

Measured on three real 10x datasets, against a 7.4M stock whitelist:

dataset cells stock list as-is with --n_cells
ONT StereoQ, 600k 5000 74.36 / 99.60 87.33 / 99.75
ONT cDNA R10.4, 304k 5000 76.27 / 99.58 89.61 / 99.83
concat, 800k (split) 8975 74.31 / 99.63 86.78 / 99.81

(recall / precision). In every case the result is identical to supplying the true cell list
— the user no longer has to know which barcodes are the cells. --n_cells auto equals an exact
count, and selection tolerates being off by ±25%.

--split_molecules {true,false,auto} replaces the split mode names, so IsoQuantMode has
one member per chemistry. auto (default) splits wherever the protocol supports it; true
aborts on a protocol that cannot split rather than silently not splitting. Defaulting to auto
is justified by measurement: splitting improves recall ~1 point on non-concatenated data at
unchanged precision. tenX_v3_split, tenX_v2_split and stereoseq_nosplit still work as
deprecated aliases. visium_5prime gains splitting via the 10x detector.

Bug fixes, each found while working nearby:

  • score_diff never rejected anything — the runner-up was untracked and same-offset ties
    invisible. See the baseline note below.
  • estimate_cell_number reported a single cell on a flat count distribution (argmax of an
    all-zero array).
  • Cell selection computed its count cutoff over all observed barcodes, so one abundant
    non-whitelisted artifact pushed genuine cells below it.
  • Barcode calling was relaunched once per input file (an indentation bug), and the whitelist
    index was pickled to every worker per 100k-read chunk (−32% peak RSS).
  • Mouse.10x.v2.737K.full ran under tenX_v3, reading a 12bp UMI from a 10bp-UMI chemistry.
  • Dead last_potential_pos guard, unreachable h5 whitelist path, dead min_scores branch,
    a broken lazy-logging call.

Baseline changes reviewers should know about

Repairing score_diff makes ambiguous calls get rejected instead of guessed. The effect scales
with how many ties the whitelist creates — negligible for 10x, large for custom_sc:

test whitelist recall precision errors removed
custom_sc 10x 5k 85.48 → 84.85 99.73 → 99.76 1407 → 1231
custom_sc VisiumHD large 644k 3.64 → 3.49 96.79 → 98.95 −69%
custom_sc VisiumHD small 644k 60.02 → 54.93 90.88 → 98.95 −90%

The VisiumHD cases trade real recall for large precision gains. Accepted deliberately: a
misassigned barcode puts a read in the wrong cell and silently corrupts that cell's counts,
whereas a dropped read only costs depth. Five baselines refreshed from measurement. Attribution
was confirmed, not inferred — setting SCORE_DIFF = 0 reproduces the old numbers exactly. The
native visium_hd mode is unaffected (different function).

Testing

1401 unit tests pass. All 13 dispatchable barcode workflows ran on this branch: 9 pass, and the
4 failures are exactly the score_diff baseline shifts above, now refreshed.

The three Barcode.Mouse.10x.CellSelection configs were run locally through the CI runner
(99.60/74.36, 99.75/87.33, 99.81/86.78 — all matching baselines) because a new workflow file
cannot be dispatched until it is on the default branch. That workflow's first real run will
be after this merge.

Notes

  • Badger's edit-distance graph correction was implemented and benchmarked, then dropped: given
    the same barcodes, the existing SSW matcher scored 87.35 vs 86.55 recall, because a local
    alignment slides to find a shifted barcode and a fixed-window edit distance cannot. Only the
    counting and selection remain.
  • Pass 1 writes nothing — workers return count dicts that the parent merges. Materialising a
    barcode per read cost ~126 bytes/read, about 12 GB on a 100M-read run, written and re-read
    for nothing.
  • --resume on a run started before this change will fail to unpickle the removed mode
    members. Normal for a version bump, but worth a changelog line.

See .claude/CELL_BARCODE_SELECTION.md for the full design, benchmarks and gotchas.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant