TSO 500 splice calls: represent RNA junctions as their own thing, not genomic <DEL>s
- Dominant language
- Python
- Stars
- 30
- Forks
- 3
- Avg merge
- 9h 28m
- Merged PRs (30d)
- 42
Description
🤖 Written by Claude.
We currently import TSO 500 RNA splice calls as ordinary genomic deletions, and the only place that knows better is the case report, which sniffs `sample.vcf.source` for `^SpliceGirl` (`classification/report/case_report_context.py:188`, 71e29cbe6). Nothing is deployed yet, so this is the moment to scrap that and represent the calls for what they are.
## What the caller actually reports
SpliceGirl is the RNA splice variant caller of the TSO 500 / TruSight Tumor 170 lineage (DRAGEN ships the same thing in its RNA pipeline). It is not a DNA caller:
- Input is the splice-aware alignment (STAR `SJ.out.tab` / DRAGEN's mapper), not a pileup.
- It reports **novel intragenic splice junctions**. Junctions already in the reference GTF are dropped unless whitelisted, which is how the panel surfaces the clinically wanted ones — on TSO 500 the passing calls are EGFR (EGFRvIII), MET (exon 14 skipping) and AR.
- A read supports a junction if it carries the gap in its CIGAR with >= 6 bp overhang each side; reads are split by PCR-duplicate status and unique/multi-mapping (`NH`). Candidates are scored 0-1 by a trained model, `PASS` at or above the threshold and `LowQ` below. **The VCF is unfiltered** — 15 of the 17 records in our test file are `LowQ`.
- Intergenic candidates are not in this VCF at all; they go to fusion merging, which is why `AllFusions.csv` carries `Caller = SpliceGirl` rows.
The `` in the VCF is a convenience encoding of donor->acceptor, not a claim about the genome: the skipped intron and exon are still present in the tumour DNA.
## What we do with it now
`snpdb/migrations/0204_vcf_source_settings_splicegirl.py` remaps `AD` -> alt depth and `DP` -> ref depth, so VAF comes out as the junction ratio `ALTDEDUP / (ALTDEDUP + REFDEDUP)`. That part is right. Everything after it treats the record as a genomic deletion:
- **The same file annotates two different ways depending on span.** `VARIANT_SYMBOLIC_ALT_SIZE = 1000` and `VariantCoordinate.as_internal_canonical_form` (`snpdb/models/models_variant.py:561`) expand a symbolic `` under 1 kb to an explicit REF/ALT read off the FASTA. Four of the seventeen test records (826, 855, 311 and 49 bp) become plain small deletions: standard VEP pipeline, ordinary consequence, `c.123_171del`. The rest go to the SV pipeline (`annotation/annotation_version_querysets.py:26`) and pick up gnomAD-SV overlap and an AnnotSV ACMG SV class. Both are answering "what would deleting this genomic interval do", which nobody asked; the gnomAD-SV frequency on a grid is actively misleading.
- **Identity.** `Variant` is `(locus, alt, svlen)`, so an RNA junction and a DNA deletion of the same span are one row, one `Allele`, one classification hub, one tag list. Collisions are unlikely in practice, but the allele a splice classification hangs off still means "a deletion of chr7:116411708-116414934", which is not what was observed.
- **Two sources of truth for "is this SpliceGirl".** `VCFSourceSettings.source_regex` is data; the report's regex is code. The `source` string is client-supplied (see `upload/test_data/tso500/README.md`), so a site declaring `DRAGEN TSO500 RNA SpliceVariants` gets the field remapping and silently loses the splice report kind.
- Smaller warts that exist only because junctions are crammed into locus space: two junctions sharing a donor sum their depths (`upload/tests/vcf/test_vcf_processors.py:test_splicegirl_shared_locus_is_recorded` — `chr2:47637511` comes out 1/182 rather than 1/91), and the junction ratio wears a column labelled VAF.
- Grid shows "DEL 3.2 kb" for MET exon 14 skipping, "DEL 136 kb" for EGFRvIII, and no badge at all for the four that were expanded.
## The junction TSV
The VCF is the lossy view. The caller's real output is a 26-column TSV per junction — strand, splice motif, annotated-or-novel, unique/multi-mapping and dedup/total read counts, MAPQ, and the model score itself — plus a separate intergenic TSV that feeds fusion merging. A real one is obtainable (@davmlaw), and it is what the design should be drawn against rather than the VCF's five INFO fields.
## Proposal
Two decisions, and they are separable.
**1. Where the "this is a junction, not a deletion" fact lives.** A genomic interval genuinely can be a real DNA deletion from another caller, so the natural home is the call (sample x variant), not the variant. The cheap version of that is a `call_kind` on `VCFSourceSettings` beside `sample_field_overrides`: one row then configures both the field remapping and the report kind, and the regex leaves `case_report_context.py`. Display then comes nearly free, because a node already knows its VCF — a `SPLICE` kind badge instead of `DEL 3.2 kb`, and a "Splice calls" column off the `CohortGenotype.info` blob exactly like the Fusion calls column (`analysis/models/nodes/cohort_mixin.py:360`). Display time is also the only place the gnomAD-SV / AnnotSV columns can be suppressed correctly, since that annotation legitimately belongs to whoever else called the interval.
**2. Whether a junction gets its own variant identity.** The honest answer is that it should: a ``-style symbolic alt on the real contig, or the fake-contig treatment gene fusions and gene-level CNVs get (`snpdb/gene_level_variants.py`, #1506). That buys a distinct `Allele`, no merging with DNA deletions, exclusion from both VEP pipelines, and a place to put the TSV's strand/motif/score. It costs HGVS, search, the grid and the pipeline predicates. Unlike a fusion, a junction does have real coordinates and a usable `c.HGVS`, so the payoff is smaller — but with nothing in prod, the migration cost that normally decides this is zero.
Independent of both: **stop expanding splice calls to explicit sequence.** We should not be asserting missing genomic bases that are not missing, and one file should not annotate two ways depending on span.
## Open questions
- Does the junction TSV give us enough to name the event (`MET exon 14 skipping`) without the scientist typing it? `splice_label` is hand-entered today (`classification/migrations/0182_splice_label_ekey.py`).
- Do we ingest `LowQ` calls at all, or filter at import? 15 of 17 in the sample file are noise-level background junctions.
- What does a splice call's `c.HGVS` mean for classification equivalence, and does it belong in the same discordance machinery as fusions (#1871)?
- Does the intergenic TSV change anything about how the SpliceGirl rows in `AllFusions.csv` are merged?
Related: #444 (multi-variant classification and reporting), #1506 / #1871 (gene-level events), #1711 (TSO 500 per-file loaders).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading snpdb/migrations/0204_vcf_source_settings_splicegirl.py, snpdb/models/models_variant.py, annotation/annotation_version_querysets.py, and analysis/models/nodes/cohort_mixin.py. Review upload/tests/vcf/test_vcf_processors.py and the TSO 500 test data, then resolve the representation, ingestion, identity, HGVS, and LowQ questions with maintainers. Done means the design is agreed and covered by import, identity, annotation, and display tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, bioinformatics, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100