Core Concepts02/06
Starting sandbox…
Beginner·4 graded tests

Coordinate Systems

Coordinate Systems

Core Principle: Genomic data formats utilize two distinct and incompatible coordinate systems: 1-based fully closed intervals and 0-based half-open intervals. Distinguishing between them is essential to prevent off-by-one positioning errors.

The two systems

1-based, fully closed ("interbase off"): the first base is position 1, and an interval [start, end] includes both endpoints. A single base at position 5 is represented as 5-5, and interval length is calculated as end - start + 1.

0-based, half-open: the first base is position 0, and an interval [start, end) includes the start but excludes the end. A single base at index 5 is represented as [5, 6), and interval length is calculated as end - start.

sequence:    A   C   G   T
1-based:     1   2   3   4
0-based:   0   1   2   3   4     ← coordinates sit *between* bases

The 0-based system is sometimes called "interbase" because the numbers label the gaps between bases, not the bases themselves.

Which format uses which

Format / tool System
BED 0-based, half-open
BAM (binary fields) 0-based
GFF / GTF 1-based, closed
VCF 1-based, closed
SAM (text POS field) 1-based
HGVS 1-based, closed
Python slicing / NumPy 0-based, half-open
Ensembl API 1-based
UCSC browser display 1-based (but its BED files are 0-based)

That last row is the classic trap: UCSC shows you 1-based coordinates but exports 0-based BED.

Converting

# 1-based closed  →  0-based half-open  (i.e. to a Python slice)
start0, end0 = start1 - 1, end1

# 0-based half-open  →  1-based closed
start1, end1 = start0 + 1, end0

seq[start1 - 1 : end1]   # extracting a GFF/VCF feature from a Python string

Only the start shifts. The end is numerically identical. That asymmetry is why the bug is so easy to make and so easy to miss; half of standard test cases may still pass.

Biopython does this for you

SeqFeature.location always stores 0-based half-open internally, regardless of the source format. A GFF feature at 1-based 100-200 becomes [99:200] in Biopython. When you print a location you see [99:200], and feature.extract(record.seq) gives the right subsequence.

Do not add or subtract 1 yourself when working with a SeqFeature, as Biopython automatically applies the conversion. See SeqFeature and Location.

hgvs and the extra subtleties

HGVS is 1-based, but c. coordinates carry two extra complications:

  • There is no position 0. In c. numbering, position 1 is the A of the initiator ATG, and the base immediately before it is c.-1, not c.0. Likewise *1 is the first base after the stop codon.
  • Intronic offsets. c.87+3 means "3 bases into the intron after coding position 87"; c.88-3 means "3 bases before coding position 88". These are offset values on the position object, not part of the base number.

hgvs models this with BaseOffsetPosition (fields base, offset, datum). Never do naive arithmetic on .base. See SequenceVariant and hgvs Pitfalls.

Insertions are where everyone dies

An insertion has no width, so where does it go?

  • HGVS: c.100_101insA explicitly specifies the two flanking bases.
  • VCF: POS=100 REF=A ALT=AT anchors on the preceding base and includes it in both alleles.
  • BED: start == end defines a zero-width interval.

Converting between these is not a coordinate shift, it is a representation change. Use hgvs Normalizer and a real VCF library rather than string surgery.

Practical defences

  • Name your variables start0 / start1, never bare start.
  • Write one assertion per interval-producing function: assert end >= start for half-open, assert end >= start and length == end - start + 1 for closed.
  • Test with a single-base feature and a two-base feature. Off-by-ones survive tests on long intervals.
  • Test at a chromosome start (position 1 / index 0). Underflow shows up there.

See also

SeqFeature and Location · Applied - GFF and Genomic Intervals · Applied - Genomic File Formats · hgvs Pitfalls · HGVS Nomenclature · Indexing and Slicing

Exercise

Write convert(records), where records is a list of (system, start, end) triples. system is bed for 0-based half-open or gff for 1-based fully closed. Return one string per record, which the driver prints one per line. Remember that only the start shifts; the end is numerically identical in both systems.

Apply these rules in order:

  • bed with start < 0, or gff with start < 1, is impossible — there is no negative index and no position 0. Emit error.
  • bed with end < start, or gff with end < start - 1, is also error.
  • bed with start == end is a zero-length insertion point, which a fully closed interval cannot express. Emit gff ins <left> <right> len=0, naming the two 1-based bases the insertion falls between. At index 0 there is no base to the left, so <left> is ..
  • gff with end == start - 1 is the same insertion point written the other way. Half-open notation handles it directly, so emit bed <start-1> <start-1> len=0.
  • Otherwise emit gff <start+1> <end> len=<n> or bed <start-1> <end> len=<n>, where n is the length of the interval in the system it was given in.
exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdinbed 100 105 gff 101 105 bed 7676153 7676154 gff 7676154 7676154 gff 101 105 len=5 bed 100 105 len=5 gff 7676154 7676154 len=1 bed 7676153 7676154 len=1
2stdinbed 100 100 bed 0 0 gff 101 100 gff 1 0 gff ins 100 101 len=0 gff ins . 1 len=0 bed 100 100 len=0 bed 0 0 len=0
3stdingff 0 5 bed -1 4 gff 10 8 bed 9 4 error error error error
4stdinbed 0 1 gff 1 1 len=1

Hints · 3

01Hint

The length of a half-open interval is `end - start`; the length of a closed interval is `end - start + 1`. Compute it before converting, not after.

02Hint

Check the two degenerate cases before the general one — `start == end` in BED and `end == start - 1` in GFF both pass the ordinary validity test.

03Hint

An insertion at 0-based point `p` sits between the 1-based bases `p` and `p + 1`.