gwseq-io 0.2.0

Rust library for processing bigWig, bigBed, BAM and HiC files
Documentation

gwseq-io

Reading and writing bigWig, bigBed, BAM and HiC files, from a local path or over HTTP.

[dependencies]
gwseq-io = "0.2"

Pure Rust, and #![forbid(unsafe_code)]. Compression is zlib-rs through flate2 and HTTP is ureq with rustls, so a build needs a linker and nothing else — no C compiler, no CMake, no system zlib, no OpenSSL.

Reading

The format is sniffed from the file's magic number, not its extension, so open returns a Reader you match on.

use gwseq_io::bbi::{ValuesRequest, Zoom};
use gwseq_io::genomic::Locs;
use gwseq_io::{open, Reader};

# fn main() -> Result<(), Box<dyn std::error::Error>> {
let chr_ids = vec!["chr1".to_string(), "chr2".to_string()];
let starts = vec![1_000_000, 2_000_000];
let ends = vec![1_010_000, 2_010_000];

let Reader::Bbi(bw) = open("track.bigwig", Default::default())? else {
    panic!("not a bigwig")
};

let values = bw.read_values(
    &ValuesRequest::new(Locs::spans(&chr_ids, &starts, &ends)?)
        .bin_size(10.0)
        .zoom(Zoom::Auto),
)?; // Array2<f32>, shape (loci, bins)
# Ok(())
# }

A read takes up to fourteen optional parameters, so each request shape is a builder — one place where every default is written down.

Locs makes the four legal ways to spell a set of loci into four constructors — spans, from_starts, from_ends, centered — so an illegal combination cannot be expressed.

The other readers follow the same shape: BamReader::read_entries and iter_entries, HiCReader::read_values and read_sparse_values.

Writing

bigWig and bigBed, plus streaming converters from bedGraph, WIG and BED.

use gwseq_io::bbi::{BbiKind, BbiWriter, BbiWriterOptions};
use gwseq_io::genomic::ChrMap;

# fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = BbiWriterOptions {
    kind: BbiKind::BigWig,
    chr_sizes: Some(ChrMap::from_entries([("chr1".to_string(), 195_471_971)])),
    ..Default::default()
};

let mut writer = BbiWriter::create("out.bigwig", options)?;
writer.write_values("chr1", 0, 10, &[1.0, 2.0, 3.0])?;
writer.close()?;
# Ok(())
# }

gwseq_io::bbi::convert_to_bigwig and convert_to_bigbed do the same from a text file without holding it in memory: nothing is sorted or spooled, so a conversion of any size holds about a megabyte of input and one open section.

Threads and handles

A reader owns parallel worker threads and one file handle for its lifetime. Reads are positioned and take &self, so one handle serves every worker and there is no pool to check handles out of. close() gives the threads and the handle back and is idempotent; a closed reader fails every read with Error::Closed while the headers it read at open stay available. A reader that is never closed gives everything back when it is dropped.

Errors

One Error enum, and its variants carry their parts rather than a pre-assembled string. Format (this is not a file of that kind) and Corrupt (it is, and its bytes contradict each other) are distinct because only one of them is worth retrying.

Nothing is allocated from a number a file names. Every count, length and offset read out of a file is clamped where it is read, and the block cache every reader sits behind clamps a requested length to what the file can give before it reserves anything — because this library parses hostile input for a living and Rust answers a failed allocation with an abort no Result can carry. Every parser is fuzzed on each cargo test, through both a bare source and the cache.

Features

url (default) HTTP/HTTPS sources via ureq. Off, this crate touches no network stack at all.
npz (default) save_to, writing a HiC read straight to a .npz.

Related

gwseq_io on PyPI is this crate behind a PyO3 extension, with the results handed to numpy without a copy.