#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(unreachable_pub)]
#![warn(rust_2018_idioms)]
use std::borrow::Borrow;
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Cursor, ErrorKind::InvalidData, Read};
use std::iter::zip;
use std::mem::MaybeUninit;
use std::mem::size_of;
use std::ops::Range;
use std::path::Path;
use std::sync::OnceLock;
mod decode;
use decode::{NUCS_PER_U8, decode};
mod prefetch;
use prefetch::PrefetchBatcher;
#[cfg(feature = "python")]
mod python;
use byteorder::{BigEndian, ByteOrder, LittleEndian, ReadBytesExt};
use memmap2::{Mmap, MmapOptions};
use seq_macro::seq;
#[cfg(target_endian = "big")]
compile_error!("twobitreader is not yet implemented for big-endian targets.");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Endianness {
Big,
Little,
}
#[derive(Debug)]
pub struct TwobitReader {
file: File, mmap: Mmap, masked: bool, endianness: Endianness, seqs: Vec<TwobitSequence>, seq_by_name: HashMap<String, usize>, }
#[derive(Debug)]
struct TwobitSequence {
data: OnceLock<TwobitSequenceData>, name: String, data_offset: u64, }
#[derive(Debug)]
pub(crate) struct TwobitSequenceData {
dna_offset: usize, dna_bytes: usize, dna_len: usize, nblocks: Blocks, masks: Blocks, }
#[derive(Debug)]
struct Blocks {
starts: Vec<u32>,
ends: Vec<u32>,
}
impl TwobitReader {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
Self::open_impl(path, false)
}
pub fn open_masked<P: AsRef<Path>>(path: P) -> io::Result<Self> {
Self::open_impl(path, true)
}
fn open_impl<P: AsRef<Path>>(path: P, masked: bool) -> io::Result<Self> {
let file = File::open(path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
let endianness = read_endianness(&mmap)?;
let seqs = read_seqs(&mmap, endianness)?;
let seq_by_name = HashMap::from_iter(seqs.iter().enumerate().map(|(i, seq)| (seq.name.clone(), i)));
if seq_by_name.len() != seqs.len() {
return Err(io::Error::new(InvalidData, "duplicate sequence name detected."));
}
Ok(Self { file, mmap, masked, endianness, seqs, seq_by_name })
}
pub fn num_seqs(&self) -> usize {
self.seqs.len()
}
pub fn iter_names(&self) -> impl Iterator<Item = &str> + '_ {
self.seqs.iter().map(|seq| seq.name.as_str())
}
pub fn names(&self) -> Vec<&str> {
self.iter_names().collect()
}
pub fn contains_name<N: AsRef<str>>(&self, name: N) -> bool {
self.seq_by_name.contains_key(name.as_ref())
}
pub fn seq_len<N: AsRef<str>>(&self, name: N) -> usize {
self.get_seq_data_by_name(name).dna_len
}
pub fn get<N: AsRef<str>>(&self, name: N, start: usize, end: usize) -> String {
let mut dst = String::new();
self.get_into(name, start, end, &mut dst);
dst
}
pub fn get_into<N: AsRef<str>>(&self, name: N, start: usize, end: usize, dst: &mut String) {
let seq = self.get_seq_data_by_name(name);
check_range(seq, start, end);
dst.clear();
let buf = unsafe { dst.as_mut_vec() };
buf.reserve_exact(end - start);
decode_and_append(&self.mmap, seq, start, end, buf);
}
pub fn get_batch<'a, N, T, I>(&'a self, batch: I) -> impl Iterator<Item = String> + 'a
where
N: AsRef<str>,
T: Borrow<(N, usize, usize)>,
I: IntoIterator<Item = T>,
I::IntoIter: 'a,
{
batch.into_iter().map(|item| {
let (chrom, start, end) = item.borrow();
self.get(chrom, *start, *end)
})
}
pub fn get_inclusive<N: AsRef<str>>(&self, name: N, start: usize, end: usize) -> String {
check_start_inclusive(start, 1);
self.get(name, start - 1, end)
}
pub fn get_inclusive_into<N: AsRef<str>>(&self, name: N, start: usize, end: usize, dst: &mut String) {
check_start_inclusive(start, 1);
self.get_into(name, start - 1, end, dst);
}
pub fn get_batch_inclusive<'a, N, T, I>(&'a self, batch: I) -> impl Iterator<Item = String> + 'a
where
N: AsRef<str>,
T: Borrow<(N, usize, usize)>,
I: IntoIterator<Item = T>,
I::IntoIter: 'a,
{
batch.into_iter().map(|item| {
let (chrom, start, end) = item.borrow();
self.get_inclusive(chrom, *start, *end)
})
}
pub fn concat<N, R>(&self, name: N, ranges: R) -> String
where
N: AsRef<str>,
R: AsRef<[(usize, usize)]>,
{
self.concat_impl(name, ranges, 0)
}
fn concat_impl<N, R>(&self, name: N, ranges: R, base: usize) -> String
where
N: AsRef<str>,
R: AsRef<[(usize, usize)]>,
{
let seq = self.get_seq_data_by_name(name);
let ranges = ranges.as_ref();
let total_len = ranges.iter().fold(0, |total, &(start, end)| {
check_start_inclusive(start, base); check_range(seq, start - base, end);
total + end - (start - base)
});
let mut buf = Vec::<u8>::new();
if buf.try_reserve_exact(total_len).is_err() {
panic!("could not allocate {total_len} bytes for the concatenated sequence");
}
for &(start, end) in ranges.iter() {
decode_and_append(&self.mmap, seq, start - base, end, &mut buf);
}
debug_assert!(buf.is_ascii(), "decoded nucleotides were not valid ascii");
unsafe { String::from_utf8_unchecked(buf) }
}
pub fn concat_iter<N, I>(&self, name: N, ranges: I) -> String
where
N: AsRef<str>,
I: IntoIterator<Item = (usize, usize)>,
{
let seq = self.get_seq_data_by_name(name);
let mut buf = Vec::<u8>::new();
for (start, end) in ranges.into_iter() {
check_range(seq, start, end);
if buf.try_reserve(end - start).is_err() {
panic!("could not allocate {} bytes for the concatenated sequence", buf.len() + end - start);
}
decode_and_append(&self.mmap, seq, start, end, &mut buf);
}
debug_assert!(buf.is_ascii(), "decoded nucleotides were not valid ascii");
unsafe { String::from_utf8_unchecked(buf) }
}
pub fn concat_inclusive<N, R>(&self, name: N, ranges: R) -> String
where
N: AsRef<str>,
R: AsRef<[(usize, usize)]>,
{
self.concat_impl(name, ranges, 1)
}
pub fn concat_iter_inclusive<N, I>(&self, name: N, ranges: I) -> String
where
N: AsRef<str>,
I: IntoIterator<Item = (usize, usize)>,
{
self.concat_iter(
name,
ranges.into_iter().map(|(start, end)| {
check_start_inclusive(start, 1);
(start - 1, end)
}),
)
}
pub fn prefetch<N, T, I>(&self, batch: I)
where
N: AsRef<str>,
T: Borrow<(N, usize, usize)>,
I: IntoIterator<Item = T>,
{
self.prefetch_impl(batch, 0)
}
fn prefetch_impl<N, T, I>(&self, batch: I, base: usize)
where
N: AsRef<str>,
T: Borrow<(N, usize, usize)>,
I: IntoIterator<Item = T>,
{
let mut ranges_by_seq: Vec<Vec<Range<usize>>> = self.seqs.iter().map(|_| Vec::new()).collect();
for item in batch.into_iter() {
let (name, start, end) = item.borrow();
let seq_index = self.seq_by_name.get(name.as_ref()).expect("sequence name not found");
check_start_inclusive(*start, base);
let start = *start - base;
assert!(start <= *end, "invalid range (start > end)");
ranges_by_seq[*seq_index].push(start..*end);
}
let mut prefetch = PrefetchBatcher::new(&self.file, &self.mmap);
const BLOCKS_PREFETCH_BYTES: usize = 4 * 1024;
for (seq, ranges) in zip(self.seqs.iter(), ranges_by_seq.iter()) {
if !ranges.is_empty() {
let data_offset = seq.data_offset as usize;
prefetch.push(data_offset..data_offset + BLOCKS_PREFETCH_BYTES);
}
}
prefetch.flush();
for (seq_index, mut ranges) in ranges_by_seq.into_iter().enumerate() {
if ranges.is_empty() {
continue;
}
const AGGREGATION_GAP_THRESHOLD: usize = NUCS_PER_U8 * 16 * 1024;
ranges.sort_unstable_by_key(|range| range.start);
let mut i = 0;
for j in 1..ranges.len() {
if ranges[i].end.saturating_add(AGGREGATION_GAP_THRESHOLD) >= ranges[j].start {
ranges[i].end = ranges[i].end.max(ranges[j].end);
} else {
i += 1;
ranges[i] = ranges[j].clone();
}
}
ranges.truncate(i + 1);
let seq = self.get_seq_data_by_index(seq_index);
for range in ranges.iter_mut() {
if range.start < range.end {
let first_byte = seq.dna_offset + range.start / NUCS_PER_U8;
let last_byte = seq.dna_offset + range.end.div_ceil(NUCS_PER_U8);
prefetch.push(first_byte..last_byte);
}
}
}
prefetch.flush();
}
pub fn prefetch_inclusive<N, T, I>(&self, batch: I)
where
N: AsRef<str>,
T: Borrow<(N, usize, usize)>,
I: IntoIterator<Item = T>,
{
self.prefetch_impl(batch, 1)
}
fn get_seq_data_by_name<N: AsRef<str>>(&self, name: N) -> &TwobitSequenceData {
self.try_get_seq_data_by_name(name).expect("sequence name not found")
}
fn try_get_seq_data_by_name<N: AsRef<str>>(&self, name: N) -> Option<&TwobitSequenceData> {
let index = *self.seq_by_name.get(name.as_ref())?;
Some(self.get_seq_data_by_index(index))
}
fn get_seq_data_by_index(&self, index: usize) -> &TwobitSequenceData {
let seq = &self.seqs[index];
seq.data.get_or_init(|| read_seq_data(&self.mmap, self.masked, self.endianness, seq.data_offset))
}
}
#[inline]
fn check_range(seq: &TwobitSequenceData, start: usize, end: usize) {
if start > end {
panic!("invalid range (start > end)");
}
if end > seq.dna_len {
panic!("invalid end (end > dna_len)");
}
}
#[inline]
fn check_start_inclusive(start: usize, base: usize) {
if start < base {
debug_assert_eq!(base, 1, "expected base=1 but found {base}");
panic!("invalid start (0) for 1-based range");
}
}
fn read_endianness(mmap: &Mmap) -> io::Result<Endianness> {
const SIGNATURE_LILEND: u32 = 0x1A412743;
const SIGNATURE_BIGEND: u32 = 0x4327411A;
let signature = Cursor::new(mmap).read_u32::<LittleEndian>()?;
match signature {
SIGNATURE_LILEND => Ok(Endianness::Little),
SIGNATURE_BIGEND => Ok(Endianness::Big),
_ => Err(io::Error::new(InvalidData, "failed to read a valid 2bit file signature.")),
}
}
fn read_seqs(mmap: &Mmap, endianness: Endianness) -> io::Result<Vec<TwobitSequence>> {
match endianness {
Endianness::Little => read_seqs_endian::<LittleEndian>(mmap),
Endianness::Big => read_seqs_endian::<BigEndian>(mmap),
}
}
fn read_seqs_endian<B: ByteOrder>(mmap: &Mmap) -> io::Result<Vec<TwobitSequence>> {
let mut cursor = Cursor::new(mmap);
cursor.set_position(size_of::<u32>() as u64);
let version = cursor.read_u32::<B>()?;
if version != 0 {
return Err(io::Error::new(InvalidData, "file version not recognized."));
}
let num_seqs = cursor.read_u32::<B>()? as usize;
let _reserved = cursor.read_u32::<B>()?;
const MIN_BYTES_PER_SEQ: usize = size_of::<u8>() + 1 + size_of::<u32>();
let remaining_bytes = cursor.get_ref().len() - cursor.position() as usize;
if num_seqs > remaining_bytes / MIN_BYTES_PER_SEQ {
return Err(io::Error::new(InvalidData, "sequence count exceeds file size; file may be malformed."));
}
let mut seqs = Vec::with_capacity(num_seqs);
for _ in 0..num_seqs {
let name_size = cursor.read_u8()? as usize;
let mut name_bytes = vec![0u8; name_size];
cursor.read_exact(&mut name_bytes)?;
let name = match String::from_utf8(name_bytes) {
Ok(name) => name,
Err(_) => return Err(io::Error::new(InvalidData, "failed reading sequence name as utf8.")),
};
let data_offset = cursor.read_u32::<B>()? as u64;
let curr_offset = cursor.position();
if data_offset <= curr_offset {
return Err(io::Error::new(InvalidData, "invalid data offset; file may be malformed."));
}
let data = OnceLock::new();
seqs.push(TwobitSequence { data, name, data_offset });
}
Ok(seqs)
}
fn read_seq_data(mmap: &Mmap, masked: bool, endianness: Endianness, data_offset: u64) -> TwobitSequenceData {
match endianness {
Endianness::Little => read_seq_data_endian::<LittleEndian>(mmap, masked, data_offset),
Endianness::Big => read_seq_data_endian::<BigEndian>(mmap, masked, data_offset),
}
}
fn read_seq_data_endian<B: ByteOrder>(mmap: &Mmap, masked: bool, data_offset: u64) -> TwobitSequenceData {
let mut cursor = Cursor::new(mmap);
cursor.set_position(data_offset);
let dna_len = cursor.read_u32::<B>().expect("Failed to read DNA length from 2bit file.") as usize;
let nblocks = read_blocks::<B>(&mut cursor, true);
let masks = read_blocks::<B>(&mut cursor, masked);
cursor.set_position(cursor.position() + size_of::<u32>() as u64);
check_blocks(&nblocks, dna_len);
check_blocks(&masks, dna_len);
let dna_offset = cursor.position() as usize;
let dna_bytes = dna_len.div_ceil(NUCS_PER_U8);
assert!(
dna_offset + dna_bytes <= cursor.get_ref().len(),
"Failed to read DNA from 2bit file. DNA data was truncated."
);
TwobitSequenceData { dna_offset, dna_bytes, dna_len, nblocks, masks }
}
fn read_blocks<B: ByteOrder>(cursor: &mut Cursor<&Mmap>, used: bool) -> Blocks {
let num_blocks = cursor.read_u32::<B>().expect("Failed to read number of blocks from 2bit file.") as usize;
let mut starts = Vec::new();
let mut ends = Vec::new();
let block_bytes = 2 * size_of::<u32>();
let remaining_bytes = cursor.get_ref().len() - cursor.position() as usize;
assert!(num_blocks <= remaining_bytes / block_bytes, "num_blocks too large, may be corrupt");
if used {
starts.resize(num_blocks, 0);
cursor.read_u32_into::<B>(&mut starts).expect("Failed to read block starts from 2bit file.");
ends.resize(num_blocks, 0);
cursor.read_u32_into::<B>(&mut ends).expect("Failed to read block ends from 2bit file.");
for (start, end) in zip(&starts, &mut ends) {
*end += *start;
}
} else {
cursor.set_position(cursor.position() + (num_blocks * block_bytes) as u64);
}
Blocks { starts, ends }
}
#[cfg(debug_assertions)]
fn check_blocks(blocks: &Blocks, dna_len: usize) {
assert!(blocks.starts.is_sorted(), "block start indices are not sorted; file may be malformed.");
assert!(blocks.ends.is_sorted(), "block end indices are not sorted; file may be malformed.");
for (start, end) in zip(&blocks.starts, &blocks.ends) {
assert!(start < end, "block start >= end; file may be malformed.");
assert!(*start as usize <= dna_len, "block start beyond DNA extents; file may be malformed.");
assert!(*end as usize <= dna_len, "block end beyond DNA extents; file may be malformed.");
}
}
#[cfg(not(debug_assertions))]
fn check_blocks(_blocks: &Blocks, _dna_len: usize) {}
fn decode_and_append(mmap: &Mmap, seq: &TwobitSequenceData, start: usize, end: usize, dst: &mut Vec<u8>) {
if start >= end {
return;
}
debug_assert!(dst.capacity() - dst.len() >= end - start);
let range_len = end - start;
let buf_uninit = &mut dst.spare_capacity_mut()[..range_len];
#[cfg(debug_assertions)]
buf_uninit.fill(MaybeUninit::new(0xfe));
decode_from_mmap(mmap, seq, start, buf_uninit);
let old_len = dst.len();
unsafe {
dst.set_len(old_len + range_len);
}
let buf_init = &mut dst[old_len..];
fill_blocks(seq, start, buf_init);
debug_assert!(buf_init.is_ascii());
}
pub(crate) fn decode_from_mmap(mmap: &Mmap, seq: &TwobitSequenceData, start: usize, dst: &mut [MaybeUninit<u8>]) {
let dna = &mmap[seq.dna_offset..seq.dna_offset + seq.dna_bytes];
decode(start, dna, dst);
}
pub(crate) fn fill_blocks(seq: &TwobitSequenceData, start: usize, dst: &mut [u8]) {
search_blocks(&seq.nblocks, start, dst, nblock_fill);
search_blocks(&seq.masks, start, dst, mask_fill);
}
pub(crate) fn search_blocks<F>(blocks: &Blocks, start: usize, dst: &mut [u8], f: F)
where
F: Fn(&mut [u8]),
{
let i = blocks.ends.partition_point(|&x| x <= start as u32);
let end = start + dst.len();
for (block_start, block_end) in zip(&blocks.starts[i..], &blocks.ends[i..]) {
let block_start = (*block_start as usize).max(start);
let block_end = (*block_end as usize).min(end);
if block_start >= block_end {
break;
}
f(&mut dst[block_start - start..block_end - start]);
}
}
#[inline]
fn nblock_fill(dna: &mut [u8]) {
dna.fill(b'N');
}
#[inline]
fn mask_fill(dna: &mut [u8]) {
for nuc in dna {
debug_assert!(*nuc >= b'A' && *nuc <= b'Z');
*nuc += b'a' - b'A';
}
}
const NUC_COMPLEMENT_U8: [u8; 256] = seq!(i in 0..256 {[#(
match i {
b'A' => b'T', b'a' => b't',
b'C' => b'G', b'c' => b'g',
b'G' => b'C', b'g' => b'c',
b'T' => b'A', b't' => b'a',
b'N' => b'N', b'n' => b'n',
_ => b'?',
},
)*]});
pub fn reverse_complement(mut seq: String) -> String {
if !seq.is_empty() {
let buf = unsafe { seq.as_mut_vec() };
buf.reverse();
for byte in buf {
*byte = NUC_COMPLEMENT_U8[*byte as usize];
}
}
debug_assert!(!seq.contains('?'), "invalid character in DNA string.");
seq
}