solo: --soloFeatures Transcript3p, with --soloClusterCBfile - #158
Open
BenjaminDEMAILLE wants to merge 4 commits into
Open
solo: --soloFeatures Transcript3p, with --soloClusterCBfile#158BenjaminDEMAILLE wants to merge 4 commits into
BenjaminDEMAILLE wants to merge 4 commits into
Conversation
Quantifies transcripts rather than genes, from where each read's 3' end sits
relative to each transcript's. In a 3'-biased assay that distance is what
separates isoforms: a read 200 bases from the end of one and 4000 from the end
of another is evidence for the first. The distribution of those distances is
estimated from the run's own histogram, smoothed and cut where the 3' peak
decays into the body, and used as the likelihood in an EM over UMIs.
Concordance needed no new code. `align_to_transcripts` already refuses to
project an alignment that leaves the transcript, touches an intron, or crosses a
junction the transcript does not have — which is exactly STAR's `Concordant`
(`Transcriptome_classifyAlign.cpp`). A projection that survives is concordant;
one that does not, is not. The projection also puts the 5' end at coordinate
zero for both strands, so the distance to the 3' end is one expression rather
than two.
Two behaviours worth stating because they are not the obvious ones:
Output is per cluster, not per cell, and `--soloClusterCBfile` is required.
A single cell does not have enough UMIs to resolve isoforms, so the EM would be
fitting noise. Asking for the feature without a clustering is refused rather
than run.
A UMI seen on several reads contributes the *intersection* of their transcript
sets. Those reads came from one molecule, so a transcript missing from any of
them cannot be its source. Taking the union would let a single stray read
resurrect an isoform every other read excluded.
Two of STAR's quirks are reproduced rather than corrected, because the cut point
and every weight depend on them: the running-average divisor is `min(2N+1,
i + N)` rather than the number of elements actually summed, and the transcript
length factor is taken from the cumulative distribution at `trLen - 1`
(`SoloFeature_quantTranscript.cpp`).
Numbers are formatted the way C++'s default stream prints them — six
significant digits, fixed inside `[1e-4, 1e6)` and scientific outside — since
the normalised distribution runs down to ~1e-4 where Rust's `{}` and C++'s
default disagree on both notation and digit count.
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.
Records are accumulated under a mutex, so there are no partials to merge. It was dead from the moment it was written; CONTRIBUTING.md rules out shipping it.
parametersDefault puts both Transcript3p and --soloClusterCBfile between "#####UnderDevelopment_begin : not supported - do not use" and "#####UnderDevelopment_end", and STAR --help prints that banner around them. The module said none of this. It matters for how the port is read: it follows STAR's code, so it inherits the unfinished parts of that code, and a differential against STAR compares two implementations of something STAR does not support. A reviewer should be told that before deciding to take it.
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
--soloFeatures Transcript3p: quantify transcripts rather than genes, from where each read's 3' end sits relative to each transcript's.What changed
In a 3'-biased assay that distance is what separates isoforms: a read 200 bases from the end of one and 4000 from the end of another is evidence for the first. The feature records, per read, every transcript the alignment is concordant with and the spliced distance to that transcript's 3' end. The distribution of those distances is estimated from the run's own histogram, smoothed with a running average, cut where the 3' peak decays into the body, and used as the likelihood in an EM over UMIs.
--soloClusterCBfile(new) assigns cells to clusters.Output under
Solo.out/Transcript3p/raw/:matrix.mtx(transcripts × clusters),features.tsv, andtranscriptEndDistanceDistribution.txt.Two behaviours that are not the obvious ones
Output is per cluster, not per cell, and
--soloClusterCBfileis required. One cell does not have enough UMIs to resolve isoforms, so the EM would be fitting noise. Asking for the feature without a clustering is refused rather than run.A UMI seen on several reads contributes the intersection of their transcript sets, not the union. Those reads came from one molecule, so a transcript missing from any of them cannot be its source. The union would let one stray read resurrect an isoform every other read excluded.
What this reuses rather than reimplements
Concordance needed no new code.
align_to_transcriptsalready refuses to project an alignment that leaves the transcript, touches an intron, or crosses a junction the transcript does not have, which is what STAR'sTranscriptome_classifyAlign.cppcallsConcordant. A projection that survives is concordant; one that does not, is not. The projection also puts the 5' end at coordinate zero for both strands, so the distance to the 3' end is one expression rather than two.Faithful to STAR including its quirks
Two are reproduced rather than corrected, because the cut point and every weight depend on them (
SoloFeature_quantTranscript.cpp):min(2N+1, i + N), not the number of elements actually summed;trLen - 1.Numbers are formatted as C++'s default stream prints them (six significant digits, fixed inside
[1e-4, 1e6)and scientific outside) because the normalised distribution runs down to ~1e-4, where Rust's{}and C++'s default disagree on both notation and digit count.Verification
Nine unit tests in
src/solo/transcript3p.rs:the_distance_distribution_is_normalised_and_cut_at_the_peakshort_transcripts_get_a_length_correction, and a long one gets nonea_unique_umi_goes_entirely_to_its_transcriptan_ambiguous_umi_follows_the_evidence: nine unambiguous UMIs pull the tentha_umi_seen_twice_keeps_only_the_shared_transcripts: the intersection rulea_cell_outside_every_cluster_is_skippedcluster_file_parsing_skips_unknown_barcodes,a_trailing_barcode_without_a_cluster_ends_the_parsenumbers_are_formatted_the_way_c_plus_plus_prints_themGate: 569 lib + 26 integration tests,
cargo clippy --all-targets -- -D warnings,cargo fmt --check, MSRV 1.89, all green.Output-neutral unless
--soloFeatures Transcript3pis requested; it adds a feature directory and changes nothing existing.Two corrections to the previous revision of this description
"That needs a STARsolo oracle I do not have available" was wrong. STARsolo is the same binary as STAR, and STAR 2.7.11b was available the whole time. I tried to run the differential rather than repeat the claim, and it does not run, for a reason worth stating precisely rather than as "no oracle".
STAR marks this feature as under development. In
parametersDefault, and inSTAR --help, bothTranscript3pand--soloClusterCBfilesit between:The port follows STAR's code, so it inherits whatever that code does, including its unfinished parts. This is not a stable interface upstream, and a reviewer should weigh that before taking it. The module doc now says so; it did not before.
Why the differential does not run
Transcriptome.cpp:17picks the directory it loads gene and transcript info from:An index built with
--sjdbGTFfilerecords that path ingenomeParameters.txt, and STAR reads it back, sosjdbGTFfile != "-"on any later run against that index andtrInfoDirbecomesP.sjdbInsert.outDir. That is empty when the constructor runs, so STAR opens/geneInfo.taband aborts:Reproduced on a fixture built for this: 20 cells, 5000 reads drawn 3'-biased from 200 yeast transcripts,
--soloType CB_UMI_Simple. It fails the same way on--soloFeatures Genealone, so it is not specific toTranscript3p, and passing--sjdbGTFfile -does not help becausegenomeParameters.txtis applied after the command line. With a GTF-less index plus--sjdbGTFfileat mapping, STAR does write_STARgenome/geneInfo.tab, but theTranscriptomeconstructor runs beforesjdbInsert.outDiris set and aborts anyway.So the oracle exists but I could not get a
Solo.outout of it on this machine and this fixture. I would rather report that with the source line and the command than call it "no oracle available".Still not run, then: the new matrix has not been compared against STARsolo's. The unit tests below pin the arithmetic and the output format; they do not pin agreement with STAR.
Split out of #152 following the one-theme rule in CONTRIBUTING.md.