Skip to content

feat(degnorm): DegNorm-style transcript degradation normalization - #232

Open
BenjaminDEMAILLE wants to merge 6 commits into
mainfrom
feat/degnorm
Open

feat(degnorm): DegNorm-style transcript degradation normalization#232
BenjaminDEMAILLE wants to merge 6 commits into
mainfrom
feat/degnorm

Conversation

@BenjaminDEMAILLE

Copy link
Copy Markdown
Contributor

What

Implements what DegNorm (Xiong et al., Genome Biology 2019) does: correct RNA-seq counts for sample- and gene-specific transcript degradation, producing a Degradation Index (DI) score per gene per sample plus degradation-adjusted counts.

Why it is two phases and not one

DegNorm's estimator is a rank-one over-approximation of a gene's coverage matrix across samples. With one sample the fit is exact and every DI is 0, so a DI cannot be computed inside a single alignment run. What a single run can do, essentially for free, is the expensive half DegNorm normally pays for by writing a sorted BAM, indexing it, and re-reading it with pysam: the per-gene coverage curves themselves.

Phase 1, during alignment--quantMode GeneCoverage accumulates per-gene, per-exonic-base coverage on the same unique-hit path GeneCounts already uses (unique alignment, exactly one overlapping gene), and writes GeneCoverage.out.bin (gzip: magic RSDGNCOV, per-gene table with length + raw count, gene id block, flat u32 coverage). Paired-end mate blocks are merged first, so an overlapping pair contributes one per base, matching DegNorm's paired-read handling.

Phase 2, after alignment--runMode degNorm loads N coverage files, validates a shared gene model, and runs the port of DegNorm's nmf.py: leading singular triplet by power iteration on the p x p Gram matrix, dual-ascent NMF over-approximation, bin-dropping baseline selection, and the outer depth-factor loop. Upstream's quirks are kept deliberately (the +1 DI denominator, the 0.1 / 0.2 / 0.9 thresholds, the [0, 0.9] clamp). Genes fan out over rayon; the downsampling offset comes from --runRNGseed and the gene index, so results do not depend on thread count.

New flags

Alignment: --quantMode GeneCoverage, --degNormSampleId.

Merge: --runMode degNorm with --degNormCoverageFiles (>= 2), --degNormIter (5), --degNormNmfIter (100), --degNormDownsampleRate (1), --degNormMinimaxCoverage (0), --degNormSkipBaselineSelection, --degNormBins (20), --degNormMinHighCoverage (50).

Output

<prefix>DegNorm.out/: DegradationIndex.tab, AdjustedCounts.tab, RawCounts.tab, ScaleFactors.tab, Summary.txt.

Not STAR

Both flags are rustar-aligner extensions with no STAR counterpart, off by default, and recorded in DIVERGENCE.md §4.3. Alignment output is unchanged when GeneCoverage is enabled; tests/degnorm.rs asserts the SAM records and ReadsPerGene.out.tab are identical with and without it.

Files

  • src/quant/coverage.rs — accumulator + GeneCoverage.out.bin writer/reader
  • src/degnorm/nmf.rs — rank-one fit, NMF-OA, ratio_svd
  • src/degnorm/baseline.rs — baseline selection, per-gene DI
  • src/degnorm/run.rs — multi-sample driver + output tables
  • tests/degnorm.rs — end-to-end: two synthetic samples, one with a 3'-truncated gene, DI ordering asserted

Verification

cargo test: 586 lib + 28 integration, all passing (13 new unit tests in degnorm/, 8 in quant::coverage, 5 new params tests, 2 new integration tests). cargo clippy --all-targets: 0 warnings. cargo fmt --check: clean.

Docs

User guide at docs/src/content/docs/guides/degnorm.md (sidebar entry added), design spec at docs/superpowers/specs/2026-08-20-degnorm-design.md, plus DIVERGENCE.md, CHANGELOG.md, ROADMAP.md (Phase 18), README.md, CLAUDE.md.

Known limitations

At least two samples required; coverage must come from phase 1 (third-party BAMs are not read); no coverage-curve plots, no MPI, no warm-start directory. Validation against the Python DegNorm on a real multi-sample dataset is tracked as Phase 18.3.

🤖 Generated with Claude Code

BenjaminDEMAILLE and others added 6 commits August 20, 2026 18:20
Two-phase design: --quantMode GeneCoverage captures per-gene, per-exonic-base
coverage during alignment (reusing the GeneCounts unique-hit path), and
--runMode degNorm merges N sample coverage files through a port of DegNorm's
rank-one NMF over-approximation to produce DI scores and adjusted counts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…orm run mode)

--quantMode GeneCoverage accumulates per-gene, per-exonic-base coverage on the
same unique-hit path GeneCounts uses and writes GeneCoverage.out.bin (gzip:
header, per-gene table, gene ids, flat u32 coverage). PE mate blocks are merged
so an overlapping pair contributes one per base.

--runMode degNorm merges N such files and runs a port of DegNorm's rank-one NMF
over-approximation (nmf.rs), baseline selection (baseline.rs) and outer
depth-factor loop (run.rs), writing DegradationIndex.tab, AdjustedCounts.tab,
RawCounts.tab, ScaleFactors.tab and Summary.txt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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