Back to Claude Scientific Skills

Reference builds, contig naming, and liftover

skills/genomic-coordinates/references/reference-builds.md

2.57.07.0 KB
Original Source

Reference builds, contig naming, and liftover

A coordinate is meaningless without the assembly it was measured against. Two files can share contig names, share a coordinate range, join cleanly, and refer to different parts of the genome.

All lengths below were read from the UCSC bigZips chrom.sizes for each assembly and cross-checked against the NCBI assembly report for GRCh37.p13, verified 2026-07-26. scripts/check_contigs.py carries the same table and matches files against it.

Discriminating lengths

ContigGRCh37 / hg19GRCh38 / hg38T2T-CHM13v2.0 / hs1
chr1249,250,621248,956,422248,387,328
chr2243,199,373242,193,529242,696,752
chrX155,270,560156,040,895154,259,566
chrY59,373,56657,227,41562,460,029
chrM / MT16,571 (hg19) / 16,569 (GRCh37)16,56916,569
bash
python3 check_contigs.py --identify unknown.fa.fai

GRCh37 is not hg19

They are the same assembly for every primary chromosome except the mitochondrion. UCSC's hg19 kept the older NC_001807 sequence at 16,571 bp; GRCh37 adopted the revised Cambridge Reference Sequence (rCRS, NC_012920) at 16,569 bp. GRCh38 also uses rCRS, so chrM length distinguishes hg19 from everything else but does not distinguish GRCh37 from GRCh38.

Consequences:

  • Every mitochondrial coordinate differs between an hg19 BAM and a GRCh37 VCF. Nuclear coordinates are identical, so the pipeline runs and only mtDNA results are wrong — which is the hardest kind of error to notice.
  • Mitochondrial heteroplasmy and haplogroup calls made against hg19 cannot be compared to anything rCRS-based without re-calling.

The two also differ in naming and in alternate-haplotype handling:

GRCh37 (Ensembl/NCBI)hg19 (UCSC)
Autosomes1, 2, …chr1, chr2, …
MitochondrionMT (16,569)chrM (16,571)
Alt haplotypesGL000250.1-style9 chr6_cox_hap2-style contigs
UnplacedGL000191.1-stylechrUn_gl000191

The b37 family

b37 (Broad) is GRCh37 with plain naming and rCRS MT. hs37d5 (1000 Genomes phase 2) is b37 plus a decoy contig (hs37d5) and the EBV genome. Primary coordinates are identical across all three, so they interconvert by renaming contigs — no liftover. Reads that map to the decoy in hs37d5 will map somewhere in the primary assembly in b37, which changes coverage and variant calls in the affected regions even though the coordinate system did not move.

GRCh38 and its ALT contigs

hg38 as UCSC ships it has 25 primary contigs, 261 _alt contigs, 42 _random, and 127 chrUn_. The ALT contigs are alternate representations of regions that are genuinely polymorphic — mostly MHC, and the HLA haplotypes.

They break naive analysis in a specific way: a read from an ALT region can map equally well to the primary contig and to its ALT, so both alignments get MAPQ 0 and every variant caller with a MAPQ filter drops the region entirely. Coverage plots show a hole where the MHC should be.

The usual fixes:

  • No-ALT analysis set — the primary assembly with ALT contigs removed. The simplest option and the right default unless you specifically want HLA typing.
  • ALT-aware alignmentbwa-mem with the .alt file and bwa-postalt.js, which lifts ALT alignments back to the primary contigs.

Analysis sets also hard-mask the pseudoautosomal regions on chrY, so that PAR reads map to chrX rather than splitting between the two. Contig lengths are unchanged by masking, so check_contigs.py still identifies a masked analysis set as GRCh38 — masking is invisible in the contig table and has to be checked by looking at the sequence.

Patch releases (GRCh38.p13, p14) add _fix and new _alt contigs but never move a coordinate on a primary chromosome. A p13 coordinate is a p14 coordinate.

T2T-CHM13

CHM13v2.0 is a genuinely different assembly, not a patch: every coordinate differs, and it adds sequence that has no GRCh38 coordinate at all (centromeric satellite arrays, acrocentric short arms). There is no clean liftover for the newly resolved regions, because there is nothing to lift them to. Most public annotation, most clinical variant databases, and most published coordinates are still GRCh38.

Contig naming

Four naming schemes are in circulation for the same chromosome:

chr1            UCSC
1               Ensembl, NCBI, GATK b37
NC_000001.11    RefSeq accession (GRCh38); NC_000001.10 is GRCh37
CM000663.2      GenBank accession (GRCh38); CM000663.1 is GRCh37

Note that the accession's version suffix, not the base accession, carries the build. NC_000001.10 and NC_000001.11 differ only in the last character and are different assemblies.

Renaming is the fix, and bcftools annotate --rename-chrs, samtools reheader, and a two-column mapping file all do it. Two rules:

  • Rename the smaller, cheaper file, and rename it to match the reference — never rename the reference.
  • chrMMT is a rename only between GRCh37 and GRCh38-family files. Between hg19 and anything rCRS-based it is a lie, because the sequences differ.

A join across naming schemes does not error. It returns the rows that happen to match — often zero, sometimes a misleading subset when one file is partly renamed. check_contigs.py reports the naming style of each file and refuses to call two files compatible when they disagree.

Liftover

liftOver (UCSC, with a .chain file) and CrossMap (which also handles BAM, VCF, and BigWig) are the working tools. Both are approximate by nature:

  • Coordinates can vanish. A region deleted from the newer assembly has no target. liftOver writes these to its unmapped file, which is easy to ignore and should be counted every time.
  • Mappings can be one-to-many. A region duplicated in the target maps to several places; taking the first is a silent choice.
  • Strand can flip. Inverted segments between builds mean a plus-strand feature lifts to the minus strand. Interval files carry this fine; anything where sequence orientation matters (primer sites, guide RNAs, motif hits) does not.
  • Interval endpoints can lift independently. A long feature can lift to a different length, or split.
  • Variants need more than coordinates. After lifting a VCF, REF may no longer match the new reference, and if the segment inverted, REF and ALT need reverse-complementing. CrossMap vcf handles this; a coordinate-only lift does not. Always re-run normalize_variant.py against the target reference afterwards and count the MISMATCH rows.

Lifting twice — 37 → 38 → 37 — does not reliably return the original coordinates. When the original data can be re-processed against the target build, that is more accurate than any liftover.

A note on what to record

Coordinates in a results table, a figure, or a supplementary file should say which build they are in, next to the numbers. "chr7:5,530,601-5,530,625" is not a location. "chr7:5,530,601-5,530,625 (GRCh38)" is.