slow5lib 0.1.0

Rust re-implementation of slow5lib: read and write SLOW5/BLOW5 nanopore sequencing files
Documentation
# slow5lib

[![Crates.io](https://img.shields.io/crates/v/slow5lib.svg)](https://crates.io/crates/slow5lib)
[![docs.rs](https://docs.rs/slow5lib/badge.svg)](https://docs.rs/slow5lib)
[![CI](https://github.com/Psy-Fer/slow5lib-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/Psy-Fer/slow5lib-rs/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![MSRV: 1.87](https://img.shields.io/badge/rustc-1.87+-blue.svg)](https://blog.rust-lang.org/2025/05/15/Rust-1.87.0/)

> **This is an independent Rust re-implementation of [hasindu2008/slow5lib]https://github.com/hasindu2008/slow5lib. It is not a port, not a wrapper, and shares no code with the original C library. The C implementation is the authoritative specification for wire format and behaviour; this crate is designed to be wire-compatible with it.**

Read and write **SLOW5** (text) and **BLOW5** (binary) files containing Oxford Nanopore Technologies sequencing signal data. The library exposes sequential and indexed access patterns with optional parallel decompression via rayon.

**[API reference](https://docs.rs/slow5lib)**

## What are SLOW5 and BLOW5?

SLOW5 and BLOW5 are file formats for storing raw Oxford Nanopore sequencing signal data, developed by [Hasindu Gamaarachchi](https://github.com/hasindu2008) as a simpler, more performant alternative to FAST5/POD5.

- **SLOW5** is a tab-separated text format, human-readable and easy to process with standard tools.
- **BLOW5** is the binary equivalent: same information, significantly smaller files and faster I/O. Records can be block-compressed with zstd or zlib, and the raw signal is compressed with SVB-ZD (StreamVByte + delta + zigzag) or ex-zd (quantize-trailing-shift + zigzag-delta + patched exceptions).

Both formats store one record per sequencing read, with primary fields (`read_id`, `raw_signal`, calibration parameters) and optional auxiliary fields for per-read metadata.

## Installation

```toml
[dependencies]
slow5lib = "0.1"

# optional: parallel decompression via rayon
slow5lib = { version = "0.1", features = ["rayon"] }
```

## Quick start

### Reading a SLOW5 file

```rust,no_run
use slow5lib::{Slow5Reader, Result};

fn main() -> Result<()> {
    let mut reader = Slow5Reader::open("reads.slow5")?;
    for record in reader.records() {
        let rec = record?;
        println!("{}: {} samples", rec.read_id, rec.raw_signal.len());
    }
    Ok(())
}
```

### Reading a BLOW5 file

```rust,no_run
use slow5lib::{Slow5Reader, Result};

fn main() -> Result<()> {
    let mut reader = Slow5Reader::open("reads.blow5")?;
    for record in reader.records() {
        let rec = record?;
        // calibrated picoamperes: (raw + offset) * range / digitisation
        let pa: Vec<f32> = rec.raw_signal.iter()
            .map(|&s| (s as f64 + rec.offset) * rec.range / rec.digitisation)
            .map(|v| v as f32)
            .collect();
        println!("{}: {} samples, first pA = {:.2}", rec.read_id, pa.len(), pa[0]);
    }
    Ok(())
}
```

### Parallel decompression

For high-throughput pipelines, disk I/O stays on one thread while decompression fans out across the rayon pool:

```rust,no_run
use slow5lib::{Slow5Reader, Result};
use rayon::prelude::*;

fn main() -> Result<()> {
    let mut reader = Slow5Reader::open("reads.blow5")?;
    reader.records_raw()
        .par_bridge()
        .map(|r| r?.decompress())
        .for_each(|rec| {
            let rec = rec.unwrap();
            println!("{}: {} samples", rec.read_id, rec.raw_signal.len());
        });
    Ok(())
}
```

Or use the convenience wrapper with the `rayon` feature:

```rust,no_run
# #[cfg(feature = "rayon")]
use rayon::prelude::*;

# #[cfg(feature = "rayon")]
reader.par_records().for_each(|rec| { /* ... */ });
```

### Indexed random access

```rust,no_run
use slow5lib::{Slow5IndexedReader, Result};

fn main() -> Result<()> {
    // Opens the .blow5.idx index (builds it if absent)
    let reader = Slow5IndexedReader::open("reads.blow5")?;

    // Thread-safe: multiple threads can call get() concurrently
    let rec = reader.get("50abece6-9476-46be-a86f-d48cc3fddeb6")?;
    println!("{} samples", rec.raw_signal.len());
    Ok(())
}
```

### Writing a BLOW5 file

```rust,no_run
use slow5lib::{Slow5Writer, Record, Result};
use slow5lib::header::{Header, RecordCompression, SignalCompression, ReadGroup};
use slow5lib::aux::AuxMeta;

fn main() -> Result<()> {
    let header = Header {
        version: (0, 2, 0),
        num_read_groups: 1,
        record_compression: RecordCompression::Zstd,
        signal_compression: SignalCompression::SvbZd,
        read_groups: vec![ReadGroup::default()],
        aux_meta: AuxMeta::default(),
    };

    let mut writer = Slow5Writer::create("out.blow5", header)?;

    let record = Record {
        read_id: "my-read-001".into(),
        read_group: 0,
        digitisation: 2048.0,
        offset: -224.0,
        range: 1463.0,
        sampling_rate: 4000.0,
        raw_signal: vec![512i16, 515, 510, 518, 511],
        aux: Default::default(),
    };

    writer.write(&record)?;
    writer.finish()?;
    Ok(())
}
```

## Compression

| Record compression | Signal compression | Notes |
|---|---|---|
| None | None | Uncompressed; byte-identical to C library |
| None | SVB-ZD | StreamVByte + delta + zigzag; byte-identical to C library |
| None | ex-zd | Quantize-shift + zigzag-delta + patched exceptions; byte-identical to C library |
| Zstd | SVB-ZD | Recommended default; smallest files, fastest random access |
| Zstd | ex-zd | Alternative signal codec; compression relative to SVB-ZD depends on the data |
| Zstd | None | |
| Zlib | SVB-ZD | Compatible with tools that do not support Zstd |
| Zlib | ex-zd | |
| Zlib | None | |

SVB-ZD and ex-zd signal compression use the [`svb`](https://crates.io/crates/svb) crate with SIMD-accelerated encode/decode (AVX2, SSSE3, NEON).

## Design notes

**No mmap.** The library uses `BufReader<File>` for sequential reads and `FileExt::read_at` (pread) for indexed random access. Memory-mapped I/O is deliberately avoided -- it is the main source of poor memory behaviour in C and Python implementations of similar formats.

**Two reader types reflect two access patterns.** `Slow5Reader` is a sequential cursor (not `Sync`). `Slow5IndexedReader` holds a `File` for `read_at` and is `Send + Sync` -- multiple threads can call `get()` concurrently with no locking.

**Decompression is separated from I/O.** `records_raw()` yields `RawRecord` values (compressed bytes, no CPU work). `RawRecord::decompress()` pays the CPU cost. Splitting the two lets rayon's `par_bridge()` decompress in parallel while disk reads stay sequential.

## Known limitations

- **Windows is not in CI.** The indexed reader uses Unix `read_at` (`FileExt::read_at`); a Windows equivalent (`seek_read`) has not been wired in. WSL and Git Bash both expose Unix `read_at` and are unaffected.
- **Parity testing covers one dataset.** Byte-identical and value-level parity is validated against a single author-supplied BLOW5 file and against `slow5tools` committed fixtures; it has not yet been validated across multiple organisms, sequencing platforms, or library preps.

## Validation

Wire-compatibility is validated two ways:

- `tests/parity.rs` compares Rust writer output against reference files committed to the repo, generated with `slow5tools` v1.3.0 (v1.4.0 for the ex-zd fixtures, since ex-zd support was added upstream after v1.3.0).
- `tests/c_parity.rs` runs live against whatever `slow5tools` build is on `PATH` at test time (skips automatically if absent).

If you re-validate against a different upstream version and find a wire-format or behavioural difference, please open an issue -- see [CONTRIBUTING.md](CONTRIBUTING.md) for the policy on re-validating after upstream releases.

## Acknowledgements

SLOW5 and BLOW5 were designed by [Hasindu Gamaarachchi](https://github.com/hasindu2008) and contributors. The original C library ([hasindu2008/slow5lib](https://github.com/hasindu2008/slow5lib)) is the reference implementation and specification for the format. This crate is an independent re-implementation; the C library's behaviour and wire format are authoritative wherever they differ from any written specification.

The original slow5lib is released under the MIT licence. A copy is included in [`LICENSE-slow5lib`](LICENSE-slow5lib).

If you use SLOW5/BLOW5 in your research, please cite:

Gamaarachchi, H., Samarakoon, H., Jenner, S.P. et al. Fast nanopore sequencing data analysis with SLOW5. *Nat Biotechnol* **40**, 1026-1029 (2022). https://doi.org/10.1038/s41587-021-01147-4

Samarakoon, H., Ferguson, J.M., Jenner, S.P. et al. Flexible and efficient handling of nanopore sequencing signal data with slow5tools. *Genome Biol* **24**, 69 (2023). https://doi.org/10.1186/s13059-023-02910-3

## AI assistance

This library was developed with AI assistance (Claude). Architecture decisions, wire-compatibility validation, and algorithm choices are the author's own; AI tooling served as an accelerator over existing skill. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

## MSRV

1.87 (edition 2024).

## License

MIT. See [LICENSE](LICENSE). Copyright 2026 James Ferguson.

The SLOW5/BLOW5 format and original C implementation are copyright Hasindu Gamaarachchi and contributors, MIT licence. See [LICENSE-slow5lib](LICENSE-slow5lib).