use crate::rad_types::RadIntId;
use std::io::{Read, Write};
pub const UNMAPPED_BC_FORMAT_VERSION: u8 = 1;
#[derive(Debug, Clone)]
pub struct UnmappedBcFormat {
pub field_types: Vec<RadIntId>,
}
impl UnmappedBcFormat {
pub fn single(bc_type: RadIntId) -> Self {
Self {
field_types: vec![bc_type],
}
}
pub fn multi(field_types: Vec<RadIntId>) -> Self {
Self { field_types }
}
pub fn num_fields(&self) -> usize {
self.field_types.len()
}
pub fn record_bytes(&self) -> usize {
self.field_types.iter().map(|t| t.size_of()).sum::<usize>() + std::mem::size_of::<u32>()
}
pub fn write_header<W: Write>(&self, writer: &mut W) -> anyhow::Result<()> {
writer.write_all(&[UNMAPPED_BC_FORMAT_VERSION])?;
writer.write_all(&[self.field_types.len() as u8])?;
for ft in &self.field_types {
let id: u8 = (*ft).into();
writer.write_all(&[id])?;
}
Ok(())
}
pub fn read_header<R: Read>(reader: &mut R) -> anyhow::Result<Option<Self>> {
let mut version_buf = [0u8; 1];
match reader.read_exact(&mut version_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e.into()),
}
let version = version_buf[0];
if version != UNMAPPED_BC_FORMAT_VERSION {
anyhow::bail!(
"Unsupported unmapped BC format version: {} (expected {})",
version,
UNMAPPED_BC_FORMAT_VERSION,
);
}
let mut num_fields_buf = [0u8; 1];
reader.read_exact(&mut num_fields_buf)?;
let num_fields = num_fields_buf[0] as usize;
let mut field_types = Vec::with_capacity(num_fields);
for _ in 0..num_fields {
let mut type_buf = [0u8; 1];
reader.read_exact(&mut type_buf)?;
field_types.push(RadIntId::from(type_buf[0]));
}
Ok(Some(Self { field_types }))
}
}
pub struct UnmappedBcRecordWriter {
format: UnmappedBcFormat,
buf: Vec<u8>,
}
impl UnmappedBcRecordWriter {
pub fn new(format: UnmappedBcFormat) -> Self {
Self {
format,
buf: Vec::with_capacity(4096),
}
}
pub fn write_record(&mut self, barcodes: &[u64], count: u32) {
debug_assert_eq!(barcodes.len(), self.format.num_fields());
for (bc, ft) in barcodes.iter().zip(self.format.field_types.iter()) {
match ft {
RadIntId::U8 => self.buf.extend_from_slice(&(*bc as u8).to_le_bytes()),
RadIntId::U16 => self.buf.extend_from_slice(&(*bc as u16).to_le_bytes()),
RadIntId::U32 => self.buf.extend_from_slice(&(*bc as u32).to_le_bytes()),
RadIntId::U64 => self.buf.extend_from_slice(&bc.to_le_bytes()),
_ => panic!("Unsupported RadIntId for unmapped BC: {:?}", ft),
}
}
self.buf.extend_from_slice(&count.to_le_bytes());
}
pub fn flush_to<W: Write>(&mut self, writer: &mut W) -> anyhow::Result<()> {
if !self.buf.is_empty() {
writer.write_all(&self.buf)?;
self.buf.clear();
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
}
pub struct UnmappedBcRecordReader {
format: UnmappedBcFormat,
record_buf: Vec<u8>,
}
impl UnmappedBcRecordReader {
pub fn new(format: UnmappedBcFormat) -> Self {
let record_bytes = format.record_bytes();
Self {
format,
record_buf: vec![0u8; record_bytes],
}
}
pub fn read_record<R: Read>(
&mut self,
reader: &mut R,
) -> anyhow::Result<Option<(Vec<u64>, u32)>> {
match reader.read_exact(&mut self.record_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e.into()),
}
let mut offset = 0usize;
let mut barcodes = Vec::with_capacity(self.format.num_fields());
for ft in &self.format.field_types {
let val: u64 = match ft {
RadIntId::U8 => self.record_buf[offset] as u64,
RadIntId::U16 => {
u16::from_le_bytes(self.record_buf[offset..offset + 2].try_into().unwrap())
as u64
}
RadIntId::U32 => {
u32::from_le_bytes(self.record_buf[offset..offset + 4].try_into().unwrap())
as u64
}
RadIntId::U64 => {
u64::from_le_bytes(self.record_buf[offset..offset + 8].try_into().unwrap())
}
_ => panic!("Unsupported RadIntId for unmapped BC: {:?}", ft),
};
barcodes.push(val);
offset += ft.size_of();
}
let count = u32::from_le_bytes(self.record_buf[offset..offset + 4].try_into().unwrap());
Ok(Some((barcodes, count)))
}
pub fn format(&self) -> &UnmappedBcFormat {
&self.format
}
}
pub enum CollatedUnmappedCounts {
Single {
counts: std::collections::HashMap<u64, u32>,
bc_type: RadIntId,
},
Multi {
counts: std::collections::HashMap<(u64, u64), u32>,
field_types: Vec<RadIntId>,
},
}
impl CollatedUnmappedCounts {
pub fn new_single(bc_type: RadIntId) -> Self {
CollatedUnmappedCounts::Single {
counts: std::collections::HashMap::new(),
bc_type,
}
}
pub fn new_multi(field_types: Vec<RadIntId>) -> Self {
CollatedUnmappedCounts::Multi {
counts: std::collections::HashMap::new(),
field_types,
}
}
pub fn get_single(&self, cell_bc: u64) -> u32 {
match self {
CollatedUnmappedCounts::Single { counts, .. } => {
counts.get(&cell_bc).copied().unwrap_or(0)
}
CollatedUnmappedCounts::Multi { .. } => 0,
}
}
pub fn get_multi(&self, sample_bc: u64, cell_bc: u64) -> u32 {
match self {
CollatedUnmappedCounts::Multi { counts, .. } => {
counts.get(&(sample_bc, cell_bc)).copied().unwrap_or(0)
}
CollatedUnmappedCounts::Single { counts, .. } => {
counts.get(&cell_bc).copied().unwrap_or(0)
}
}
}
pub fn insert_single(&mut self, cell_bc: u64, count: u32) {
if let CollatedUnmappedCounts::Single { counts, .. } = self {
*counts.entry(cell_bc).or_insert(0) += count;
}
}
pub fn insert_multi(&mut self, sample_bc: u64, cell_bc: u64, count: u32) {
if let CollatedUnmappedCounts::Multi { counts, .. } = self {
*counts.entry((sample_bc, cell_bc)).or_insert(0) += count;
}
}
pub fn is_multi(&self) -> bool {
matches!(self, CollatedUnmappedCounts::Multi { .. })
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> anyhow::Result<()> {
match self {
CollatedUnmappedCounts::Single { counts, bc_type } => {
let fmt = UnmappedBcFormat::single(*bc_type);
fmt.write_header(writer)?;
let mut rec_writer = UnmappedBcRecordWriter::new(fmt);
for (&bc, &count) in counts {
rec_writer.write_record(&[bc], count);
}
rec_writer.flush_to(writer)?;
}
CollatedUnmappedCounts::Multi {
counts,
field_types,
} => {
let fmt = UnmappedBcFormat::multi(field_types.clone());
fmt.write_header(writer)?;
let mut rec_writer = UnmappedBcRecordWriter::new(fmt);
for (&(sample_bc, cell_bc), &count) in counts {
rec_writer.write_record(&[sample_bc, cell_bc], count);
}
rec_writer.flush_to(writer)?;
}
}
Ok(())
}
pub fn read_from<R: Read>(reader: &mut R) -> anyhow::Result<Self> {
let format = match UnmappedBcFormat::read_header(reader)? {
Some(fmt) => fmt,
None => {
return Ok(CollatedUnmappedCounts::new_single(RadIntId::U32));
}
};
let mut rec_reader = UnmappedBcRecordReader::new(format.clone());
if format.num_fields() == 1 {
let mut counts = std::collections::HashMap::new();
while let Some((bcs, count)) = rec_reader.read_record(reader)? {
*counts.entry(bcs[0]).or_insert(0) += count;
}
Ok(CollatedUnmappedCounts::Single {
counts,
bc_type: format.field_types[0],
})
} else {
let mut counts = std::collections::HashMap::new();
while let Some((bcs, count)) = rec_reader.read_record(reader)? {
let sample_bc = bcs[0];
let cell_bc = *bcs.last().unwrap_or(&0);
*counts.entry((sample_bc, cell_bc)).or_insert(0) += count;
}
Ok(CollatedUnmappedCounts::Multi {
counts,
field_types: format.field_types,
})
}
}
pub fn write_to_file(&self, path: &std::path::Path) -> anyhow::Result<()> {
let file = std::fs::File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
self.write_to(&mut writer)
}
pub fn read_from_file(path: &std::path::Path) -> anyhow::Result<Self> {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
Self::read_from(&mut reader)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn single_barcode_roundtrip() {
let fmt = UnmappedBcFormat::single(RadIntId::U32);
assert_eq!(fmt.num_fields(), 1);
assert_eq!(fmt.record_bytes(), 8);
let mut buf = Vec::new();
fmt.write_header(&mut buf).unwrap();
let mut writer = UnmappedBcRecordWriter::new(fmt.clone());
writer.write_record(&[12345], 42);
writer.write_record(&[67890], 7);
writer.flush_to(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf);
let read_fmt = UnmappedBcFormat::read_header(&mut cursor).unwrap().unwrap();
assert_eq!(read_fmt.num_fields(), 1);
let mut reader = UnmappedBcRecordReader::new(read_fmt);
let (bcs, count) = reader.read_record(&mut cursor).unwrap().unwrap();
assert_eq!(bcs, vec![12345]);
assert_eq!(count, 42);
let (bcs, count) = reader.read_record(&mut cursor).unwrap().unwrap();
assert_eq!(bcs, vec![67890]);
assert_eq!(count, 7);
assert!(reader.read_record(&mut cursor).unwrap().is_none());
}
#[test]
fn multi_barcode_roundtrip() {
let fmt = UnmappedBcFormat::multi(vec![RadIntId::U16, RadIntId::U32]);
assert_eq!(fmt.num_fields(), 2);
assert_eq!(fmt.record_bytes(), 10);
let mut buf = Vec::new();
fmt.write_header(&mut buf).unwrap();
let mut writer = UnmappedBcRecordWriter::new(fmt.clone());
writer.write_record(&[0xABCD, 0x12345678], 100);
writer.flush_to(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf);
let read_fmt = UnmappedBcFormat::read_header(&mut cursor).unwrap().unwrap();
assert_eq!(read_fmt.num_fields(), 2);
let mut reader = UnmappedBcRecordReader::new(read_fmt);
let (bcs, count) = reader.read_record(&mut cursor).unwrap().unwrap();
assert_eq!(bcs, vec![0xABCD, 0x12345678]);
assert_eq!(count, 100);
}
#[test]
fn empty_file() {
let cursor = Cursor::new(Vec::<u8>::new());
let result = UnmappedBcFormat::read_header(&mut cursor.clone()).unwrap();
assert!(result.is_none());
}
#[test]
fn collated_single_roundtrip() {
let mut counts = CollatedUnmappedCounts::new_single(RadIntId::U32);
counts.insert_single(100, 5);
counts.insert_single(200, 10);
counts.insert_single(100, 3);
assert_eq!(counts.get_single(100), 8);
assert_eq!(counts.get_single(200), 10);
assert_eq!(counts.get_single(999), 0);
let mut buf = Vec::new();
counts.write_to(&mut buf).unwrap();
let restored = CollatedUnmappedCounts::read_from(&mut Cursor::new(&buf)).unwrap();
assert!(!restored.is_multi());
assert_eq!(restored.get_single(100), 8);
assert_eq!(restored.get_single(200), 10);
}
#[test]
fn collated_multi_roundtrip() {
let mut counts = CollatedUnmappedCounts::new_multi(vec![RadIntId::U16, RadIntId::U32]);
counts.insert_multi(1, 100, 5); counts.insert_multi(2, 100, 10); counts.insert_multi(1, 200, 3);
assert_eq!(counts.get_multi(1, 100), 5);
assert_eq!(counts.get_multi(2, 100), 10);
assert_eq!(counts.get_multi(1, 200), 3);
assert_eq!(counts.get_multi(1, 999), 0);
let mut buf = Vec::new();
counts.write_to(&mut buf).unwrap();
let restored = CollatedUnmappedCounts::read_from(&mut Cursor::new(&buf)).unwrap();
assert!(restored.is_multi());
assert_eq!(restored.get_multi(1, 100), 5);
assert_eq!(restored.get_multi(2, 100), 10);
assert_eq!(restored.get_multi(1, 200), 3);
}
}