Skip to content

Solo: refuse what the read loops cannot do, check barcode and UMI input - #360

Open
BenjaminDEMAILLE wants to merge 1 commit into
mainfrom
fix/solo-input-checks
Open

BenjaminDEMAILLE wants to merge 1 commit into
mainfrom
fix/solo-input-checks

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Part of #354 (findings 4, 8, 9, and items 21, 22, 23, 26, 27).

Base: main. See "Conflicts" below for #345 and #346. This is one PR because all of it is the same theme: solo input that is accepted, then gives a wrong or empty result with exit 0.

Fixture for the tables: the tiny solo genome from the audit, 200 reads, --soloBarcodeReadLength 0. "Solo STAR" has a limit on this machine: the Homebrew 2.7.11b binary stops at could not open input file /geneInfo.tab on any solo run that reaches the transcriptome (#345 notes the same), so STAR's exit codes below only exist where it stops earlier. Everything about STAR is observed output; the source was not read.

Summary of measurements

Case STAR main This branch
14-base whitelist, --soloCBlen 16 exit 104 exit 0, matrix has 0 entries exit 1, names the file and both lengths
--soloUMIlen 33 exit 102 exit 0, matrix has 0 entries exit 2
CB_UMI_Complex, no --soloUMIposition abort, exit 134 exit 0 exit 2
CB_UMI_Complex, --soloUMIposition oops abort, exit 134 exit 0 exit 2, quotes the spec
--quantMode GeneCounts not reached (geneInfo.tab) exit 0, zero counts exit 2
--soloUMIdedup 1MM_All Exact not reached (geneInfo.tab) exit 0, second method ignored exit 2

Finding 4: side outputs that are empty

Problem

--soloType with --quantMode GeneCounts TranscriptomeSAM --outReadsUnmapped Fastx exits 0 with zero counts, an empty BAM and an empty FASTQ. align_reads_solo and align_reads_solo_pe take no quantification context, no unmapped writers and no chimeric or BySJout stage, and run_smartseq has none of them either.

Change

The STARsolo block of src/params/mod.rs refuses, with --soloType, each of: --quantMode GeneCounts, --quantMode TranscriptomeSAM, --quantGeneCoverage, --quantGeneSplicing, --outReadsUnmapped Fastx, --outFilterType BySJout, --chimSegmentMin above 0, --waspOutputMode.

Deliberate divergence: DIVERGENCE.md §1.10

Needs maintainer sign-off. STAR's documentation allows these with solo; I could not run them here. Refusing can stop a command that "worked" before, but what it produced was empty files. If someone uses --outFilterType BySJout with solo today, its filter was silently not applied, and that run now stops. The right long-term answer is implementing them, which finding 19 (shared read loop stage) would make easier. test/parity_suite.sh's two solo runs use none of these flags.

§1.10 is numbered after §1.9, which #356, #347 and #348 all take, so a renumber is likely when they land.

Finding 8: whitelist length

Problem

A 14-base whitelist with --soloCBlen 16 gives an empty matrix and exit 0. Worse, packing drops the length, so a barcode made of leading As and a shorter entry can compare equal.

What STAR does

Exit 104, the total length of barcode sequence is 14 not equal to expected.

Change

  • SoloContext::build compares the whitelist with layout.cb_segment_lens().
  • load_complex does the same for each CB_UMI_Complex file, so 6+10 cannot pass for 10+6.
  • match_cb returns NoMatch on a length difference. The anchored layout, where length can vary by read, is the only way to get there now. A segment whose two ends use different anchors has no fixed length and so skips the startup check.

Exit code is 1, not STAR's 104. This repo does not reproduce STAR's exit numbers in this path.

Finding 9: CB_UMI_Complex UMI

Problem

  • With no --soloUMIposition, unwrap_or((0, 0)) gave every read the same empty UMI: 200 reads count as 1.
  • A bad position was dropped by filter_map, so a CB segment could vanish.
  • The UMI width used for dedup and the UB tag was --soloUMIlen, not the position's. An 8-base UMI with the default of 10 got a 10-base UB.

Change

  • check_positions runs at validation: every spec must parse, with the anchored parser when an adapter is set.
  • SoloContext.umi_len comes from layout.umi_len(). Only a UMI whose ends use different anchors, which has no fixed width, falls back to --soloUMIlen.
  • All three uses switched to it (count.rs twice, lib.rs once).

Item 21: --soloUMIlen above 32

UMIs pack into a u64. 33 bases corrupted them; STAR exits 102. Now refused at validation, and a CB_UMI_Complex position wider than 32 also. Exit code is 2 (clap), not 102.

Item 22: several --soloUMIdedup

Each value was validated, then only the first was used. STAR's documentation says it writes one matrix per method. I chose refusing over implementing, since I cannot compare the file layout with STAR on this machine. It is in §1.10.

Item 23: features.tsv in soloCellFiltering

When the raw folder held features.tsv.gz, std::fs::copy put gzip bytes in a file called features.tsv. It now goes through the existing open_maybe_gzip. The filtered matrix and barcodes were already plain.

Item 26: gene name tag

--sjdbGTFtagExonParentGeneName worked for an index built from a GTF, but a GTF given at mapping always read gene_name. GeneAnnotation::from_gtf_exons_configured now takes the tags and shares find_attr_list with the transcriptome tables, so several tags behave as they do there (the last one wins).

Item 27: CellReads.stats

record_cell_read used any across all features and shared one accumulator, so every feature's file held the same numbers. There is now one accumulator per feature, as the file's location already implied.

With one exonic and one intronic read, --soloFeatures Gene GeneFull:

File countedU on main This branch
Gene/CellReads.stats 2 1
GeneFull/CellReads.stats 2 (same accumulator; not read separately) 2

Not compared with STAR's file, for the geneInfo.tab reason above. The expected numbers follow from what the two matrices count.

Tests

tests/solo_input_checks.rs, all 10 of which fail on main (I ran the file there):

solo_refuses_side_outputs_its_read_loops_do_not_write, umi_longer_than_a_u64_is_refused, several_umi_dedup_methods_are_refused, complex_needs_a_valid_umi_position, whitelist_of_another_length_is_fatal, complex_segment_whitelist_of_another_length_is_fatal, complex_umi_width_comes_from_its_position, cell_reads_stats_are_per_feature, gene_name_tag_applies_to_a_gtf_given_at_mapping, cell_filtering_writes_plain_features_from_gzipped_ones.

Plus barcode_of_another_length_never_matches in src/solo/whitelist.rs.

cargo test --release, cargo clippy --all-targets and cargo fmt are clean on macOS.

Conflicts

#345 and #346 change solo/count.rs and solo/mod.rs too. git merge-tree against the heads of #344, #345 and #346 reports a conflict in CHANGELOG.md only (both add under [Unreleased]), so this does not need to be stacked. That is a textual check: I did not build or test the merged result.

Not checked

  • test/parity_suite.sh was not run, so there is no check on real solo data. The parity flags are all still accepted, by reading them; no run confirmed it.
  • No solo output in this PR was compared with STAR's, since STAR cannot finish a solo run on this machine. The table shows only where it stops early.
  • The speed cost of one lock per feature per read in record_cell_read was not measured. It applies only with --soloCellReadStats CB.
  • The --chimOutType and --outSAMsplicingStatus options were not reviewed for solo; only --chimSegmentMin turns chimeric detection on, per the audit.

🤖 Generated with Claude Code

Options the solo loops have no code for, and more than one --soloUMIdedup,
are refused instead of writing empty or zero files with exit 0. A whitelist
of the wrong length is fatal. CB_UMI_Complex positions must parse and set the
UMI width. --soloUMIlen above 32 is refused. soloCellFiltering decompresses
features.tsv. The gene name tag reaches a GTF given at mapping.
CellReads.stats is counted per feature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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