Repository navigation
GeneCounts: stranded columns use each exon's strand, as STAR - #372
Open
BenjaminDEMAILLE wants to merge 2 commits into
Open
BenjaminDEMAILLE wants to merge 2 commits into
BenjaminDEMAILLE wants to merge 2 commits into
Conversation
ReadsPerGene.out.tab columns 3 and 4 took one strand per gene (its first
exon's). STAR keeps a strand per exon (exG.str, the exon's transcript
strand) and a read counts for a gene in a stranded column when an exon it
overlaps is on that strand; a strand-0 ('.') exon counts in both. The
transcriptome tables now write strand 0 for '.' transcripts, as STAR's
GTF.cpp does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Part of #354 (GeneCounts stranded columns: strand per exon, not per gene).
Problem
--quantMode GeneCountsgave each gene one strand, taken from its first exon (gene_is_reverseinsrc/quant/mod.rs). Columns 3 and 4 ofReadsPerGene.out.tabfiltered genes on that strand. So when one gene ID has exons on both strands, every read went to the column of the first exon's strand. This happens with a GTF that reuses a gene_id on a + and a - transcript, and with GFF3 exons that all fall into STAR'sMissingGeneID(#356).A second, smaller case: a transcript with strand
.was stored as+(strand 1). STAR stores it as strand 0, and strand-0 exons count in both stranded columns.What STAR does (2.7.11b source)
GTF.cpp:138-144: a transcript's strand is 1 for+, 2 for-, 0 for anything else, from its first exon line.GTF_transcriptGeneSJ.cpp:40: each exon inexonGeTrInfo.tabcarries its transcript's strand.Transcriptome.cpp:79-91:exG.stris loaded per exon fromexonGeTrInfo.tab. There is no per-gene strand for counting.Transcriptome_geneCountsAddAlign.cpp:30-45: for each exon overlapping a block of the read,str1 = exG.str - 1. Column "same strand" skips the exon whena.Str != str1 && str1 < 2; column "reverse" skips it whena.Str == str1 && str1 < 2. So the strand test is per exon, and a strand-0 exon (str1wraps to a large value) is never skipped.Change
GeneAnnotationgainschr_exons_strand, parallel tochr_exons: a strand mask per exon (+, -, or both for strand 0). Exons with the same start, end and gene are merged and their masks OR-ed, which counts the same as STAR's separate entries.tr_strand(what STAR writes intoexonGeTrInfo.tab).overlapping_gene_strandsreturns each overlapping gene with the OR of the masks of the exons the read overlaps (both mates for PE).GeneCounts::apply_three_columnskeeps a gene in a stranded column when that mask has the bit for the column's strand. The overlap walk is shared withoverlapping_genes(for_each_overlap), so the segment tree is unchanged.TranscriptomeIndexstores strand 0 for a.transcript (was 1). This makestranscriptInfo.tabandexonGeTrInfo.tabbyte-identical to STAR for such transcripts. Other code only teststr_strand == 2or== 1; the== 1test in solo (src/solo/gene.rs) is STAR'strStr==1 ? aG.Str : 1-aG.Str(Transcriptome_classifyAlign.cpp:208), so a.transcript now behaves there as in STAR too.gene_is_reversestays: STARsolo still uses it.chr_exonsis now sorted by (start, end, gene) rather than (start, end). The olddedupcould miss duplicates that were not adjacent.Measured against STAR 2.7.11b
Synthetic fixture: 20 kb random chromosome, 105 single-end 50 bp reads, Linux STAR in docker, each tool with its own index. G1 has transcript T1 on + and T2 on - (30 forward reads on T1, 30 reverse reads on T2). G2 is a + gene with 20 reverse reads. G3 is a
.transcript with 10 forward and 15 reverse reads.ReadsPerGene.out.tab.)Both rustar gene-model sources give the same table: the index tables, and
--sjdbGTFfileat mapping.transcriptInfo.tabandexonGeTrInfo.tabof this fixture are now byte-identical to STAR's (main wrote strand 1 for T4).Yeast parity config (ERR12389696, 100k PE,
--quantMode TranscriptomeSAM GeneCounts --outSAMtype BAM SortedByCoordinate --runThreadN 8):ReadsPerGene.out.tabis byte-identical to STAR's, on main and with this PR. That annotation has no gene on both strands, so this is a no-regression check only.Tests
test_gene_counts_strand_per_exon: a gene with a + and a - exon, and a.gene, each hit by one forward read. Fails on main (strand1is[2, 1]instead of[1, 1]), passes here.from_transcriptome_matches_from_gtfnow has a.transcript and also compareschr_exons_strand. It fails with main'stranscriptome.rs(strand 1 for.), passes here.cargo test --release,cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean on macOS.Not checked
TranscriptomeIndexalready skips such transcripts with a warning, but the GeneCounts model does not. Markedshortcut:in the code.MissingGeneIDcase was not rerun here (that branch is not merged). It goes through the same code path.gene_is_reverse) was not changed or compared.sjdbList.fromGTF.out.tabnow writes.for junctions of a.transcript, as STAR'sstrandChar(GTF_transcriptGeneSJ.cpp:142) does; my fixture has no spliced.transcript, so this was not compared.🤖 Generated with Claude Code