use crate::backend::{self, Backend, BackendChoice};
use crate::error::{Error, Result};
use crate::hit::BestHit;
use crate::inter::{self, Layout, LayoutChoice, PackedDb, SimdScratch};
use crate::kernel::{DpBuffers, align_core};
use crate::mode::Mode;
use crate::scoring::Scoring;
use crate::search::SearchType;
use crate::width::ScoreWidth;
#[derive(Debug, Clone)]
pub struct Database {
sequences: Vec<Vec<u8>>,
scoring: Scoring,
mode: Mode,
search_type: SearchType,
max_query_len: usize,
max_target_len: usize,
width: ScoreWidth,
backend: Backend,
packed: Option<PackedDb>,
}
impl Database {
#[must_use]
pub fn builder() -> DatabaseBuilder {
DatabaseBuilder::new()
}
#[must_use]
pub fn scan(&self, scratch: &mut Scratch, query: &[u8]) -> BestHit {
debug_assert!(
query.len() <= self.max_query_len,
"query length {} exceeds declared max_query_len {}",
query.len(),
self.max_query_len
);
if let Some(packed) = &self.packed {
return inter::scan_dispatch(
self.backend,
packed,
&self.sequences,
&self.scoring,
self.mode,
self.search_type,
query,
&mut scratch.simd,
&mut scratch.buf,
);
}
let mut best_score = i32::MIN;
let mut best_index = 0usize;
let mut best_query_end = None;
let mut best_target_end = None;
for (index, seq) in self.sequences.iter().enumerate() {
let (score, query_end, target_end) =
align_core(query, seq, &self.scoring, self.mode, &mut scratch.buf);
if score > best_score {
best_score = score;
best_index = index;
best_query_end = query_end;
best_target_end = target_end;
}
}
let (query_end, target_end) = if self.search_type.tracks_end() {
(best_query_end, best_target_end)
} else {
(None, None)
};
BestHit {
score: best_score,
db_index: best_index,
query_end,
target_end,
}
}
#[must_use]
pub fn sequence_count(&self) -> usize {
self.sequences.len()
}
#[must_use]
pub fn mode(&self) -> Mode {
self.mode
}
#[must_use]
pub fn search_type(&self) -> SearchType {
self.search_type
}
#[must_use]
pub fn scoring(&self) -> &Scoring {
&self.scoring
}
#[must_use]
pub fn score_width(&self) -> ScoreWidth {
self.width
}
#[must_use]
pub fn backend(&self) -> Backend {
self.backend
}
#[must_use]
pub fn layout(&self) -> Option<Layout> {
self.packed.as_ref().map(PackedDb::layout)
}
#[must_use]
pub fn max_query_len(&self) -> usize {
self.max_query_len
}
#[must_use]
pub fn max_target_len(&self) -> usize {
self.max_target_len
}
}
#[derive(Debug, Clone, Default)]
pub struct DatabaseBuilder {
sequences: Option<Vec<Vec<u8>>>,
scoring: Option<Scoring>,
mode: Option<Mode>,
search_type: Option<SearchType>,
max_query_len: Option<usize>,
backend_choice: Option<BackendChoice>,
layout_choice: Option<LayoutChoice>,
}
impl DatabaseBuilder {
fn new() -> Self {
DatabaseBuilder::default()
}
#[must_use]
pub fn sequences<S: AsRef<[u8]>>(mut self, sequences: &[S]) -> Self {
self.sequences = Some(sequences.iter().map(|s| s.as_ref().to_vec()).collect());
self
}
#[must_use]
pub fn scoring(mut self, scoring: Scoring) -> Self {
self.scoring = Some(scoring);
self
}
#[must_use]
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = Some(mode);
self
}
#[must_use]
pub fn search_type(mut self, search_type: SearchType) -> Self {
self.search_type = Some(search_type);
self
}
#[must_use]
pub fn max_query_len(mut self, max_query_len: usize) -> Self {
self.max_query_len = Some(max_query_len);
self
}
#[must_use]
pub fn backend(mut self, choice: BackendChoice) -> Self {
self.backend_choice = Some(choice);
self
}
#[must_use]
pub fn layout(mut self, choice: LayoutChoice) -> Self {
self.layout_choice = Some(choice);
self
}
pub fn build(self) -> Result<Database> {
let sequences = self
.sequences
.ok_or(Error::IncompleteBuilder { field: "sequences" })?;
let scoring = self
.scoring
.ok_or(Error::IncompleteBuilder { field: "scoring" })?;
let mode = self
.mode
.ok_or(Error::IncompleteBuilder { field: "mode" })?;
let max_query_len = self.max_query_len.ok_or(Error::IncompleteBuilder {
field: "max_query_len",
})?;
let search_type = self.search_type.unwrap_or(SearchType::Score);
if sequences.is_empty() {
return Err(Error::EmptyDatabase);
}
let alphabet_len = scoring.alphabet_len();
for seq in &sequences {
for &sym in seq {
if sym as usize >= alphabet_len {
return Err(Error::SymbolOutOfRange {
symbol: sym as usize,
alphabet_len,
});
}
}
}
let max_target_len = sequences.iter().map(Vec::len).max().unwrap_or(0);
let width = scoring.required_width(mode, max_query_len, max_target_len)?;
let choice = match self.backend_choice {
Some(choice) => choice,
None => backend::choice_from_env()?.unwrap_or(BackendChoice::Auto),
};
let resolved = backend::resolve(choice)?;
let applicable = crate::inter::kernel_applies(width, scoring.alphabet_len());
let backend = if resolved == Backend::Scalar || applicable {
resolved
} else {
match choice {
BackendChoice::Force(_) => {
return Err(Error::BackendUnavailable { backend: resolved });
}
BackendChoice::Auto => Backend::Scalar,
}
};
let layout_choice = self.layout_choice.unwrap_or_default();
let packed = backend.simd_lanes().map(|lanes| {
let layout = inter::choose_layout(&sequences, lanes, alphabet_len, layout_choice);
PackedDb::build(&sequences, lanes, layout, &scoring)
});
Ok(Database {
sequences,
scoring,
mode,
search_type,
max_query_len,
max_target_len,
width,
backend,
packed,
})
}
}
#[derive(Debug)]
pub struct Scratch {
buf: DpBuffers,
simd: SimdScratch,
}
impl Scratch {
#[must_use]
pub fn new(db: &Database) -> Self {
let simd = match db.backend().simd_lanes() {
Some(lanes) => SimdScratch::new(db.max_target_len(), lanes),
None => SimdScratch::empty(),
};
Scratch {
buf: DpBuffers::with_capacity(db.max_query_len(), db.max_target_len()),
simd,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dna_scoring() -> Scoring {
Scoring::new(
4,
vec![
2, -1, -1, -1, -1, 2, -1, -1, -1, -1, 2, -1, -1, -1, -1, 2,
],
2,
1,
)
.unwrap()
}
fn db_with(seqs: &[Vec<u8>], mode: Mode, st: SearchType) -> Database {
Database::builder()
.sequences(seqs)
.scoring(dna_scoring())
.mode(mode)
.search_type(st)
.max_query_len(16)
.build()
.unwrap()
}
#[test]
fn database_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Database>();
}
#[test]
fn build_reports_every_missing_required_field() {
let base = || Database::builder();
assert_eq!(
base().build().unwrap_err(),
Error::IncompleteBuilder { field: "sequences" }
);
assert_eq!(
base().sequences(&[vec![0u8]]).build().unwrap_err(),
Error::IncompleteBuilder { field: "scoring" }
);
assert_eq!(
base()
.sequences(&[vec![0u8]])
.scoring(dna_scoring())
.build()
.unwrap_err(),
Error::IncompleteBuilder { field: "mode" }
);
assert_eq!(
base()
.sequences(&[vec![0u8]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.build()
.unwrap_err(),
Error::IncompleteBuilder {
field: "max_query_len"
}
);
}
#[test]
fn empty_database_is_rejected() {
let empty: [Vec<u8>; 0] = [];
let err = Database::builder()
.sequences(&empty)
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.build()
.unwrap_err();
assert_eq!(err, Error::EmptyDatabase);
}
#[test]
fn out_of_range_symbol_in_a_sequence_is_rejected() {
let err = Database::builder()
.sequences(&[vec![0u8, 1], vec![2u8, 9]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.build()
.unwrap_err();
assert_eq!(
err,
Error::SymbolOutOfRange {
symbol: 9,
alphabet_len: 4
}
);
}
#[test]
fn search_type_defaults_to_score() {
let db = Database::builder()
.sequences(&[vec![0u8]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.build()
.unwrap();
assert_eq!(db.search_type(), SearchType::Score);
}
#[test]
fn accessors_reflect_construction() {
let db = db_with(
&[vec![0u8, 1, 2, 3], vec![2u8, 2]],
Mode::Hw,
SearchType::ScoreEnd,
);
assert_eq!(db.sequence_count(), 2);
assert_eq!(db.mode(), Mode::Hw);
assert_eq!(db.search_type(), SearchType::ScoreEnd);
assert_eq!(db.max_query_len(), 16);
assert_eq!(db.max_target_len(), 4);
assert!(db.backend().is_available());
assert_eq!(db.score_width(), ScoreWidth::I8);
}
#[test]
fn forcing_scalar_backend_builds_and_reports_scalar() {
let db = Database::builder()
.sequences(&[vec![0u8]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.backend(BackendChoice::Force(Backend::Scalar))
.build()
.unwrap();
assert_eq!(db.backend(), Backend::Scalar);
}
#[test]
fn forcing_a_backend_tracks_availability() {
for b in [Backend::Sse41, Backend::Avx2, Backend::Neon] {
let result = Database::builder()
.sequences(&[vec![0u8]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.backend(BackendChoice::Force(b))
.build();
if b.is_available() {
assert_eq!(result.unwrap().backend(), b);
} else {
assert_eq!(
result.unwrap_err(),
Error::BackendUnavailable { backend: b }
);
}
}
}
#[test]
fn forcing_simd_on_an_ineligible_database_errors() {
let scoring = Scoring::new(2, vec![100, -100, -100, 100], 2, 1).unwrap();
let long = vec![0u8; 200];
let result = Database::builder()
.sequences(&[long])
.scoring(scoring)
.mode(Mode::Nw)
.max_query_len(200)
.backend(BackendChoice::Force(Backend::Sse41))
.build();
assert!(matches!(result, Err(Error::BackendUnavailable { .. })));
}
#[test]
fn layout_is_reported_and_overridable() {
use crate::LayoutChoice;
let scalar = Database::builder()
.sequences(&[vec![0u8, 1, 2, 3]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.backend(BackendChoice::Force(Backend::Scalar))
.build()
.unwrap();
assert_eq!(
scalar.layout(),
None,
"scalar backend does not pack the database"
);
let build = |b: Backend, choice: Option<LayoutChoice>| {
let mut builder = Database::builder()
.sequences(&[vec![0u8, 1, 2, 3], vec![2u8, 2]])
.scoring(dna_scoring())
.mode(Mode::Ov)
.max_query_len(8)
.backend(BackendChoice::Force(b));
if let Some(c) = choice {
builder = builder.layout(c);
}
builder.build().unwrap()
};
for b in [Backend::Sse41, Backend::Avx2, Backend::Neon] {
if !b.is_available() {
continue;
}
assert_eq!(
build(b, None).layout(),
Some(Layout::Precomputed),
"{b} auto"
);
for layout in [Layout::Gathered, Layout::Precomputed] {
let db = build(b, Some(LayoutChoice::Force(layout)));
assert_eq!(db.layout(), Some(layout), "{b} forced {layout}");
}
}
}
#[test]
fn explicit_scalar_choice_overrides_auto_detection() {
let db = Database::builder()
.sequences(&[vec![0u8]])
.scoring(dna_scoring())
.mode(Mode::Sw)
.max_query_len(8)
.backend(BackendChoice::Force(Backend::Scalar))
.build()
.unwrap();
assert_eq!(db.backend(), Backend::Scalar);
}
#[test]
fn scan_picks_the_best_scoring_sequence() {
let db = db_with(
&[vec![2u8, 2, 2, 2], vec![0u8, 1, 2, 3]],
Mode::Sw,
SearchType::ScoreEnd,
);
let mut scratch = Scratch::new(&db);
let hit = db.scan(&mut scratch, &[0u8, 1, 2, 3]);
assert_eq!(hit.db_index, 1);
assert_eq!(hit.score, 8);
assert_eq!((hit.query_end, hit.target_end), (Some(3), Some(3)));
}
#[test]
fn tie_break_prefers_smallest_database_index() {
let db = db_with(
&[vec![0u8, 1, 2, 3], vec![0u8, 1, 2, 3], vec![3u8]],
Mode::Sw,
SearchType::ScoreEnd,
);
let mut scratch = Scratch::new(&db);
let hit = db.scan(&mut scratch, &[0u8, 1, 2, 3]);
assert_eq!(hit.db_index, 0);
assert_eq!(hit.score, 8);
}
#[test]
fn score_search_type_suppresses_ends_in_scan() {
let db = db_with(&[vec![0u8, 1, 2, 3]], Mode::Sw, SearchType::Score);
let mut scratch = Scratch::new(&db);
let hit = db.scan(&mut scratch, &[0u8, 1, 2, 3]);
assert_eq!(hit.score, 8);
assert_eq!((hit.query_end, hit.target_end), (None, None));
}
#[test]
fn scratch_reuse_across_many_scans_is_consistent() {
let db = db_with(
&[vec![0u8, 1], vec![0u8, 1, 2, 3, 3, 2, 1, 0], vec![2u8]],
Mode::Nw,
SearchType::ScoreEnd,
);
let mut scratch = Scratch::new(&db);
let queries: [&[u8]; 4] = [
&[0, 1, 2, 3],
&[2],
&[0, 1, 2, 3, 3, 2, 1, 0],
&[0, 1, 2, 3],
];
let mut first_repeat = None;
for q in queries {
let hit = db.scan(&mut scratch, q);
let mut fresh = Scratch::new(&db);
let hit_fresh = db.scan(&mut fresh, q);
assert_eq!(hit, hit_fresh, "reused vs fresh scratch differ for {q:?}");
if q == [0u8, 1, 2, 3].as_slice() {
match first_repeat {
None => first_repeat = Some(hit),
Some(prev) => {
assert_eq!(prev, hit, "same query gave different result on reuse")
}
}
}
}
}
}