# gwseq-io
Reading and writing bigWig, bigBed, BAM and HiC files, from a local path or
over HTTP.
```toml
[dependencies]
gwseq-io = "0.2"
```
Pure Rust, and `#![forbid(unsafe_code)]`. Compression is
[zlib-rs](https://crates.io/crates/zlib-rs) through `flate2` and HTTP is
[ureq](https://crates.io/crates/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.
```rust,no_run
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.
```rust,no_run
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.