Genomic software has a subtle bug class that has haunted bioinformatics for years: coordinate system mismatches. A one-based position from one library silently becomes zero-based in another, shifting every result by a single nucleotide. A Rust developer named Clay McLennan has released omics, a set of crates that attaches the coordinate convention to the type itself, so the compiler catches the mistake instead of a downstream paper retracting a finding.

The problem omics solves

Genomics uses two coordinate systems. Base positions name nucleotides directly (1 through 7 for a seven-nucleotide sequence), which is what humans prefer. Interbase positions name the boundaries between nucleotides (0 through 7 for the same sequence), which is what software prefers because zero-based, half-open intervals make length arithmetic trivial. Both systems exist for good reason. The trouble starts when code passes a value from one system into a function expecting the other.

The bedtools project hit this in issue 311. Intersecting a BED interval with a one-based VCF record at position 32 produced a zero-length interval instead of the correct span. The fix required adding an isZeroBased() method so output code could recover the convention from the model. rust-htslib exposes positions as plain i64, inheriting BAM's zero-based convention but leaving the caller to remember that from documentation. noodles-core uses a Position type that is nonzero and one-based, but its VCF module still needs record-level logic to derive variant endpoints, and breakend coordinates remain untyped strings.

The Rust type system should be able to prevent this class of error. omics does exactly that.

How the type system carries the convention

Position is generic over the coordinate system. Position<Base> and Position<Interbase> both store an integer, but they are different types. Passing one where the other is expected is a compiler error. Position<Base>::try_new(0) returns an error because base position 0 is invalid. Position<Interbase>::new(0) succeeds because the boundary before the first nucleotide is well-defined.

Coordinate<S> adds contig and strand to a Position<S>, keeping the system parameter through the composition. Interval<S> takes two coordinates and verifies that endpoints share a contig, strand, and that they are ordered correctly for the strand's direction. On the positive strand, start cannot exceed end. On the negative strand, the interval runs in the molecule's 5-prime-to-3-prime direction, so the numeric values decrease from start to end.

Converting between systems is explicit. nudge_forward() and nudge_backward() move along the molecule, returning None on overflow or base position 0. The caller makes the scientific decision about which adjacent boundary to select, and the method names make the intent clear.

Performance that does not get in the way

The author benchmarked typed position operations against raw u32 counterparts. Construction, validation, and checked addition all stayed within about 4% of the raw equivalents. The compiler erases Base and Interbase at compile time, so the coordinate system adds no storage to a Position. Interval construction was about 7% slower than a raw-endpoint control, which the author attributes to keeping more intermediate state on the stack and masking the Strand value before branching. A memory inspection during benchmarking caught that intervals were storing contig and strand twice, once in each endpoint, which was fixed before release.

For a library that will sit in hot paths across bioinformatics pipelines, those numbers matter. The overhead is the cost of correctness, and it is small.

What it looks like in practice

The chainfile crate, which translates genomic locations between genome builds, is the first real consumer. UCSC chain files use zero-based, half-open intervals, which map directly to Interval<Interbase>. The crate parses header spans into that type, advances coordinates through aligned blocks and gaps, and accepts the same type in the liftover function. Conversion to another coordinate system happens only at the API boundary where a consumer needs a different representation, not scattered through the calculation.

A second example shows conversion from noodles. The FASTA lookup receives a one-based inclusive interval from noodles_core. The conversion function calls into_equivalent_base(), then orders the endpoints by reference position. The inclusive range is constructed only inside that function, not spread through the liftover code. Each library uses the representation its API expects, and the conversion remains localized.

What comes next

The coordinate model is the foundation. The author has started exploring whether the same approach works for variation: small variants, structural variants, and copy-number variants. A variant's coordinate system must agree with what the alteration does to the reference and alternate sequences, making it a useful test of whether type-level enforcement prevents contradictory descriptions of biological events.

Work on copy-number variants is underway, along with deeper variation normalization and an immutable interval index. None has a release date. The author is soliciting API reviews now, before downstream projects depend on the interfaces. The goal is for omics to become shared infrastructure that the Rust bioinformatics community builds and maintains together.

The project is MIT licensed, requires Rust, and lives at github.com/claymcleod/omics.