Skip to content

feat(quant): unspliced targets in TranscriptomeSAM, splicing-status tag and GeneSplicing counts for bulk total RNA-seq - #283

Open
BenjaminDEMAILLE wants to merge 15 commits into
mainfrom
feature/unspliced-bulk-quant
Open

BenjaminDEMAILLE wants to merge 15 commits into
mainfrom
feature/unspliced-bulk-quant

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Ribo-depleted ("total") RNA-seq contains a lot of unspliced pre-mRNA, and --quantMode TranscriptomeSAM has nowhere to put it. The projection (Transcriptome_quantAlign.cpp) sends a genomic alignment to every transcript whose exons contain it. An unspliced read inside an intron fits no fully spliced isoform, but GENCODE annotates many retained_intron isoforms whose single exon covers that intron: the read is projected onto that isoform only and Salmon / RSEM must assign it there. The amount follows the library's pre-mRNA content, not the isoform's abundance. Bulk users also have no STAR measure of spliced vs unspliced reads (GeneFull / Velocyto exist only in STARsolo).

Motivating measurements (made by the PR author with Salmon, not with rustar-aligner): 96 whole-blood libraries, ribo-depleted total RNA, reverse-stranded, 2x51 bp. With Salmon and full GRCh38 decoys, about 58% of fragments went to decoys (intronic pre-mRNA, expected in total RNA). A pre-mRNA read compatible with a retained_intron isoform is assigned to it; library by library, the share of reads on retained_intron isoforms tracks the decoy share (Spearman rho = 0.94, n = 96), unchanged at fixed residual gDNA and unrelated to TIN. Isoform proportions, hence tximport avgTxLength offsets, did not reproduce between technical replicates (r ~ 0.05).

The public benchmark below reproduces the pattern with rustar-aligner's STAR-faithful projection: over 16 public whole-blood libraries the retained-intron share tracks the unspliced fraction with Spearman rho = 0.92.

Refs COMBINE-lab/salmon#1229. See also https://support.bioconductor.org/p/9163851/, He et al. 2022 (splici, Nat Methods, doi:10.1038/s41592-022-01408-3) and Soneson et al. 2021 (PLoS Comput Biol, doi:10.1371/journal.pcbi.1008585). I know of no published bulk benchmark, so this PR brings its own.

What this PR adds (all opt-in, all rustar-aligner extensions)

1. Unspliced targets in the transcriptome BAM: --quantTranscriptomeUnspliced None|Intron|PreMRNA

Default None = STAR. With Intron or PreMRNA, one target per gene, <gene_id>-I (splici naming), is appended after the annotated transcripts, @SQ lines in gene order:

  • Intron: union of the introns of all the gene's isoforms (retained introns and cassette-exon introns included), merged, extended by --quantTranscriptomeUnsplicedFlank on each side (default -1 = the index's sjdbOverhang, i.e. read length - 1 by STAR convention), clipped to the gene body, merged again, concatenated in genome order.
  • PreMRNA: the whole gene body.

Projection is the unchanged STAR logic applied to all targets: an alignment goes to every target that contains all its blocks, spliced and unspliced alike. A read in a retained intron is written to both the retained-intron isoform and <gene_id>-I (NH / HI / MAPQ count all targets) and the EM decides; a read in a constitutive intron now reaches <gene_id>-I instead of being dropped; a fragment crossing a splice junction only goes to spliced targets. --quantTranscriptomeSAMoutput rules (indels, soft-clip extension, single-end) apply to every target; minus-strand targets are read 3' to 5' like transcripts.

Also written: Aligned.toTranscriptome.targets.tsv (target_id, gene_id, gene_name, spliced|unspliced status, length, in @SQ order: a tx2gene table) and, with --quantTranscriptomeUnsplicedFasta Yes, Aligned.toTranscriptome.unspliced.fa (the -I sequences, needed next to the transcript FASTA by salmon quant -a; off by default because it is genome-sized and only depends on index + flank).

Targets are built at alignment time from the transcript tables of any GTF-aware index: no index format change, flank adjustable per run, works with indexes built before this PR. Cost measured below: none visible in wall time, +0.3 to 0.4 GiB RSS.

2. sp:A splicing-status tag in the genomic SAM/BAM: --outSAMattributes ... sp

In no preset (default output unchanged). sp:A:S spliced (compatible only with mature mRNA), sp:A:U unspliced (needs pre-mRNA), sp:A:A ambiguous; no tag when no annotated transcript contains the alignment; both mates carry the fragment status. The name is not used by STAR / STARsolo (checked against parametersDefault); lower-case tags are reserved for local use by the SAM spec.

3. Per-gene table: --quantMode GeneSplicing (secondary, cheap)

ReadsPerGeneSplicing.out.tab (spliced / unspliced / ambiguous per gene for the unstranded / forward / reverse conventions of ReadsPerGene.out.tab) and ReadsPerGeneSplicing.summary.tsv (read accounting and fractions). It only runs when requested and reuses the classifier of the sp tag, so I kept it; its unspliced fraction is the per-library pre-mRNA measure used in the benchmark.

The classifier behind 2 and 3 is a port of STARsolo's Velocyto logic (provenance): alignToTranscriptMinOverlap from Transcriptome_classifyAlign.cpp (minOverlapMinusOne = 6, 1 Mb intron cap, spliced alignments touching an intron rejected) and the per-UMI collapse of SoloFeature_countVelocyto.cpp, one read or pair standing for one UMI.

Also in this PR

  • fix(transcriptome): with the default BanSingleEnd_BanIndels_ExtendSoftclip, a CIGAR such as 1S97M2S lost its right clip when rebuilt (single-op body), producing a 98M record for a 100-base read; BAM encoding then failed and main aborts the whole run (read length-sequence length mismatch) on the public data below. STAR extends both ends (ReadAlign_quantTranscriptome.cpp); fixed, with a unit test.
  • fix(params): unknown --quantMode values were silently ignored (a typo gave no counts and no error); now rejected as STAR does (Parameters.cpp).

Design history and rejected alternatives

This PR went through two earlier designs, kept in the commit history (no force-push):

  • A Velocyto-named bulk mode (--quantMode GeneVelocyto): renamed, Velocyto being a single-cell notion (refactor(quant): rename GeneVelocyto to GeneSplicing).
  • Dropping reads instead of adding targets (--quantTranscriptomePreMRNA BanRetainedIntron: do not project an unspliced read overlapping an intron that another isoform retains): removed. It also deleted the only evidence for genuinely expressed retained-intron isoforms (simulation: true share 0.333 estimated as 0.000) and pushed the exonic pre-mRNA reads onto the spliced isoforms (+45% on them). Giving the EM an unspliced target is the principled fix and is what splici does.
  • Separate spliced / unspliced counting only (Velocyto-style): kept as the secondary GeneSplicing table, but it does not fix isoform quantification, which needs both kinds of targets in one reference.
  • Other alternatives considered: one -I record per disjoint intron interval (splici's -I1, -I2...; loses pairs whose mates fall in two introns, so the intervals are concatenated into one target per gene); using GENCODE's transcript_type (annotation-specific; the rules here use structure only); tagging projections instead of adding targets (Salmon / RSEM ignore tags); building targets at genomeGenerate (would change the index format and freeze the flank).

Compatibility / divergence from STAR

  • Without the new options, outputs are unchanged:
    • tests/bulk_unspliced.rs::new_options_leave_existing_outputs_unchanged: GeneSplicing + --quantTranscriptomeUnspliced None leave Aligned.out.sam (minus @PG/@CO), ReadsPerGene.out.tab, SJ.out.tab, Log.final.out (minus times) and the transcriptome BAM records identical; Intron leaves the genomic outputs identical and only appends @SQ lines after the annotated ones.
    • splicing_status_tag_in_genomic_sam: records with sp minus the tag equal the default records.
    • Against a main build on public data (scripts/bench_bulk_unspliced/compare_main.sh, 2 libraries x 2 M pairs, GRCh38 + GENCODE v50): Aligned.out.bam records and header (minus @PG/@CO), transcriptome BAM records, SJ.out.tab and Log.final.out identical, for the branch with and without GeneSplicing --quantTranscriptomeUnspliced None (4.4 M genomic and 33 to 37 M transcriptome records). This check uses --quantTranscriptomeSAMoutput BanSingleEnd because main aborts on this data with the default mode (fix above); with the default mode the branch differs from main exactly where main fails.
  • All three options are non-STAR and documented in DIVERGENCE.md 1.4, including two small departures of the classifier port from classifyAlign (containment test on the true pair span; blocks sorted before the scan). Needs maintainer sign-off per CONTRIBUTING.
  • Shared-file changes: QuantContext holds optional GeneCounts / GeneSplicing / sp parts (4 call sites in lib.rs); filter_and_project takes a fragment-level junction flag; TranscriptomeIndex gains an optional unspliced marker; SamAttributes gains SP (bit 15). Default hot path: unchanged work (new code behind Options).

Tests

  • src/quant/transcriptome.rs: unspliced_mode_from_str, unspliced_intervals_intron_and_premrna (union of isoform introns incl. retained ones, flanks clipped and merged, gene body), single_exon_gene_has_no_intron_target, unspliced_targets_are_appended_in_gene_order (annotated order preserved), unspliced_target_projection (retained intron -> RI isoform + -I, spliced fragment -> spliced only, constitutive exon untouched, intron/exon boundary needs the flank, cassette exon -> inclusion isoform + -I), unspliced_target_on_minus_strand_is_flipped, targets_table_and_unspliced_fasta, softclip_rebuild_folds_both_clips_into_a_single_op.
  • src/quant/splice_status.rs (19 tests): per-block calls, STAR's collapse rules, full classification on a synthetic GTF (retained intron, antisense gene inside an intron, boundary exons, protruding read, single-exon chrM gene, GENCODE _PAR_Y copies, pooled pair blocks, indel merging), table / summary writers, read_status_ignores_gene_boundaries, sp_tag_on_records.
  • src/params/mod.rs: quant_gene_splicing_is_opt_in, sp_attribute_is_opt_in, unknown_quant_mode_is_rejected.
  • tests/bulk_unspliced.rs (synthetic genome, 3 genes on both strands, each with a spliced isoform A and a retained-intron isoform R; reverse-stranded library of 300 mature A, 0 / 150 / 0 mature R and 400 pre-mRNA reads per gene):
    • simulated_total_rna_mixture: every pre-mRNA read inside the retained intron is written to both R and <gene>-I. Minimal effective-length EM on the transcriptome BAM:

      gene (truth RI share) STAR projection A / R / -I, RI share Intron PreMRNA
      G1 (0.000) 259 / 306 / 0, 0.541 389 / 79 / 232, 0.169 269 / 47 / 384, 0.148
      G2 (0.333) 295 / 412 / 0, 0.582 426 / 172 / 252, 0.288 303 / 132 / 415, 0.304
      G3 (0.000) 275 / 294 / 0, 0.517 400 / 74 / 226, 0.156 284 / 43 / 373, 0.132

      Summed RI-share error 1.308 / 0.370 / 0.310; summed relative error on A 0.236 / 1.049 / 0.164; PreMRNA recovers the 400 simulated pre-mRNA reads per gene on -I within 15%. Intron cannot place the exonic part of pre-mRNA anywhere but the spliced isoforms (splici has the same limitation), which the test states.

    • splicing_status_tag_in_genomic_sam: pre-mRNA reads in exon 1 / the constitutive intron / the retained intron get S / U / A.

cargo fmt --check, cargo clippy --all-targets (0 warnings) and cargo test pass locally and in CI.

Public benchmark

Scripts and raw results: scripts/bench_bulk_unspliced/ (results_PRJEB57727.txt). GRCh38 primary assembly + GENCODE v50 comprehensive annotation (full genome index, no subset); ENA PRJEB57727 (whole blood in PAXgene, rRNA + globin depleted, reverse-stranded, 2x100 bp), 8 runs, first 4 M pairs of each split into two 2 M-pair pseudo-replicates (16 libraries). Each library aligned three times (STAR projection, Intron, PreMRNA), each transcriptome BAM quantified with salmon quant -a (salmon 2.8.0). macOS aarch64, 16 threads.

STAR projection Intron PreMRNA
fragments in transcriptome BAM per input pair 0.617 0.917 0.924
Salmon mapping rate 0.986 0.983 0.985
Salmon fragments on -I targets 0 0.350 0.470
retained_intron share of spliced-target fragments (mean) 0.0537 0.0440 0.0512
slope of RI share vs unspliced fraction 0.151 0.107 0.111
Spearman rho, RI share vs unspliced fraction (n = 16; rep1 only n = 8) 0.92 (0.93) 0.92 (0.93) 0.92 (0.93)
pseudo-replicate r of within-gene isoform proportions (median of 8) 0.817 0.819 0.828
pseudo-replicate r of log avgTxLength (median of 8) 0.975 0.974 0.972
wall time per 2 M pairs (mean, noisy shared machine) 49 s 48 s 55 s
peak RSS 29.2 GiB 29.5 GiB 29.6 GiB

GeneSplicing (reverse column, picked from the data): unspliced fraction 0.29 to 0.58 across the 16 libraries (mean 0.39). genomeGenerate (full GRCh38 + GENCODE v50, 644 k transcripts): 926 s, peak RSS 20.7 GiB.

What this says, honestly:

  • The bias the PR targets is real on public data: with STAR's projection the retained-intron share tracks the unspliced fraction almost perfectly across libraries.
  • Unspliced targets reduce the retained-intron share (-18% with Intron) and its dependence on pre-mRNA content (slope -26% to -29%), and raise the fragments reaching the transcriptome BAM from 0.62 to 0.92 per pair, but they do not remove the rank correlation (rho stays 0.92). With Salmon's length-normalised EM, a short retained-intron isoform still explains local intronic coverage better than a long gene-body target; part of the correlation may also be biological (libraries richer in nascent RNA also carry more genuine retention). This is the same limit splici has.
  • Pseudo-replicates share library prep, so they only measure sampling noise; isoform proportions improve slightly with PreMRNA (0.817 -> 0.828). The technical-replicate effect in the motivation (r ~ 0.05) cannot be tested on this dataset; the PR author will run the private benchmark and comment the numbers here.
  • Limits of this benchmark: 2 M pairs per library; one tissue; the index was built with --sjdbOverhang 49 (ENA metadata suggested 2x50 bp, the reads are 2x100 bp), which is valid but not STAR's recommended value and sets the Intron flank to 49; wall times are noisy (other jobs on the machine).

Limits

  • Intron vs PreMRNA: Intron counts the exonic part of pre-mRNA as mature and drops pairs with one mate in an exon and one in an intron (unless an isoform retains that intron); PreMRNA avoids both but makes every exonic read also compatible with -I, leaving the split to the EM.
  • Overlapping genes: each gene gets its own -I; an intronic read of gene A inside gene B's body goes to both -I targets (EM decides). GeneSplicing drops multi-gene reads (N_multiGene, as STAR's Velocyto does); with GENCODE v50 that is 12% to 25% (mean 18%) of uniquely mapped pairs on the reverse column in the benchmark, which I attribute to overlapping lncRNA / readthrough annotation (not measured). The sp tag ignores gene boundaries and strand.
  • Genes on several chromosomes (shared gene_id): the -I target uses the chromosome and strand of the gene's first transcript.
  • Genuine intron retention cannot be separated from pre-mRNA read by read; the targets only let the EM try.
  • Ambiguity rules of the classifier are STAR's, including the 6-base tolerance and "span in one model + only-exonic in another = spliced".
  • Cost: target construction is a single pass over the transcript tables at startup; the -I FASTA is genome-sized, hence opt-in.

This PR was prepared with Claude Code and reviewed by the user.

🤖 Generated with Claude Code

BenjaminDEMAILLE and others added 6 commits September 29, 2026 22:23
…k reads

Add src/quant/velocyto.rs, a port of the two steps STARsolo uses for
--soloFeatures Velocyto, made usable on bulk reads (one read or read pair
plays the role of one UMI):

- alignToTranscriptMinOverlap (Transcriptome_classifyAlign.cpp), with the
  hard-coded minOverlapMinusOne = 6 and the 1 Mb intron cap, run against
  every transcript that contains the alignment;
- the per-UMI collapse of SoloFeature_countVelocyto.cpp into spliced /
  unspliced / ambiguous per gene, multi-gene reads dropped.

Reads are classified once and filtered per strand convention (unstranded,
forward, reverse), so one run serves any library type. Includes the
counters and the per-gene table / summary writers used by the next
commit, and unit tests on a synthetic annotation (retained intron,
antisense overlapping genes, boundary exons, single-exon chrM gene,
GENCODE _PAR_Y copies, pooled paired-end blocks, indel merging).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…us gene table

New opt-in --quantMode value (not in STAR) that runs the Velocyto
classifier on every uniquely mapped read or pair and writes:

- ReadsPerGeneVelocyto.out.tab: header + one row per gene (geneInfo.tab
  order) with spliced / unspliced / ambiguous counts for each strand
  convention (unstranded, forward, reverse), mirroring the three strand
  columns of ReadsPerGene.out.tab;
- ReadsPerGeneVelocyto.summary.tsv: N_unmapped, N_multimapping,
  N_noFeature, N_multiGene, category totals and the spliced / unspliced /
  ambiguous fractions per strand convention.

Unmapped / multimapping accounting follows GeneCounts. The mode uses the
transcriptome tables already loaded for TranscriptomeSAM, so it has the
same index requirement. QuantContext now holds optional GeneCounts and
GeneVelocyto parts behind count_se_read / count_pe_read / write_outputs;
without the new value the counting and ReadsPerGene.out.tab paths are
unchanged.

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

New opt-in flag (not in STAR, default Keep = STAR behaviour). With
BanRetainedIntron, an unspliced read or pair is not projected onto the
transcripts of gene G when one of its aligned blocks (after the usual
soft-clip extension) overlaps a retained-intron interval of G.

A retained-intron interval is an intron of one isoform that another
isoform of the same gene covers with a single exon reaching into both
flanking exons. Such reads are equally compatible with the gene's
pre-mRNA and with the retaining isoform; STAR projects them onto the
retaining isoform only, so in total RNA-seq retained_intron isoforms
absorb intronic pre-mRNA reads. Cassette exons and alternative 5'/3'
splice sites create no interval, and fragments that cross a junction
are kept (processed RNA).

The intervals are computed once from the transcriptome tables
(RetainedIntrons::build) and attached to the TranscriptomeIndex only
when the flag is set; filter_and_project takes the fragment-level
junction flag, so a pair is dropped from a gene as soon as either mate
overlaps. Counts of dropped alignments/projections are logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/bulk_unspliced.rs builds a synthetic genome with three genes (two
on +, one on -), each with a fully spliced isoform and a retained-intron
isoform, and simulates a reverse-stranded total RNA library (mature
spliced, mature retained-intron and pre-mRNA reads).

new_options_leave_existing_outputs_unchanged aligns it three times
(GeneCounts + TranscriptomeSAM; the same + GeneVelocyto +
--quantTranscriptomePreMRNA Keep; and BanRetainedIntron) and checks that
Aligned.out.sam (minus the @PG/@co command lines), ReadsPerGene.out.tab,
SJ.out.tab and Log.final.out (minus wall-clock lines) are identical, that
the transcriptome BAM records are identical under Keep, that
BanRetainedIntron only removes transcriptome records, and that the new
files appear only when requested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simulated_total_rna_mixture aligns the simulated library (per gene: 300
mature spliced reads, 0 or 150 mature retained-intron reads, 400
pre-mRNA reads over the gene body) with and without
--quantTranscriptomePreMRNA BanRetainedIntron and estimates reads per
transcript from Aligned.toTranscriptome.out.bam with a minimal
Salmon-like EM (effective-length weighted).

- Genes with no retained-intron molecules: the retained-intron isoform
  share goes from > 0.5 (Keep, i.e. STAR) to 0 (BanRetainedIntron);
  the summed absolute share error over the three genes must at least
  halve (it goes from 1.31 to 0.33 on this data).
- GeneVelocyto, reverse columns: N_unspliced equals the number of
  pre-mRNA reads reaching more than 6 bases into the constitutive intron
  (389/389), mature reads are spliced, forward columns are empty.

The per-gene estimates are printed with --nocapture, including the
known trade-off: the gene that really expresses its retained-intron
isoform loses it under BanRetainedIntron.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New guide docs/.../guides/bulk-total-rna.md: why pre-mRNA inflates
  retained-intron isoforms in TranscriptomeSAM, the GeneVelocyto
  classification and output files, the BanRetainedIntron rule and its
  trade-off (a genuinely expressed retained-intron isoform loses its
  distinguishing reads).
- CLI parameter, output-file and quantification pages point to it.
- DIVERGENCE.md 1.4 records both extensions, the STAR code they start
  from, and the two small departures from classifyAlign (true pair span
  for the containment test, blocks sorted before the scan).
- CHANGELOG Features entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BenjaminDEMAILLE
BenjaminDEMAILLE marked this pull request as ready for review September 29, 2026 21:06
BenjaminDEMAILLE and others added 9 commits September 29, 2026 23:14
Velocyto is a single-cell notion; for bulk data the mode is about the
splicing status of reads. Rename every user-facing name:

- --quantMode GeneVelocyto -> GeneSplicing;
- ReadsPerGeneVelocyto.out.tab / .summary.tsv ->
  ReadsPerGeneSplicing.out.tab / .summary.tsv;
- src/quant/velocyto.rs -> src/quant/splice_status.rs, with
  SpliceStatus / ReadSplicing / SplicingCounts / SplicingQuant types;
- docs, CHANGELOG, DIVERGENCE and test names.

STARsolo's Velocyto feature is still cited as the origin of the
classification, in code comments and STAR source references only.
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New opt-in --quantTranscriptomeUnspliced None|Intron|PreMRNA (default
None = STAR behaviour, unchanged). With Intron or PreMRNA, one unspliced
target per gene is appended to the transcriptome after the annotated
transcripts, in gene order, named <gene_id>-I (splici convention):

- Intron: union of the introns of all the gene's isoforms (retained
  introns and cassette-exon introns included), merged, extended by
  --quantTranscriptomeUnsplicedFlank on each side (default -1 = the
  index's sjdbOverhang, read length - 1 by STAR convention), clipped to
  the gene body, merged again, concatenated in genome order;
- PreMRNA: the whole gene body.

Projection is unchanged: an alignment goes to every target whose blocks
contain it, spliced and unspliced alike, so an intronic read compatible
with a retained-intron isoform is written to both and the downstream EM
decides (NH / MAPQ count all targets). A fragment that crosses a
junction only goes to spliced targets. --quantTranscriptomeSAMoutput
rules (indels, soft-clip extension, single-end) apply as before. Minus
strand targets are read 3' to 5' like transcripts.

Also writes Aligned.toTranscriptome.targets.tsv (target_id, gene_id,
gene_name, spliced|unspliced status, length, in @sq order: a tx2gene
table) and, with --quantTranscriptomeUnsplicedFasta Yes, the unspliced
target sequences to Aligned.toTranscriptome.unspliced.fa for Salmon's
alignment mode (off by default: genome-sized).

The targets are built at alignment time from the transcript tables of
any GTF-aware index (no index format change, flank adjustable per run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new_options_leave_existing_outputs_unchanged now checks that GeneSplicing
and --quantTranscriptomeUnspliced None leave every output identical
(including the transcriptome BAM records), that Intron leaves the genomic
outputs identical and only appends G1-I, G2-I, G3-I after the annotated
@sq lines, and that the targets table / FASTA appear only when asked.

simulated_total_rna_mixture compares STAR's projection with Intron and
PreMRNA targets on the simulated total RNA library:
- every pre-mRNA read inside the retained intron is written to both the
  retained-intron isoform and <gene_id>-I;
- EM retained-intron share error (summed over genes): STAR 1.308,
  Intron 0.370, PreMRNA 0.310;
- relative error on the spliced isoform: STAR 0.236, Intron 1.049,
  PreMRNA 0.164. PreMRNA recovers the simulated pre-mRNA reads on -I
  (within 15%); Intron cannot place exonic pre-mRNA reads anywhere but
  the spliced isoforms (same limitation as splici), which the test states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Superseded by --quantTranscriptomeUnspliced: instead of dropping unspliced
reads that overlap a retained intron (which also removed the only
evidence for genuinely expressed retained-intron isoforms and gave the
exonic pre-mRNA reads to the spliced isoforms), those reads are now
written to both the retained-intron isoform and the gene's <gene_id>-I
target, and the downstream EM decides.

Removes the flag, QuantTranscriptomePreMRNA, RetainedIntrons, the
TranscriptomeIndex::retained_introns field and their tests.
filter_and_project keeps its fragment-level junction flag, now used to
keep spliced fragments off the unspliced targets. Documentation is
updated in the following docs commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New --outSAMattributes value sp (rustar-aligner extension, not in any
preset, so default output is unchanged). Each alignment record gets
sp:A:S (spliced / mature-compatible), sp:A:U (unspliced, needs
pre-mRNA) or sp:A:A (ambiguous), from the same STARsolo-derived
classifier as --quantMode GeneSplicing, collapsed over every annotated
transcript that contains the alignment on either strand (a read-level
status, independent of gene assignment). No tag when no transcript
contains the alignment. For pairs both mates carry the fragment status.

The tag name is not used by STAR / STARsolo (their list: NH HI AS nM NM
MD jM jI XS MC ch vA vG vW rB ha GX GN CB UB CR CY UR UY sM sS sQ sF
cN) and lower-case tags are reserved for local use by the SAM spec.
Needs a GTF-aware index; uses the annotated transcripts only (never the
<gene_id>-I targets).

Tests: read_status_ignores_gene_boundaries, sp_tag_on_records,
sp_attribute_is_opt_in, and splicing_status_tag_in_genomic_sam (records
minus the tag equal the default run; pre-mRNA reads in exon 1 / the
constitutive intron / the retained intron get S / U / A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rustar-aligner accepted any --quantMode word and silently ignored the
unknown ones (a typo such as Genecounts produced no counts and no
error). STAR exits with "unrecognized option in --quantMode"
(Parameters.cpp); do the same, keeping "-" as "none". Allowed values:
TranscriptomeSAM, GeneCounts, and the rustar-aligner extension
GeneSplicing. Only invalid command lines change behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the default --quantTranscriptomeSAMoutput
BanSingleEnd_BanIndels_ExtendSoftclip, rebuild_cigar_without_softclips
folded the left clip into the first op and the right clip into the last
op in one if / else-if chain, so a CIGAR such as 1S97M2S (a single match
op between two clips) lost its right clip: the projected record had a
98M CIGAR for a 100-base read, and BAM encoding failed with "read
length-sequence length mismatch", aborting the whole run.

Found while benchmarking on public 2x100 human total RNA (ENA
PRJEB57727): main aborts on the first such read with --quantMode
TranscriptomeSAM. STAR extends both ends (ReadAlign_quantTranscriptome
.cpp), which the fix now does. Unit test:
softclip_rebuild_folds_both_clips_into_a_single_op.

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

Rewrite the bulk total RNA-seq guide around the new design: the
<gene_id>-I targets of --quantTranscriptomeUnspliced (Intron vs
PreMRNA, flank, projection rules, targets table, FASTA, Salmon recipe),
the sp:A splicing-status tag, and GeneSplicing as a secondary per-gene
table. CLI, output-file and quantification pages, CHANGELOG and
DIVERGENCE.md 1.4 follow; references to the removed BanRetainedIntron
are gone. The CHANGELOG also records the --quantMode validation fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/bench_bulk_unspliced/: fetch_data.sh (GRCh38 primary assembly +
GENCODE v50, 8 runs of ENA PRJEB57727, first 4 M pairs split into two
2 M pseudo-replicates), run.sh (STAR projection vs --quantTranscriptome
Unspliced Intron / PreMRNA, each quantified with salmon quant -a),
analyze.py (retained-intron share, -I share, GeneSplicing unspliced
fraction, cross-library Spearman rho and slope, pseudo-replicate
isoform-proportion and avgTxLength correlations, time, RSS) and
compare_main.sh (outputs identical to a main build). Results of the run
behind the PR in results_PRJEB57727.txt.

Headline (16 libraries, reverse-stranded, 39% unspliced on average):
retained-intron share 0.0537 (STAR projection) / 0.0440 (Intron) /
0.0512 (PreMRNA); slope against the unspliced fraction 0.151 / 0.107 /
0.111, Spearman rho 0.92 in all three; fragments reaching the
transcriptome BAM 0.62 -> 0.92 per pair; wall time and RSS within noise
(peak RSS 29.2 -> 29.6 GiB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BenjaminDEMAILLE BenjaminDEMAILLE changed the title feat(quant): bulk spliced/unspliced gene counts and pre-mRNA-aware TranscriptomeSAM for total RNA-seq feat(quant): unspliced targets in TranscriptomeSAM, splicing-status tag and GeneSplicing counts for bulk total RNA-seq Sep 29, 2026

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