#![allow(unsafe_code)]
#[cfg(target_arch = "x86_64")]
mod avx2;
mod band;
mod fill;
mod lanes;
#[cfg(target_arch = "aarch64")]
mod neon;
mod profile;
#[cfg(target_arch = "x86_64")]
mod sse41;
pub use band::BandConfig;
use crate::align::backtrack::CellRead;
use crate::align::sisd::{ScalarInit, NEG_INF};
use crate::align::{Alignment, AlignmentEngine, AlignmentType, Scoring, SisdEngine};
use crate::graph::Graph;
use band::BandState;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
struct StripedBuffers<V> {
profile: Vec<V>,
h: Vec<V>,
e: Vec<V>,
f: Vec<V>,
o: Vec<V>,
q: Vec<V>,
masks: Vec<V>,
penalties: Vec<V>,
penalties_c: Vec<V>,
cached_elem_width: usize,
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
impl<V> Default for StripedBuffers<V> {
fn default() -> StripedBuffers<V> {
StripedBuffers {
profile: Vec::new(),
h: Vec::new(),
e: Vec::new(),
f: Vec::new(),
o: Vec::new(),
q: Vec::new(),
masks: Vec::new(),
penalties: Vec::new(),
penalties_c: Vec::new(),
cached_elem_width: 0,
}
}
}
#[derive(Default)]
enum StripedScratch {
#[cfg(target_arch = "x86_64")]
Sse41(StripedBuffers<core::arch::x86_64::__m128i>),
#[cfg(target_arch = "x86_64")]
Avx2(StripedBuffers<core::arch::x86_64::__m256i>),
#[cfg(target_arch = "aarch64")]
NeonI16(StripedBuffers<core::arch::aarch64::int16x8_t>),
#[cfg(target_arch = "aarch64")]
NeonI32(StripedBuffers<core::arch::aarch64::int32x4_t>),
#[default]
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Escalation {
Int16,
Int32,
Fallback,
}
fn escalate(scoring: &Scoring, seq_len: usize, node_count: usize) -> Escalation {
let worst_case = scoring.worst_case_alignment_score(seq_len as i64 + 8, node_count as i64);
if worst_case < i64::from(i32::MIN) + 1024 {
Escalation::Fallback
} else if worst_case < i64::from(i16::MIN) + 1024 {
Escalation::Int32
} else {
Escalation::Int16
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Isa {
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
Avx2,
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
Sse41,
#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
Neon,
None,
}
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
const FORCE_ISA_ENV: &str = "SPOARS_FORCE_ISA";
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
fn should_force_sse41(value: Option<&str>) -> bool {
value.is_some_and(|raw| raw.eq_ignore_ascii_case("sse41"))
}
fn detect_isa() -> Isa {
#[cfg(target_arch = "x86_64")]
{
let force_sse41 = should_force_sse41(std::env::var(FORCE_ISA_ENV).ok().as_deref());
if is_x86_feature_detected!("avx2") && !force_sse41 {
return Isa::Avx2;
}
if is_x86_feature_detected!("sse4.1") {
return Isa::Sse41;
}
}
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
return Isa::Neon;
}
}
Isa::None
}
pub struct SimdEngine {
#[cfg_attr(
not(any(target_arch = "x86_64", target_arch = "aarch64")),
allow(dead_code)
)]
alignment_type: AlignmentType,
scoring: Scoring,
inner: SisdEngine,
#[cfg_attr(
not(any(target_arch = "x86_64", target_arch = "aarch64")),
allow(dead_code)
)]
scratch: ScalarInit,
#[cfg_attr(
not(any(target_arch = "x86_64", target_arch = "aarch64")),
allow(dead_code)
)]
striped: StripedScratch,
band: Option<BandConfig>,
}
impl SimdEngine {
pub fn new(alignment_type: AlignmentType, scoring: Scoring) -> SimdEngine {
SimdEngine {
alignment_type,
scoring,
inner: SisdEngine::new(alignment_type, scoring),
scratch: ScalarInit::default(),
striped: StripedScratch::None,
band: None,
}
}
pub fn banded(alignment_type: AlignmentType, scoring: Scoring, band: BandConfig) -> SimdEngine {
let mut engine = SimdEngine::new(alignment_type, scoring);
engine.band = Some(band);
engine
}
pub fn alignment_type(&self) -> AlignmentType {
self.alignment_type
}
pub fn scoring(&self) -> Scoring {
self.scoring
}
#[cfg(target_arch = "x86_64")]
fn sse41_scratch(
&mut self,
) -> (
&mut ScalarInit,
&mut StripedBuffers<core::arch::x86_64::__m128i>,
) {
if !matches!(self.striped, StripedScratch::Sse41(_)) {
self.striped = StripedScratch::Sse41(StripedBuffers::default());
}
let striped = match &mut self.striped {
StripedScratch::Sse41(buffers) => buffers,
_ => unreachable!("just ensured the Sse41 variant"),
};
(&mut self.scratch, striped)
}
#[cfg(target_arch = "x86_64")]
fn avx2_scratch(
&mut self,
) -> (
&mut ScalarInit,
&mut StripedBuffers<core::arch::x86_64::__m256i>,
) {
if !matches!(self.striped, StripedScratch::Avx2(_)) {
self.striped = StripedScratch::Avx2(StripedBuffers::default());
}
let striped = match &mut self.striped {
StripedScratch::Avx2(buffers) => buffers,
_ => unreachable!("just ensured the Avx2 variant"),
};
(&mut self.scratch, striped)
}
#[cfg(target_arch = "aarch64")]
fn neon_i16_scratch(
&mut self,
) -> (
&mut ScalarInit,
&mut StripedBuffers<core::arch::aarch64::int16x8_t>,
) {
if !matches!(self.striped, StripedScratch::NeonI16(_)) {
self.striped = StripedScratch::NeonI16(StripedBuffers::default());
}
let striped = match &mut self.striped {
StripedScratch::NeonI16(buffers) => buffers,
_ => unreachable!("just ensured the NeonI16 variant"),
};
(&mut self.scratch, striped)
}
#[cfg(target_arch = "aarch64")]
fn neon_i32_scratch(
&mut self,
) -> (
&mut ScalarInit,
&mut StripedBuffers<core::arch::aarch64::int32x4_t>,
) {
if !matches!(self.striped, StripedScratch::NeonI32(_)) {
self.striped = StripedScratch::NeonI32(StripedBuffers::default());
}
let striped = match &mut self.striped {
StripedScratch::NeonI32(buffers) => buffers,
_ => unreachable!("just ensured the NeonI32 variant"),
};
(&mut self.scratch, striped)
}
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
#[inline(always)]
fn align_simd_linear<S>(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
seeded: &mut ScalarInit,
striped: &mut StripedBuffers<S::Vec>,
band: Option<&mut BandState>,
) -> (Alignment, i32)
where
S: lanes::Simd,
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
use crate::align::backtrack::backtrack_linear_impl;
use crate::align::sisd::reseed_scalar_buffers;
use profile::{build_masks, build_penalties, build_profile, ElemFromI32};
reseed_scalar_buffers(seeded, alignment_type, scoring, seq, graph);
build_profile::<S>(&mut striped.profile, graph, seq, scoring);
let elem_width = core::mem::size_of::<S::Elem>();
if striped.cached_elem_width != elem_width {
striped.masks = build_masks::<S>(S::NEG_INF);
striped.penalties = build_penalties::<S>(S::Elem::from_i32(i32::from(scoring.g)));
striped.cached_elem_width = elem_width;
}
let is_banded = band.is_some();
let (max_i, max_j, max_score) = fill::fill_linear::<S>(
graph,
seq.len(),
scoring,
alignment_type,
seeded,
&striped.profile,
&striped.masks,
&striped.penalties,
&mut striped.h,
band,
);
if is_banded && max_score == NEG_INF {
return (Alignment::new(), NEG_INF);
}
let matrix_width_vecs = seq.len().div_ceil(S::LANES);
let h_view = StripedView::<S> {
boundary: &seeded.h,
striped: &striped.h,
width_scalar: seeded.matrix_width,
width_vecs: matrix_width_vecs,
};
let alignment = backtrack_linear_impl(
graph,
&seeded.node_id_to_rank,
&seeded.sequence_profile,
&h_view,
seeded.matrix_width,
alignment_type,
&scoring,
max_i,
max_j,
max_score,
);
(alignment, max_score)
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
#[inline(always)]
fn align_simd_affine<S>(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
seeded: &mut ScalarInit,
striped: &mut StripedBuffers<S::Vec>,
band: Option<&mut BandState>,
) -> (Alignment, i32)
where
S: lanes::Simd,
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
use crate::align::backtrack::backtrack_affine_impl;
use crate::align::sisd::reseed_scalar_buffers;
use profile::{build_masks, build_penalties, build_profile, ElemFromI32};
reseed_scalar_buffers(seeded, alignment_type, scoring, seq, graph);
build_profile::<S>(&mut striped.profile, graph, seq, scoring);
let elem_width = core::mem::size_of::<S::Elem>();
if striped.cached_elem_width != elem_width {
striped.masks = build_masks::<S>(S::NEG_INF);
striped.penalties = build_penalties::<S>(S::Elem::from_i32(i32::from(scoring.e)));
striped.cached_elem_width = elem_width;
}
let is_banded = band.is_some();
let (max_i, max_j, max_score) = fill::fill_affine::<S>(
graph,
seq.len(),
scoring,
alignment_type,
seeded,
&striped.profile,
&striped.masks,
&striped.penalties,
&mut striped.h,
&mut striped.e,
&mut striped.f,
band,
);
if is_banded && max_score == NEG_INF {
return (Alignment::new(), NEG_INF);
}
let matrix_width_vecs = seq.len().div_ceil(S::LANES);
let width_scalar = seeded.matrix_width;
let h_view = StripedView::<S> {
boundary: &seeded.h,
striped: &striped.h,
width_scalar,
width_vecs: matrix_width_vecs,
};
let e_view = StripedView::<S> {
boundary: &seeded.e,
striped: &striped.e,
width_scalar,
width_vecs: matrix_width_vecs,
};
let f_view = StripedView::<S> {
boundary: &seeded.f,
striped: &striped.f,
width_scalar,
width_vecs: matrix_width_vecs,
};
let alignment = backtrack_affine_impl(
graph,
&seeded.node_id_to_rank,
&seeded.sequence_profile,
&h_view,
&e_view,
&f_view,
seeded.matrix_width,
alignment_type,
&scoring,
max_i,
max_j,
max_score,
);
(alignment, max_score)
}
struct StripedView<'a, S: lanes::Simd>
where
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
boundary: &'a [i32],
striped: &'a [S::Vec],
width_scalar: usize,
width_vecs: usize,
}
impl<S: lanes::Simd> CellRead for StripedView<'_, S>
where
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
#[inline(always)]
fn get(&self, i: usize, j: usize) -> i32 {
if i == 0 || j == 0 {
return self.boundary[i * self.width_scalar + j];
}
let lanes = <S as lanes::Simd>::LANES;
let pos = j - 1; let seg = pos / lanes;
let lane = pos % lanes;
debug_assert!(lanes <= 32);
let mut buf = [<S::Elem as profile::ElemFromI32>::from_i32(0); 32];
<S as lanes::Simd>::storeu(self.striped[i * self.width_vecs + seg], &mut buf[..lanes]);
<S::Elem as profile::ElemToI32>::to_i32(buf[lane])
}
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
#[inline(always)]
fn align_simd_convex<S>(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
seeded: &mut ScalarInit,
striped: &mut StripedBuffers<S::Vec>,
band: Option<&mut BandState>,
) -> (Alignment, i32)
where
S: lanes::Simd,
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
use crate::align::backtrack::backtrack_convex_impl;
use crate::align::sisd::reseed_scalar_buffers;
use profile::{build_masks, build_penalties, build_profile, ElemFromI32};
reseed_scalar_buffers(seeded, alignment_type, scoring, seq, graph);
build_profile::<S>(&mut striped.profile, graph, seq, scoring);
let elem_width = core::mem::size_of::<S::Elem>();
if striped.cached_elem_width != elem_width {
striped.masks = build_masks::<S>(S::NEG_INF);
striped.penalties = build_penalties::<S>(S::Elem::from_i32(i32::from(scoring.e)));
striped.penalties_c = build_penalties::<S>(S::Elem::from_i32(i32::from(scoring.c)));
striped.cached_elem_width = elem_width;
}
let is_banded = band.is_some();
let (max_i, max_j, max_score) = fill::fill_convex::<S>(
graph,
seq.len(),
scoring,
alignment_type,
seeded,
&striped.profile,
&striped.masks,
&striped.penalties,
&striped.penalties_c,
&mut striped.h,
&mut striped.e,
&mut striped.f,
&mut striped.o,
&mut striped.q,
band,
);
if is_banded && max_score == NEG_INF {
return (Alignment::new(), NEG_INF);
}
let matrix_width_vecs = seq.len().div_ceil(S::LANES);
let width_scalar = seeded.matrix_width;
let h_view = StripedView::<S> {
boundary: &seeded.h,
striped: &striped.h,
width_scalar,
width_vecs: matrix_width_vecs,
};
let e_view = StripedView::<S> {
boundary: &seeded.e,
striped: &striped.e,
width_scalar,
width_vecs: matrix_width_vecs,
};
let f_view = StripedView::<S> {
boundary: &seeded.f,
striped: &striped.f,
width_scalar,
width_vecs: matrix_width_vecs,
};
let o_view = StripedView::<S> {
boundary: &seeded.o,
striped: &striped.o,
width_scalar,
width_vecs: matrix_width_vecs,
};
let q_view = StripedView::<S> {
boundary: &seeded.q,
striped: &striped.q,
width_scalar,
width_vecs: matrix_width_vecs,
};
let alignment = backtrack_convex_impl(
graph,
&seeded.node_id_to_rank,
&seeded.sequence_profile,
&h_view,
&e_view,
&f_view,
&o_view,
&q_view,
seeded.matrix_width,
alignment_type,
&scoring,
max_i,
max_j,
max_score,
);
(alignment, max_score)
}
macro_rules! define_simd_runners {
($arch:literal, $feature:literal, $linear:ident, $affine:ident, $convex:ident) => {
#[cfg(target_arch = $arch)]
#[target_feature(enable = $feature)]
unsafe fn $linear<S>(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
seeded: &mut ScalarInit,
striped: &mut StripedBuffers<S::Vec>,
band: Option<&mut BandState>,
) -> (Alignment, i32)
where
S: lanes::Simd,
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
align_simd_linear::<S>(alignment_type, scoring, seq, graph, seeded, striped, band)
}
#[cfg(target_arch = $arch)]
#[target_feature(enable = $feature)]
unsafe fn $affine<S>(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
seeded: &mut ScalarInit,
striped: &mut StripedBuffers<S::Vec>,
band: Option<&mut BandState>,
) -> (Alignment, i32)
where
S: lanes::Simd,
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
align_simd_affine::<S>(alignment_type, scoring, seq, graph, seeded, striped, band)
}
#[cfg(target_arch = $arch)]
#[target_feature(enable = $feature)]
unsafe fn $convex<S>(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
seeded: &mut ScalarInit,
striped: &mut StripedBuffers<S::Vec>,
band: Option<&mut BandState>,
) -> (Alignment, i32)
where
S: lanes::Simd,
S::Elem: profile::ElemFromI32 + profile::ElemToI32,
{
align_simd_convex::<S>(alignment_type, scoring, seq, graph, seeded, striped, band)
}
};
}
define_simd_runners!(
"x86_64",
"avx2",
run_avx2_linear,
run_avx2_affine,
run_avx2_convex
);
define_simd_runners!(
"x86_64",
"sse4.1",
run_sse41_linear,
run_sse41_affine,
run_sse41_convex
);
define_simd_runners!(
"aarch64",
"neon",
run_neon_linear,
run_neon_affine,
run_neon_convex
);
impl AlignmentEngine for SimdEngine {
fn align(&mut self, seq: &[u8], graph: &Graph) -> (Alignment, i32) {
if graph.nodes.is_empty() || seq.is_empty() {
return (Alignment::new(), 0);
}
let escalation = escalate(&self.scoring, seq.len(), graph.nodes.len());
let isa = detect_isa();
let alignment_type = self.alignment_type;
let scoring = self.scoring;
let mut band_state = self.band.map(|cfg| {
let mut node_id_to_rank = vec![0u32; graph.nodes.len()];
for (rank, &nid) in graph.rank_to_node.iter().enumerate() {
node_id_to_rank[nid.0 as usize] = rank as u32;
}
BandState::new(graph, &node_id_to_rank, seq.len(), cfg)
});
match escalation {
Escalation::Fallback => self.inner.align(seq, graph),
Escalation::Int32 => match isa {
Isa::Avx2 => {
#[cfg(target_arch = "x86_64")]
{
use crate::align::GapMode;
use avx2::Avx2I32;
let (scratch, striped) = self.avx2_scratch();
match scoring.gap_mode() {
GapMode::Linear => unsafe {
run_avx2_linear::<Avx2I32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Affine => unsafe {
run_avx2_affine::<Avx2I32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Convex => unsafe {
run_avx2_convex::<Avx2I32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
}
}
#[cfg(not(target_arch = "x86_64"))]
{
self.inner.align(seq, graph)
}
}
Isa::Sse41 => {
#[cfg(target_arch = "x86_64")]
{
use crate::align::GapMode;
use sse41::Sse41I32;
let (scratch, striped) = self.sse41_scratch();
match scoring.gap_mode() {
GapMode::Linear => unsafe {
run_sse41_linear::<Sse41I32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Affine => unsafe {
run_sse41_affine::<Sse41I32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Convex => unsafe {
run_sse41_convex::<Sse41I32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
}
}
#[cfg(not(target_arch = "x86_64"))]
{
self.inner.align(seq, graph)
}
}
Isa::Neon => {
#[cfg(target_arch = "aarch64")]
{
use crate::align::GapMode;
use neon::NeonI32;
let (scratch, striped) = self.neon_i32_scratch();
match scoring.gap_mode() {
GapMode::Linear => unsafe {
run_neon_linear::<NeonI32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Affine => unsafe {
run_neon_affine::<NeonI32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Convex => unsafe {
run_neon_convex::<NeonI32>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
}
}
#[cfg(not(target_arch = "aarch64"))]
{
self.inner.align(seq, graph)
}
}
Isa::None => self.inner.align(seq, graph),
},
Escalation::Int16 => match isa {
Isa::Avx2 => {
#[cfg(target_arch = "x86_64")]
{
use crate::align::GapMode;
use avx2::Avx2I16;
let (scratch, striped) = self.avx2_scratch();
match scoring.gap_mode() {
GapMode::Linear => unsafe {
run_avx2_linear::<Avx2I16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Affine => unsafe {
run_avx2_affine::<Avx2I16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Convex => unsafe {
run_avx2_convex::<Avx2I16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
}
}
#[cfg(not(target_arch = "x86_64"))]
{
self.inner.align(seq, graph)
}
}
Isa::Sse41 => {
#[cfg(target_arch = "x86_64")]
{
use crate::align::GapMode;
use sse41::Sse41I16;
let (scratch, striped) = self.sse41_scratch();
match scoring.gap_mode() {
GapMode::Linear => unsafe {
run_sse41_linear::<Sse41I16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Affine => unsafe {
run_sse41_affine::<Sse41I16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Convex => unsafe {
run_sse41_convex::<Sse41I16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
}
}
#[cfg(not(target_arch = "x86_64"))]
{
self.inner.align(seq, graph)
}
}
Isa::Neon => {
#[cfg(target_arch = "aarch64")]
{
use crate::align::GapMode;
use neon::NeonI16;
let (scratch, striped) = self.neon_i16_scratch();
match scoring.gap_mode() {
GapMode::Linear => unsafe {
run_neon_linear::<NeonI16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Affine => unsafe {
run_neon_affine::<NeonI16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
GapMode::Convex => unsafe {
run_neon_convex::<NeonI16>(
alignment_type,
scoring,
seq,
graph,
scratch,
striped,
band_state.as_mut(),
)
},
}
}
#[cfg(not(target_arch = "aarch64"))]
{
self.inner.align(seq, graph)
}
}
Isa::None => self.inner.align(seq, graph),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
fn linear_graph(seed: &[u8]) -> Graph {
let mut graph = Graph::new();
graph.add_alignment_weight(&[], seed, 1).unwrap();
graph
}
fn assert_matches_sisd(
alignment_type: AlignmentType,
scoring: Scoring,
seq: &[u8],
graph: &Graph,
) {
let mut simd_engine = SimdEngine::new(alignment_type, scoring);
let mut sisd_engine = SisdEngine::new(alignment_type, scoring);
let simd_result = simd_engine.align(seq, graph);
let sisd_result = sisd_engine.align(seq, graph);
assert_eq!(simd_result, sisd_result);
}
#[test]
fn simd_engine_matches_sisd_engine_on_tiny_input() {
let alignment_type = AlignmentType::Global;
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
let seq = b"ACGT";
let graph = Graph::new();
let mut simd_engine = SimdEngine::new(alignment_type, scoring);
let mut sisd_engine = SisdEngine::new(alignment_type, scoring);
let simd_result = simd_engine.align(seq, &graph);
let sisd_result = sisd_engine.align(seq, &graph);
assert_eq!(simd_result, sisd_result);
}
#[test]
fn simd_engine_matches_sisd_engine_with_default_scoring() {
let graph = linear_graph(b"ACGTACGTAC");
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
for alignment_type in [
AlignmentType::Local,
AlignmentType::Global,
AlignmentType::Overlap,
] {
assert_matches_sisd(alignment_type, scoring, b"ACGTTCGTAC", &graph);
}
}
#[test]
fn simd_engine_matches_sisd_engine_with_mid_range_affine_scoring() {
let graph = linear_graph(b"GATTACAGATTACA");
let scoring = Scoring::new(3, -3, -5, -2, -5, -2).unwrap();
assert_matches_sisd(AlignmentType::Local, scoring, b"GATTACAGATTAA", &graph);
}
#[test]
fn simd_engine_matches_sisd_engine_when_escalation_forces_int32_branch() {
let seq_len = 300usize;
let node_count = 300usize;
let scoring = Scoring::new(127, -128, -128, -128, -128, -128).unwrap();
assert_eq!(escalate(&scoring, seq_len, node_count), Escalation::Int32);
let seed = vec![b'A'; node_count];
let graph = linear_graph(&seed);
let seq = vec![b'C'; seq_len];
assert_matches_sisd(AlignmentType::Global, scoring, &seq, &graph);
}
#[test]
fn escalation_predicate_selects_fallback_before_i32_overflow() {
let scoring = Scoring::new(127, -128, -128, -128, -128, -128).unwrap();
let huge_node_count = 20_000_000usize;
let huge_seq_len = 20_000_000usize;
assert_eq!(
escalate(&scoring, huge_seq_len, huge_node_count),
Escalation::Fallback
);
}
#[test]
fn empty_graph_returns_empty_alignment_and_zero_score() {
let alignment_type = AlignmentType::Global;
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
let graph = Graph::new();
let mut engine = SimdEngine::new(alignment_type, scoring);
let (alignment, score) = engine.align(b"ACGT", &graph);
assert_eq!(alignment, Vec::new());
assert_eq!(score, 0);
}
#[test]
fn empty_sequence_returns_empty_alignment_and_zero_score() {
let alignment_type = AlignmentType::Global;
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
let graph = linear_graph(b"ACGT");
let mut engine = SimdEngine::new(alignment_type, scoring);
let (alignment, score) = engine.align(b"", &graph);
assert_eq!(alignment, Vec::new());
assert_eq!(score, 0);
}
#[test]
fn detect_isa_returns_a_defined_variant_and_align_still_matches_sisd() {
let isa = detect_isa();
assert!(matches!(
isa,
Isa::Avx2 | Isa::Sse41 | Isa::Neon | Isa::None
));
let graph = linear_graph(b"ACGTACGTAC");
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
assert_matches_sisd(AlignmentType::Global, scoring, b"ACGTTCGTAC", &graph);
}
#[test]
fn should_force_sse41_only_matches_the_sse41_token() {
assert!(should_force_sse41(Some("sse41")));
assert!(should_force_sse41(Some("SSE41")));
assert!(should_force_sse41(Some("Sse41")));
assert!(!should_force_sse41(None));
assert!(!should_force_sse41(Some("")));
assert!(!should_force_sse41(Some("avx2")));
assert!(!should_force_sse41(Some("sse4.1")));
assert!(!should_force_sse41(Some("neon")));
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
#[test]
fn banded_global_guard_returns_empty_alignment_and_neg_inf() {
#[cfg(target_arch = "aarch64")]
type TestSimd = crate::align::simd::neon::NeonI16;
#[cfg(target_arch = "x86_64")]
type TestSimd = crate::align::simd::sse41::Sse41I16;
let seq = b"ACGTTGCAGATCCGTAAGCTTACGGATCAGTTCAGGATCACGTTGCAA";
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
let graph = linear_graph(b"ACGT");
let n = graph.num_nodes();
let mut seeded = ScalarInit::default();
let mut striped =
StripedBuffers::<<TestSimd as crate::align::simd::lanes::Simd>::Vec>::default();
let mut band = BandState {
r: vec![seq.len() as u32; n],
best_col: vec![0; n],
w: 1,
};
let (alignment, score) = align_simd_linear::<TestSimd>(
AlignmentType::Global,
scoring,
seq,
&graph,
&mut seeded,
&mut striped,
Some(&mut band),
);
assert!(
alignment.is_empty(),
"guard must return the empty alignment"
);
assert_eq!(
score, NEG_INF,
"guard must return the NEG_INF sentinel score"
);
}
#[test]
fn banded_engine_matches_exact_on_near_identical_family() {
let alignment_type = AlignmentType::Global;
let scoring = Scoring::spoa_default(); let family: [&[u8]; 4] = [
b"ACGTACGTACGTACGTACGT",
b"ACGTACGTATGTACGTACGT", b"ACGTACGTACGTACCTACGT", b"ACGTACGAACGTACGTACGT", ];
let mut graph = Graph::new();
let mut builder = SimdEngine::new(alignment_type, scoring);
for read in family {
crate::align::align_and_add(&mut graph, &mut builder, read, 1).unwrap();
}
let mut exact = SimdEngine::new(alignment_type, scoring);
let mut banded = SimdEngine::banded(alignment_type, scoring, BandConfig::default());
for read in family {
assert_eq!(
banded.align(read, &graph),
exact.align(read, &graph),
"banded must equal exact for in-band near-identical read {read:?}"
);
}
}
#[test]
fn banded_engine_documents_large_indel_miss() {
let alignment_type = AlignmentType::Global;
let scoring = Scoring::spoa_default();
let backbone: Vec<u8> = b"ACGT".iter().cycle().take(120).copied().collect();
let graph = linear_graph(&backbone);
let insertion = b"GGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCC"; let mut query = Vec::with_capacity(backbone.len() + insertion.len());
query.extend_from_slice(&backbone[..60]);
query.extend_from_slice(insertion);
query.extend_from_slice(&backbone[60..]);
let mut exact = SimdEngine::new(alignment_type, scoring);
let mut banded =
SimdEngine::banded(alignment_type, scoring, BandConfig { base: 2, frac: 0.0 });
let (_exact_alignment, exact_score) = exact.align(&query, &graph);
let (banded_alignment, banded_score) = banded.align(&query, &graph);
assert_ne!(
banded_score, exact_score,
"a w=2 band must miss the wide-indel optimum (documented heuristic miss)"
);
assert!(
banded_score <= exact_score,
"a banded search is a subset of the exact search, so it can never beat it"
);
for &(node_idx, query_idx) in &banded_alignment {
assert!(
node_idx == -1 || (node_idx >= 0 && (node_idx as usize) < graph.num_nodes()),
"node index {node_idx} out of range"
);
assert!(
query_idx == -1 || (query_idx >= 0 && (query_idx as usize) < query.len()),
"query index {query_idx} out of range"
);
}
}
#[test]
fn simd_engine_exposes_its_alignment_type_and_scoring() {
let s = Scoring::spoa_default();
let e = SimdEngine::new(AlignmentType::Overlap, s);
assert_eq!(e.alignment_type(), AlignmentType::Overlap);
assert_eq!(e.scoring(), s);
}
}