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
13type 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 fragment_kind: None,
96 fragment_ordinal: None,
97 };
98
99 let contribution_source = ContributionSource {
100 intensity_contribution: *abundance,
101 source_type: SourceType::Signal,
102 signal_attributes: Some(signal_attributes)
103 };
104
105 annotations.push(PeakAnnotation {
106 contributions: vec![contribution_source]
107 });
108 }
109
110 MzSpectrumAnnotated::new(isotopic_distribution.iter().map(|(mz, _)| *mz).collect(), isotopic_distribution.iter().map(|(_, abundance)| *abundance).collect(), annotations)
111 }
112}
113
114#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
115pub enum FragmentType { A, B, C, X, Y, Z, }
116
117impl std::fmt::Display for FragmentType {
119 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
120 match self {
121 FragmentType::A => write!(f, "a"),
122 FragmentType::B => write!(f, "b"),
123 FragmentType::C => write!(f, "c"),
124 FragmentType::X => write!(f, "x"),
125 FragmentType::Y => write!(f, "y"),
126 FragmentType::Z => write!(f, "z"),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct PeptideProductIon {
133 pub kind: FragmentType,
134 pub ion: PeptideIon,
135}
136
137impl PeptideProductIon {
138 pub fn new(kind: FragmentType, sequence: String, charge: i32, intensity: f64, peptide_id: Option<i32>) -> Self {
139 PeptideProductIon {
140 kind,
141 ion: PeptideIon {
142 sequence: PeptideSequence::new(sequence, peptide_id),
143 charge,
144 intensity,
145 },
146 }
147 }
148
149 pub fn mono_isotopic_mass(&self) -> f64 {
150 calculate_peptide_product_ion_mono_isotopic_mass(self.ion.sequence.sequence.as_str(), self.kind)
151 }
152
153 pub fn atomic_composition(&self) -> HashMap<&str, i32> {
154
155 let mut composition = peptide_sequence_to_atomic_composition(&self.ion.sequence);
156
157 match self.kind {
158 FragmentType::A => {
159 *composition.entry("H").or_insert(0) -= 2;
160 *composition.entry("O").or_insert(0) -= 2;
161 *composition.entry("C").or_insert(0) -= 1;
162 },
163
164 FragmentType::B => {
165 *composition.entry("H").or_insert(0) -= 2;
167 *composition.entry("O").or_insert(0) -= 1;
168 },
169
170 FragmentType::C => {
171 *composition.entry("H").or_insert(0) += 1;
173 *composition.entry("N").or_insert(0) += 1;
174 *composition.entry("O").or_insert(0) -= 1;
175 },
176
177 FragmentType::X => {
178 *composition.entry("C").or_insert(0) += 1;
180 *composition.entry("O").or_insert(0) += 1;
181 },
182
183 FragmentType::Y => {
184 ()
185 },
186
187 FragmentType::Z => {
188 *composition.entry("H").or_insert(0) -= 1;
189 *composition.entry("N").or_insert(0) -= 3;
190 },
191 }
192 composition
193 }
194
195 pub fn mz(&self) -> f64 {
196 calculate_mz(self.mono_isotopic_mass(), self.ion.charge)
197 }
198
199 pub fn isotope_distribution(
200 &self,
201 mass_tolerance: f64,
202 abundance_threshold: f64,
203 max_result: i32,
204 intensity_min: f64,
205 ) -> IsotopeDistribution {
206
207 let atomic_composition: HashMap<String, i32> = self.atomic_composition().iter().map(|(k, v)| (k.to_string(), *v)).collect();
208
209 let distribution: IsotopeDistribution = crate::algorithm::isotope::generate_isotope_distribution(&atomic_composition, mass_tolerance, abundance_threshold, max_result)
210 .into_iter().filter(|&(_, abundance)| abundance > intensity_min).collect();
211
212 let mz_distribution = distribution.iter().map(|(mass, _)| calculate_mz(*mass, self.ion.charge)).zip(distribution.iter().map(|&(_, abundance)| abundance)).collect();
213
214 mz_distribution
215 }
216
217 pub fn complementary_isotope_distribution(
234 &self,
235 precursor_composition: &HashMap<&str, i32>,
236 mass_tolerance: f64,
237 abundance_threshold: f64,
238 max_result: i32,
239 ) -> Vec<(f64, f64)> {
240 let fragment_composition = self.atomic_composition();
241 let complementary_composition = crate::algorithm::peptide::calculate_complementary_fragment_composition(
242 precursor_composition,
243 &fragment_composition,
244 );
245
246 crate::algorithm::isotope::generate_isotope_distribution(
247 &complementary_composition,
248 mass_tolerance,
249 abundance_threshold,
250 max_result,
251 )
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
256pub struct PeptideSequence {
257 pub sequence: String,
258 pub peptide_id: Option<i32>,
259}
260
261impl PeptideSequence {
262 pub fn new(raw_sequence: String, peptide_id: Option<i32>) -> Self {
263
264 let pattern = Regex::new(r"\[UNIMOD:(\d+)]").unwrap();
266
267 let sequence = pattern.replace_all(&raw_sequence, "").to_string();
269
270 let valid_amino_acids = sequence.chars().all(|c| amino_acid_masses().contains_key(&c.to_string()[..]));
272 if !valid_amino_acids {
273 panic!("Invalid amino acid sequence, use only valid amino acids: ARNDCQEGHILKMFPSTWYVU, and modifications in the format [UNIMOD:ID]");
274 }
275
276 PeptideSequence { sequence: raw_sequence, peptide_id }
277 }
278
279 pub fn mono_isotopic_mass(&self) -> f64 {
280 calculate_peptide_mono_isotopic_mass(self)
281 }
282
283 pub fn atomic_composition(&self) -> HashMap<&str, i32> {
284 peptide_sequence_to_atomic_composition(self)
285 }
286
287 pub fn to_tokens(&self, group_modifications: bool) -> Vec<String> {
288 unimod_sequence_to_tokens(&*self.sequence, group_modifications)
289 }
290
291 pub fn to_sage_representation(&self) -> (String, Vec<f64>) {
292 find_unimod_patterns(&*self.sequence)
293 }
294
295 pub fn amino_acid_count(&self) -> usize {
296 self.to_tokens(true).len()
297 }
298
299 pub fn calculate_mono_isotopic_product_ion_spectrum(&self, charge: i32, fragment_type: FragmentType) -> MzSpectrum {
300 let product_ions = self.calculate_product_ion_series(charge, fragment_type);
301 product_ions.generate_mono_isotopic_spectrum()
302 }
303
304 pub fn calculate_mono_isotopic_product_ion_spectrum_annotated(&self, charge: i32, fragment_type: FragmentType) -> MzSpectrumAnnotated {
305 let product_ions = self.calculate_product_ion_series(charge, fragment_type);
306 product_ions.generate_mono_isotopic_spectrum_annotated()
307 }
308
309 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 {
310 let product_ions = self.calculate_product_ion_series(charge, fragment_type);
311 product_ions.generate_isotopic_spectrum(mass_tolerance, abundance_threshold, max_result, intensity_min)
312 }
313
314 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 {
315 let product_ions = self.calculate_product_ion_series(charge, fragment_type);
316 product_ions.generate_isotopic_spectrum_annotated(mass_tolerance, abundance_threshold, max_result, intensity_min)
317 }
318
319 pub fn calculate_product_ion_series(&self, target_charge: i32, fragment_type: FragmentType) -> PeptideProductIonSeries {
320 let tokens = unimod_sequence_to_tokens(self.sequence.as_str(), true);
322 let mut n_terminal_ions = Vec::new();
323 let mut c_terminal_ions = Vec::new();
324
325 for i in 1..tokens.len() {
327 let n_ion_seq = tokens[..i].join("");
328 n_terminal_ions.push(PeptideProductIon {
329 kind: match fragment_type {
330 FragmentType::A => FragmentType::A,
331 FragmentType::B => FragmentType::B,
332 FragmentType::C => FragmentType::C,
333 FragmentType::X => FragmentType::A,
334 FragmentType::Y => FragmentType::B,
335 FragmentType::Z => FragmentType::C,
336 },
337 ion: PeptideIon {
338 sequence: PeptideSequence {
339 sequence: n_ion_seq,
340 peptide_id: self.peptide_id,
341 },
342 charge: target_charge,
343 intensity: 1.0, },
345 });
346 }
347
348 for i in 1..tokens.len() {
350 let c_ion_seq = tokens[tokens.len() - i..].join("");
351 c_terminal_ions.push(PeptideProductIon {
352 kind: match fragment_type {
353 FragmentType::A => FragmentType::X,
354 FragmentType::B => FragmentType::Y,
355 FragmentType::C => FragmentType::Z,
356 FragmentType::X => FragmentType::X,
357 FragmentType::Y => FragmentType::Y,
358 FragmentType::Z => FragmentType::Z,
359 },
360 ion: PeptideIon {
361 sequence: PeptideSequence {
362 sequence: c_ion_seq,
363 peptide_id: self.peptide_id,
364 },
365 charge: target_charge,
366 intensity: 1.0, },
368 });
369 }
370
371 PeptideProductIonSeries::new(target_charge, n_terminal_ions, c_terminal_ions)
372 }
373
374 pub fn associate_with_predicted_intensities(
375 &self,
376 charge: i32,
378 fragment_type: FragmentType,
379 flat_intensities: Vec<f64>,
380 normalize: bool,
381 half_charge_one: bool,
382 ) -> PeptideProductIonSeriesCollection {
383
384 let reshaped_intensities = reshape_prosit_array(flat_intensities);
385 let max_charge = std::cmp::min(charge, 3).max(1); let mut sum_intensity = if normalize { 0.0 } else { 1.0 };
387 let num_tokens = self.amino_acid_count() - 1; let mut peptide_ion_collection = Vec::new();
390
391 if normalize {
392 for z in 1..=max_charge {
393
394 let intensity_c: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[0][z as usize - 1]).filter(|&x| x > 0.0).collect();
395 let intensity_n: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[1][z as usize - 1]).filter(|&x| x > 0.0).collect();
396
397 sum_intensity += intensity_n.iter().sum::<f64>() + intensity_c.iter().sum::<f64>();
398 }
399 }
400
401 for z in 1..=max_charge {
402
403 let mut product_ions = self.calculate_product_ion_series(z, fragment_type);
404 let intensity_n: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[1][z as usize - 1]).collect();
405 let intensity_c: Vec<f64> = reshaped_intensities[..num_tokens].iter().map(|x| x[0][z as usize - 1]).collect(); let adjusted_sum_intensity = if max_charge == 1 && half_charge_one { sum_intensity * 2.0 } else { sum_intensity };
408
409 for (i, ion) in product_ions.n_ions.iter_mut().enumerate() {
410 ion.ion.intensity = intensity_n[i] / adjusted_sum_intensity;
411 }
412 for (i, ion) in product_ions.c_ions.iter_mut().enumerate() {
413 ion.ion.intensity = intensity_c[i] / adjusted_sum_intensity;
414 }
415
416 peptide_ion_collection.push(PeptideProductIonSeries::new(z, product_ions.n_ions, product_ions.c_ions));
417 }
418
419 PeptideProductIonSeriesCollection::new(peptide_ion_collection)
420 }
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct PeptideProductIonSeries {
425 pub charge: i32,
426 pub n_ions: Vec<PeptideProductIon>,
427 pub c_ions: Vec<PeptideProductIon>,
428}
429
430impl PeptideProductIonSeries {
431 pub fn new(charge: i32, n_ions: Vec<PeptideProductIon>, c_ions: Vec<PeptideProductIon>) -> Self {
432 PeptideProductIonSeries {
433 charge,
434 n_ions,
435 c_ions,
436 }
437 }
438
439 pub fn generate_mono_isotopic_spectrum(&self) -> MzSpectrum {
440 let mz_i_n = self.n_ions.iter().map(|ion| (ion.mz(), ion.ion.intensity)).collect_vec();
441 let mz_i_c = self.c_ions.iter().map(|ion| (ion.mz(), ion.ion.intensity)).collect_vec();
442 let n_spectrum = MzSpectrum::new(mz_i_n.iter().map(|(mz, _)| *mz).collect(), mz_i_n.iter().map(|(_, abundance)| *abundance).collect());
443 let c_spectrum = MzSpectrum::new(mz_i_c.iter().map(|(mz, _)| *mz).collect(), mz_i_c.iter().map(|(_, abundance)| *abundance).collect());
444 MzSpectrum::from_collection(vec![n_spectrum, c_spectrum]).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
445 }
446
447 pub fn generate_mono_isotopic_spectrum_annotated(&self) -> MzSpectrumAnnotated {
448 let mut annotations: Vec<PeakAnnotation> = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
449 let mut mz_values = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
450 let mut intensity_values = Vec::with_capacity(self.n_ions.len() + self.c_ions.len());
451
452 for (index, n_ion) in self.n_ions.iter().enumerate() {
453 let charge = n_ion.ion.charge;
454 let mz = n_ion.mz();
455 let intensity = n_ion.ion.intensity;
456 let signal_attributes = SignalAttributes {
457 charge_state: charge,
458 peptide_id: n_ion.ion.sequence.peptide_id.unwrap_or(-1),
459 isotope_peak: 0,
460 fragment_kind: Some(n_ion.kind),
463 fragment_ordinal: Some((index + 1) as i32),
464 };
465 let contribution_source = ContributionSource {
466 intensity_contribution: intensity,
467 source_type: SourceType::Signal,
468 signal_attributes: Some(signal_attributes)
469 };
470
471 annotations.push(PeakAnnotation {
472 contributions: vec![contribution_source]
473 });
474 mz_values.push(mz);
475 intensity_values.push(intensity);
476 }
477
478 for (index, c_ion) in self.c_ions.iter().enumerate() {
479 let charge = c_ion.ion.charge;
480 let mz = c_ion.mz();
481 let intensity = c_ion.ion.intensity;
482 let signal_attributes = SignalAttributes {
483 charge_state: charge,
484 peptide_id: c_ion.ion.sequence.peptide_id.unwrap_or(-1),
485 isotope_peak: 0,
486 fragment_kind: Some(c_ion.kind),
487 fragment_ordinal: Some((index + 1) as i32),
488 };
489 let contribution_source = ContributionSource {
490 intensity_contribution: intensity,
491 source_type: SourceType::Signal,
492 signal_attributes: Some(signal_attributes)
493 };
494
495 annotations.push(PeakAnnotation {
496 contributions: vec![contribution_source]
497 });
498 mz_values.push(mz);
499 intensity_values.push(intensity);
500 }
501
502 MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
503 }
504
505 pub fn generate_isotopic_spectrum(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
506 let mut spectra: Vec<MzSpectrum> = Vec::new();
507
508 for ion in &self.n_ions {
509 let n_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
510 let spectrum = MzSpectrum::new(n_isotopes.iter().map(|(mz, _)| *mz).collect(), n_isotopes.iter().map(|(_, abundance)| *abundance * ion.ion.intensity).collect());
511 spectra.push(spectrum);
512 }
513
514 for ion in &self.c_ions {
515 let c_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
516 let spectrum = MzSpectrum::new(c_isotopes.iter().map(|(mz, _)| *mz).collect(), c_isotopes.iter().map(|(_, abundance)| *abundance * ion.ion.intensity).collect());
517 spectra.push(spectrum);
518 }
519
520 MzSpectrum::from_collection(spectra).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
521 }
522
523 pub fn generate_isotopic_spectrum_annotated(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
524 let mut annotations: Vec<PeakAnnotation> = Vec::new();
525 let mut mz_values = Vec::new();
526 let mut intensity_values = Vec::new();
527
528 for (index, ion) in self.n_ions.iter().enumerate() {
529 let n_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
530 let mut isotope_counter = 0;
531 let mut previous_mz = n_isotopes[0].0;
532
533 for (mz, abundance) in n_isotopes.iter() {
534 let ppm_tolerance = (mz / 1e6) * 25.0;
535
536 if (mz - previous_mz).abs() > ppm_tolerance {
537 isotope_counter += 1;
538 previous_mz = *mz;
539 }
540
541 let signal_attributes = SignalAttributes {
542 charge_state: ion.ion.charge,
543 peptide_id: ion.ion.sequence.peptide_id.unwrap_or(-1),
544 isotope_peak: isotope_counter,
545 fragment_kind: Some(ion.kind),
550 fragment_ordinal: Some((index + 1) as i32),
551 };
552
553 let contribution_source = ContributionSource {
554 intensity_contribution: *abundance * ion.ion.intensity,
555 source_type: SourceType::Signal,
556 signal_attributes: Some(signal_attributes)
557 };
558
559 annotations.push(PeakAnnotation {
560 contributions: vec![contribution_source]
561 });
562 mz_values.push(*mz);
563 intensity_values.push(*abundance * ion.ion.intensity);
564 }
565 }
566
567 for (index, ion) in self.c_ions.iter().enumerate() {
568 let c_isotopes = ion.isotope_distribution(mass_tolerance, abundance_threshold, max_result, intensity_min);
569 let mut isotope_counter = 0;
570 let mut previous_mz = c_isotopes[0].0;
571
572 for (mz, abundance) in c_isotopes.iter() {
573 let ppm_tolerance = (mz / 1e6) * 25.0;
574
575 if (mz - previous_mz).abs() > ppm_tolerance {
576 isotope_counter += 1;
577 previous_mz = *mz;
578 }
579
580 let signal_attributes = SignalAttributes {
581 charge_state: ion.ion.charge,
582 peptide_id: ion.ion.sequence.peptide_id.unwrap_or(-1),
583 isotope_peak: isotope_counter,
584 fragment_kind: Some(ion.kind), fragment_ordinal: Some((index + 1) as i32),
586 };
587
588 let contribution_source = ContributionSource {
589 intensity_contribution: *abundance * ion.ion.intensity,
590 source_type: SourceType::Signal,
591 signal_attributes: Some(signal_attributes)
592 };
593
594 annotations.push(PeakAnnotation {
595 contributions: vec![contribution_source]
596 });
597
598 mz_values.push(*mz);
599 intensity_values.push(*abundance * ion.ion.intensity);
600 }
601 }
602 MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
603 }
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct PeptideProductIonSeriesCollection {
608 pub peptide_ions: Vec<PeptideProductIonSeries>,
609}
610impl PeptideProductIonSeriesCollection {
611 pub fn new(peptide_ions: Vec<PeptideProductIonSeries>) -> Self {
612 PeptideProductIonSeriesCollection {
613 peptide_ions,
614 }
615 }
616
617 pub fn find_ion_series(&self, charge: i32) -> Option<&PeptideProductIonSeries> {
618 self.peptide_ions.iter().find(|ion_series| ion_series.charge == charge)
619 }
620
621 pub fn generate_isotopic_spectrum(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrum {
622 let mut spectra: Vec<MzSpectrum> = Vec::new();
623
624 for ion_series in &self.peptide_ions {
625 let isotopic_spectrum = ion_series.generate_isotopic_spectrum(mass_tolerance, abundance_threshold, max_result, intensity_min);
626 spectra.push(isotopic_spectrum);
627 }
628
629 MzSpectrum::from_collection(spectra).filter_ranged(0.0, 5_000.0, 1e-6, 1e6)
630 }
631
632 pub fn generate_isotopic_spectrum_annotated(&self, mass_tolerance: f64, abundance_threshold: f64, max_result: i32, intensity_min: f64) -> MzSpectrumAnnotated {
633 let mut annotations: Vec<PeakAnnotation> = Vec::new();
634 let mut mz_values = Vec::new();
635 let mut intensity_values = Vec::new();
636
637 for ion_series in &self.peptide_ions {
638 let isotopic_spectrum = ion_series.generate_isotopic_spectrum_annotated(mass_tolerance, abundance_threshold, max_result, intensity_min);
639 for (mz, intensity) in isotopic_spectrum.mz.iter().zip(isotopic_spectrum.intensity.iter()) {
640 mz_values.push(*mz);
641 intensity_values.push(*intensity);
642 }
643 annotations.extend(isotopic_spectrum.annotations.iter().cloned());
644 }
645
646 MzSpectrumAnnotated::new(mz_values, intensity_values, annotations)
647 }
648}
649
650#[cfg(test)]
651mod annotated_perf_tests {
652 use super::*;
653
654 #[test]
662 fn isotopic_annotated_keeps_full_fragment_identity_and_spectrum() {
663 let seq = PeptideSequence::new("PEPTIDEK".to_string(), Some(7));
664 let series = seq.calculate_product_ion_series(1, FragmentType::B);
665 let spec = series.generate_isotopic_spectrum_annotated(1e-2, 1e-3, 100, 1e-5);
666
667 assert!(!spec.mz.is_empty());
669 assert_eq!(spec.mz.len(), spec.intensity.len());
670 assert_eq!(spec.mz.len(), spec.annotations.len());
671
672 assert!(spec.mz.windows(2).all(|w| w[0] <= w[1]));
674
675 let mut max_isotope = 0;
676 for ann in &spec.annotations {
677 assert_eq!(ann.contributions.len(), 1);
678 let c = &ann.contributions[0];
679 assert_eq!(c.source_type, SourceType::Signal);
680 let sa = c.signal_attributes.as_ref().expect("signal attributes present");
681 assert_eq!(sa.peptide_id, 7); assert!(sa.charge_state >= 1); assert!(sa.isotope_peak >= 0); let kind = sa.fragment_kind.expect("fragment kind present");
686 let ordinal = sa.fragment_ordinal.expect("fragment ordinal present");
687 assert!(ordinal >= 1); assert_eq!(
690 sa.description(),
691 Some(format!("{}_{}_{}", kind, ordinal, sa.isotope_peak)),
692 );
693 max_isotope = max_isotope.max(sa.isotope_peak);
694 }
695 assert!(max_isotope >= 1);
697 }
698
699 #[test]
700 fn mono_annotated_keeps_full_fragment_identity() {
701 let seq = PeptideSequence::new("PEPTIDEK".to_string(), Some(3));
702 let series = seq.calculate_product_ion_series(1, FragmentType::B);
703 let spec = series.generate_mono_isotopic_spectrum_annotated();
704
705 assert!(!spec.mz.is_empty());
706 assert_eq!(spec.mz.len(), spec.annotations.len());
707 for ann in &spec.annotations {
708 let sa = ann.contributions[0].signal_attributes.as_ref().unwrap();
709 assert_eq!(sa.peptide_id, 3);
710 assert_eq!(sa.isotope_peak, 0); assert!(sa.fragment_kind.is_some());
712 assert!(sa.fragment_ordinal.unwrap() >= 1);
713 assert!(sa.description().unwrap().ends_with("_0"));
715 }
716 }
717}