use std::num::NonZeroU32;
use mzcore::{
chemistry::MassOutputMode,
prelude::{MassMode, PeptidoformIonSet},
system::thomson,
};
#[cfg(feature = "isotopes")]
use mzcore::{molecular_formula, system::MassOverCharge};
use mzdata::mzpeaks::PeakCollection;
#[cfg(feature = "isotopes")]
use crate::fragment::Isotope;
use crate::prelude::{AnnotatedSpectrum, Fragment, MatchingParameters};
pub trait AnnotatableSpectrum<Mode: MassOutputMode>: Sized {
fn empty_annotated(self, peptide: PeptidoformIonSet) -> AnnotatedSpectrum<Mode>;
fn annotate(
self,
peptide: PeptidoformIonSet,
theoretical_fragments: &[Fragment<Mode>],
parameters: &MatchingParameters,
mode: MassMode,
) -> AnnotatedSpectrum<Mode> {
let tolerance = match parameters.tolerance {
mzcore::quantities::Tolerance::Absolute(mz) => mzdata::prelude::Tolerance::Da(mz.value),
mzcore::quantities::Tolerance::Relative(ratio) => {
mzdata::prelude::Tolerance::PPM(ratio.get::<mzcore::system::ratio::ppm>())
}
};
#[cfg(feature = "isotopes")]
let isotope_shift = molecular_formula!([13 C 1] [12 C -1]).mass(MassMode::Monoisotopic);
let mut annotated = Self::empty_annotated(self, peptide);
for fragment in theoretical_fragments {
if let Some(mz) = fragment.mz(mode) {
if !parameters.mz_range.contains(&mz) {
continue;
}
if let Some(index) = annotated.peaks.search(mz.get::<thomson>(), tolerance) {
#[cfg(feature = "isotopes")]
if parameters.match_isotopes
&& let Some(formula) = &fragment.formula
{
let envelope =
formula.isotopic_distribution(parameters.isotope_minimum_probability);
let base = if mode == MassMode::Monoisotopic {
mz
} else {
formula.mass(MassMode::Monoisotopic) / fragment.charge.to_float()
};
let mut matched_envelope = Vec::with_capacity(envelope.len());
let mut matches = Vec::with_capacity(envelope.len());
for (index, intensity) in envelope.into_iter().enumerate() {
let isotope_mz =
base + (index as f64 * isotope_shift) / fragment.charge.to_float();
let peak =
annotated.peaks.search(isotope_mz.get::<thomson>(), tolerance);
matched_envelope.push((
isotope_mz.value,
peak.map_or(0.0, |i| annotated.peaks[i].mz.value),
intensity.sqrt(),
peak.map_or(0.0, |i| {
f64::from(annotated.peaks[i].intensity.sqrt())
}),
));
matches.push(peak);
}
let similarity = cosine_similarity(&matched_envelope);
if similarity >= parameters.isotope_filter {
for (index, peak) in matches.into_iter().enumerate() {
if let Some(peak) = peak {
let frag = fragment
.clone()
.with_isotope(&[(index as i32, Isotope::General)]);
let frag_index = annotated.peaks[peak]
.annotations
.binary_search(&frag)
.unwrap_or_else(|i| i);
annotated.peaks[peak].annotations.insert(frag_index, frag);
}
}
}
} else {
match annotated.peaks[index].annotations.binary_search(fragment) {
Ok(ai) | Err(ai) => {
annotated.peaks[index].annotations.insert(ai, fragment.clone())
}
}
}
#[cfg(not(feature = "isotopes"))]
{
match annotated.peaks[index].annotations.binary_search(fragment) {
Ok(ai) | Err(ai) => {
annotated.peaks[index].annotations.insert(ai, fragment.clone())
}
}
}
}
}
}
annotated
}
}
impl<T: Into<AnnotatedSpectrum<Mode>>, Mode: MassOutputMode> AnnotatableSpectrum<Mode> for T {
fn empty_annotated(self, peptide: PeptidoformIonSet) -> AnnotatedSpectrum<Mode> {
let mut spectrum: AnnotatedSpectrum<Mode> = self.into();
for (index, peptidoform_ion) in peptide.into_peptidoform_ions().into_iter().enumerate() {
spectrum.analytes.push(crate::mzspeclib::Analyte::new(
NonZeroU32::new(index as u32 + 1).unwrap(),
crate::mzspeclib::AnalyteTarget::PeptidoformIon(peptidoform_ion),
));
}
spectrum
}
}
#[cfg(feature = "isotopes")]
fn cosine_similarity(peaks: &[(f64, f64, f64, f64)]) -> f64 {
let sumx = peaks.iter().map(|(m1, m2, i1, i2)| m1 * m2 * i1 * i2).sum::<f64>();
let sumx2 = peaks.iter().map(|(m1, _, i1, _)| (m1 * i1).powi(2)).sum::<f64>().sqrt()
* peaks.iter().map(|(_, m2, _, i2)| (m2 * i2).powi(2)).sum::<f64>().sqrt();
sumx / sumx2
}