use std::{
borrow::Cow,
cmp::Ordering,
fmt::{Debug, Display, Write},
sync::LazyLock,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use serde::{Deserialize, Serialize};
#[cfg(feature = "glycan-render")]
use crate::glycan::GlycanSelection;
use crate::{
glycan::{GlycanBranchIndex, GlycanBranchMassIndex, MonoSaccharide},
model::{ChargeRange, PossiblePrimaryIons},
molecular_charge::{CachedCharge, MolecularCharge},
system::{
f64::{MassOverCharge, Ratio},
usize::Charge,
OrderedMassOverCharge,
},
AmbiguousLabel, AminoAcid, Chemical, IsAminoAcid, MassMode, Modification, MolecularFormula,
Multi, MultiChemical, NeutralLoss, SemiAmbiguous, SequenceElement, SequencePosition, Tolerance,
};
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Default)]
pub struct Fragment {
pub formula: Option<MolecularFormula>,
pub charge: Charge,
pub ion: FragmentType,
pub peptidoform_ion_index: Option<usize>,
pub peptidoform_index: Option<usize>,
pub neutral_loss: Vec<NeutralLoss>,
pub deviation: Option<Tolerance<OrderedMassOverCharge>>,
pub confidence: Option<OrderedFloat<f64>>,
pub auxiliary: bool,
}
impl Fragment {
#[allow(non_snake_case)]
pub fn to_mzPAF(&self) -> String {
let mut output = String::new();
if self.auxiliary {
output.push('&');
}
match &self.ion {
FragmentType::a(pos, variant)
| FragmentType::b(pos, variant)
| FragmentType::c(pos, variant)
| FragmentType::x(pos, variant)
| FragmentType::y(pos, variant) => write!(
&mut output,
"{}{}{}",
self.ion.kind(),
pos.series_number,
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap(),
FragmentType::z(pos, variant) => write!(
&mut output,
"{}{}{}",
self.ion.kind(),
pos.series_number,
if *variant == 1 {
String::new()
} else {
format!("{:+}H", variant - 1)
}
)
.unwrap(),
FragmentType::d(pos, aa, distance, variant, label) => {
if *distance == 0 {
write!(
&mut output,
"d{label}{}{}",
pos.series_number,
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap();
} else if let Some(loss) = aa
.satellite_ion_fragments(
pos.sequence_index,
self.peptidoform_index.unwrap_or_default(),
)
.and_then(|fragments| {
fragments
.iter()
.find(|f| f.0 == *label)
.map(|(_, loss)| loss.clone())
})
{
write!(
&mut output,
"a{}-{loss}{}",
pos.series_number,
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap();
} else {
write!(&mut output, "?",).unwrap();
}
}
FragmentType::v(pos, aa, distance, variant) => {
if *distance == 0 {
write!(
&mut output,
"v{}{}",
pos.series_number,
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap();
} else {
write!(
&mut output,
"y{}-{}{}",
pos.series_number,
aa.formulas()
.first()
.map(|f| f - LazyLock::force(&crate::aminoacid::BACKBONE))
.unwrap_or_default(),
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap();
}
}
FragmentType::w(pos, aa, distance, variant, label) => {
if *distance == 0 {
write!(
&mut output,
"w{label}{}{}",
pos.series_number,
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap();
} else if let Some(loss) = aa
.satellite_ion_fragments(
pos.sequence_index,
self.peptidoform_index.unwrap_or_default(),
)
.and_then(|fragments| {
fragments
.iter()
.find(|f| f.0 == *label)
.map(|(_, loss)| loss.clone())
})
{
write!(
&mut output,
"z{}-{loss}{}",
pos.series_number,
if *variant == 0 {
String::new()
} else {
format!("{variant:+}H")
}
)
.unwrap();
} else {
write!(&mut output, "?",).unwrap();
}
}
FragmentType::Precursor => write!(&mut output, "p").unwrap(),
FragmentType::PrecursorSideChainLoss(_, aa) => {
write!(&mut output, "p-r[sidechain_{aa}]").unwrap();
}
FragmentType::Immonium(_, seq) => write!(
&mut output,
"I{}{}",
seq.aminoacid,
seq.modifications.iter().map(|m| format!("[{m}]")).join("") )
.unwrap(),
FragmentType::Unknown(num) => write!(
&mut output,
"?{}",
num.map_or(String::new(), |u| u.to_string())
)
.unwrap(),
FragmentType::Diagnostic(_)
| FragmentType::B { .. }
| FragmentType::BComposition(_, _)
| FragmentType::Y(_)
| FragmentType::YComposition(_, _) => {
if let Some(formula) = &self.formula {
write!(&mut output, "f{{{formula}}}",).unwrap();
} else {
write!(&mut output, "?",).unwrap();
}
}
FragmentType::Internal(Some(name), a, b) => write!(
&mut output,
"m{}:{}{}",
a.sequence_index + 1,
b.sequence_index + 1,
match name {
(BackboneNFragment::a, BackboneCFragment::x)
| (BackboneNFragment::b, BackboneCFragment::y)
| (BackboneNFragment::c, BackboneCFragment::z) => "",
(BackboneNFragment::a, BackboneCFragment::y) => "-CO",
(BackboneNFragment::a, BackboneCFragment::z) => "-CHNO",
(BackboneNFragment::b, BackboneCFragment::x) => "+CO",
(BackboneNFragment::b, BackboneCFragment::z) => "-NH",
(BackboneNFragment::c, BackboneCFragment::x) => "+CHNO",
(BackboneNFragment::c, BackboneCFragment::y) => "+NH",
}
)
.unwrap(),
FragmentType::Internal(None, a, b) => write!(
&mut output,
"m{}:{}",
a.sequence_index + 1,
b.sequence_index + 1
)
.unwrap(),
}
for loss in &self.neutral_loss {
match loss {
NeutralLoss::SideChainLoss(_, aa) => {
write!(&mut output, "-r[sidechain_{aa}]").unwrap();
}
l => write!(&mut output, "{l}").unwrap(),
}
}
if self.charge.value != 1 {
write!(&mut output, "^{}", self.charge.value).unwrap();
}
match self.deviation {
Some(Tolerance::Absolute(abs)) => write!(&mut output, "/{}", abs.value).unwrap(),
Some(Tolerance::Relative(ppm)) => write!(&mut output, "/{}ppm", ppm.value).unwrap(),
None => (),
}
if let Some(confidence) = self.confidence {
write!(&mut output, "*{confidence}").unwrap();
}
output
}
pub fn mz(&self, mode: MassMode) -> Option<MassOverCharge> {
self.formula.as_ref().map(|f| {
f.mass(mode)
/ crate::system::f64::Charge::new::<crate::system::charge::e>(
self.charge.value as f64,
)
})
}
pub fn ppm(&self, other: &Self, mode: MassMode) -> Option<Ratio> {
self.mz(mode)
.and_then(|mz| other.mz(mode).map(|omz| (mz, omz)))
.map(|(mz, omz)| mz.ppm(omz))
}
#[must_use]
pub fn new(
theoretical_mass: MolecularFormula,
charge: Charge,
peptidoform_ion_index: usize,
peptidoform_index: usize,
ion: FragmentType,
) -> Self {
Self {
formula: Some(theoretical_mass),
charge,
ion,
peptidoform_ion_index: Some(peptidoform_ion_index),
peptidoform_index: Some(peptidoform_index),
neutral_loss: Vec::new(),
deviation: None,
confidence: None,
auxiliary: false,
}
}
#[expect(clippy::too_many_arguments)]
#[must_use]
pub fn generate_all(
theoretical_mass: &Multi<MolecularFormula>,
peptidoform_ion_index: usize,
peptidoform_index: usize,
annotation: &FragmentType,
termini: &Multi<MolecularFormula>,
neutral_losses: &[Vec<NeutralLoss>],
charge_carriers: &mut CachedCharge,
charge_range: ChargeRange,
) -> Vec<Self> {
termini
.iter()
.cartesian_product(theoretical_mass.iter())
.cartesian_product(charge_carriers.range(charge_range))
.cartesian_product(std::iter::once(None).chain(neutral_losses.iter().map(Some)))
.map(|(((term, mass), charge), losses)| Self {
formula: Some(
term + mass
+ charge.formula_inner(SequencePosition::default(), peptidoform_index)
+ losses
.iter()
.flat_map(|l| l.iter())
.sum::<MolecularFormula>(),
),
charge: Charge::new::<crate::system::e>(charge.charge().value.try_into().unwrap()),
ion: annotation.clone(),
peptidoform_ion_index: Some(peptidoform_ion_index),
peptidoform_index: Some(peptidoform_index),
neutral_loss: losses.cloned().unwrap_or_default(),
deviation: None,
confidence: None,
auxiliary: false,
})
.collect()
}
#[must_use]
pub fn generate_series(
theoretical_mass: &Multi<MolecularFormula>,
peptidoform_ion_index: usize,
peptidoform_index: usize,
annotation: &FragmentType,
termini: &Multi<MolecularFormula>,
charge_carriers: &mut CachedCharge,
settings: &PossiblePrimaryIons,
) -> Vec<Self> {
termini
.iter()
.cartesian_product(theoretical_mass.iter())
.cartesian_product(charge_carriers.range(settings.1))
.cartesian_product(std::iter::once(None).chain(settings.0.iter().map(Some)))
.cartesian_product(settings.2.iter())
.map(|((((term, mass), charge), losses), variant)| Self {
formula: Some(
term + mass
+ charge.formula_inner(SequencePosition::default(), peptidoform_index)
+ losses
.iter()
.flat_map(|l| l.iter())
.sum::<MolecularFormula>()
+ molecular_formula!(H 1) * variant,
),
charge: Charge::new::<crate::system::e>(charge.charge().value.try_into().unwrap()),
ion: annotation.with_variant(*variant),
peptidoform_ion_index: Some(peptidoform_ion_index),
peptidoform_index: Some(peptidoform_index),
neutral_loss: losses.cloned().unwrap_or_default(),
deviation: None,
confidence: None,
auxiliary: false,
})
.collect()
}
#[must_use]
fn with_charge(&self, charge: &MolecularCharge) -> Self {
let formula = charge
.formula()
.with_labels(&[AmbiguousLabel::ChargeCarrier(charge.formula())]);
let c = Charge::new::<crate::system::charge::e>(
usize::try_from(formula.charge().value).unwrap(),
);
Self {
formula: Some(self.formula.clone().unwrap_or_default() + &formula),
charge: c,
..self.clone()
}
}
pub fn with_charge_range(
self,
charge_carriers: &mut CachedCharge,
charge_range: ChargeRange,
) -> impl Iterator<Item = Self> {
charge_carriers
.range(charge_range)
.into_iter()
.map(move |c| self.with_charge(&c))
}
#[must_use]
pub fn with_neutral_loss(&self, neutral_loss: &NeutralLoss) -> Self {
let mut new_neutral_loss = self.neutral_loss.clone();
new_neutral_loss.push(neutral_loss.clone());
Self {
formula: Some(self.formula.clone().unwrap_or_default() + neutral_loss),
neutral_loss: new_neutral_loss,
..self.clone()
}
}
#[must_use]
pub fn with_neutral_losses(&self, neutral_losses: &[NeutralLoss]) -> Vec<Self> {
let mut output = Vec::with_capacity(neutral_losses.len() + 1);
output.push(self.clone());
output.extend(
neutral_losses
.iter()
.map(|loss| self.with_neutral_loss(loss)),
);
output
}
}
impl Display for Fragment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}@{}{:+}{}",
self.ion,
self.mz(MassMode::Monoisotopic)
.map_or(String::new(), |mz| mz.value.to_string()),
self.charge.value,
self.neutral_loss
.iter()
.map(std::string::ToString::to_string)
.join("")
)
}
}
#[derive(
Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Debug, Serialize, Deserialize,
)]
#[non_exhaustive]
pub struct PeptidePosition {
pub sequence_index: SequencePosition,
pub series_number: usize,
pub sequence_length: usize,
}
impl PeptidePosition {
pub const fn n(sequence_index: SequencePosition, length: usize) -> Self {
Self {
sequence_index,
series_number: match sequence_index {
SequencePosition::NTerm => 0,
SequencePosition::Index(i) => i + 1,
SequencePosition::CTerm => length,
},
sequence_length: length,
}
}
pub const fn c(sequence_index: SequencePosition, length: usize) -> Self {
Self {
sequence_index,
series_number: match sequence_index {
SequencePosition::NTerm => length,
SequencePosition::Index(i) => length - i,
SequencePosition::CTerm => 0,
},
sequence_length: length,
}
}
pub fn is_n_terminal(&self) -> bool {
self.sequence_index == SequencePosition::NTerm
}
pub fn is_c_terminal(&self) -> bool {
self.sequence_index == SequencePosition::CTerm
}
#[must_use]
pub const fn flip_terminal(self) -> Self {
Self {
sequence_index: self.sequence_index,
series_number: self.sequence_length + 1 - self.series_number,
sequence_length: self.sequence_length,
}
}
}
include!("shared/glycan_position.rs");
impl GlycanPosition {
pub fn branch_names(&self) -> String {
self.branch
.iter()
.enumerate()
.map(|(i, (_, b))| {
if i == 0 {
char::from_u32(
(0x03B1..=0x03C9)
.chain(0x0391..=0x03A9)
.nth(*b)
.expect("Too many branches in glycan, out of greek letters"),
)
.unwrap()
.to_string()
} else if i == 1 {
"\'".repeat(*b)
} else {
format!(",{b}")
}
})
.collect::<String>()
}
pub fn label(&self) -> String {
format!("{}{}", self.series_number, self.branch_names())
}
pub fn attachment(&self) -> String {
self.attachment
.map(|(aa, pos)| format!("{aa}{pos}"))
.unwrap_or_default()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
pub enum DiagnosticPosition {
Glycan(GlycanPosition, MonoSaccharide),
GlycanCompositional(MonoSaccharide, Option<(AminoAcid, SequencePosition)>),
Peptide(PeptidePosition, AminoAcid),
Labile(Modification),
Reporter,
}
#[derive(
Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize, Default,
)]
pub enum SatelliteLabel {
#[default]
None,
A,
B,
}
impl std::fmt::Display for SatelliteLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::None => "",
Self::A => "a",
Self::B => "b",
}
)
}
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize, Default)]
#[expect(non_camel_case_types)]
pub enum FragmentType {
a(PeptidePosition, i8),
b(PeptidePosition, i8),
c(PeptidePosition, i8),
d(PeptidePosition, AminoAcid, u8, i8, SatelliteLabel),
v(PeptidePosition, AminoAcid, u8, i8),
w(PeptidePosition, AminoAcid, u8, i8, SatelliteLabel),
x(PeptidePosition, i8),
y(PeptidePosition, i8),
z(PeptidePosition, i8),
Y(Vec<GlycanPosition>),
B {
b: GlycanPosition,
y: Vec<GlycanPosition>,
end: Vec<GlycanPosition>,
},
BComposition(
Vec<(MonoSaccharide, isize)>,
Option<(AminoAcid, SequencePosition)>,
),
YComposition(
Vec<(MonoSaccharide, isize)>,
Option<(AminoAcid, SequencePosition)>,
),
Immonium(PeptidePosition, SequenceElement<SemiAmbiguous>),
PrecursorSideChainLoss(PeptidePosition, AminoAcid),
Diagnostic(DiagnosticPosition),
Internal(
Option<(BackboneNFragment, BackboneCFragment)>,
PeptidePosition,
PeptidePosition,
),
Unknown(Option<usize>),
#[default]
Precursor,
}
impl std::cmp::Ord for FragmentType {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(Self::Precursor, Self::Precursor) => Ordering::Equal,
(Self::Precursor, _) => Ordering::Less,
(_, Self::Precursor) => Ordering::Greater,
(Self::a(s, sv), Self::a(o, ov)) => s.cmp(o).then(sv.cmp(ov)),
(Self::a(_, _), _) => Ordering::Less,
(_, Self::a(_, _)) => Ordering::Greater,
(Self::b(s, sv), Self::b(o, ov)) => s.cmp(o).then(sv.cmp(ov)),
(Self::b(_, _), _) => Ordering::Less,
(_, Self::b(_, _)) => Ordering::Greater,
(Self::c(s, sv), Self::c(o, ov)) => s.cmp(o).then(sv.cmp(ov)),
(Self::c(_, _), _) => Ordering::Less,
(_, Self::c(_, _)) => Ordering::Greater,
(Self::x(s, sv), Self::x(o, ov)) => s.cmp(o).then(sv.cmp(ov)),
(Self::x(_, _), _) => Ordering::Less,
(_, Self::x(_, _)) => Ordering::Greater,
(Self::y(s, sv), Self::y(o, ov)) => s.cmp(o).then(sv.cmp(ov)),
(Self::y(_, _), _) => Ordering::Less,
(_, Self::y(_, _)) => Ordering::Greater,
(Self::z(s, sv), Self::z(o, ov)) => s.cmp(o).then(sv.cmp(ov)),
(Self::z(_, _), _) => Ordering::Less,
(_, Self::z(_, _)) => Ordering::Greater,
(Self::d(s, _, sd, sv, sl), Self::d(o, _, od, ov, ol)) => {
s.cmp(o).then(sd.cmp(od)).then(sv.cmp(ov)).then(sl.cmp(ol))
}
(Self::d(_, _, _, _, _), _) => Ordering::Less,
(_, Self::d(_, _, _, _, _)) => Ordering::Greater,
(Self::w(s, _, sd, sv, sl), Self::w(o, _, od, ov, ol)) => {
s.cmp(o).then(sd.cmp(od)).then(sv.cmp(ov)).then(sl.cmp(ol))
}
(Self::w(_, _, _, _, _), _) => Ordering::Less,
(_, Self::w(_, _, _, _, _)) => Ordering::Greater,
(Self::v(s, _, sd, sv), Self::v(o, _, od, ov)) => {
s.cmp(o).then(sd.cmp(od)).then(sv.cmp(ov))
}
(Self::v(_, _, _, _), _) => Ordering::Less,
(_, Self::v(_, _, _, _)) => Ordering::Greater,
(Self::Immonium(s, _), Self::Immonium(o, _)) => s.cmp(o),
(Self::Immonium(_, _), _) => Ordering::Less,
(_, Self::Immonium(_, _)) => Ordering::Greater,
(Self::PrecursorSideChainLoss(s, _), Self::PrecursorSideChainLoss(o, _)) => s.cmp(o),
(Self::PrecursorSideChainLoss(_, _), _) => Ordering::Less,
(_, Self::PrecursorSideChainLoss(_, _)) => Ordering::Greater,
(Self::Internal(st, sa, sb), Self::Internal(ot, oa, ob)) => {
sa.cmp(oa).then(sb.cmp(ob)).then(st.cmp(ot))
}
(Self::Internal(_, _, _), _) => Ordering::Less,
(_, Self::Internal(_, _, _)) => Ordering::Greater,
(Self::B { b: sb, y: sy, .. }, Self::B { b: ob, y: oy, .. }) => {
sy.len().cmp(&oy.len()).then(sb.cmp(ob))
}
(Self::Y(s), Self::Y(o)) => s.len().cmp(&o.len()),
(Self::B { y: sy, .. }, Self::Y(o)) => {
(sy.len() + 1).cmp(&o.len()).then(Ordering::Greater)
}
(Self::Y(s), Self::B { y: oy, .. }) => {
s.len().cmp(&(oy.len() + 1)).then(Ordering::Less)
}
(Self::B { .. }, _) => Ordering::Less,
(_, Self::B { .. }) => Ordering::Greater,
(Self::Y(_), _) => Ordering::Less,
(_, Self::Y(_)) => Ordering::Greater,
(Self::BComposition(s, sl), Self::BComposition(o, ol))
| (Self::YComposition(s, sl), Self::YComposition(o, ol)) => {
s.len().cmp(&o.len()).then(sl.cmp(ol))
}
(Self::BComposition(s, sl), Self::YComposition(o, ol)) => s
.len()
.cmp(&o.len())
.then(sl.cmp(ol))
.then(Ordering::Greater),
(Self::YComposition(s, sl), Self::BComposition(o, ol)) => {
s.len().cmp(&o.len()).then(sl.cmp(ol)).then(Ordering::Less)
}
(Self::BComposition(_, _), _) => Ordering::Less,
(_, Self::BComposition(_, _)) => Ordering::Greater,
(Self::YComposition(_, _), _) => Ordering::Less,
(_, Self::YComposition(_, _)) => Ordering::Greater,
(Self::Diagnostic(s), Self::Diagnostic(o)) => s.cmp(o),
(Self::Diagnostic(_), _) => Ordering::Less,
(_, Self::Diagnostic(_)) => Ordering::Greater,
(Self::Unknown(s), Self::Unknown(o)) => s.cmp(o),
}
}
}
impl std::cmp::PartialOrd for FragmentType {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl FragmentType {
#[must_use]
pub fn with_variant(&self, variant: i8) -> Self {
match self {
Self::a(p, _) => Self::a(*p, variant),
Self::b(p, _) => Self::b(*p, variant),
Self::c(p, _) => Self::c(*p, variant),
Self::d(p, a, d, _, l) => Self::d(*p, *a, *d, variant, *l),
Self::v(p, a, d, _) => Self::v(*p, *a, *d, variant),
Self::w(p, a, d, _, l) => Self::w(*p, *a, *d, variant, *l),
Self::x(p, _) => Self::x(*p, variant),
Self::y(p, _) => Self::y(*p, variant),
Self::z(p, _) => Self::z(*p, variant),
other => other.clone(),
}
}
pub const fn position(&self) -> Option<&PeptidePosition> {
match self {
Self::a(n, _)
| Self::b(n, _)
| Self::c(n, _)
| Self::d(n, _, _, _, _)
| Self::v(n, _, _, _)
| Self::w(n, _, _, _, _)
| Self::x(n, _)
| Self::y(n, _)
| Self::z(n, _)
| Self::Diagnostic(DiagnosticPosition::Peptide(n, _))
| Self::Immonium(n, _)
| Self::PrecursorSideChainLoss(n, _) => Some(n),
_ => None,
}
}
pub const fn glycan_position(&self) -> Option<&GlycanPosition> {
match self {
Self::Diagnostic(DiagnosticPosition::Glycan(b, _)) | Self::B { b, .. } => Some(b),
_ => None,
}
}
#[cfg(feature = "glycan-render")]
pub fn glycan_break_positions(
&self,
) -> Option<(Option<SequencePosition>, GlycanSelection<'_>)> {
match self {
Self::Diagnostic(DiagnosticPosition::Glycan(n, _)) => Some((
n.attachment.map(|(_, p)| p),
GlycanSelection::SingleSugar(n),
)),
Self::Y(breaks) => Some((
breaks.first().and_then(|p| p.attachment.map(|(_, p)| p)),
GlycanSelection::Subtree(None, breaks),
)),
Self::B { b, y, .. } => Some((
b.attachment.map(|(_, p)| p),
GlycanSelection::Subtree(Some(b), y),
)),
_ => None,
}
}
pub fn position_label(&self) -> Option<String> {
match self {
Self::a(n, _)
| Self::b(n, _)
| Self::c(n, _)
| Self::d(n, _, _, _, _)
| Self::v(n, _, _, _)
| Self::w(n, _, _, _, _)
| Self::x(n, _)
| Self::y(n, _)
| Self::z(n, _)
| Self::Diagnostic(DiagnosticPosition::Peptide(n, _))
| Self::Immonium(n, _)
| Self::PrecursorSideChainLoss(n, _) => Some(n.series_number.to_string()),
Self::Diagnostic(DiagnosticPosition::Glycan(n, _)) => Some(n.label()),
Self::Y(bonds) => Some(bonds.iter().map(GlycanPosition::label).join("Y")),
Self::B { b, y, end } => Some(
b.label()
+ "Y"
+ &y.iter()
.chain(end.iter())
.map(GlycanPosition::label)
.join("Y"),
),
Self::YComposition(sugars, _) | Self::BComposition(sugars, _) => Some(
sugars
.iter()
.map(|(sugar, amount)| format!("{sugar}{amount}"))
.join(""),
),
Self::Internal(_, pos1, pos2) => {
Some(format!("{}:{}", pos1.sequence_index, pos2.sequence_index,))
}
Self::Precursor
| Self::Unknown(_)
| Self::Diagnostic(
DiagnosticPosition::Labile(_)
| DiagnosticPosition::GlycanCompositional(_, _)
| DiagnosticPosition::Reporter,
) => None,
}
}
pub fn label(&self) -> (Option<String>, Cow<str>) {
let get_label = |ion: &'static str, v: i8| {
if v == 0 {
Cow::Borrowed(ion)
} else {
Cow::Owned(format!(
"{ion}{}",
if v < 0 {
"\'".repeat((-v) as usize)
} else {
"·".repeat(v as usize)
}
))
}
};
match self {
Self::a(_, v) => (None, get_label("a", *v)),
Self::b(_, v) => (None, get_label("b", *v)),
Self::c(_, v) => (None, get_label("c", *v)),
Self::d(_, _, n, v, l) => (
(*n != 0).then_some(n.to_string()),
Cow::Owned(format!(
"d{l}{}",
if *v < 0 {
"\'".repeat((-v) as usize)
} else {
"·".repeat(*v as usize)
}
)),
),
Self::v(_, _, n, v) => ((*n != 0).then_some(n.to_string()), get_label("v", *v)),
Self::w(_, _, n, v, l) => (
(*n != 0).then_some(n.to_string()),
Cow::Owned(format!(
"w{l}{}",
if *v < 0 {
"\'".repeat((-v) as usize)
} else {
"·".repeat(*v as usize)
}
)),
),
Self::x(_, v) => (None, get_label("x", *v)),
Self::y(_, v) => (None, get_label("y", *v)),
Self::z(_, v) => (None, get_label("z", *v)),
Self::B { .. } | Self::BComposition(_, _) => (None, Cow::Borrowed("B")),
Self::Y(_) | Self::YComposition(_, _) => (None, Cow::Borrowed("Y")),
Self::Diagnostic(DiagnosticPosition::Peptide(_, aa)) => (
None,
Cow::Owned(
aa.one_letter_code()
.map(|c| format!("d{c}"))
.or_else(|| aa.three_letter_code().map(|c| format!("d{c}")))
.unwrap_or_else(|| format!("d{}", aa.name())),
),
),
Self::Diagnostic(DiagnosticPosition::Reporter) => (None, Cow::Borrowed("r")),
Self::Diagnostic(DiagnosticPosition::Labile(m)) => (None, Cow::Owned(format!("d{m}"))),
Self::Diagnostic(
DiagnosticPosition::Glycan(_, sug)
| DiagnosticPosition::GlycanCompositional(sug, _),
) => (None, Cow::Owned(format!("d{sug}"))),
Self::Immonium(_, aa) => (
None,
Cow::Owned(
aa.aminoacid
.one_letter_code()
.map(|c| format!("i{c}"))
.or_else(|| aa.aminoacid.three_letter_code().map(|c| format!("i{c}")))
.unwrap_or_else(|| format!("i{}", aa.aminoacid.name())),
),
),
Self::PrecursorSideChainLoss(_, aa) => (
None,
Cow::Owned(
aa.one_letter_code()
.map(|c| format!("p-s{c}"))
.or_else(|| aa.three_letter_code().map(|c| format!("p-s{c}")))
.unwrap_or_else(|| format!("p-s{}", aa.name())),
),
),
Self::Precursor => (None, Cow::Borrowed("p")),
Self::Internal(fragmentation, _, _) => (
None,
Cow::Owned(format!(
"m{}",
fragmentation.map_or(String::new(), |(n, c)| format!("{n}:{c}")),
)),
),
Self::Unknown(series) => (
None,
Cow::Owned(format!(
"?{}",
series.map_or(String::new(), |s| s.to_string()),
)),
),
}
}
pub const fn kind(&self) -> FragmentKind {
match self {
Self::a(_, _) => FragmentKind::a,
Self::b(_, _) => FragmentKind::b,
Self::c(_, _) => FragmentKind::c,
Self::d(_, _, _, _, _) => FragmentKind::d,
Self::v(_, _, _, _) => FragmentKind::v,
Self::w(_, _, _, _, _) => FragmentKind::w,
Self::x(_, _) => FragmentKind::x,
Self::y(_, _) => FragmentKind::y,
Self::z(_, _) => FragmentKind::z,
Self::Y(_) | Self::YComposition(_, _) => FragmentKind::Y,
Self::Diagnostic(
DiagnosticPosition::Glycan(_, _) | DiagnosticPosition::GlycanCompositional(_, _),
)
| Self::B { .. }
| Self::BComposition(_, _) => FragmentKind::B,
Self::Diagnostic(_) => FragmentKind::diagnostic,
Self::Immonium(_, _) => FragmentKind::immonium,
Self::PrecursorSideChainLoss(_, _) => FragmentKind::precursor_side_chain_loss,
Self::Precursor => FragmentKind::precursor,
Self::Internal(_, _, _) => FragmentKind::internal,
Self::Unknown(_) => FragmentKind::unknown,
}
}
}
impl Display for FragmentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (sup, label) = self.label();
write!(
f,
"{}{}{}",
sup.unwrap_or_default(),
label,
self.position_label().unwrap_or_default()
)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[expect(non_camel_case_types)]
pub enum BackboneNFragment {
a,
b,
c,
}
impl Display for BackboneNFragment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::a => "a",
Self::b => "b",
Self::c => "c",
}
)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[expect(non_camel_case_types)]
pub enum BackboneCFragment {
x,
y,
z,
}
impl Display for BackboneCFragment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::x => "x",
Self::y => "y",
Self::z => "z",
}
)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[expect(non_camel_case_types)]
pub enum FragmentKind {
a,
b,
c,
d,
v,
w,
x,
y,
z,
Y,
B,
immonium,
precursor_side_chain_loss,
diagnostic,
internal,
precursor,
unknown,
}
impl Display for FragmentKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::a => "a",
Self::b => "b",
Self::c => "c",
Self::d => "d",
Self::x => "x",
Self::y => "y",
Self::v => "v",
Self::w => "w",
Self::z => "z",
Self::Y => "Y",
Self::B => "oxonium",
Self::immonium => "immonium",
Self::precursor_side_chain_loss => "precursor side chain loss",
Self::diagnostic => "diagnostic",
Self::internal => "m",
Self::precursor => "precursor",
Self::unknown => "unknown",
}
)
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
pub enum GlycanBreakPos {
End(GlycanPosition),
Y(GlycanPosition),
B(GlycanPosition),
}
impl GlycanBreakPos {
pub const fn position(&self) -> &GlycanPosition {
match self {
Self::B(p) | Self::End(p) | Self::Y(p) => p,
}
}
pub const fn label(&self) -> &str {
match self {
Self::End(_) => "End",
Self::Y(_) => "Y",
Self::B(_) => "B",
}
}
}
impl std::fmt::Display for GlycanBreakPos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.label(), self.position().label())
}
}
#[cfg(test)]
#[expect(clippy::missing_panics_doc)]
mod tests {
use crate::{AminoAcid, MultiChemical};
use super::*;
#[test]
fn neutral_loss() {
let a = Fragment::new(
AminoAcid::AsparticAcid.formulas()[0].clone(),
Charge::new::<crate::system::charge::e>(1),
0,
0,
FragmentType::Precursor,
);
let loss = a.with_neutral_losses(&[NeutralLoss::Loss(molecular_formula!(H 2 O 1))]);
dbg!(&a, &loss);
assert_eq!(a.formula, loss[0].formula);
assert_eq!(
a.formula.unwrap(),
&loss[1].formula.clone().unwrap() + &molecular_formula!(H 2 O 1)
);
}
#[test]
fn flip_terminal() {
let n0 = PeptidePosition::n(SequencePosition::Index(0), 2);
let n1 = PeptidePosition::n(SequencePosition::Index(1), 2);
let n2 = PeptidePosition::n(SequencePosition::Index(2), 2);
let c0 = PeptidePosition::c(SequencePosition::Index(0), 2);
let c1 = PeptidePosition::c(SequencePosition::Index(1), 2);
let c2 = PeptidePosition::c(SequencePosition::Index(2), 2);
assert_eq!(n0.flip_terminal(), c0);
assert_eq!(n1.flip_terminal(), c1);
assert_eq!(n2.flip_terminal(), c2);
}
}