ex-zd Pipeline
The ex-zd pipeline improves on SVB-ZD for 16-bit signal data. It is wire-compatible with the slow5lib SLOW5_COMPRESS_EX_ZD format used in BLOW5 files. The pipeline chains four stages:
raw i16 samples
→ qts (find largest right-shift q ≤ 5 that loses no low bits, apply it)
→ zigzag-delta (delta of differences → zigzag16 to make values unsigned, u16 domain)
→ patched exceptions (values ≤ 255 → literal byte; values > 255 → position + residual,
both StreamVByte-encoded with U32Classic)
→ zstd (outer entropy coding; NOT part of this crate)
Two differences from SVB-ZD:
- qts pre-pass. ADC samples are frequently multiples of a power of two (the low bits carry no information). Shifting them out before delta/zigzag makes the resulting deltas smaller and more compressible. The shift is lossless and reversed on decode.
- Patched/exception encoding instead of a per-value StreamVByte tag. Rather than SVB-ZD's 2-bit-tag-per-value scheme, ex-zd stores most zigzag-delta values as a single literal byte and pulls the rare large values ("exceptions") out into a separate, StreamVByte-encoded side channel. This tends to compress better when most deltas are small and only occasional spikes need the full range.
Unlike encode_vbz/encode_svbzd, the ex-zd frame is self-describing: it embeds a version byte and the sample count, so decode_exzd takes no n parameter.
High-level API
#![allow(unused)] fn main() { use svb::{encode_exzd, decode_exzd}; let samples: Vec<i16> = vec![100, 101, 103, 102, 98]; // Encode: i16 → qts → zigzag-delta → patched(U32Classic) bytes let encoded = encode_exzd(&samples); // Decode: bytes are self-describing (version + sample count embedded) let decoded = decode_exzd(&encoded).unwrap(); assert_eq!(decoded, samples); }
Low-level / into variants
For zero-allocation usage or appending to an existing buffer:
#![allow(unused)] fn main() { use svb::{encode_exzd_into, decode_exzd_into}; let samples: Vec<i16> = vec![100, 101, 103, 102, 98]; let mut buf: Vec<u8> = Vec::new(); encode_exzd_into(&samples, &mut buf); let mut out: Vec<i16> = Vec::new(); decode_exzd_into(&buf, &mut out).unwrap(); assert_eq!(out, samples); }
Composable primitives
The qts and patched/exception stages are exposed independently, matching delta and zigzag:
- [
svb::quantize] —find_qts,apply_shift,unshift_inplace, fixed toi16. - [
svb::patched] —encode_into/decode_intoover&[u16], generically useful for any patched/exception encoding scenario beyond ex-zd.
Wire format
u8 version (0)
u64 nin (sample count, little-endian)
u8 q (qts shift, 0..=5)
u16 zd[0] (first zigzag-delta value, stored raw — no predecessor to patch against)
u32 nex (exception count over the remaining nin-1 values)
if nex > 1:
u32 nex_pos_press_bytes
.. nex_pos_press_bytes (U32Classic-encoded, off-by-one delta-encoded exception positions)
u32 nex_press_bytes
.. nex_press_bytes (U32Classic-encoded exception residuals, value - 256)
elif nex == 1:
u32 position
u32 residual (value - 256)
.. (nin - 1 - nex) literal bytes, one per non-exception value, in stream order
The off-by-one position delta trick (pos[0] raw, pos[i] - pos[i-1] - 1 thereafter) is specific to this exception-position encoding and is not part of the general-purpose svb::delta module — it relies on positions being strictly increasing, which only holds for this use case.