use std::collections::BTreeSet;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::sync::{Arc, mpsc};
const BUF_SIZE: usize = 512 * 1024;
use crate::Result;
use crate::error::SlowError;
use crate::header::{Header, RecordCompression, SignalCompression};
use crate::record::Record;
const TYPE_HEADER: &str = "#char*\tuint32_t\tdouble\tdouble\tdouble\tdouble\tuint64_t\tint16_t*\n";
const COL_HEADER: &str = "#read_id\tread_group\tdigitisation\toffset\trange\tsampling_rate\tlen_raw_signal\traw_signal\n";
pub struct Slow5Writer {
state: WriterState,
}
enum WriterState {
Blow5 {
inner: BufWriter<File>,
record_compression: RecordCompression,
signal_compression: SignalCompression,
aux_meta: crate::aux::AuxMeta,
zstd_compressor: Option<zstd::bulk::Compressor<'static>>,
},
Slow5 {
inner: BufWriter<File>,
aux_meta: crate::aux::AuxMeta,
},
}
impl Slow5Writer {
pub fn create(path: impl AsRef<Path>, header: Header) -> Result<Self> {
let path = path.as_ref();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"blow5" => Self::create_blow5(path, header),
"slow5" => Self::create_slow5(path, header),
other => Err(SlowError::InvalidFormat(format!(
"unrecognised extension '.{other}': expected '.blow5' or '.slow5'"
))),
}
}
fn create_blow5(path: &Path, header: Header) -> Result<Self> {
let file = File::create(path)?;
let mut inner = BufWriter::with_capacity(BUF_SIZE, file);
let header_bytes = build_blow5_header(&header)?;
inner.write_all(&header_bytes)?;
let zstd_compressor = if matches!(header.record_compression, RecordCompression::Zstd) {
let c = zstd::bulk::Compressor::new(1)
.map_err(|e| SlowError::Decompression(e.to_string()))?;
Some(c)
} else {
None
};
Ok(Self {
state: WriterState::Blow5 {
inner,
record_compression: header.record_compression,
signal_compression: header.signal_compression,
aux_meta: header.aux_meta,
zstd_compressor,
},
})
}
fn create_slow5(path: &Path, header: Header) -> Result<Self> {
let file = File::create(path)?;
let mut inner = BufWriter::with_capacity(BUF_SIZE, file);
crate::slow5::write_header(&mut inner, &header)?;
Ok(Self {
state: WriterState::Slow5 {
inner,
aux_meta: header.aux_meta,
},
})
}
pub fn write(&mut self, record: &Record) -> Result<()> {
match &mut self.state {
WriterState::Blow5 {
inner,
record_compression,
signal_compression,
aux_meta,
zstd_compressor,
} => {
let record_buf = encode_record(record, *signal_compression, aux_meta)?;
let (size_bytes, payload) = match record_compression {
RecordCompression::None => {
let size = record_buf.len() as u64;
(size.to_le_bytes(), record_buf)
}
RecordCompression::Zstd => {
let compressor = zstd_compressor.as_mut().expect("zstd compressor present");
let compressed = compressor
.compress(&record_buf)
.map_err(|e| SlowError::Decompression(e.to_string()))?;
let size = compressed.len() as u64;
(size.to_le_bytes(), compressed)
}
RecordCompression::Zlib => {
let compressed = crate::compression::compress_record_zlib(&record_buf)?;
let size = compressed.len() as u64;
(size.to_le_bytes(), compressed)
}
};
inner.write_all(&size_bytes)?;
inner.write_all(&payload)?;
Ok(())
}
WriterState::Slow5 { inner, aux_meta } => {
crate::slow5::write_record(inner, record, aux_meta)
}
}
}
#[cfg(feature = "rayon")]
pub fn write_all_par(&mut self, records: &[Record]) -> Result<()> {
use rayon::prelude::*;
if matches!(self.state, WriterState::Slow5 { .. }) {
for record in records {
self.write(record)?;
}
return Ok(());
}
let (rc, sc, am) = match &self.state {
WriterState::Blow5 {
record_compression,
signal_compression,
aux_meta,
..
} => (*record_compression, *signal_compression, aux_meta.clone()),
WriterState::Slow5 { .. } => unreachable!(),
};
let chunks: Vec<Result<(u64, Vec<u8>)>> = records
.par_iter()
.map(|record| {
let record_buf = encode_record(record, sc, &am)?;
compress_chunk(&record_buf, rc)
})
.collect();
let WriterState::Blow5 { inner, .. } = &mut self.state else {
unreachable!()
};
for chunk in chunks {
let (size, payload) = chunk?;
inner.write_all(&size.to_le_bytes())?;
inner.write_all(&payload)?;
}
Ok(())
}
pub fn finish(self) -> Result<()> {
match self.state {
WriterState::Blow5 { mut inner, .. } => {
inner.write_all(crate::blow5::EOF_MARKER)?;
inner.flush().map_err(SlowError::Io)
}
WriterState::Slow5 { mut inner, .. } => inner.flush().map_err(SlowError::Io),
}
}
}
#[derive(Clone)]
pub struct Slow5WriteHandle {
tx: mpsc::SyncSender<(u64, Vec<u8>)>,
record_compression: RecordCompression,
signal_compression: SignalCompression,
aux_meta: Arc<crate::aux::AuxMeta>,
}
impl Slow5WriteHandle {
pub fn write(&self, record: &crate::record::Record) -> Result<()> {
let buf = encode_record(record, self.signal_compression, &self.aux_meta)?;
let (size, payload) = compress_chunk(&buf, self.record_compression)?;
self.tx.send((size, payload)).map_err(|_| {
SlowError::Io(io::Error::new(
io::ErrorKind::BrokenPipe,
"writer thread has exited",
))
})
}
}
pub struct ParallelSlow5Writer {
tx: Option<mpsc::SyncSender<(u64, Vec<u8>)>>,
handle: Option<std::thread::JoinHandle<Result<()>>>,
record_compression: RecordCompression,
signal_compression: SignalCompression,
aux_meta: Arc<crate::aux::AuxMeta>,
}
impl ParallelSlow5Writer {
pub fn create(path: impl AsRef<Path>, header: Header, channel_cap: usize) -> Result<Self> {
let path = path.as_ref();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext != "blow5" {
return Err(SlowError::InvalidFormat(
"ParallelSlow5Writer only supports .blow5 files".into(),
));
}
let file = File::create(path)?;
let mut inner = BufWriter::with_capacity(BUF_SIZE, file);
let header_bytes = build_blow5_header(&header)?;
inner.write_all(&header_bytes)?;
let record_compression = header.record_compression;
let signal_compression = header.signal_compression;
let aux_meta = Arc::new(header.aux_meta);
let (tx, rx) = mpsc::sync_channel::<(u64, Vec<u8>)>(channel_cap);
let handle = std::thread::spawn(move || -> Result<()> {
for (size, payload) in rx {
inner
.write_all(&size.to_le_bytes())
.map_err(SlowError::Io)?;
inner.write_all(&payload).map_err(SlowError::Io)?;
}
inner
.write_all(crate::blow5::EOF_MARKER)
.map_err(SlowError::Io)?;
inner.flush().map_err(SlowError::Io)
});
Ok(Self {
tx: Some(tx),
handle: Some(handle),
record_compression,
signal_compression,
aux_meta,
})
}
pub fn write_handle(&self) -> Slow5WriteHandle {
Slow5WriteHandle {
tx: self.tx.as_ref().expect("writer already finished").clone(),
record_compression: self.record_compression,
signal_compression: self.signal_compression,
aux_meta: Arc::clone(&self.aux_meta),
}
}
pub fn finish(mut self) -> Result<()> {
drop(self.tx.take());
let h = self.handle.take().expect("writer already finished");
match h.join() {
Ok(result) => result,
Err(_) => Err(SlowError::Io(io::Error::other("writer thread panicked"))),
}
}
}
impl Drop for ParallelSlow5Writer {
fn drop(&mut self) {
drop(self.tx.take());
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
fn compress_chunk(buf: &[u8], rc: RecordCompression) -> Result<(u64, Vec<u8>)> {
match rc {
RecordCompression::None => Ok((buf.len() as u64, buf.to_vec())),
RecordCompression::Zstd => {
let c = crate::compression::compress_record_zstd_tl(buf)?;
Ok((c.len() as u64, c))
}
RecordCompression::Zlib => {
let c = crate::compression::compress_record_zlib(buf)?;
Ok((c.len() as u64, c))
}
}
}
fn build_blow5_header(header: &Header) -> Result<Vec<u8>> {
let mut buf: Vec<u8> = Vec::with_capacity(256);
buf.extend_from_slice(crate::blow5::MAGIC);
buf.push(header.version.0);
buf.push(header.version.1);
buf.push(header.version.2);
buf.push(header.record_compression.to_byte());
buf.extend_from_slice(&header.num_read_groups.to_le_bytes());
if (header.version.0, header.version.1) >= (0, 2) {
buf.push(header.signal_compression.to_byte());
}
while buf.len() < 64 {
buf.push(0);
}
let header_size_pos = buf.len(); buf.extend_from_slice(&0u32.to_le_bytes());
let ascii_start = buf.len();
let all_keys: BTreeSet<&str> = header
.read_groups
.iter()
.flat_map(|rg| rg.attributes.keys().map(String::as_str))
.collect();
for key in &all_keys {
buf.push(b'@');
buf.extend_from_slice(key.as_bytes());
for rg in &header.read_groups {
buf.push(b'\t');
let val = rg.attributes.get(*key).map(String::as_str).unwrap_or("");
if val.is_empty() {
buf.push(b'.');
} else {
buf.extend_from_slice(val.as_bytes());
}
}
buf.push(b'\n');
}
buf.extend_from_slice(TYPE_HEADER.trim_end_matches('\n').as_bytes());
for typ in &header.aux_meta.types {
buf.push(b'\t');
buf.extend_from_slice(crate::aux::aux_type_str(typ).as_bytes());
}
buf.push(b'\n');
buf.extend_from_slice(COL_HEADER.trim_end_matches('\n').as_bytes());
for name in &header.aux_meta.names {
buf.push(b'\t');
buf.extend_from_slice(name.as_bytes());
}
buf.push(b'\n');
let ascii_len = (buf.len() - ascii_start) as u32;
buf[header_size_pos..header_size_pos + 4].copy_from_slice(&ascii_len.to_le_bytes());
Ok(buf)
}
fn encode_record(
record: &Record,
signal_compression: SignalCompression,
aux_meta: &crate::aux::AuxMeta,
) -> Result<Vec<u8>> {
let read_id_bytes = record.read_id.as_bytes();
let read_id_len = u16::try_from(read_id_bytes.len()).map_err(|_| {
SlowError::InvalidFormat(format!(
"read_id '{}' is too long ({} bytes, max 65535)",
record.read_id,
read_id_bytes.len()
))
})?;
let mut buf =
Vec::with_capacity(2 + read_id_bytes.len() + 4 + 8 * 5 + 2 * record.raw_signal.len());
buf.extend_from_slice(&read_id_len.to_le_bytes());
buf.extend_from_slice(read_id_bytes);
buf.extend_from_slice(&record.read_group.to_le_bytes());
buf.extend_from_slice(&record.digitisation.to_le_bytes());
buf.extend_from_slice(&record.offset.to_le_bytes());
buf.extend_from_slice(&record.range.to_le_bytes());
buf.extend_from_slice(&record.sampling_rate.to_le_bytes());
match signal_compression {
SignalCompression::None => {
let n_samples = record.raw_signal.len() as u64;
buf.extend_from_slice(&n_samples.to_le_bytes());
for &s in &record.raw_signal {
buf.extend_from_slice(&s.to_le_bytes());
}
}
SignalCompression::SvbZd => {
let compressed = crate::compression::compress_signal_svb_zd(&record.raw_signal)?;
let byte_count = compressed.len() as u64;
buf.extend_from_slice(&byte_count.to_le_bytes());
buf.extend_from_slice(&compressed);
}
SignalCompression::ExZd => {
let compressed = crate::compression::compress_signal_ex_zd(&record.raw_signal)?;
let byte_count = compressed.len() as u64;
buf.extend_from_slice(&byte_count.to_le_bytes());
buf.extend_from_slice(&compressed);
}
}
for (name, typ) in aux_meta.names.iter().zip(aux_meta.types.iter()) {
let val = record
.aux
.get(name)
.unwrap_or(&crate::aux::AuxValue::Missing);
crate::aux::encode_binary(val, typ, &mut buf);
}
Ok(buf)
}