use crate::{
data::{
array_types::{elem_max, is_subset},
mappings::{ByteIndexMap, DNA_PROFILE_MAP},
},
math::AnyInt,
};
use std::fmt::Display;
pub(crate) mod aa;
mod parse;
pub use aa::*;
pub use parse::*;
pub(crate) static PHYSIOCHEMICAL_FACTORS: [[Option<f32>; 256]; 256] = {
const AA: [u8; 43] = *b"ACDEFGHIKLMNPQRSTVWYXacdefghiklmnpqrstvwyx-";
const PCF: [[f64; 5]; 43] = [
[-0.59, -1.3, -0.73, 1.57, -0.15],
[-1.34, 0.47, -0.86, -1.02, -0.26],
[1.05, 0.3, -3.66, -0.26, -3.24],
[1.36, -1.45, 1.48, 0.11, -0.84],
[-1.01, -0.59, 1.89, -0.4, 0.41],
[-0.38, 1.65, 1.33, 1.05, 2.06],
[0.34, -0.42, -1.67, -1.47, -0.08],
[-1.24, -0.55, 2.13, 0.39, 0.82],
[1.83, -0.56, 0.53, -0.28, 1.65],
[-1.02, -0.99, -1.51, 1.27, -0.91],
[-0.66, -1.52, 2.22, -1.01, 1.21],
[0.95, 0.83, 1.3, -0.17, 0.93],
[0.19, 2.08, -1.63, 0.42, -1.39],
[0.93, -0.18, -3.01, -0.5, -1.85],
[1.54, -0.06, 1.5, 0.44, 2.9],
[-0.23, 1.4, -4.76, 0.67, -2.65],
[-0.03, 0.33, 2.21, 0.91, 1.31],
[-1.34, -0.28, -0.54, 1.24, -1.26],
[-0.6, 0.01, 0.67, -2.13, -0.18],
[0.26, 0.83, 3.1, -0.84, 1.51],
[0.0, 0.0, 0.0, 0.0, 0.0],
[-0.59, -1.3, -0.73, 1.57, -0.15],
[-1.34, 0.47, -0.86, -1.02, -0.26],
[1.05, 0.3, -3.66, -0.26, -3.24],
[1.36, -1.45, 1.48, 0.11, -0.84],
[-1.01, -0.59, 1.89, -0.4, 0.41],
[-0.38, 1.65, 1.33, 1.05, 2.06],
[0.34, -0.42, -1.67, -1.47, -0.08],
[-1.24, -0.55, 2.13, 0.39, 0.82],
[1.83, -0.56, 0.53, -0.28, 1.65],
[-1.02, -0.99, -1.51, 1.27, -0.91],
[-0.66, -1.52, 2.22, -1.01, 1.21],
[0.95, 0.83, 1.3, -0.17, 0.93],
[0.19, 2.08, -1.63, 0.42, -1.39],
[0.93, -0.18, -3.01, -0.5, -1.85],
[1.54, -0.06, 1.5, 0.44, 2.9],
[-0.23, 1.4, -4.76, 0.67, -2.65],
[-0.03, 0.33, 2.21, 0.91, 1.31],
[-1.34, -0.28, -0.54, 1.24, -1.26],
[-0.6, 0.01, 0.67, -2.13, -0.18],
[0.26, 0.83, 3.1, -0.84, 1.51],
[0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0],
];
let mut pcd = [[None; 256]; 256];
let mut aa1: usize = 0;
while aa1 < 43 {
let mut aa2: usize = 0;
while aa2 < 43 {
pcd[AA[aa1] as usize][AA[aa2] as usize] = if AA[aa1].eq_ignore_ascii_case(&AA[aa2]) {
Some(0.0)
} else {
let mut d: f64 = 0.0;
let mut k: usize = 0;
while k < 5 {
d += (PCF[aa1][k] - PCF[aa2][k]) * (PCF[aa1][k] - PCF[aa2][k]);
k += 1;
}
#[allow(clippy::cast_possible_truncation)]
Some(crate::math::sqrt_baby(d) as f32)
};
aa2 += 1;
}
aa1 += 1;
}
pcd
};
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct WeightMatrix<'a, T: AnyInt, const S: usize> {
pub weights: [[T; S]; S],
pub mapping: &'a ByteIndexMap<S>,
pub(crate) bias: T,
}
impl<T: AnyInt, const S: usize> WeightMatrix<'_, T, S> {
#[inline]
#[must_use]
pub const fn get_weight(&self, ref_residue: u8, query_residue: u8) -> T {
self.weights[self.mapping.to_index(ref_residue)][self.mapping.to_index(query_residue)]
}
#[inline]
#[must_use]
const fn get_weight_mut(&mut self, ref_residue: u8, query_residue: u8) -> &mut T {
&mut self.weights[self.mapping.to_index(ref_residue)][self.mapping.to_index(query_residue)]
}
const fn get_subset_unadjusted<'a, const L: usize>(&self, subset: &'a ByteIndexMap<L>) -> WeightMatrix<'a, T, L> {
assert!(
is_subset(self.mapping.byte_keys(), subset.byte_keys()),
"The provided keys are not a subset of the original weight matrix!"
);
let mut weights = WeightMatrix {
weights: [[T::ZERO; L]; L],
mapping: subset,
bias: self.bias,
};
let mut i = 0;
while i < L {
let reference_residue = subset.byte_keys()[i];
let mut j = 0;
while j < L {
let query_residue = subset.byte_keys()[j];
*weights.get_weight_mut(reference_residue, query_residue) =
self.get_weight(reference_residue, query_residue);
j += 1;
}
i += 1;
}
weights
}
}
impl WeightMatrix<'_, u8, 5> {
#[inline]
#[must_use]
pub const fn new_biased_dna_matrix(matching: i8, mismatch: i8, ignoring: Option<u8>) -> Self {
WeightMatrix::new(&DNA_PROFILE_MAP, matching, mismatch, ignoring).to_biased_matrix()
}
}
impl<const S: usize> WeightMatrix<'_, u8, S> {
const fn recompute_bias(&mut self) {
let mut min = u8::MAX;
let mut i = 0;
while i < S {
let mut j = 0;
while j < S {
if self.weights[i][j] < min {
min = self.weights[i][j];
}
j += 1;
}
i += 1;
}
let new_bias = self.bias.saturating_sub(min);
let decrement = self.bias - new_bias;
let mut i = 0;
while i < S {
let mut j = 0;
while j < S {
self.weights[i][j] -= decrement;
j += 1;
}
i += 1;
}
self.bias = new_bias;
}
#[must_use]
pub const fn get_subset<'a, const L: usize>(&self, subset: &'a ByteIndexMap<L>) -> WeightMatrix<'a, u8, L> {
let mut out = self.get_subset_unadjusted(subset);
out.recompute_bias();
out
}
}
impl<'a, const S: usize> WeightMatrix<'a, i8, S> {
#[must_use]
pub const fn new(mapping: &'a ByteIndexMap<S>, matching: i8, mismatch: i8, ignoring: Option<u8>) -> Self {
let mut weights = [[0i8; S]; S];
let skip_index = match ignoring {
Some(ignoring) => {
if mapping.in_byte_keys(ignoring) {
Some(mapping.to_index(ignoring))
} else {
panic!("An invalid byte was specified for the ignoring field.")
}
}
None => None,
};
let mut i = 0;
while i < S {
let mut j = 0;
while j < S {
if let Some(k) = skip_index
&& (k == i || k == j)
{
j += 1;
continue;
}
if i == j {
weights[i][j] = matching;
} else {
weights[i][j] = mismatch;
}
j += 1;
}
i += 1;
}
WeightMatrix {
weights,
mapping,
bias: 0,
}
}
#[must_use]
pub const fn new_custom(mapping: &'a ByteIndexMap<S>, weights: [[i8; S]; S]) -> Self {
WeightMatrix {
weights,
mapping,
bias: 0,
}
}
pub fn new_from_fn<F>(mapping: &'a ByteIndexMap<S>, weight_fn: F) -> Self
where
F: Fn(u8, u8) -> i8, {
let mut out = WeightMatrix::new(mapping, 0, 0, None);
for ref_residue in mapping.byte_keys() {
for query_residue in mapping.byte_keys() {
let score = weight_fn(*ref_residue, *query_residue);
*out.get_weight_mut(*ref_residue, *query_residue) = score;
}
}
out
}
#[must_use]
const fn get_bias(&self) -> i8 {
let mut min = 0;
let mut i = 0;
while i < S {
let mut j = 0;
while j < S {
if self.weights[i][j] < min {
min = self.weights[i][j];
}
j += 1;
}
i += 1;
}
min
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub const fn to_biased_matrix(&self) -> WeightMatrix<'a, u8, S> {
let bias = self.get_bias();
let mut weights = [[0u8; S]; S];
let mapping = self.mapping;
let mut i = 0;
while i < S {
let mut j = 0;
while j < S {
weights[i][j] = (self.weights[i][j] as i16 - bias as i16) as u8;
j += 1;
}
i += 1;
}
let bias = bias.unsigned_abs();
WeightMatrix { weights, mapping, bias }
}
#[must_use]
pub const fn get_subset<'b, const L: usize>(&self, subset: &'b ByteIndexMap<L>) -> WeightMatrix<'b, i8, L> {
self.get_subset_unadjusted(subset)
}
}
impl WeightMatrix<'_, i8, 5> {
#[must_use]
pub const fn new_dna_matrix(matching: i8, mismatch: i8, ignoring: Option<u8>) -> Self {
WeightMatrix::new(&DNA_PROFILE_MAP, matching, mismatch, ignoring)
}
}
impl<T: AnyInt, const S: usize> Display for WeightMatrix<'_, T, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut col_widths = self.weights.iter().fold([0; S], |acc, row| {
let widths = row.map(|val| val.to_string().len());
elem_max(&acc, &widths)
});
for width in col_widths.iter_mut().skip(1) {
*width += 1;
}
let residues = self.mapping.byte_keys();
write!(f, " ")?;
for (residue, width) in residues.iter().zip(&col_widths) {
write!(f, "{residue:>width$}", residue = *residue as char)?;
}
writeln!(f)?;
for (row, residue) in self.weights.iter().zip(residues) {
write!(f, "{residue} ", residue = *residue as char)?;
for (val, width) in row.iter().zip(&col_widths) {
write!(f, "{val:>width$}")?;
}
writeln!(f)?;
}
if self.bias != T::ZERO {
write!(f, "Bias: {}", self.bias)?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
static AA_MATS: [&WeightMatrix<'static, i8, 25>; 22] = [
&BLOSUM_30,
&BLOSUM_35,
&BLOSUM_40,
&BLOSUM_45,
&BLOSUM_50,
&BLOSUM_55,
&BLOSUM_60,
&BLOSUM_62,
&BLOSUM_65,
&BLOSUM_70,
&BLOSUM_75,
&BLOSUM_80,
&BLOSUM_85,
&BLOSUM_90,
&BLOSUM_95,
&BLOSUM_100,
&PAM_30,
&PAM_40,
&PAM_70,
&PAM_120,
&PAM_200,
&PAM_250,
];
#[test]
fn create_simple() {
static RESIDUE_MAP: ByteIndexMap<2> = ByteIndexMap::new(*b"AB", b'A');
let result1 = WeightMatrix {
weights: [[1, 0], [0, 1]],
mapping: &RESIDUE_MAP,
bias: 0,
};
let result2 = WeightMatrix::new(&RESIDUE_MAP, 1, 0, None);
let result3 = WeightMatrix::new_custom(&RESIDUE_MAP, [[1, 0], [0, 1]]);
assert_eq!(result1, result2);
assert_eq!(result1, result3);
}
#[test]
#[should_panic(expected = "An invalid byte was specified for the ignoring field.")]
fn test_invalid_char() {
let _ = WeightMatrix::new_dna_matrix(1, 0, Some(b'U'));
}
#[allow(non_snake_case)]
#[test]
fn create_IRMA_matrix() {
let result1 = WeightMatrix {
weights: [
[2, -5, -5, -5, 0],
[-5, 2, -5, -5, 0],
[-5, -5, 2, -5, 0],
[-5, -5, -5, 2, 0],
[0, 0, 0, 0, 0],
],
mapping: &DNA_PROFILE_MAP,
bias: 0,
};
let result2 = WeightMatrix::new(&DNA_PROFILE_MAP, 2, -5, Some(b'N'));
let result3 = WeightMatrix::new_custom(
&DNA_PROFILE_MAP,
[
[2, -5, -5, -5, 0],
[-5, 2, -5, -5, 0],
[-5, -5, 2, -5, 0],
[-5, -5, -5, 2, 0],
[0, 0, 0, 0, 0],
],
);
assert_eq!(result1, result2);
assert_eq!(result1, result3);
}
#[test]
fn test_blosum_matrices() {
assert!(aa_mat_from_name("BLOSUM30").is_some());
assert!(aa_mat_from_name("blosum 62").is_some());
assert!(aa_mat_from_name("BLOSUM_80").is_some());
assert!(aa_mat_from_name("blosum-100").is_some());
assert!(aa_mat_from_name("BLOSUM 45").is_some());
assert!(aa_mat_from_name("BLOSUM29").is_none());
assert!(aa_mat_from_name("BLOSUM101").is_none());
assert!(aa_mat_from_name("BLOSUM").is_none());
}
#[test]
fn test_pam_matrices() {
assert!(aa_mat_from_name("PAM30").is_some());
assert!(aa_mat_from_name("pam-70").is_some());
assert!(aa_mat_from_name("PAM_120").is_some());
assert!(aa_mat_from_name("PAM 200").is_some());
assert!(aa_mat_from_name("PAM39").is_none());
assert!(aa_mat_from_name("PAM2500").is_none());
assert!(aa_mat_from_name("PAM").is_none());
}
#[test]
fn test_invalid_names() {
assert!(aa_mat_from_name("INVALID30").is_none());
assert!(aa_mat_from_name("UNKNOWN").is_none());
assert!(aa_mat_from_name("PAM_").is_none());
assert!(aa_mat_from_name("").is_none());
}
#[test]
fn test_subset_unbiased() {
const OTHER_MAP: ByteIndexMap<21> = ByteIndexMap::new_ignoring_case(*b"DIEVRSAWGMCYTXNPFLHKQ", b'X');
for mat in AA_MATS {
let other_mat = mat.get_subset(&OTHER_MAP);
for aa1 in u8::MIN..=u8::MAX {
for aa2 in u8::MIN..=u8::MAX {
let other_aa1 = if OTHER_MAP.byte_keys().contains(&aa1.to_ascii_uppercase()) {
aa1
} else {
b'X'
};
let other_aa2 = if OTHER_MAP.byte_keys().contains(&aa2.to_ascii_uppercase()) {
aa2
} else {
b'X'
};
let score = mat.get_weight(other_aa1, other_aa2);
let other_score = other_mat.get_weight(aa1, aa2);
assert_eq!(score, other_score, "{aa1} vs {aa2}");
}
}
}
}
#[test]
#[allow(clippy::cast_possible_wrap)]
fn test_subset_biased() {
const OTHER_MAP: ByteIndexMap<4> = ByteIndexMap::new_ignoring_case(*b"AWGX", b'X');
for mat in AA_MATS.map(WeightMatrix::to_biased_matrix) {
let other_mat = mat.get_subset(&OTHER_MAP);
for aa1 in u8::MIN..=u8::MAX {
for aa2 in u8::MIN..=u8::MAX {
let other_aa1 = if OTHER_MAP.byte_keys().contains(&aa1.to_ascii_uppercase()) {
aa1
} else {
b'X'
};
let other_aa2 = if OTHER_MAP.byte_keys().contains(&aa2.to_ascii_uppercase()) {
aa2
} else {
b'X'
};
let score =
i8::try_from(mat.get_weight(other_aa1, other_aa2)).unwrap() - i8::try_from(mat.bias).unwrap();
let other_score =
i8::try_from(other_mat.get_weight(aa1, aa2)).unwrap() - i8::try_from(other_mat.bias).unwrap();
assert_eq!(score, other_score, "{aa1} vs {aa2}");
}
}
}
}
#[test]
#[allow(clippy::cast_possible_wrap)]
fn test_subset_correct_bias() {
for byte_keys in generate_subsets::<u8, 14, 4>(b"ADFHIKMQTVY*BZ") {
let mapping =
ByteIndexMap::new_ignoring_case([byte_keys[0], byte_keys[1], byte_keys[2], byte_keys[3], b'X'], b'X');
for mat in AA_MATS.map(WeightMatrix::to_biased_matrix) {
let other_mat = mat.get_subset(&mapping);
let min_weight = *other_mat.weights.map(|row| *row.iter().min().unwrap()).iter().min().unwrap();
assert_eq!(min_weight, 0);
}
}
}
fn generate_subsets<T: Copy + Default, const S: usize, const L: usize>(arr: &[T; S]) -> Vec<[T; L]> {
fn generate_subsets_inner<T: Copy, const S: usize, const L: usize>(
arr: &[T; S], start: usize, current: [T; L], current_len: usize, out: &mut Vec<[T; L]>,
) {
if current_len == L {
out.push(current);
return;
}
for i in start..S {
let mut new_current = current;
new_current[current_len] = arr[i];
generate_subsets_inner::<T, S, L>(arr, i + 1, new_current, current_len + 1, out);
}
}
let mut out = Vec::new();
generate_subsets_inner::<T, S, L>(arr, 0, [T::default(); L], 0, &mut out);
out
}
}