mscore/data/
peptide.rs

1use std::collections::{HashMap};
2use bincode::{Decode, Encode};
3use itertools::Itertools;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use crate::algorithm::peptide::{calculate_peptide_mono_isotopic_mass, calculate_peptide_product_ion_mono_isotopic_mass, peptide_sequence_to_atomic_composition};
7use crate::chemistry::amino_acid::{amino_acid_masses};
8use crate::chemistry::formulas::calculate_mz;
9use crate::chemistry::utility::{find_unimod_patterns, reshape_prosit_array, unimod_sequence_to_tokens};
10use crate::data::spectrum::MzSpectrum;
11use crate::simulation::annotation::{MzSpectrumAnnotated, ContributionSource, SignalAttributes, SourceType, PeakAnnotation};
12
13// helper types for easier reading
14type Mass = f64;
15type Abundance = f64;
16type IsotopeDistribution = Vec<(Mass, Abundance)>;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PeptideIon {
20    pub sequence: PeptideSequence,
21    pub charge: i32,
22    pub intensity: f64,
23}
24
25impl PeptideIon {
26    pub fn new(sequence: String, charge: i32, intensity: f64, peptide_id: Option<i32>) -> Self {
27        PeptideIon {
28            sequence: PeptideSequence::new(sequence, peptide_id),
29            charge,
30            intensity,
31        }
32    }
33    pub fn mz(&self) -> f64 {
34        calculate_mz(self.sequence.mono_isotopic_mass(), self.charge)
35    }
36
37    pub fn calculate_isotope_distribution(
38        &self,
39        mass_tolerance: f64,
40        abundance_threshold: f64,
41        max_result: i32,
42        intensity_min: f64,
43    ) -> IsotopeDistribution {
44
45        let atomic_composition: HashMap<String, i32> = self.sequence.atomic_composition().iter().map(|(k, v)| (k.to_string(), *v)).collect();
46
47        let distribution: IsotopeDistribution = crate::algorithm::isotope::generate_isotope_distribution(&atomic_composition, mass_tolerance, abundance_threshold, max_result)
48            .into_iter().filter(|&(_, abundance)| abundance > intensity_min).collect();
49
50        let mz_distribution = distribution.iter().map(|(mass, _)| calculate_mz(*mass, self.charge))
51            .zip(distribution.iter().map(|&(_, abundance)| abundance)).collect();
52
53        mz_distribution
54    }
55
56    pub fn calculate_isotopic_spectrum(
57        &self,
58        mass_tolerance: f64,
59        abundance_threshold: f64,
60        max_result: i32,
61        intensity_min: f64,
62    ) -> MzSpectrum {
63        let isotopic_distribution = self.calculate_isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
64        MzSpectrum::new(isotopic_distribution.iter().map(|(mz, _)| *mz).collect(), isotopic_distribution.iter().map(|(_, abundance)| *abundance).collect()) * self.intensity
65    }
66
67    pub fn calculate_isotopic_spectrum_annotated(
68        &self,
69        mass_tolerance: f64,
70        abundance_threshold: f64,
71        max_result: i32,
72        intensity_min: f64,
73    ) -> MzSpectrumAnnotated {
74        let isotopic_distribution = self.calculate_isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
75        let mut annotations = Vec::new();
76        let mut isotope_counter = 0;
77        let mut previous_mz = isotopic_distribution[0].0;
78
79
80
81        for (mz, abundance) in isotopic_distribution.iter() {
82
83            let ppm_tolerance = (mz / 1e6) * 25.0;
84
85            if (mz - previous_mz).abs() > ppm_tolerance {
86                isotope_counter += 1;
87                previous_mz = *mz;
88            }
89
90            let signal_attributes = SignalAttributes {
91                charge_state: self.charge,
92                peptide_id: self.sequence.peptide_id.unwrap_or(-1),
93                isotope_peak: isotope_counter,
94                description: None,
95            };
96
97            let contribution_source = ContributionSource {
98                intensity_contribution: *abundance,
99                source_type: SourceType::Signal,
100                signal_attributes: Some(signal_attributes)
101            };
102
103            annotations.push(PeakAnnotation {
104                contributions: vec![contribution_source]
105            });
106        }
107
108        MzSpectrumAnnotated::new(isotopic_distribution.iter().map(|(mz, _)| *mz).collect(), isotopic_distribution.iter().map(|(_, abundance)| *abundance).collect(), annotations)
109    }
110}
111
112#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
113pub enum FragmentType { A, B, C, X, Y, Z, }
114
115// implement to string for fragment type
116impl std::fmt::Display for FragmentType {
117    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
118        match self {
119            FragmentType::A => write!(f, "a"),
120            FragmentType::B => write!(f, "b"),
121            FragmentType::C => write!(f, "c"),
122            FragmentType::X => write!(f, "x"),
123            FragmentType::Y => write!(f, "y"),
124            FragmentType::Z => write!(f, "z"),
125        }
126    }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PeptideProductIon {
131    pub kind: FragmentType,
132    pub ion: PeptideIon,
133}
134
135impl PeptideProductIon {
136    pub fn new(kind: FragmentType, sequence: String, charge: i32, intensity: f64, peptide_id: Option<i32>) -> Self {
137        PeptideProductIon {
138            kind,
139            ion: PeptideIon {
140                sequence: PeptideSequence::new(sequence, peptide_id),
141                charge,
142                intensity,
143            },
144        }
145    }
146
147    pub fn mono_isotopic_mass(&self) -> f64 {
148        calculate_peptide_product_ion_mono_isotopic_mass(self.ion.sequence.sequence.as_str(), self.kind)
149    }
150
151    pub fn atomic_composition(&self) -> HashMap<&str, i32> {
152
153        let mut composition = peptide_sequence_to_atomic_composition(&self.ion.sequence);
154
155        match self.kind {
156            FragmentType::A => {
157                *composition.entry("H").or_insert(0) -= 2;
158                *composition.entry("O").or_insert(0) -= 2;
159                *composition.entry("C").or_insert(0) -= 1;
160            },
161
162            FragmentType::B => {
163                // B: peptide_mass - Water
164                *composition.entry("H").or_insert(0) -= 2;
165                *composition.entry("O").or_insert(0) -= 1;
166            },
167
168            FragmentType::C => {
169                // C: peptide_mass + NH3 - Water
170                *composition.entry("H").or_insert(0) += 1;
171                *composition.entry("N").or_insert(0) += 1;
172                *composition.entry("O").or_insert(0) -= 1;
173            },
174
175            FragmentType::X => {
176                // X: peptide_mass + CO + 2*H - Water
177                *composition.entry("C").or_insert(0) += 1;
178                *composition.entry("O").or_insert(0) += 1;
179            },
180
181            FragmentType::Y => {
182                ()
183            },
184
185            FragmentType::Z => {
186                *composition.entry("H").or_insert(0) -= 1;
187                *composition.entry("N").or_insert(0) -= 3;
188            },
189        }
190        composition
191    }
192
193    pub fn mz(&self) -> f64 {
194        calculate_mz(self.mono_isotopic_mass(), self.ion.charge)
195    }
196
197    pub fn isotope_distribution(
198        &self,
199        mass_tolerance: f64,
200        abundance_threshold: f64,
201        max_result: i32,
202        intensity_min: f64,
203    ) -> IsotopeDistribution {
204
205        let atomic_composition: HashMap<String, i32> = self.atomic_composition().iter().map(|(k, v)| (k.to_string(), *v)).collect();
206
207        let distribution: IsotopeDistribution = crate::algorithm::isotope::generate_isotope_distribution(&atomic_composition, mass_tolerance, abundance_threshold, max_result)
208            .into_iter().filter(|&(_, abundance)| abundance > intensity_min).collect();
209
210        let mz_distribution = distribution.iter().map(|(mass, _)| calculate_mz(*mass, self.ion.charge)).zip(distribution.iter().map(|&(_, abundance)| abundance)).collect();
211
212        mz_distribution
213    }
214
215    /// Calculate the isotope distribution of the complementary fragment.
216    ///
217    /// This is used for quad-selection dependent isotope transmission calculations.
218    /// The complementary fragment is the portion of the precursor that remains
219    /// after the fragment ion is produced.
220    ///
221    /// # Arguments
222    ///
223    /// * `precursor_composition` - atomic composition of the full precursor
224    /// * `mass_tolerance` - mass tolerance for isotope distribution calculation
225    /// * `abundance_threshold` - minimum abundance threshold
226    /// * `max_result` - maximum number of isotope peaks
227    ///
228    /// # Returns
229    ///
230    /// * `Vec<(f64, f64)>` - complementary fragment isotope distribution as (mass, abundance) pairs
231    pub fn complementary_isotope_distribution(
232        &self,
233        precursor_composition: &HashMap<&str, i32>,
234        mass_tolerance: f64,
235        abundance_threshold: f64,
236        max_result: i32,
237    ) -> Vec<(f64, f64)> {
238        let fragment_composition = self.atomic_composition();
239        let complementary_composition = crate::algorithm::peptide::calculate_complementary_fragment_composition(
240            precursor_composition,
241            &fragment_composition,
242        );
243
244        crate::algorithm::isotope::generate_isotope_distribution(
245            &complementary_composition,
246            mass_tolerance,
247            abundance_threshold,
248            max_result,
249        )
250    }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
254pub struct PeptideSequence {
255    pub sequence: String,
256    pub peptide_id: Option<i32>,
257}
258
259impl PeptideSequence {
260    pub fn new(raw_sequence: String, peptide_id: Option<i32>) -> Self {
261
262        // constructor will parse the sequence and check if it is valid
263        let pattern = Regex::new(r"\[UNIMOD:(\d+)]").unwrap();
264
265        // remove the modifications from the sequence
266        let sequence = pattern.replace_all(&raw_sequence, "").to_string();
267
268        // check if all remaining characters are valid amino acids
269        let valid_amino_acids = sequence.chars().all(|c| amino_acid_masses().contains_key(&c.to_string()[..]));
270        if !valid_amino_acids {
271            panic!("Invalid amino acid sequence, use only valid amino acids: ARNDCQEGHILKMFPSTWYVU, and modifications in the format [UNIMOD:ID]");
272        }
273
274        PeptideSequence { sequence: raw_sequence, peptide_id }
275    }
276
277    pub fn mono_isotopic_mass(&self) -> f64 {
278        calculate_peptide_mono_isotopic_mass(self)
279    }
280
281    pub fn atomic_composition(&self) -> HashMap<&str, i32> {
282        peptide_sequence_to_atomic_composition(self)
283    }
284
285    pub fn to_tokens(&self, group_modifications: bool) -> Vec<String> {
286        unimod_sequence_to_tokens(&*self.sequence, group_modifications)
287    }
288
289    pub fn to_sage_representation(&self) -> (String, Vec<f64>) {
290        find_unimod_patterns(&*self.sequence)
291    }
292
293    pub fn amino_acid_count(&self) -> usize {
294        self.to_tokens(true).len()
295    }
296
297    pub fn calculate_mono_isotopic_product_ion_spectrum(&self, charge: i32, fragment_type: FragmentType) -> MzSpectrum {
298        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
299        product_ions.generate_mono_isotopic_spectrum()
300    }
301
302    pub fn calculate_mono_isotopic_product_ion_spectrum_annotated(&self, charge: i32, fragment_type: FragmentType) -> MzSpectrumAnnotated {
303        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
304        product_ions.generate_mono_isotopic_spectrum_annotated()
305    }
306
307    pub fn calculate_isotopic_product_ion_spectrum(&self, charge: i32, fragment_type: FragmentType, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
308        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
309        product_ions.generate_isotopic_spectrum(mass_tolerance, abundance_threshold, max_result, intensity_min)
310    }
311
312    pub fn calculate_isotopic_product_ion_spectrum_annotated(&self, charge: i32, fragment_type: FragmentType, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
313        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
314        product_ions.generate_isotopic_spectrum_annotated(mass_tolerance, abundance_threshold, max_result, intensity_min)
315    }
316
317    pub fn calculate_product_ion_series(&self, target_charge: i32, fragment_type: FragmentType) -> PeptideProductIonSeries {
318        // TODO: check for n-terminal modifications
319        let tokens = unimod_sequence_to_tokens(self.sequence.as_str(), true);
320        let mut n_terminal_ions = Vec::new();
321        let mut c_terminal_ions = Vec::new();
322
323        // Generate n ions
324        for i in 1..tokens.len() {
325            let n_ion_seq = tokens[..i].join("");
326            n_terminal_ions.push(PeptideProductIon {
327                kind: match fragment_type {
328                    FragmentType::A => FragmentType::A,
329                    FragmentType::B => FragmentType::B,
330                    FragmentType::C => FragmentType::C,
331                    FragmentType::X => FragmentType::A,
332                    FragmentType::Y => FragmentType::B,
333                    FragmentType::Z => FragmentType::C,
334                },
335                ion: PeptideIon {
336                    sequence: PeptideSequence {
337                        sequence: n_ion_seq,
338                        peptide_id: self.peptide_id,
339                    },
340                    charge: target_charge,
341                    intensity: 1.0, // Placeholder intensity
342                },
343            });
344        }
345
346        // Generate c ions
347        for i in 1..tokens.len() {
348            let c_ion_seq = tokens[tokens.len() - i..].join("");
349            c_terminal_ions.push(PeptideProductIon {
350                kind: match fragment_type {
351                    FragmentType::A => FragmentType::X,
352                    FragmentType::B => FragmentType::Y,
353                    FragmentType::C => FragmentType::Z,
354                    FragmentType::X => FragmentType::X,
355                    FragmentType::Y => FragmentType::Y,
356                    FragmentType::Z => FragmentType::Z,
357                },
358                ion: PeptideIon {
359                    sequence: PeptideSequence {
360                        sequence: c_ion_seq,
361                        peptide_id: self.peptide_id,
362                    },
363                    charge: target_charge,
364                    intensity: 1.0, // Placeholder intensity
365                },
366            });
367        }
368
369        PeptideProductIonSeries::new(target_charge, n_terminal_ions, c_terminal_ions)
370    }
371
372    pub fn associate_with_predicted_intensities(
373        &self,
374        // TODO: check docs of prosit if charge is meant as precursor charge or max charge of fragments to generate
375        charge: i32,
376        fragment_type: FragmentType,
377        flat_intensities: Vec<f64>,
378        normalize: bool,
379        half_charge_one: bool,
380    ) -> PeptideProductIonSeriesCollection {
381
382        let reshaped_intensities = reshape_prosit_array(flat_intensities);
383        let max_charge = std::cmp::min(charge, 3).max(1); // Ensure at least 1 for loop range
384        let mut sum_intensity = if normalize { 0.0 } else { 1.0 };
385        let num_tokens = self.amino_acid_count() - 1; // Full sequence length is not counted as fragment, since nothing is cleaved off, therefore -1
386
387        let mut peptide_ion_collection = Vec::new();
388
389        if normalize {
390            for z in 1..=max_charge {
391
392                let intensity_c: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[0][z as usize - 1]).filter(|&x| x > 0.0).collect();
393                let intensity_n: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[1][z as usize - 1]).filter(|&x| x > 0.0).collect();
394
395                sum_intensity += intensity_n.iter().sum::<f64>() + intensity_c.iter().sum::<f64>();
396            }
397        }
398
399        for z in 1..=max_charge {
400
401            let mut product_ions = self.calculate_product_ion_series(z, fragment_type);
402            let intensity_n: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[1][z as usize - 1]).collect();
403            let intensity_c: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[0][z as usize - 1]).collect(); // Reverse for y
404
405            let adjusted_sum_intensity = if max_charge == 1 && half_charge_one { sum_intensity * 2.0 } else { sum_intensity };
406
407            for (i, ion) in product_ions.n_ions.iter_mut().enumerate() {
408                ion.ion.intensity = intensity_n[i] / adjusted_sum_intensity;
409            }
410            for (i, ion) in product_ions.c_ions.iter_mut().enumerate() {
411                ion.ion.intensity = intensity_c[i] / adjusted_sum_intensity;
412            }
413
414            peptide_ion_collection.push(PeptideProductIonSeries::new(z, product_ions.n_ions, product_ions.c_ions));
415        }
416
417        PeptideProductIonSeriesCollection::new(peptide_ion_collection)
418    }
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct PeptideProductIonSeries {
423    pub charge: i32,
424    pub n_ions: Vec<PeptideProductIon>,
425    pub c_ions: Vec<PeptideProductIon>,
426}
427
428impl PeptideProductIonSeries {
429    pub fn new(charge: i32, n_ions: Vec<PeptideProductIon>, c_ions: Vec<PeptideProductIon>) -> Self {
430        PeptideProductIonSeries {
431            charge,
432            n_ions,
433            c_ions,
434        }
435    }
436
437    pub fn generate_mono_isotopic_spectrum(&self) -> MzSpectrum {
438        let mz_i_n = self.n_ions.iter().map(|ion| (ion.mz(), ion.ion.intensity)).collect_vec();
439        let mz_i_c = self.c_ions.iter().map(|ion| (ion.mz(), ion.ion.intensity)).collect_vec();
440        let n_spectrum = MzSpectrum::new(mz_i_n.iter().map(|(mz, _)| *mz).collect(), mz_i_n.iter().map(|(_, abundance)| *abundance).collect());
441        let c_spectrum = MzSpectrum::new(mz_i_c.iter().map(|(mz, _)| *mz).collect(), mz_i_c.iter().map(|(_, abundance)| *abundance).collect());
442        MzSpectrum::from_collection(vec![n_spectrum, c_spectrum]).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
443    }
444
445    pub fn generate_mono_isotopic_spectrum_annotated(&self) -> MzSpectrumAnnotated {
446        let mut annotations: Vec<PeakAnnotation> = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
447        let mut mz_values = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
448        let mut intensity_values = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
449
450        for (index, n_ion) in self.n_ions.iter().enumerate() {
451            let kind = n_ion.kind;
452            let charge = n_ion.ion.charge;
453            let mz = n_ion.mz();
454            let intensity = n_ion.ion.intensity;
455            let signal_attributes = SignalAttributes {
456                charge_state: charge,
457                peptide_id: n_ion.ion.sequence.peptide_id.unwrap_or(-1),
458                isotope_peak: 0,
459                description: Some(format!("{}_{}_{}", kind, index + 1, 0)),
460            };
461            let contribution_source = ContributionSource {
462                intensity_contribution: intensity,
463                source_type: SourceType::Signal,
464                signal_attributes: Some(signal_attributes)
465            };
466
467            annotations.push(PeakAnnotation {
468                contributions: vec![contribution_source]
469            });
470            mz_values.push(mz);
471            intensity_values.push(intensity);
472        }
473
474        for (index, c_ion) in self.c_ions.iter().enumerate() {
475            let kind = c_ion.kind;
476            let charge = c_ion.ion.charge;
477            let mz = c_ion.mz();
478            let intensity = c_ion.ion.intensity;
479            let signal_attributes = SignalAttributes {
480                charge_state: charge,
481                peptide_id: c_ion.ion.sequence.peptide_id.unwrap_or(-1),
482                isotope_peak: 0,
483                description: Some(format!("{}_{}_{}", kind, index + 1, 0)),
484            };
485            let contribution_source = ContributionSource {
486                intensity_contribution: intensity,
487                source_type: SourceType::Signal,
488                signal_attributes: Some(signal_attributes)
489            };
490
491            annotations.push(PeakAnnotation {
492                contributions: vec![contribution_source]
493            });
494            mz_values.push(mz);
495            intensity_values.push(intensity);
496        }
497
498        MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
499    }
500
501    pub fn generate_isotopic_spectrum(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
502        let mut spectra: Vec<MzSpectrum> = Vec::new();
503
504        for ion in &self.n_ions {
505            let n_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
506            let spectrum = MzSpectrum::new(n_isotopes.iter().map(|(mz, _)| *mz).collect(), n_isotopes.iter().map(|(_, abundance)| *abundance * ion.ion.intensity).collect());
507            spectra.push(spectrum);
508        }
509
510        for ion in &self.c_ions {
511            let c_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
512            let spectrum = MzSpectrum::new(c_isotopes.iter().map(|(mz, _)| *mz).collect(), c_isotopes.iter().map(|(_, abundance)| *abundance * ion.ion.intensity).collect());
513            spectra.push(spectrum);
514        }
515
516        MzSpectrum::from_collection(spectra).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
517    }
518
519    pub fn generate_isotopic_spectrum_annotated(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
520        let mut annotations: Vec<PeakAnnotation> = Vec::new();
521        let mut mz_values = Vec::new();
522        let mut intensity_values = Vec::new();
523
524        for (index, ion) in self.n_ions.iter().enumerate() {
525            let n_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
526            let mut isotope_counter = 0;
527            let mut previous_mz = n_isotopes[0].0;
528
529            for (mz, abundance) in n_isotopes.iter() {
530                let ppm_tolerance = (mz / 1e6) * 25.0;
531
532                if (mz - previous_mz).abs() > ppm_tolerance {
533                    isotope_counter += 1;
534                    previous_mz = *mz;
535                }
536
537                let signal_attributes = SignalAttributes {
538                    charge_state: ion.ion.charge,
539                    peptide_id: ion.ion.sequence.peptide_id.unwrap_or(-1),
540                    isotope_peak: isotope_counter,
541                    // use convention of 1-based indexing for fragment ion enumeration
542                    description: Some(format!("{}_{}_{}", ion.kind, index + 1, isotope_counter)),
543                };
544
545                let contribution_source = ContributionSource {
546                    intensity_contribution: *abundance * ion.ion.intensity,
547                    source_type: SourceType::Signal,
548                    signal_attributes: Some(signal_attributes)
549                };
550
551                annotations.push(PeakAnnotation {
552                    contributions: vec![contribution_source]
553                });
554                mz_values.push(*mz);
555                intensity_values.push(*abundance * ion.ion.intensity);
556            }
557        }
558
559        for (index, ion) in self.c_ions.iter().enumerate() {
560            let c_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
561            let mut isotope_counter = 0;
562            let mut previous_mz = c_isotopes[0].0;
563
564            for (mz, abundance) in c_isotopes.iter() {
565                let ppm_tolerance = (mz / 1e6) * 25.0;
566
567                if (mz - previous_mz).abs() > ppm_tolerance {
568                    isotope_counter += 1;
569                    previous_mz = *mz;
570                }
571
572                let signal_attributes = SignalAttributes {
573                    charge_state: ion.ion.charge,
574                    peptide_id: ion.ion.sequence.peptide_id.unwrap_or(-1),
575                    isotope_peak: isotope_counter,
576                    description: Some(format!("{}_{}_{}", ion.kind, index + 1, isotope_counter)),
577                };
578
579                let contribution_source = ContributionSource {
580                    intensity_contribution: *abundance * ion.ion.intensity,
581                    source_type: SourceType::Signal,
582                    signal_attributes: Some(signal_attributes)
583                };
584
585                annotations.push(PeakAnnotation {
586                    contributions: vec![contribution_source]
587                });
588
589                mz_values.push(*mz);
590                intensity_values.push(*abundance * ion.ion.intensity);
591            }
592        }
593        MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
594    }
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct PeptideProductIonSeriesCollection {
599    pub peptide_ions: Vec<PeptideProductIonSeries>,
600}
601impl PeptideProductIonSeriesCollection {
602    pub fn new(peptide_ions: Vec<PeptideProductIonSeries>) -> Self {
603        PeptideProductIonSeriesCollection {
604            peptide_ions,
605        }
606    }
607
608    pub fn find_ion_series(&self, charge: i32) -> Option<&PeptideProductIonSeries> {
609        self.peptide_ions.iter().find(|ion_series| ion_series.charge == charge)
610    }
611
612    pub fn generate_isotopic_spectrum(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
613        let mut spectra: Vec<MzSpectrum> = Vec::new();
614
615        for ion_series in &self.peptide_ions {
616            let isotopic_spectrum = ion_series.generate_isotopic_spectrum(mass_tolerance, abundance_threshold, max_result, intensity_min);
617            spectra.push(isotopic_spectrum);
618        }
619
620        MzSpectrum::from_collection(spectra).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
621    }
622
623    pub fn generate_isotopic_spectrum_annotated(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
624        let mut annotations: Vec<PeakAnnotation> = Vec::new();
625        let mut mz_values = Vec::new();
626        let mut intensity_values = Vec::new();
627
628        for ion_series in &self.peptide_ions {
629            let isotopic_spectrum = ion_series.generate_isotopic_spectrum_annotated(mass_tolerance, abundance_threshold, max_result, intensity_min);
630            for (mz, intensity) in isotopic_spectrum.mz.iter().zip(isotopic_spectrum.intensity.iter()) {
631                mz_values.push(*mz);
632                intensity_values.push(*intensity);
633            }
634            annotations.extend(isotopic_spectrum.annotations.iter().cloned());
635        }
636
637        MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
638    }
639}