Skip to content

solo: --soloCellReadStats CB writes CellReads.stats - #157

Open
BenjaminDEMAILLE wants to merge 4 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/solo-cellreads-stats
Open

solo: --soloCellReadStats CB writes CellReads.stats#157
BenjaminDEMAILLE wants to merge 4 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/solo-cellreads-stats

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

--soloCellReadStats CB writes Solo.out/<feature>/CellReads.stats.

What changed

One row per cell barcode with the fifteen counters STARsolo reports: how the barcode matched (cbMatch, cbPerfect, cbMMunique, cbMMmultiple), whether the read mapped to one genomic locus or several (genomeU/genomeM), whether it landed on a feature (featureU/featureM), where in the gene and on which strand (exonic, intronic, exonicAS, intronicAS), whether it was mitochondrial (mito), and whether it reached the matrix (countedU/countedM). Then nUMIunique and nGenesUnique, taken from the raw matrix so they agree with it by construction.

--genomeChrSetMitochondrial (new) names the chromosomes behind the mito column. Without it the column is zero throughout, which is honest: no chromosome was declared mitochondrial.

Reads whose barcode never resolved are summed into a single CBnotInPasslist row rather than dropped. That row is the reason the file is useful: it separates "these cells look thin" from "most of the input never reached a cell at all".

The region columns split by strand — an antisense read counts under exonicAS or intronicAS, never under exonic or intronic.

Divergence

DIVERGENCE.md §3.2. STAR emits these rows by walking a libc++ unordered_map, which at these map sizes is the reverse of each barcode's first appearance. That is reproduced, including across threads: the accumulator merges in read order, so a threaded run writes the same file as a serial one. It stops being reproducible past the point where libc++ rehashes, since the order then depends on the bucket count. The values never differ, only which line they sit on.

Verification

Six unit tests in src/solo/cell_reads.rs:

  • each_row_fills_the_header — twenty columns, every row complete
  • flags_accumulate_per_readcbMatch counts every read; each flag counts only its own
  • unresolved_reads_land_in_the_passlist_miss_row
  • rows_are_emitted_in_reverse_first_appearance_order — the divergence above
  • umi_and_gene_totals_come_from_the_matrix — a cell absent from the matrix prints zeros rather than inheriting a read count

Reads are folded in under a mutex held by SoloContext, so the row order is the order reads were processed in regardless of --runThreadN.

Gate: 565 lib + 26 integration tests, cargo clippy --all-targets -- -D warnings, cargo fmt --check, MSRV 1.89 — all green.

Output-neutral: the file is written only when --soloCellReadStats CB asks for it, and nothing else changes.

Split out of #152 following the one-theme rule in CONTRIBUTING.md.

One row per cell barcode with the fifteen counters STARsolo reports: how the
barcode matched, whether the read mapped to one locus or several, whether it
landed on a feature, where in the gene and on which strand, whether it was
mitochondrial, and whether it reached the matrix. The per-cell UMI and gene
totals come from the raw matrix rather than from the read counters, so they
agree with what the matrix says by construction.

Reads whose barcode never resolved are summed into a single `CBnotInPasslist`
row instead of being dropped. That row is the reason the file is useful: it is
the difference between "these cells look thin" and "most of the input never
reached a cell at all".

The region columns split by strand — an antisense read counts under `exonicAS`
or `intronicAS`, never under `exonic` or `intronic`.

`--genomeChrSetMitochondrial` names the chromosomes behind the `mito` column.
Without it the column is zero throughout, which is honest: no chromosome was
declared mitochondrial.

D24 comes with it. STAR emits these rows by walking a libc++ `unordered_map`,
which at these sizes is the reverse of each barcode's first appearance. That is
reproduced, including across threads: the per-read accumulator merges in read
order, so a threaded run writes the same file as a serial one. It stops being
reproducible past the point where libc++ rehashes, since the order then depends
on the bucket count. The values never differ, only which line they sit on.
Recorded in docs-old/dev/divergences.md.
Section 3.2, in the format CONTRIBUTING.md asks for.
CONTRIBUTING.md requires the description to match the code; the entries for the
other themes split out of scverse#152 belong to their own PRs.
Reads are folded in under a mutex, so there are no per-thread partials to merge;
the function was reachable only from its own test. CONTRIBUTING.md rules out
shipping a function no production path reaches, and the PR description claimed
its test as evidence of thread-safety that the mutex actually provides.
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