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