use std::{collections::HashMap, hash::Hash, ops::RangeInclusive};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::{
fragment::FragmentKind, glycan::MonoSaccharide, model::ChargeRange, AminoAcid, NeutralLoss,
};
use super::built_in::glycan_losses;
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct GlycanModel {
pub allow_structural: bool,
pub compositional_range: RangeInclusive<usize>,
pub neutral_losses: Vec<NeutralLoss>,
pub specific_neutral_losses: Vec<(MonoSaccharide, bool, Vec<NeutralLoss>)>,
pub default_peptide_fragment: GlycanPeptideFragment,
pub peptide_fragment_rules: Vec<(Vec<AminoAcid>, Vec<FragmentKind>, GlycanPeptideFragment)>,
pub oxonium_charge_range: ChargeRange,
pub other_charge_range: ChargeRange,
}
impl GlycanModel {
#[must_use]
pub fn allow_structural(self, allow_structural: bool) -> Self {
Self {
allow_structural,
..self
}
}
#[must_use]
pub fn compositional_range(self, compositional_range: RangeInclusive<usize>) -> Self {
Self {
compositional_range,
..self
}
}
#[must_use]
pub fn neutral_losses(self, neutral_losses: Vec<NeutralLoss>) -> Self {
Self {
neutral_losses,
..self
}
}
#[must_use]
pub fn oxonium_charge_range(self, oxonium_charge_range: ChargeRange) -> Self {
Self {
oxonium_charge_range,
..self
}
}
#[must_use]
pub fn other_charge_range(self, other_charge_range: ChargeRange) -> Self {
Self {
other_charge_range,
..self
}
}
#[must_use]
pub fn default_peptide_fragment(self, default_peptide_fragment: GlycanPeptideFragment) -> Self {
Self {
default_peptide_fragment,
..self
}
}
#[must_use]
pub fn peptide_fragment_rules(
self,
peptide_fragment_rules: Vec<(Vec<AminoAcid>, Vec<FragmentKind>, GlycanPeptideFragment)>,
) -> Self {
Self {
peptide_fragment_rules,
..self
}
}
pub fn default_allow() -> Self {
Self {
allow_structural: true,
compositional_range: 1..=10,
neutral_losses: Vec::new(),
specific_neutral_losses: glycan_losses().clone(),
default_peptide_fragment: GlycanPeptideFragment::CORE_AND_FREE,
peptide_fragment_rules: Vec::new(),
oxonium_charge_range: ChargeRange::ONE,
other_charge_range: ChargeRange::ONE_TO_PRECURSOR,
}
}
pub const DISALLOW: Self = Self {
allow_structural: false,
compositional_range: 0..=0,
neutral_losses: Vec::new(),
specific_neutral_losses: Vec::new(),
default_peptide_fragment: GlycanPeptideFragment::FULL,
peptide_fragment_rules: Vec::new(),
oxonium_charge_range: ChargeRange::ONE,
other_charge_range: ChargeRange::ONE_TO_PRECURSOR,
};
pub fn get_peptide_fragments(
&self,
attachment: Option<AminoAcid>,
) -> (
GlycanPeptideFragment,
HashMap<FragmentKind, GlycanPeptideFragment>,
) {
let base = self
.peptide_fragment_rules
.iter()
.find(|rule| attachment.is_some_and(|a| rule.0.contains(&a)) && rule.1.is_empty())
.map_or(self.default_peptide_fragment, |rule| rule.2);
(
base,
self.peptide_fragment_rules
.iter()
.filter(|rule| attachment.is_some_and(|a| rule.0.contains(&a)))
.flat_map(|rule| rule.1.iter().map(|f| (f, rule.2)))
.into_group_map()
.into_iter()
.map(|(f, settings)| (*f, settings.iter().fold(settings[0], |acc, v| acc + v)))
.collect(),
)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct GlycanPeptideFragment {
full: bool,
core: Option<(u8, u8)>,
}
impl std::ops::Add<&Self> for GlycanPeptideFragment {
type Output = Self;
fn add(self, rhs: &Self) -> Self::Output {
Self {
full: self.full || rhs.full,
core: self
.core
.and_then(|c| rhs.core.map(|r| (c, r)))
.map(|(c, r)| (c.0.min(r.0), c.1.max(r.1)))
.or(self.core)
.or(rhs.core),
}
}
}
impl GlycanPeptideFragment {
pub const FULL: Self = Self {
full: true,
core: None,
};
pub const CORE_AND_FREE: Self = Self {
full: false,
core: Some((0, 1)),
};
pub const CORE: Self = Self {
full: false,
core: Some((1, 1)),
};
pub const FREE: Self = Self {
full: false,
core: Some((0, 0)),
};
pub const fn full(self) -> bool {
self.full
}
pub fn core(self) -> Option<RangeInclusive<u8>> {
self.core.map(|(min, max)| min..=max)
}
}