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
216#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
217pub struct PeptideSequence {
218    pub sequence: String,
219    pub peptide_id: Option<i32>,
220}
221
222impl PeptideSequence {
223    pub fn new(raw_sequence: String, peptide_id: Option<i32>) -> Self {
224
225        // constructor will parse the sequence and check if it is valid
226        let pattern = Regex::new(r"\[UNIMOD:(\d+)]").unwrap();
227
228        // remove the modifications from the sequence
229        let sequence = pattern.replace_all(&raw_sequence, "").to_string();
230
231        // check if all remaining characters are valid amino acids
232        let valid_amino_acids = sequence.chars().all(|c| amino_acid_masses().contains_key(&c.to_string()[..]));
233        if !valid_amino_acids {
234            panic!("Invalid amino acid sequence, use only valid amino acids: ARNDCQEGHILKMFPSTWYVU, and modifications in the format [UNIMOD:ID]");
235        }
236
237        PeptideSequence { sequence: raw_sequence, peptide_id }
238    }
239
240    pub fn mono_isotopic_mass(&self) -> f64 {
241        calculate_peptide_mono_isotopic_mass(self)
242    }
243
244    pub fn atomic_composition(&self) -> HashMap<&str, i32> {
245        peptide_sequence_to_atomic_composition(self)
246    }
247
248    pub fn to_tokens(&self, group_modifications: bool) -> Vec<String> {
249        unimod_sequence_to_tokens(&*self.sequence, group_modifications)
250    }
251
252    pub fn to_sage_representation(&self) -> (String, Vec<f64>) {
253        find_unimod_patterns(&*self.sequence)
254    }
255
256    pub fn amino_acid_count(&self) -> usize {
257        self.to_tokens(true).len()
258    }
259
260    pub fn calculate_mono_isotopic_product_ion_spectrum(&self, charge: i32, fragment_type: FragmentType) -> MzSpectrum {
261        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
262        product_ions.generate_mono_isotopic_spectrum()
263    }
264
265    pub fn calculate_mono_isotopic_product_ion_spectrum_annotated(&self, charge: i32, fragment_type: FragmentType) -> MzSpectrumAnnotated {
266        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
267        product_ions.generate_mono_isotopic_spectrum_annotated()
268    }
269
270    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 {
271        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
272        product_ions.generate_isotopic_spectrum(mass_tolerance, abundance_threshold, max_result, intensity_min)
273    }
274
275    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 {
276        let product_ions = self.calculate_product_ion_series(charge, fragment_type);
277        product_ions.generate_isotopic_spectrum_annotated(mass_tolerance, abundance_threshold, max_result, intensity_min)
278    }
279
280    pub fn calculate_product_ion_series(&self, target_charge: i32, fragment_type: FragmentType) -> PeptideProductIonSeries {
281        // TODO: check for n-terminal modifications
282        let tokens = unimod_sequence_to_tokens(self.sequence.as_str(), true);
283        let mut n_terminal_ions = Vec::new();
284        let mut c_terminal_ions = Vec::new();
285
286        // Generate n ions
287        for i in 1..tokens.len() {
288            let n_ion_seq = tokens[..i].join("");
289            n_terminal_ions.push(PeptideProductIon {
290                kind: match fragment_type {
291                    FragmentType::A => FragmentType::A,
292                    FragmentType::B => FragmentType::B,
293                    FragmentType::C => FragmentType::C,
294                    FragmentType::X => FragmentType::A,
295                    FragmentType::Y => FragmentType::B,
296                    FragmentType::Z => FragmentType::C,
297                },
298                ion: PeptideIon {
299                    sequence: PeptideSequence {
300                        sequence: n_ion_seq,
301                        peptide_id: self.peptide_id,
302                    },
303                    charge: target_charge,
304                    intensity: 1.0, // Placeholder intensity
305                },
306            });
307        }
308
309        // Generate c ions
310        for i in 1..tokens.len() {
311            let c_ion_seq = tokens[tokens.len() - i..].join("");
312            c_terminal_ions.push(PeptideProductIon {
313                kind: match fragment_type {
314                    FragmentType::A => FragmentType::X,
315                    FragmentType::B => FragmentType::Y,
316                    FragmentType::C => FragmentType::Z,
317                    FragmentType::X => FragmentType::X,
318                    FragmentType::Y => FragmentType::Y,
319                    FragmentType::Z => FragmentType::Z,
320                },
321                ion: PeptideIon {
322                    sequence: PeptideSequence {
323                        sequence: c_ion_seq,
324                        peptide_id: self.peptide_id,
325                    },
326                    charge: target_charge,
327                    intensity: 1.0, // Placeholder intensity
328                },
329            });
330        }
331
332        PeptideProductIonSeries::new(target_charge, n_terminal_ions, c_terminal_ions)
333    }
334
335    pub fn associate_with_predicted_intensities(
336        &self,
337        // TODO: check docs of prosit if charge is meant as precursor charge or max charge of fragments to generate
338        charge: i32,
339        fragment_type: FragmentType,
340        flat_intensities: Vec<f64>,
341        normalize: bool,
342        half_charge_one: bool,
343    ) -> PeptideProductIonSeriesCollection {
344
345        let reshaped_intensities = reshape_prosit_array(flat_intensities);
346        let max_charge = std::cmp::min(charge, 3).max(1); // Ensure at least 1 for loop range
347        let mut sum_intensity = if normalize { 0.0 } else { 1.0 };
348        let num_tokens = self.amino_acid_count() - 1; // Full sequence length is not counted as fragment, since nothing is cleaved off, therefore -1
349
350        let mut peptide_ion_collection = Vec::new();
351
352        if normalize {
353            for z in 1..=max_charge {
354
355                let intensity_c: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[0][z as usize - 1]).filter(|&x| x > 0.0).collect();
356                let intensity_n: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[1][z as usize - 1]).filter(|&x| x > 0.0).collect();
357
358                sum_intensity += intensity_n.iter().sum::<f64>() + intensity_c.iter().sum::<f64>();
359            }
360        }
361
362        for z in 1..=max_charge {
363
364            let mut product_ions = self.calculate_product_ion_series(z, fragment_type);
365            let intensity_n: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[1][z as usize - 1]).collect();
366            let intensity_c: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[0][z as usize - 1]).collect(); // Reverse for y
367
368            let adjusted_sum_intensity = if max_charge == 1 && half_charge_one { sum_intensity * 2.0 } else { sum_intensity };
369
370            for (i, ion) in product_ions.n_ions.iter_mut().enumerate() {
371                ion.ion.intensity = intensity_n[i] / adjusted_sum_intensity;
372            }
373            for (i, ion) in product_ions.c_ions.iter_mut().enumerate() {
374                ion.ion.intensity = intensity_c[i] / adjusted_sum_intensity;
375            }
376
377            peptide_ion_collection.push(PeptideProductIonSeries::new(z, product_ions.n_ions, product_ions.c_ions));
378        }
379
380        PeptideProductIonSeriesCollection::new(peptide_ion_collection)
381    }
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct PeptideProductIonSeries {
386    pub charge: i32,
387    pub n_ions: Vec<PeptideProductIon>,
388    pub c_ions: Vec<PeptideProductIon>,
389}
390
391impl PeptideProductIonSeries {
392    pub fn new(charge: i32, n_ions: Vec<PeptideProductIon>, c_ions: Vec<PeptideProductIon>) -> Self {
393        PeptideProductIonSeries {
394            charge,
395            n_ions,
396            c_ions,
397        }
398    }
399
400    pub fn generate_mono_isotopic_spectrum(&self) -> MzSpectrum {
401        let mz_i_n = self.n_ions.iter().map(|ion| (ion.mz(), ion.ion.intensity)).collect_vec();
402        let mz_i_c = self.c_ions.iter().map(|ion| (ion.mz(), ion.ion.intensity)).collect_vec();
403        let n_spectrum = MzSpectrum::new(mz_i_n.iter().map(|(mz, _)| *mz).collect(), mz_i_n.iter().map(|(_, abundance)| *abundance).collect());
404        let c_spectrum = MzSpectrum::new(mz_i_c.iter().map(|(mz, _)| *mz).collect(), mz_i_c.iter().map(|(_, abundance)| *abundance).collect());
405        MzSpectrum::from_collection(vec![n_spectrum, c_spectrum]).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
406    }
407
408    pub fn generate_mono_isotopic_spectrum_annotated(&self) -> MzSpectrumAnnotated {
409        let mut annotations: Vec<PeakAnnotation> = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
410        let mut mz_values = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
411        let mut intensity_values = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
412
413        for (index, n_ion) in self.n_ions.iter().enumerate() {
414            let kind = n_ion.kind;
415            let charge = n_ion.ion.charge;
416            let mz = n_ion.mz();
417            let intensity = n_ion.ion.intensity;
418            let signal_attributes = SignalAttributes {
419                charge_state: charge,
420                peptide_id: n_ion.ion.sequence.peptide_id.unwrap_or(-1),
421                isotope_peak: 0,
422                description: Some(format!("{}_{}_{}", kind, index + 1, 0)),
423            };
424            let contribution_source = ContributionSource {
425                intensity_contribution: intensity,
426                source_type: SourceType::Signal,
427                signal_attributes: Some(signal_attributes)
428            };
429
430            annotations.push(PeakAnnotation {
431                contributions: vec![contribution_source]
432            });
433            mz_values.push(mz);
434            intensity_values.push(intensity);
435        }
436
437        for (index, c_ion) in self.c_ions.iter().enumerate() {
438            let kind = c_ion.kind;
439            let charge = c_ion.ion.charge;
440            let mz = c_ion.mz();
441            let intensity = c_ion.ion.intensity;
442            let signal_attributes = SignalAttributes {
443                charge_state: charge,
444                peptide_id: c_ion.ion.sequence.peptide_id.unwrap_or(-1),
445                isotope_peak: 0,
446                description: Some(format!("{}_{}_{}", kind, index + 1, 0)),
447            };
448            let contribution_source = ContributionSource {
449                intensity_contribution: intensity,
450                source_type: SourceType::Signal,
451                signal_attributes: Some(signal_attributes)
452            };
453
454            annotations.push(PeakAnnotation {
455                contributions: vec![contribution_source]
456            });
457            mz_values.push(mz);
458            intensity_values.push(intensity);
459        }
460
461        MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
462    }
463
464    pub fn generate_isotopic_spectrum(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
465        let mut spectra: Vec<MzSpectrum> = Vec::new();
466
467        for ion in &self.n_ions {
468            let n_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
469            let spectrum = MzSpectrum::new(n_isotopes.iter().map(|(mz, _)| *mz).collect(), n_isotopes.iter().map(|(_, abundance)| *abundance * ion.ion.intensity).collect());
470            spectra.push(spectrum);
471        }
472
473        for ion in &self.c_ions {
474            let c_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
475            let spectrum = MzSpectrum::new(c_isotopes.iter().map(|(mz, _)| *mz).collect(), c_isotopes.iter().map(|(_, abundance)| *abundance * ion.ion.intensity).collect());
476            spectra.push(spectrum);
477        }
478
479        MzSpectrum::from_collection(spectra).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
480    }
481
482    pub fn generate_isotopic_spectrum_annotated(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
483        let mut annotations: Vec<PeakAnnotation> = Vec::new();
484        let mut mz_values = Vec::new();
485        let mut intensity_values = Vec::new();
486
487        for (index, ion) in self.n_ions.iter().enumerate() {
488            let n_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
489            let mut isotope_counter = 0;
490            let mut previous_mz = n_isotopes[0].0;
491
492            for (mz, abundance) in n_isotopes.iter() {
493                let ppm_tolerance = (mz / 1e6) * 25.0;
494
495                if (mz - previous_mz).abs() > ppm_tolerance {
496                    isotope_counter += 1;
497                    previous_mz = *mz;
498                }
499
500                let signal_attributes = SignalAttributes {
501                    charge_state: ion.ion.charge,
502                    peptide_id: ion.ion.sequence.peptide_id.unwrap_or(-1),
503                    isotope_peak: isotope_counter,
504                    // use convention of 1-based indexing for fragment ion enumeration
505                    description: Some(format!("{}_{}_{}", ion.kind, index + 1, isotope_counter)),
506                };
507
508                let contribution_source = ContributionSource {
509                    intensity_contribution: *abundance * ion.ion.intensity,
510                    source_type: SourceType::Signal,
511                    signal_attributes: Some(signal_attributes)
512                };
513
514                annotations.push(PeakAnnotation {
515                    contributions: vec![contribution_source]
516                });
517                mz_values.push(*mz);
518                intensity_values.push(*abundance * ion.ion.intensity);
519            }
520        }
521
522        for (index, ion) in self.c_ions.iter().enumerate() {
523            let c_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
524            let mut isotope_counter = 0;
525            let mut previous_mz = c_isotopes[0].0;
526
527            for (mz, abundance) in c_isotopes.iter() {
528                let ppm_tolerance = (mz / 1e6) * 25.0;
529
530                if (mz - previous_mz).abs() > ppm_tolerance {
531                    isotope_counter += 1;
532                    previous_mz = *mz;
533                }
534
535                let signal_attributes = SignalAttributes {
536                    charge_state: ion.ion.charge,
537                    peptide_id: ion.ion.sequence.peptide_id.unwrap_or(-1),
538                    isotope_peak: isotope_counter,
539                    description: Some(format!("{}_{}_{}", ion.kind, index + 1, isotope_counter)),
540                };
541
542                let contribution_source = ContributionSource {
543                    intensity_contribution: *abundance * ion.ion.intensity,
544                    source_type: SourceType::Signal,
545                    signal_attributes: Some(signal_attributes)
546                };
547
548                annotations.push(PeakAnnotation {
549                    contributions: vec![contribution_source]
550                });
551
552                mz_values.push(*mz);
553                intensity_values.push(*abundance * ion.ion.intensity);
554            }
555        }
556        MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
557    }
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct PeptideProductIonSeriesCollection {
562    pub peptide_ions: Vec<PeptideProductIonSeries>,
563}
564impl PeptideProductIonSeriesCollection {
565    pub fn new(peptide_ions: Vec<PeptideProductIonSeries>) -> Self {
566        PeptideProductIonSeriesCollection {
567            peptide_ions,
568        }
569    }
570
571    pub fn find_ion_series(&self, charge: i32) -> Option<&PeptideProductIonSeries> {
572        self.peptide_ions.iter().find(|ion_series| ion_series.charge == charge)
573    }
574
575    pub fn generate_isotopic_spectrum(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
576        let mut spectra: Vec<MzSpectrum> = Vec::new();
577
578        for ion_series in &self.peptide_ions {
579            let isotopic_spectrum = ion_series.generate_isotopic_spectrum(mass_tolerance, abundance_threshold, max_result, intensity_min);
580            spectra.push(isotopic_spectrum);
581        }
582
583        MzSpectrum::from_collection(spectra).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
584    }
585
586    pub fn generate_isotopic_spectrum_annotated(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
587        let mut annotations: Vec<PeakAnnotation> = Vec::new();
588        let mut mz_values = Vec::new();
589        let mut intensity_values = Vec::new();
590
591        for ion_series in &self.peptide_ions {
592            let isotopic_spectrum = ion_series.generate_isotopic_spectrum_annotated(mass_tolerance, abundance_threshold, max_result, intensity_min);
593            for (mz, intensity) in isotopic_spectrum.mz.iter().zip(isotopic_spectrum.intensity.iter()) {
594                mz_values.push(*mz);
595                intensity_values.push(*intensity);
596            }
597            annotations.extend(isotopic_spectrum.annotations.iter().cloned());
598        }
599
600        MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
601    }
602}