Skip to Content
Silent failuresMy intervals are off by one

My intervals are off by one

Your feature boundaries are all one base out. Not randomly — consistently, by exactly one, at every start. Small enough that most downstream numbers barely move, which is why it survives review; large enough to change a codon, miss the first base of an exon, or shift a variant out of the region that was supposed to contain it.

What actually happened

You mixed two coordinate conventions. Both are correct, in their own formats:

FormatCounts fromEndchr1 100 200 means
BED, BAM’s binary encoding, BigWig0excludedbases 101–200, 1-based
GFF, GTF, VCF, SAM’s text records1includedbases 100–200, 1-based

A BED interval 100 200 and a GFF interval 100 200 describe different stretches of sequence: 100 bases starting at the 101st, against 101 bases starting at the 100th. They differ at the start by one and at the end by one.

The convention is a property of the format, not of the file, so it is never written down anywhere in the data.

Why nothing crashed

Both numbers are integers in range. Both intervals are well-formed. Every tool in the chain receives two numbers it can use.

Worse, the error is mostly invisible in aggregate. Shift every exon start by one base and total coverage changes by a fraction of a percent; a differential expression result will move slightly and still look like a differential expression result. The failure announces itself only at the boundary — in the one variant that falls just outside a region, or the one reading frame that comes out shifted.

An off-by-one that is correct 99.9% of the time is harder to find than one that is always wrong.

What Ligase says

DimensionErrorin stepintersectligase check — nothing ran
a.coordinates=0-based half-openfrom BED coordinate convention: crates/ligase-core/tests/fixtures/tiny_bare.bed
b.coordinates=1-based closedfrom GFF coordinate convention: crates/ligase-core/tests/fixtures/tiny_bare.gff
These must match. Did you mean to convert between coordinate systems?

Note what the sources say: BED coordinate convention and GFF coordinate convention. The engine did not find this in the file contents, because it is not in the file contents. It is a property of the format, inferred from it, and therefore not something you can declare incorrectly.

The question at the end is the right one to ask. Unlike a reference mismatch, mixing coordinate systems is often intentional — you do mean to intersect BED regions with a GFF annotation. What you need is a conversion, declared as a step, rather than the two being silently treated as comparable.

How it is caught

coordinates is inferred from the format:

>>> lg.intervals("peaks.bed", organism="human").dimension("coordinates").value '0-based half-open' >>> lg.intervals("genes.gff", organism="human").dimension("coordinates").value '1-based closed'

A conversion is an ordinary typed step. Because it declares what it changes, the graph type-checks afterwards and the conversion is recorded in provenance, which a hand-written awk one-liner in a shell script would not be:

@lg.step(tool="bedtools") def to_closed(regions: lg.Intervals) -> lg.Intervals: """Re-express 0-based half-open intervals as 1-based closed.""" return lg.sh("awk 'BEGIN{OFS=\"\\t\"}{print $1, $2+1, $3}' {regions} > {out}")
Last updated on