use crate::error::{Error, Result};
use crate::mode::Mode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ScoreWidth {
I8,
I16,
I32,
}
impl ScoreWidth {
const ORDER: [ScoreWidth; 3] = [ScoreWidth::I8, ScoreWidth::I16, ScoreWidth::I32];
#[must_use]
pub const fn max_abs(self) -> i64 {
match self {
ScoreWidth::I8 => i8::MAX as i64,
ScoreWidth::I16 => i16::MAX as i64,
ScoreWidth::I32 => i32::MAX as i64,
}
}
#[must_use]
pub const fn bytes(self) -> usize {
match self {
ScoreWidth::I8 => 1,
ScoreWidth::I16 => 2,
ScoreWidth::I32 => 4,
}
}
#[must_use]
pub fn narrowest_for(magnitude: i64) -> Option<ScoreWidth> {
Self::ORDER.into_iter().find(|w| magnitude <= w.max_abs())
}
}
impl core::fmt::Display for ScoreWidth {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
ScoreWidth::I8 => "i8",
ScoreWidth::I16 => "i16",
ScoreWidth::I32 => "i32",
};
f.write_str(s)
}
}
fn magnitude_bound(
mode: Mode,
min_entry: i32,
max_entry: i32,
gap_open: i32,
gap_ext: i32,
max_query_len: usize,
max_target_len: usize,
) -> i128 {
let m = max_query_len as i128;
let n = max_target_len as i128;
let min_ij = m.min(n);
let max_pos = (max_entry as i128).max(0); let max_neg = (-(min_entry as i128)).max(0); let go = gap_open as i128;
let ge = gap_ext as i128;
let positive = min_ij * max_pos;
let negative = match mode {
Mode::Sw => go,
Mode::Ov => min_ij * max_neg + go,
Mode::Nw | Mode::Hw => (m + n) * max_neg + go + (m + n - 1).max(0) * ge,
};
positive.max(negative)
}
pub fn required_width(
mode: Mode,
min_entry: i32,
max_entry: i32,
gap_open: i32,
gap_ext: i32,
max_query_len: usize,
max_target_len: usize,
) -> Result<ScoreWidth> {
let bound = magnitude_bound(
mode,
min_entry,
max_entry,
gap_open,
gap_ext,
max_query_len,
max_target_len,
);
let bound_i64 = bound.min(i64::MAX as i128) as i64;
ScoreWidth::narrowest_for(bound_i64).ok_or(Error::ScoreRangeTooWide { bound: bound_i64 })
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_MODES: [Mode; 4] = [Mode::Sw, Mode::Nw, Mode::Hw, Mode::Ov];
#[test]
fn width_constants_are_sane() {
assert_eq!(ScoreWidth::I8.max_abs(), 127);
assert_eq!(ScoreWidth::I16.max_abs(), 32_767);
assert_eq!(ScoreWidth::I32.max_abs(), 2_147_483_647);
assert!(ScoreWidth::I8 < ScoreWidth::I16 && ScoreWidth::I16 < ScoreWidth::I32);
assert_eq!(
(
ScoreWidth::I8.bytes(),
ScoreWidth::I16.bytes(),
ScoreWidth::I32.bytes()
),
(1, 2, 4)
);
}
#[test]
fn narrowest_for_picks_smallest_fit_across_boundaries() {
for (mag, expect) in [
(0, Some(ScoreWidth::I8)),
(127, Some(ScoreWidth::I8)),
(128, Some(ScoreWidth::I16)),
(32_767, Some(ScoreWidth::I16)),
(32_768, Some(ScoreWidth::I32)),
(2_147_483_647, Some(ScoreWidth::I32)),
(2_147_483_648, None),
(i64::MAX, None),
] {
assert_eq!(ScoreWidth::narrowest_for(mag), expect, "magnitude {mag}");
}
}
#[test]
fn small_local_alignment_fits_i8() {
let w = required_width(Mode::Sw, -1, 2, 3, 1, 10, 10).unwrap();
assert_eq!(w, ScoreWidth::I8);
}
#[test]
fn local_mode_ignores_mismatch_negatives_but_not_gap_open() {
let (m, n) = (8, 8);
assert_eq!(
required_width(Mode::Sw, -100, 1, 2, 1, m, n).unwrap(),
ScoreWidth::I8,
"SW ignores the -100 mismatch; positive reach is only 8, gap_open 2"
);
for mode in [Mode::Nw, Mode::Hw, Mode::Ov] {
assert!(
required_width(mode, -100, 1, 2, 1, m, n).unwrap() > ScoreWidth::I8,
"{mode} must escalate on the -100 mismatch"
);
}
assert_eq!(
required_width(Mode::Sw, -1, 1, 200, 1, m, n).unwrap(),
ScoreWidth::I16,
"SW must widen for gap_open 200 (E/F reach -200)"
);
}
#[test]
fn overlap_bound_is_tight_enough_for_cr4_but_global_is_not() {
assert_eq!(
required_width(Mode::Ov, -2, 1, 2, 2, 91, 33).unwrap(),
ScoreWidth::I8,
"CR4 overlap must stay i8"
);
assert_eq!(
required_width(Mode::Nw, -2, 1, 2, 2, 91, 33).unwrap(),
ScoreWidth::I16,
"the same lengths in global mode need i16"
);
}
#[test]
fn escalates_i8_to_i16_to_i32_as_length_grows() {
for mode in ALL_MODES {
let mut prev = ScoreWidth::I8;
for &len in &[1usize, 2, 100, 300, 327, 328, 1000, 22_000, 50_000] {
let w = required_width(mode, 0, 100, 0, 0, len, len).unwrap();
assert!(w >= prev, "{mode}: width went backwards at len {len}");
prev = w;
}
assert_eq!(
required_width(mode, 0, 100, 0, 0, 1, 1).unwrap(),
ScoreWidth::I8
);
assert_eq!(
required_width(mode, 0, 100, 0, 0, 327, 327).unwrap(),
ScoreWidth::I16
);
assert_eq!(
required_width(mode, 0, 100, 0, 0, 328, 328).unwrap(),
ScoreWidth::I32
);
assert_eq!(
required_width(mode, 0, 100, 0, 0, 50_000, 50_000).unwrap(),
ScoreWidth::I32
);
}
}
#[test]
fn positive_reach_uses_min_of_the_two_lengths() {
let short_long = required_width(Mode::Sw, 0, 100, 0, 0, 2, 1_000_000).unwrap();
assert_eq!(short_long, ScoreWidth::I16);
assert_eq!(
required_width(Mode::Sw, 0, 100, 0, 0, 1, 1_000_000).unwrap(),
ScoreWidth::I8
);
}
#[test]
fn zero_length_inputs_never_panic_and_fit_i8() {
for mode in ALL_MODES {
let w = required_width(mode, -5, 5, 4, 2, 0, 0).unwrap();
assert_eq!(w, ScoreWidth::I8, "{mode} on empty inputs");
}
}
#[test]
fn overflowing_i32_is_a_typed_error_not_a_panic() {
let err = required_width(Mode::Nw, -1, 1, 0, 1, 2_000_000_000, 2_000_000_000).unwrap_err();
match err {
Error::ScoreRangeTooWide { bound } => assert!(bound > i32::MAX as i64),
other => panic!("expected ScoreRangeTooWide, got {other:?}"),
}
}
#[test]
fn pathological_lengths_do_not_overflow_the_bound_computation() {
let err = required_width(Mode::Ov, -1, 1, 1, 1, usize::MAX, usize::MAX).unwrap_err();
assert!(matches!(err, Error::ScoreRangeTooWide { .. }));
}
#[test]
fn chosen_width_actually_contains_the_bound() {
let cases = [
(Mode::Sw, -1, 2, 3, 1, 50usize, 50usize),
(Mode::Nw, -4, 5, 8, 2, 200, 200),
(Mode::Hw, -2, 3, 6, 1, 500, 30),
(Mode::Ov, -7, 7, 10, 3, 4000, 4000),
(Mode::Nw, -1, 1, 0, 1, 40_000, 40_000),
];
for (mode, min_e, max_e, go, ge, m, n) in cases {
let bound = magnitude_bound(mode, min_e, max_e, go, ge, m, n);
let w = required_width(mode, min_e, max_e, go, ge, m, n).unwrap();
assert!(
bound <= w.max_abs() as i128,
"{mode}: bound {bound} > {w} range"
);
for narrower in ScoreWidth::ORDER.into_iter().take_while(|&x| x < w) {
assert!(
bound > narrower.max_abs() as i128,
"{mode}: {narrower} would also have fit bound {bound}; selection not minimal"
);
}
}
}
}