Skill Tracer
Skills › Scientific Agent Skills › genomic-coordinates › variant-representation.md

variant-representation.md

182 lines · skills/genomic-coordinates/references/variant-representation.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Variant representation and normalisation
2
3The same change to a genome can be written many ways. Two records that share no
4field values can describe one variant, and two records with identical `POS` can
5describe different ones. Comparisons, joins, deduplication, or annotation lookups can lose real matches
6when equivalent alleles are represented differently. Normalizing literal alleles
7reduces that problem; it does not solve every form of haplotype equivalence.
8

Variant representation and normalisation

The same change to a genome can be written many ways. Two records that share no field values can describe one variant, and two records with identical POS can describe different ones. Comparisons, joins, deduplication, or annotation lookups can lose real matches when equivalent alleles are represented differently. Normalizing literal alleles reduces that problem; it does not solve every form of haplotype equivalence.

Raw lines 1 to 8
9## Why one variant has many spellings
11Take this reference:
13```
14position 1 2 3 4 5 6 7 8 9 10
15base G G C A C A C A C T
16```
18Deleting `AC` from the `CACACAC` run yields `GGCACACT` no matter which adjacent
19`AC` you remove. All of these are the same variant:
21```
22POS=7 REF=CAC ALT=C
23POS=5 REF=CAC ALT=C
24POS=3 REF=CAC ALT=C
25POS=2 REF=GCA ALT=G
26```
28Any caller may emit any of them. Repeat regions, which is where indels
29concentrate, are exactly where the ambiguity is worst.
31Redundant flanking bases add a second axis. `POS=3 REF=CA ALT=CT` and
32`POS=4 REF=A ALT=T` are the same SNV; the first just carries a base that does not
33change.

Why one variant has many spellings

Take this reference:

position    1  2  3  4  5  6  7  8  9 10
base        G  G  C  A  C  A  C  A  C  T

Deleting AC from the CACACAC run yields GGCACACT no matter which adjacent AC you remove. All of these are the same variant:

POS=7  REF=CAC  ALT=C
POS=5  REF=CAC  ALT=C
POS=3  REF=CAC  ALT=C
POS=2  REF=GCA  ALT=G

Any caller may emit any of them. Repeat regions, which is where indels concentrate, are exactly where the ambiguity is worst.

Redundant flanking bases add a second axis. POS=3 REF=CA ALT=CT and POS=4 REF=A ALT=T are the same SNV; the first just carries a base that does not change.

Raw lines 9 to 34
35## The normalisation rule
37A variant is normalised when it is **parsimonious** (as few bases as possible,
38while keeping at least one) and **left-aligned** (shifted as far towards the
39start of the contig as it can go without changing the sequence it describes).
40This is the definition from Tan, Abecasis & Kang, *Unified representation of
41genetic variants*, Bioinformatics 31(13):2202–2204, 2015, and it is what
42`bcftools norm` and `vt normalize` implement.
44The procedure:
461. While the alleles all end with the same base: if any allele is down to one
47 base, extend every allele one base to the left using the reference and
48 decrement `POS`; then drop the last base of every allele.
492. While every allele has at least two bases and they all start with the same
50 base: drop the first base of every allele and increment `POS`.
52Step 1 walks the variant left through a repeat. Step 2 strips redundant padding.
53At the contig start, do not extend past base 1. The helper bounds left-extension
54with `--window` and reports `incomplete` if that limit stops normalization:
56```bash
57python3 normalize_variant.py --fasta ref.fa chr1 7 CAC C
58# chr1:7:CAC:C -> chr1:2:GCA:G pos_shift 5
59```
61`pos_shift` is positive when left-alignment moved the anchor left through a
62repeat, negative when trimming moved it right onto a shorter, equivalent record.

The normalisation rule

A variant is normalised when it is parsimonious (as few bases as possible, while keeping at least one) and left-aligned (shifted as far towards the start of the contig as it can go without changing the sequence it describes). This is the definition from Tan, Abecasis & Kang, Unified representation of genetic variants, Bioinformatics 31(13):2202–2204, 2015, and it is what bcftools norm and vt normalize implement.

The procedure:

  1. While the alleles all end with the same base: if any allele is down to one base, extend every allele one base to the left using the reference and decrement POS; then drop the last base of every allele.
  2. While every allele has at least two bases and they all start with the same base: drop the first base of every allele and increment POS.

Step 1 walks the variant left through a repeat. Step 2 strips redundant padding. At the contig start, do not extend past base 1. The helper bounds left-extension with --window and reports incomplete if that limit stops normalization:

python3 normalize_variant.py --fasta ref.fa chr1 7 CAC C
# chr1:7:CAC:C  ->  chr1:2:GCA:G   pos_shift 5

pos_shift is positive when left-alignment moved the anchor left through a repeat, negative when trimming moved it right onto a shorter, equivalent record.

Raw lines 35 to 63
Runs code python3 normalize_variant.py :57
64## Checking equivalence
66Normalise both and compare the four fields:
68```bash
69python3 normalize_variant.py --fasta ref.fa \
70 --compare chr1:7:CAC:C chr1:3:CAC:C chr1:2:GCA:G
71# verdict: identical -- all 3 records normalise to chr1:2:GCA:G
72```
74The verdict goes to stderr so the per-record table on stdout stays parseable.

Checking equivalence

Normalise both and compare the four fields:

python3 normalize_variant.py --fasta ref.fa \
    --compare chr1:7:CAC:C chr1:3:CAC:C chr1:2:GCA:G
# verdict: identical -- all 3 records normalise to chr1:2:GCA:G

The verdict goes to stderr so the per-record table on stdout stays parseable.

Raw lines 64 to 75
Runs code python3 normalize_variant.py :69
76## Normalisation needs the right reference
78Left-alignment reads reference bases. Handed the wrong assembly it will produce a
79confident, wrong answer, so the `REF` field is checked against the FASTA first and
80a mismatch stops that record:
82```
83ref_check MISMATCH REF says A but the reference has C at chr1:3
84```
86A `REF` mismatch is evidence of inconsistent inputs, not a diagnosis of the
87cause. Check assembly, exact contig names, strand, sequence versions, and
88coordinates. Sequence masking and reference errors can also matter. A matching
89REF at a few sites does not prove that the whole assembly matches.

Normalisation needs the right reference

Left-alignment reads reference bases. Handed the wrong assembly it will produce a confident, wrong answer, so the REF field is checked against the FASTA first and a mismatch stops that record:

ref_check  MISMATCH   REF says A but the reference has C at chr1:3

A REF mismatch is evidence of inconsistent inputs, not a diagnosis of the cause. Check assembly, exact contig names, strand, sequence versions, and coordinates. Sequence masking and reference errors can also matter. A matching REF at a few sites does not prove that the whole assembly matches.

Raw lines 76 to 90
91## Multi-allelic records
93`ALT=G,GG` contains two alternatives sharing a record. Normalization can operate
94on a multiallelic site jointly; its shared representation is not necessarily the
95same as the independently normalized biallelic keys. Choose the representation
96needed for comparison. This helper only normalizes one literal ALT at a time
97and rejects unsplit lists:
99```bash
100python3 normalize_variant.py --fasta ref.fa --split --input cohort.vcf
101```
103This produces allele-key TSV/JSON and **does not preserve INFO, FORMAT, GT, or
104sample identity**. For a VCF workflow use the native tool, for example:
106```bash
107bcftools norm -m -any -f ref.fa -c e --old-rec-tag ORIG cohort.vcf -Ov -o normalized.vcf
108```
110`bcftools norm` handles splitting and reference-based normalization in one
111command. Field handling depends on declared `Number=A/R/G`, ploidy and options;
112it is not a blanket duplication of non-A fields. Review genotype projection
113(`--multi-overlaps 0|.`), allele depths (`--keep-sum AD`), and provenance before
114using the split output for frequency or dosage calculations. Tested with a tiny
115multiallelic GT/AD/PL fixture under bcftools 1.24.

Multi-allelic records

ALT=G,GG contains two alternatives sharing a record. Normalization can operate on a multiallelic site jointly; its shared representation is not necessarily the same as the independently normalized biallelic keys. Choose the representation needed for comparison. This helper only normalizes one literal ALT at a time and rejects unsplit lists:

python3 normalize_variant.py --fasta ref.fa --split --input cohort.vcf

This produces allele-key TSV/JSON and does not preserve INFO, FORMAT, GT, or sample identity. For a VCF workflow use the native tool, for example:

bcftools norm -m -any -f ref.fa -c e --old-rec-tag ORIG cohort.vcf -Ov -o normalized.vcf

bcftools norm handles splitting and reference-based normalization in one command. Field handling depends on declared Number=A/R/G, ploidy and options; it is not a blanket duplication of non-A fields. Review genotype projection (--multi-overlaps 0|.), allele depths (--keep-sum AD), and provenance before using the split output for frequency or dosage calculations. Tested with a tiny multiallelic GT/AD/PL fixture under bcftools 1.24.

Raw lines 91 to 116
Runs code python3 normalize_variant.py :100
117## The other direction: HGVS shifts right
119VCF normalization conventionally left-aligns on the genome. HGVS instead uses
120the 3'-most position on the described reference: *"in the case of ambiguity, the most 3'
121position possible of the reference sequence is arbitrarily assigned to have been
122changed."* The shift can differ between genomic and transcript descriptions of one
123variant; it is not always opposite in genomic direction.
125Worse, HGVS's "3'" is relative to **the reference sequence being described**:
127| Description | Shifted towards | On a plus-strand gene | On a minus-strand gene |
128| --- | --- | --- | --- |
129| VCF `POS` | contig start | leftmost genomic | leftmost genomic |
130| HGVS `g.` | contig end | rightmost genomic | rightmost genomic |
131| HGVS `c.` / `n.` / `p.` | transcript 3' end | rightmost genomic | **leftmost** genomic |
133So for a minus-strand gene, an HGVS `c.` description and a left-aligned VCF
134record can coincide. On the plus strand, they can differ in repeats.
135Never convert between the two by adjusting coordinates; round-trip through a
136tool that knows the transcript model (VEP, Mutalyzer, `hgvs` in Python).
137The HGVS exon-junction exceptions also apply; a blind transcript shift is unsafe.
138`bcftools norm --gff-annot` supports transcript-aware right-alignment on forward
139transcripts, but does not create a general HGVS description.

The other direction: HGVS shifts right

VCF normalization conventionally left-aligns on the genome. HGVS instead uses the 3'-most position on the described reference: "in the case of ambiguity, the most 3' position possible of the reference sequence is arbitrarily assigned to have been changed." The shift can differ between genomic and transcript descriptions of one variant; it is not always opposite in genomic direction.

Worse, HGVS's "3'" is relative to the reference sequence being described:

DescriptionShifted towardsOn a plus-strand geneOn a minus-strand gene
VCF POScontig startleftmost genomicleftmost genomic
HGVS g.contig endrightmost genomicrightmost genomic
HGVS c. / n. / p.transcript 3' endrightmost genomicleftmost genomic

So for a minus-strand gene, an HGVS c. description and a left-aligned VCF record can coincide. On the plus strand, they can differ in repeats. Never convert between the two by adjusting coordinates; round-trip through a tool that knows the transcript model (VEP, Mutalyzer, hgvs in Python). The HGVS exon-junction exceptions also apply; a blind transcript shift is unsafe. bcftools norm --gff-annot supports transcript-aware right-alignment on forward transcripts, but does not create a general HGVS description.

Raw lines 117 to 140
141## Symbolic and structural alleles
143Symbolic alleles (`<DEL>`, `<DUP>`, etc.), breakend replacement strings, missing
144ALT `.`, and spanning-deletion ALT `*` are outside this helper's literal-allele
145normalization. Breakends can include local inserted sequence, and their REF
146need not always be a single base. They pass through unchanged with `ref_check =
147skipped`; this does not validate their REF, structural syntax, or event extent.
149In VCF 4.5, symbolic structural alleles use per-ALT positive `SVLEN`; legacy
150`END` and negative deletion lengths have backward-compatibility rules. gVCF
151reference blocks use `FORMAT/LEN` in the current specification. Follow the file's
152declared version and use a native structural-variant validator/comparator.
154`*` represents sequence absent due to an overlapping deletion. It may occur in
155sample genotypes, but should not be counted as a separate newly discovered
156sequence-change event at that position. Frequency handling is analysis-specific.

Symbolic and structural alleles

Symbolic alleles (<DEL>, <DUP>, etc.), breakend replacement strings, missing ALT ., and spanning-deletion ALT * are outside this helper's literal-allele normalization. Breakends can include local inserted sequence, and their REF need not always be a single base. They pass through unchanged with ref_check = skipped; this does not validate their REF, structural syntax, or event extent.

In VCF 4.5, symbolic structural alleles use per-ALT positive SVLEN; legacy END and negative deletion lengths have backward-compatibility rules. gVCF reference blocks use FORMAT/LEN in the current specification. Follow the file's declared version and use a native structural-variant validator/comparator.

* represents sequence absent due to an overlapping deletion. It may occur in sample genotypes, but should not be counted as a separate newly discovered sequence-change event at that position. Frequency handling is analysis-specific.

Raw lines 141 to 157
158## What to run before comparing two variant sets
160```bash
161# 1. same assembly, same contig naming?
162python3 check_contigs.py setA.vcf setB.vcf --genome ref.fa.fai
164# 2. structural conventions intact?
165python3 audit_intervals.py setA.vcf --genome ref.fa.fai
167# 3. split, check REF, trim, left-align -- both sets, same reference
168python3 normalize_variant.py --fasta ref.fa --split --input setA.vcf -o A.norm.tsv
169python3 normalize_variant.py --fasta ref.fa --split --input setB.vcf -o B.norm.tsv
170```
172Join only successful literal-allele keys. Stop on mismatches, errors, or
173`incomplete` normalization; skipped rows need a different comparison. Even fully
174normalized alleles do not establish equivalence of phased haplotypes represented
175as multiple SNVs versus one MNV, complex variants, or structural events.

What to run before comparing two variant sets

# 1. same assembly, same contig naming?
python3 check_contigs.py setA.vcf setB.vcf --genome ref.fa.fai

# 2. structural conventions intact?
python3 audit_intervals.py setA.vcf --genome ref.fa.fai

# 3. split, check REF, trim, left-align -- both sets, same reference
python3 normalize_variant.py --fasta ref.fa --split --input setA.vcf -o A.norm.tsv
python3 normalize_variant.py --fasta ref.fa --split --input setB.vcf -o B.norm.tsv

Join only successful literal-allele keys. Stop on mismatches, errors, or incomplete normalization; skipped rows need a different comparison. Even fully normalized alleles do not establish equivalence of phased haplotypes represented as multiple SNVs versus one MNV, complex variants, or structural events.

Raw lines 158 to 176
Runs code python3 check_contigs.py :162
Runs code python3 audit_intervals.py :165
Runs code python3 normalize_variant.py :168
177## Official sources reviewed 2026-10-01
179- [bcftools norm manual](https://samtools.github.io/bcftools/bcftools.html#norm).
180- [VCF 4.5 specification](https://samtools.github.io/hts-specs/VCFv4.5.pdf).
181- [HGVS general recommendations](https://hgvs-nomenclature.org/stable/recommendations/general/)
182 and [numbering](https://hgvs-nomenclature.org/stable/background/numbering/).