use mzcore::{
chemistry::{MassMode, Molecule},
prelude::AmbiguousMolecule,
quantities::{Multi, Tolerance, WithinTolerance},
sequence::{AtMax, HasPeptidoform, Linear, Peptidoform, SequenceElement},
system::{Mass, OrderedMass, dalton},
};
use crate::{
Alignment, align_matrix::Matrix, align_type::*, alignment::Score,
diagonal_array::DiagonalArray, piece::*, scoring::*,
};
pub fn align<const STEPS: u16, A: HasPeptidoform<Linear>, B: HasPeptidoform<Linear>>(
seq_a: A,
seq_b: B,
scoring: AlignScoring<'_>,
align_type: AlignType,
) -> Alignment<A, B> {
let peptidoform_a = seq_a.cast_peptidoform();
let peptidoform_b = seq_b.cast_peptidoform();
let masses_a: DiagonalArray<Multi<Mass>, STEPS> =
calculate_masses::<STEPS>(peptidoform_a, scoring.mass_mode);
let masses_b: DiagonalArray<Multi<Mass>, STEPS> =
calculate_masses::<STEPS>(peptidoform_b, scoring.mass_mode);
align_cached::<STEPS, A, B>(seq_a, &masses_a, seq_b, &masses_b, scoring, align_type)
}
#[expect(clippy::too_many_lines)]
#[allow(clippy::similar_names)]
pub(super) fn align_cached<
const STEPS: u16,
A: HasPeptidoform<Linear>,
B: HasPeptidoform<Linear>,
>(
seq_a: A,
masses_a: &DiagonalArray<Multi<Mass>, STEPS>,
seq_b: B,
masses_b: &DiagonalArray<Multi<Mass>, STEPS>,
scoring: AlignScoring<'_>,
align_type: AlignType,
) -> Alignment<A, B> {
let peptidoform_a = seq_a.cast_peptidoform();
let peptidoform_b = seq_b.cast_peptidoform();
assert!(isize::try_from(peptidoform_a.len()).is_ok());
assert!(isize::try_from(peptidoform_b.len()).is_ok());
let mut matrix = Matrix::new(peptidoform_a.len(), peptidoform_b.len());
let mut global_highest = (0, 0, 0);
if align_type.left.global_a() {
matrix.global_start(true, scoring);
}
if align_type.left.global_b() {
matrix.global_start(false, scoring);
}
let ranges_a = {
let mut ranges: DiagonalArray<(Mass, Mass), STEPS> =
DiagonalArray::new(peptidoform_a.len());
for i in 0..peptidoform_a.len() {
for j in 0..=i.min(STEPS as usize) {
let (min, max) = mass_range_expanded(
unsafe { masses_a.get_unchecked([i, j]) },
scoring.tolerance,
);
ranges[[i, j]] = (min, max);
}
}
ranges
};
let ranges_b = {
let mut ranges: DiagonalArray<(Mass, Mass), STEPS> =
DiagonalArray::new(peptidoform_b.len());
for i in 0..peptidoform_b.len() {
for j in 0..=i.min(STEPS as usize) {
let (min, max) = mass_range(unsafe { masses_b.get_unchecked([i, j]) });
ranges[[i, j]] = (min, max);
}
}
ranges
};
for index_a in 1..=peptidoform_a.len() {
for index_b in 1..=peptidoform_b.len() {
let score_gap = |gap_a: bool| {
let prev = if gap_a {
unsafe { matrix.get_unchecked([index_a - 1, index_b]) }
} else {
unsafe { matrix.get_unchecked([index_a, index_b - 1]) }
};
let is_first_step = prev.step_a == 0 && prev.step_b == 0;
let is_previous_gap = prev.step_a == 0 && !gap_a || prev.step_b == 0 && gap_a;
let is_gap_start = is_first_step || !is_previous_gap;
let score = scoring.gap_extend as isize
+ scoring.gap_start as isize * isize::from(is_gap_start);
let len_a = u16::from(gap_a);
let len_b = u16::from(!gap_a);
Piece::new(prev.score + score, score, MatchType::Gap, len_a, len_b)
};
let gap_score_a = score_gap(true);
let gap_score_b = score_gap(false);
let mut highest = if gap_score_a.score >= gap_score_b.score {
gap_score_a
} else {
gap_score_b
};
let prev = unsafe { matrix.get_unchecked([index_a - 1, index_b - 1]) };
let pair_score = score_pair(
(
unsafe { peptidoform_a.sequence().get_unchecked(index_a - 1) },
unsafe { masses_a.get_unchecked([index_a - 1, 0]) },
),
(
unsafe { peptidoform_b.sequence().get_unchecked(index_b - 1) },
unsafe { masses_b.get_unchecked([index_b - 1, 0]) },
),
scoring,
prev.score,
);
if pair_score.score > highest.score {
highest = pair_score;
}
if highest.match_type != MatchType::FullIdentity {
for len_a in 1..=index_a.min(STEPS as usize) {
let range_a = unsafe { ranges_a.get_unchecked([index_a - 1, len_a - 1]) };
let min_len_b = 1 + usize::from(len_a == 1);
for len_b in min_len_b..=index_b.min(STEPS as usize) {
let range_b = unsafe { ranges_b.get_unchecked([index_b - 1, len_b - 1]) };
if range_a.0 > range_b.1 || range_b.0 > range_a.1 {
continue;
}
let match_score = {
let prev =
unsafe { matrix.get_unchecked([index_a - len_a, index_b - len_b]) };
let base_score = prev.score;
score(
unsafe {
(
peptidoform_a
.sequence()
.get_unchecked((index_a - len_a)..index_a),
masses_a.get_unchecked([index_a - 1, len_a - 1]),
)
},
unsafe {
(
peptidoform_b
.sequence()
.get_unchecked((index_b - len_b)..index_b),
masses_b.get_unchecked([index_b - 1, len_b - 1]),
)
},
scoring,
base_score,
)
};
if let Some(p) = match_score
&& p.score > highest.score
{
highest = p;
}
}
}
}
if highest.score >= global_highest.0 {
global_highest = (highest.score, index_a, index_b);
}
if align_type.left.global() || highest.score > 0 {
unsafe {
*matrix.get_unchecked_mut([index_a, index_b]) = highest;
}
}
}
}
let (start_a, start_b, path) = matrix.trace_path(align_type, global_highest);
let score = determine_final_score(
peptidoform_a,
peptidoform_b,
start_a,
start_b,
&path,
scoring,
);
Alignment {
seq_a,
seq_b,
score,
path,
start_a,
start_b,
align_type,
maximal_step: STEPS,
}
}
pub(super) fn determine_final_score<A, B>(
seq_a: &Peptidoform<A>,
seq_b: &Peptidoform<B>,
start_a: usize,
start_b: usize,
path: &[Piece],
scoring: AlignScoring<'_>,
) -> Score {
let maximal_score = isize::midpoint(
seq_a.sequence()[start_a..start_a + path.iter().map(|p| p.step_a as usize).sum::<usize>()]
.iter()
.map(|a| {
scoring.matrix[a.aminoacid.aminoacid() as usize][a.aminoacid.aminoacid() as usize]
as isize
})
.sum::<isize>(),
seq_b.sequence()[start_b..start_b + path.iter().map(|p| p.step_b as usize).sum::<usize>()]
.iter()
.map(|a| {
scoring.matrix[a.aminoacid.aminoacid() as usize][a.aminoacid.aminoacid() as usize]
as isize
})
.sum::<isize>(),
);
let absolute_score = path.last().map(|p| p.score).unwrap_or_default();
Score {
absolute: absolute_score,
normalised: if maximal_score == 0 {
ordered_float::OrderedFloat::default()
} else {
ordered_float::OrderedFloat((absolute_score as f64 / maximal_score as f64).min(1.0))
},
max: maximal_score,
}
}
pub(super) fn score_pair<A: AtMax<Linear>, B: AtMax<Linear>>(
a: (&SequenceElement<A>, &Multi<Mass>),
b: (&SequenceElement<B>, &Multi<Mass>),
scoring: AlignScoring<'_>,
score: isize,
) -> Piece {
match (
a.0.aminoacid.aminoacid() == b.0.aminoacid.aminoacid(),
scoring.tolerance.within(a.1, b.1),
) {
(true, true) => {
let local = scoring.matrix[a.0.aminoacid.aminoacid() as usize]
[b.0.aminoacid.aminoacid() as usize] as isize;
Piece::new(score + local, local, MatchType::FullIdentity, 1, 1)
}
(true, false) => {
if (scoring.pair == PairMode::DatabaseToPeptidoform && !b.0.modifications.is_empty())
|| (scoring.pair == PairMode::PeptidoformToDatabase
&& !a.0.modifications.is_empty())
{
let local = scoring.matrix[a.0.aminoacid.aminoacid() as usize]
[b.0.aminoacid.aminoacid() as usize] as isize
+ scoring.mass_mismatch as isize;
Piece::new(score + local, local, MatchType::IdentityMassMismatch, 1, 1)
} else {
let local = scoring.matrix[a.0.aminoacid.aminoacid() as usize]
[b.0.aminoacid.aminoacid() as usize] as isize
+ scoring.mismatch as isize;
Piece::new(score + local, local, MatchType::Mismatch, 1, 1)
}
}
(false, true) => Piece::new(
score + scoring.mass_base as isize + scoring.isobaric as isize,
scoring.mass_base as isize + scoring.isobaric as isize,
MatchType::Isobaric,
1,
1,
),
(false, false) => {
let local = scoring.matrix[a.0.aminoacid.aminoacid() as usize]
[b.0.aminoacid.aminoacid() as usize] as isize
+ scoring.mismatch as isize;
Piece::new(score + local, local, MatchType::Mismatch, 1, 1)
}
}
}
pub(super) fn score_pair_mass_mismatch<A: AtMax<Linear>, B: AtMax<Linear>>(
a: &SequenceElement<A>,
b: &SequenceElement<B>,
scoring: AlignScoring<'_>,
score: isize,
) -> Piece {
if a.aminoacid.aminoacid() == b.aminoacid.aminoacid() {
if (scoring.pair == PairMode::DatabaseToPeptidoform && !b.modifications.is_empty())
|| (scoring.pair == PairMode::PeptidoformToDatabase && !a.modifications.is_empty())
{
let local = scoring.matrix[a.aminoacid.aminoacid() as usize]
[b.aminoacid.aminoacid() as usize] as isize
+ scoring.mass_mismatch as isize;
Piece::new(score + local, local, MatchType::IdentityMassMismatch, 1, 1)
} else {
let local = scoring.matrix[a.aminoacid.aminoacid() as usize]
[b.aminoacid.aminoacid() as usize] as isize
+ scoring.mismatch as isize;
Piece::new(score + local, local, MatchType::Mismatch, 1, 1)
}
} else {
let local = scoring.matrix[a.aminoacid.aminoacid() as usize]
[b.aminoacid.aminoacid() as usize] as isize
+ scoring.mismatch as isize;
Piece::new(score + local, local, MatchType::Mismatch, 1, 1)
}
}
pub(super) fn score<A: AtMax<Linear>, B: AtMax<Linear>>(
a: (&[SequenceElement<A>], &Multi<Mass>),
b: (&[SequenceElement<B>], &Multi<Mass>),
scoring: AlignScoring<'_>,
score: isize,
) -> Option<Piece> {
if scoring.tolerance.within(a.1, b.1) {
let rotated = {
a.0.len() == b.0.len() && {
let mut b_copy = vec![false; b.0.len()];
a.0.iter().all(|el| {
b_copy
.iter()
.enumerate()
.position(|(index, used)| !used && b.0[index] == *el)
.is_some_and(|pos| {
b_copy[pos] = true;
true
})
})
}
};
#[expect(clippy::cast_possible_wrap)]
let local = scoring.mass_base as isize
+ if rotated {
scoring.rotated as isize * a.0.len() as isize
} else {
scoring.isobaric as isize * (a.0.len() + b.0.len()) as isize / 2
};
Some(Piece::new(
score + local,
local,
if rotated {
MatchType::Rotation
} else {
MatchType::Isobaric
},
a.0.len() as u16,
b.0.len() as u16,
))
} else {
None
}
}
pub(super) fn mass_range_expanded(
masses: &Multi<Mass>,
tolerance: Tolerance<OrderedMass>,
) -> (Mass, Mass) {
masses.iter().fold(
(
Mass::new::<dalton>(f64::INFINITY),
Mass::new::<dalton>(f64::NEG_INFINITY),
),
|(min, max), &m| {
let range = tolerance.bounds(m);
(min.min(range.0), max.max(range.1))
},
)
}
pub(super) fn mass_range(masses: &Multi<Mass>) -> (Mass, Mass) {
masses.iter().fold(
(
Mass::new::<dalton>(f64::INFINITY),
Mass::new::<dalton>(f64::NEG_INFINITY),
),
|(min, max), &m| (min.min(m), max.max(m)),
)
}
pub(super) fn calculate_masses<const STEPS: u16>(
sequence: &Peptidoform<impl AtMax<Linear>>,
mass_mode: MassMode,
) -> DiagonalArray<Multi<Mass>, STEPS> {
let mut array = DiagonalArray::new(sequence.len());
let n = sequence
.get_n_term()
.iter()
.map(|m| m.mass(mass_mode).mass())
.sum::<Mass>();
let c = sequence
.get_c_term()
.iter()
.map(|m| m.mass(mass_mode).mass())
.sum::<Mass>();
for i in 0..sequence.len() {
for j in 0..=i.min(STEPS as usize) {
let mut seq = sequence[i - j..=i]
.iter()
.map(|p| p.masses(mass_mode).into())
.sum::<Multi<Mass>>();
if i - j == 0 {
seq += n.clone();
}
if i == sequence.len() - 1 {
seq += c.clone();
}
array[[i, j]] = seq;
}
}
array
}
#[cfg(test)]
#[expect(clippy::missing_panics_doc)]
mod tests {
use mzcore::{
chemistry::MolecularFormula,
prelude::AmbiguousMolecule,
quantities::Multi,
sequence::{CheckedAminoAcid, SequenceElement},
};
use super::score;
use crate::scoring::AlignScoring;
#[test]
fn pair() {
let a = [SequenceElement::new(CheckedAminoAcid::N, None)];
let b = [
SequenceElement::new(CheckedAminoAcid::G, None),
SequenceElement::new(CheckedAminoAcid::G, None),
];
let pair = dbg!(score(
(
&a,
&a.iter().map(AmbiguousMolecule::formulas).sum::<Multi<MolecularFormula>>()[0]
.monoisotopic_mass()
.into()
),
(
&b,
&b.iter().map(AmbiguousMolecule::formulas).sum::<Multi<MolecularFormula>>()[0]
.monoisotopic_mass()
.into()
),
AlignScoring::default(),
0,
));
assert!(pair.is_some());
}
}