1extern crate statrs;
2
3use rayon::prelude::*;
4use rayon::ThreadPoolBuilder;
5use std::collections::{BTreeMap, HashMap, HashSet};
6
7use crate::chemistry::constants::{MASS_NEUTRON, MASS_PROTON};
8use crate::chemistry::elements::{atoms_isotopic_weights, isotopic_abundance};
9use crate::data::peptide::PeptideIon;
10use crate::data::spectrum::MzSpectrum;
11use crate::data::spectrum::ToResolution;
12use statrs::distribution::{Continuous, Normal};
13
14pub fn convolve(
39 dist_a: &Vec<(f64, f64)>,
40 dist_b: &Vec<(f64, f64)>,
41 mass_tolerance: f64,
42 abundance_threshold: f64,
43 max_results: usize,
44) -> Vec<(f64, f64)> {
45 let mut result: Vec<(f64, f64)> = Vec::new();
46
47 for (mass_a, abundance_a) in dist_a {
48 for (mass_b, abundance_b) in dist_b {
49 let combined_mass = mass_a + mass_b;
50 let combined_abundance = abundance_a * abundance_b;
51
52 if combined_abundance < abundance_threshold {
54 continue;
55 }
56
57 if let Some(entry) = result
59 .iter_mut()
60 .find(|(m, _)| (*m - combined_mass).abs() < mass_tolerance)
61 {
62 entry.1 += combined_abundance;
63 } else {
64 result.push((combined_mass, combined_abundance));
65 }
66 }
67 }
68
69 result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
71
72 if result.len() > max_results {
74 result.truncate(max_results);
75 }
76
77 result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
79
80 result
81}
82
83pub fn convolve_pow(dist: &Vec<(f64, f64)>, n: i32) -> Vec<(f64, f64)> {
104 if n == 0 {
105 return vec![(0.0, 1.0)]; }
107 if n == 1 {
108 return dist.clone();
109 }
110
111 let mut result = dist.clone();
112 let mut power = 2;
113
114 while power <= n {
115 result = convolve(&result, &result, 1e-6, 1e-12, 200); power *= 2;
117 }
118
119 if power / 2 < n {
121 result = convolve(
122 &result,
123 &convolve_pow(dist, n - power / 2),
124 1e-6,
125 1e-12,
126 200,
127 );
128 }
129
130 result
131}
132
133pub fn generate_isotope_distribution(
160 atomic_composition: &HashMap<String, i32>,
161 mass_tolerance: f64,
162 abundance_threshold: f64,
163 max_result: i32,
164) -> Vec<(f64, f64)> {
165 let mut cumulative_distribution: Option<Vec<(f64, f64)>> = None;
166 let atoms_isotopic_weights: HashMap<String, Vec<f64>> = atoms_isotopic_weights()
167 .iter()
168 .map(|(k, v)| (k.to_string(), v.clone()))
169 .collect();
170 let atomic_isotope_abundance: HashMap<String, Vec<f64>> = isotopic_abundance()
171 .iter()
172 .map(|(k, v)| (k.to_string(), v.clone()))
173 .collect();
174
175 for (element, &count) in atomic_composition.iter() {
176 let elemental_isotope_weights = atoms_isotopic_weights
177 .get(element)
178 .expect("Element not found in isotopic weights table")
179 .clone();
180 let elemental_isotope_abundance = atomic_isotope_abundance
181 .get(element)
182 .expect("Element not found in isotopic abundance table")
183 .clone();
184
185 let element_distribution: Vec<(f64, f64)> = elemental_isotope_weights
186 .iter()
187 .zip(elemental_isotope_abundance.iter())
188 .map(|(&mass, &abundance)| (mass, abundance))
189 .collect();
190
191 let element_power_distribution = if count > 1 {
192 convolve_pow(&element_distribution, count)
193 } else {
194 element_distribution
195 };
196
197 cumulative_distribution = match cumulative_distribution {
198 Some(cum_dist) => Some(convolve(
199 &cum_dist,
200 &element_power_distribution,
201 mass_tolerance,
202 abundance_threshold,
203 max_result as usize,
204 )),
205 None => Some(element_power_distribution),
206 };
207 }
208
209 let final_distribution = cumulative_distribution.expect("Peptide has no elements");
210 let total_abundance: f64 = final_distribution
212 .iter()
213 .map(|&(_, abundance)| abundance)
214 .sum();
215 let result: Vec<_> = final_distribution
216 .into_iter()
217 .map(|(mass, abundance)| (mass, abundance / total_abundance))
218 .collect();
219
220 let mut sort_map: BTreeMap<i64, f64> = BTreeMap::new();
221 let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
222
223 for (mz, intensity) in result {
224 let key = quantize(mz);
225 sort_map
226 .entry(key)
227 .and_modify(|e| *e += intensity)
228 .or_insert(intensity);
229 }
230
231 let mz: Vec<f64> = sort_map
232 .keys()
233 .map(|&key| key as f64 / 1_000_000.0)
234 .collect();
235 let intensity: Vec<f64> = sort_map.values().map(|&intensity| intensity).collect();
236 mz.iter()
237 .zip(intensity.iter())
238 .map(|(&mz, &intensity)| (mz, intensity))
239 .collect()
240}
241
242pub fn normal_pdf(x: f64, mean: f64, std_dev: f64) -> f64 {
263 let normal = Normal::new(mean, std_dev).unwrap();
264 normal.pdf(x)
265}
266
267pub fn factorial(n: i32) -> f64 {
286 (1..=n).fold(1.0, |acc, x| acc * x as f64)
287}
288
289pub fn weight(mass: f64, peak_nums: Vec<i32>, normalize: bool) -> Vec<f64> {
290 let lam_val = lam(mass, 0.000594, -0.03091);
291 let factorials: Vec<f64> = peak_nums.iter().map(|&k| factorial(k)).collect();
292 let mut weights: Vec<f64> = peak_nums
293 .iter()
294 .map(|&k| {
295 let pow = lam_val.powi(k);
296 let exp = (-lam_val).exp();
297 exp * pow / factorials[k as usize]
298 })
299 .collect();
300
301 if normalize {
302 let sum: f64 = weights.iter().sum();
303 weights = weights.iter().map(|&w| w / sum).collect();
304 }
305
306 weights
307}
308
309pub fn lam(mass: f64, slope: f64, intercept: f64) -> f64 {
329 slope * mass + intercept
330}
331
332pub fn iso(
350 x: &Vec<f64>,
351 mass: f64,
352 charge: f64,
353 sigma: f64,
354 amp: f64,
355 k: usize,
356 step_size: f64,
357) -> Vec<f64> {
358 let k_range: Vec<usize> = (0..k).collect();
359 let means: Vec<f64> = k_range
360 .iter()
361 .map(|&k_val| (mass + MASS_NEUTRON * k_val as f64) / charge)
362 .collect();
363 let weights = weight(
364 mass,
365 k_range
366 .iter()
367 .map(|&k_val| k_val as i32)
368 .collect::<Vec<i32>>(),
369 true,
370 );
371
372 let mut intensities = vec![0.0; x.len()];
373 for (i, x_val) in x.iter().enumerate() {
374 for (j, &mean) in means.iter().enumerate() {
375 intensities[i] += weights[j] * normal_pdf(*x_val, mean, sigma);
376 }
377 intensities[i] *= step_size;
378 }
379 intensities
380 .iter()
381 .map(|&intensity| intensity * amp)
382 .collect()
383}
384
385pub fn generate_isotope_pattern(
410 lower_bound: f64,
411 upper_bound: f64,
412 mass: f64,
413 charge: f64,
414 amp: f64,
415 k: usize,
416 sigma: f64,
417 resolution: i32,
418) -> (Vec<f64>, Vec<f64>) {
419 let step_size = f64::min(sigma / 10.0, 1.0 / 10f64.powi(resolution));
420 let size = ((upper_bound - lower_bound) / step_size).ceil() as usize;
421 let mzs: Vec<f64> = (0..size)
422 .map(|i| lower_bound + step_size * i as f64)
423 .collect();
424 let intensities = iso(&mzs, mass, charge, sigma, amp, k, step_size);
425
426 (
427 mzs.iter().map(|&mz| mz + MASS_PROTON).collect(),
428 intensities,
429 )
430}
431
432pub fn generate_averagine_spectrum(
456 mass: f64,
457 charge: i32,
458 min_intensity: i32,
459 k: i32,
460 resolution: i32,
461 centroid: bool,
462 amp: Option<f64>,
463) -> MzSpectrum {
464 let amp = amp.unwrap_or(1e4);
465 let lb = mass / charge as f64 - 0.2;
466 let ub = mass / charge as f64 + k as f64 + 0.2;
467
468 let (mz, intensities) = generate_isotope_pattern(
469 lb,
470 ub,
471 mass,
472 charge as f64,
473 amp,
474 k as usize,
475 0.008492569002123142,
476 resolution,
477 );
478
479 let spectrum = MzSpectrum::new(mz, intensities)
480 .to_resolution(resolution)
481 .filter_ranged(lb, ub, min_intensity as f64, 1e9);
482
483 if centroid {
484 spectrum.to_centroid(
485 std::cmp::max(min_intensity, 1),
486 1.0 / 10f64.powi(resolution - 1),
487 true,
488 )
489 } else {
490 spectrum
491 }
492}
493
494pub fn generate_averagine_spectra(
522 masses: Vec<f64>,
523 charges: Vec<i32>,
524 min_intensity: i32,
525 k: i32,
526 resolution: i32,
527 centroid: bool,
528 num_threads: usize,
529 amp: Option<f64>,
530) -> Vec<MzSpectrum> {
531 let amp = amp.unwrap_or(1e5);
532 let mut spectra: Vec<MzSpectrum> = Vec::new();
533 let thread_pool = ThreadPoolBuilder::new()
534 .num_threads(num_threads)
535 .build()
536 .unwrap();
537
538 thread_pool.install(|| {
539 spectra = masses
540 .par_iter()
541 .zip(charges.par_iter())
542 .map(|(&mass, &charge)| {
543 generate_averagine_spectrum(
544 mass,
545 charge,
546 min_intensity,
547 k,
548 resolution,
549 centroid,
550 Some(amp),
551 )
552 })
553 .collect();
554 });
555
556 spectra
557}
558
559pub fn generate_precursor_spectrum(
572 sequence: &str,
573 charge: i32,
574 peptide_id: Option<i32>,
575) -> MzSpectrum {
576 let peptide_ion = PeptideIon::new(sequence.to_string(), charge, 1.0, peptide_id);
577 peptide_ion.calculate_isotopic_spectrum(1e-3, 1e-9, 200, 1e-6)
578}
579
580pub fn generate_precursor_spectra(
593 sequences: &Vec<&str>,
594 charges: &Vec<i32>,
595 num_threads: usize,
596 peptide_ids: Vec<Option<i32>>,
597) -> Vec<MzSpectrum> {
598 let thread_pool = ThreadPoolBuilder::new()
599 .num_threads(num_threads)
600 .build()
601 .unwrap();
602 let result = thread_pool.install(|| {
604 sequences
605 .par_iter()
606 .zip(charges.par_iter())
607 .zip(peptide_ids.par_iter())
608 .map(|((&sequence, &charge), &peptide_id)| {
609 generate_precursor_spectrum(sequence, charge, peptide_id)
610 })
611 .collect()
612 });
613 result
614}
615
616#[derive(Debug, Clone)]
619pub struct TransmissionDependentIsotopeDistribution {
620 pub distribution: Vec<(f64, f64)>,
622 pub transmission_factor: f64,
626}
627
628pub fn calculate_transmission_dependent_fragment_ion_isotope_distribution(
631 fragment_isotope_dist: &Vec<(f64, f64)>,
632 comp_fragment_isotope_dist: &Vec<(f64, f64)>,
633 precursor_isotopes: &HashSet<usize>,
634 max_isotope: usize,
635) -> Vec<(f64, f64)> {
636 if fragment_isotope_dist.is_empty() || comp_fragment_isotope_dist.is_empty() {
637 return Vec::new();
638 }
639
640 let mut r_max = fragment_isotope_dist.len();
641 if max_isotope != 0 && r_max > max_isotope {
642 r_max = max_isotope;
643 }
644
645 let mut result = (0..r_max)
646 .map(|i| (fragment_isotope_dist[0].0 + i as f64, 0.0))
647 .collect::<Vec<(f64, f64)>>();
648
649 for (i, &(_mz, intensity)) in fragment_isotope_dist.iter().enumerate().take(r_max) {
651 for &precursor in precursor_isotopes {
652 if precursor >= i && (precursor - i) < comp_fragment_isotope_dist.len() {
653 let comp_intensity = comp_fragment_isotope_dist[precursor - i].1;
654 result[i].1 += comp_intensity;
655 }
656 }
657 result[i].1 *= intensity;
658 }
659
660 result
661}
662
663pub fn calculate_transmission_dependent_distribution_with_factor(
692 fragment_isotope_dist: &Vec<(f64, f64)>,
693 comp_fragment_isotope_dist: &Vec<(f64, f64)>,
694 precursor_isotopes: &HashSet<usize>,
695 max_isotope: usize,
696) -> TransmissionDependentIsotopeDistribution {
697 if fragment_isotope_dist.is_empty() || comp_fragment_isotope_dist.is_empty() {
698 return TransmissionDependentIsotopeDistribution {
699 distribution: Vec::new(),
700 transmission_factor: 0.0,
701 };
702 }
703
704 let all_isotopes: HashSet<usize> = (0..fragment_isotope_dist.len().max(comp_fragment_isotope_dist.len())).collect();
706 let full_distribution = calculate_transmission_dependent_fragment_ion_isotope_distribution(
707 fragment_isotope_dist,
708 comp_fragment_isotope_dist,
709 &all_isotopes,
710 max_isotope,
711 );
712 let full_sum: f64 = full_distribution.iter().map(|(_, i)| i).sum();
713
714 let transmitted_distribution = calculate_transmission_dependent_fragment_ion_isotope_distribution(
716 fragment_isotope_dist,
717 comp_fragment_isotope_dist,
718 precursor_isotopes,
719 max_isotope,
720 );
721 let transmitted_sum: f64 = transmitted_distribution.iter().map(|(_, i)| i).sum();
722
723 let transmission_factor = if full_sum > 0.0 {
725 transmitted_sum / full_sum
726 } else {
727 0.0
728 };
729
730 TransmissionDependentIsotopeDistribution {
731 distribution: transmitted_distribution,
732 transmission_factor,
733 }
734}
735
736pub fn calculate_precursor_transmission_factor(
750 precursor_isotope_dist: &[(f64, f64)],
751 transmitted_indices: &HashSet<usize>,
752) -> f64 {
753 if precursor_isotope_dist.is_empty() || transmitted_indices.is_empty() {
754 return 0.0;
755 }
756
757 let total_intensity: f64 = precursor_isotope_dist.iter().map(|(_, i)| i).sum();
758 if total_intensity <= 0.0 {
759 return 0.0;
760 }
761
762 let transmitted_intensity: f64 = precursor_isotope_dist
763 .iter()
764 .enumerate()
765 .filter(|(idx, _)| transmitted_indices.contains(idx))
766 .map(|(_, (_, i))| i)
767 .sum();
768
769 transmitted_intensity / total_intensity
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
777 (a - b).abs() < epsilon
778 }
779
780 #[test]
783 fn test_transmission_all_isotopes() {
784 let fragment_dist = vec![
786 (500.0, 0.6),
787 (501.0, 0.3),
788 (502.0, 0.1),
789 ];
790
791 let comp_dist = vec![
793 (800.0, 0.7),
794 (801.0, 0.2),
795 (802.0, 0.1),
796 ];
797
798 let all_isotopes: HashSet<usize> = (0..3).collect();
801
802 let result = calculate_transmission_dependent_distribution_with_factor(
803 &fragment_dist,
804 &comp_dist,
805 &all_isotopes,
806 0,
807 );
808
809 assert!(
811 approx_eq(result.transmission_factor, 1.0, 0.001),
812 "Transmission factor should be 1.0 when same isotopes as reference transmitted, got {}",
813 result.transmission_factor
814 );
815
816 assert!(!result.distribution.is_empty());
818 }
819
820 #[test]
823 fn test_transmission_extra_isotopes() {
824 let fragment_dist = vec![
825 (500.0, 0.6),
826 (501.0, 0.3),
827 (502.0, 0.1),
828 ];
829
830 let comp_dist = vec![
831 (800.0, 0.7),
832 (801.0, 0.2),
833 (802.0, 0.1),
834 ];
835
836 let extra_isotopes: HashSet<usize> = [0, 1, 2, 3, 4, 5].iter().cloned().collect();
838
839 let result = calculate_transmission_dependent_distribution_with_factor(
840 &fragment_dist,
841 &comp_dist,
842 &extra_isotopes,
843 0,
844 );
845
846 assert!(
849 result.transmission_factor >= 1.0,
850 "Extra isotopes should give transmission factor >= 1.0, got {}",
851 result.transmission_factor
852 );
853 }
854
855 #[test]
857 fn test_transmission_partial_isotopes() {
858 let fragment_dist = vec![
860 (500.0, 0.5),
861 (501.0, 0.3),
862 (502.0, 0.15),
863 (503.0, 0.05),
864 ];
865
866 let comp_dist = vec![
868 (800.0, 0.6),
869 (801.0, 0.25),
870 (802.0, 0.1),
871 (803.0, 0.05),
872 ];
873
874 let all_isotopes: HashSet<usize> = [0, 1, 2, 3].iter().cloned().collect();
876 let full_result = calculate_transmission_dependent_distribution_with_factor(
877 &fragment_dist,
878 &comp_dist,
879 &all_isotopes,
880 0,
881 );
882
883 let partial_isotopes: HashSet<usize> = [0, 1].iter().cloned().collect();
885 let partial_result = calculate_transmission_dependent_distribution_with_factor(
886 &fragment_dist,
887 &comp_dist,
888 &partial_isotopes,
889 0,
890 );
891
892 assert!(
894 partial_result.transmission_factor < full_result.transmission_factor,
895 "Partial transmission ({}) should be less than full transmission ({})",
896 partial_result.transmission_factor,
897 full_result.transmission_factor
898 );
899
900 assert!(
902 partial_result.transmission_factor > 0.0 && partial_result.transmission_factor < 1.0,
903 "Partial transmission factor should be between 0 and 1, got {}",
904 partial_result.transmission_factor
905 );
906 }
907
908 #[test]
910 fn test_transmission_m0_only() {
911 let fragment_dist = vec![
912 (500.0, 0.5),
913 (501.0, 0.3),
914 (502.0, 0.15),
915 (503.0, 0.05),
916 ];
917
918 let comp_dist = vec![
919 (800.0, 0.6),
920 (801.0, 0.25),
921 (802.0, 0.1),
922 (803.0, 0.05),
923 ];
924
925 let m0_only: HashSet<usize> = [0].iter().cloned().collect();
927 let m0_result = calculate_transmission_dependent_distribution_with_factor(
928 &fragment_dist,
929 &comp_dist,
930 &m0_only,
931 0,
932 );
933
934 let m0_m1: HashSet<usize> = [0, 1].iter().cloned().collect();
936 let m0_m1_result = calculate_transmission_dependent_distribution_with_factor(
937 &fragment_dist,
938 &comp_dist,
939 &m0_m1,
940 0,
941 );
942
943 assert!(
945 m0_result.transmission_factor < m0_m1_result.transmission_factor,
946 "M0 only ({}) should transmit less than M0+M1 ({})",
947 m0_result.transmission_factor,
948 m0_m1_result.transmission_factor
949 );
950
951 let m0_intensity = m0_result.distribution.get(0).map(|(_, i)| *i).unwrap_or(0.0);
954 let m1_intensity = m0_result.distribution.get(1).map(|(_, i)| *i).unwrap_or(0.0);
955
956 assert!(
957 m0_intensity > 0.0,
958 "M0 fragment should have intensity when M0 precursor transmitted"
959 );
960 assert!(
961 approx_eq(m1_intensity, 0.0, 1e-10),
962 "M1 fragment should have zero intensity when only M0 precursor transmitted, got {}",
963 m1_intensity
964 );
965 }
966
967 #[test]
969 fn test_transmission_empty_inputs() {
970 let empty: Vec<(f64, f64)> = vec![];
971 let non_empty = vec![(500.0, 0.5)];
972 let isotopes: HashSet<usize> = [0].iter().cloned().collect();
973
974 let result1 = calculate_transmission_dependent_distribution_with_factor(
976 &empty,
977 &non_empty,
978 &isotopes,
979 0,
980 );
981 assert!(result1.distribution.is_empty());
982 assert!(approx_eq(result1.transmission_factor, 0.0, 1e-10));
983
984 let result2 = calculate_transmission_dependent_distribution_with_factor(
986 &non_empty,
987 &empty,
988 &isotopes,
989 0,
990 );
991 assert!(result2.distribution.is_empty());
992 assert!(approx_eq(result2.transmission_factor, 0.0, 1e-10));
993 }
994
995 #[test]
997 fn test_isotope_pattern_shift() {
998 let fragment_dist = vec![
1000 (500.0, 0.6),
1001 (501.0, 0.3),
1002 (502.0, 0.1),
1003 ];
1004
1005 let comp_dist = vec![
1006 (800.0, 0.7),
1007 (801.0, 0.2),
1008 (802.0, 0.1),
1009 ];
1010
1011 let all_isotopes: HashSet<usize> = [0, 1, 2].iter().cloned().collect();
1013 let full_result = calculate_transmission_dependent_distribution_with_factor(
1014 &fragment_dist,
1015 &comp_dist,
1016 &all_isotopes,
1017 0,
1018 );
1019
1020 let m0_only: HashSet<usize> = [0].iter().cloned().collect();
1022 let m0_result = calculate_transmission_dependent_distribution_with_factor(
1023 &fragment_dist,
1024 &comp_dist,
1025 &m0_only,
1026 0,
1027 );
1028
1029 let full_sum: f64 = full_result.distribution.iter().map(|(_, i)| i).sum();
1031 let m0_sum: f64 = m0_result.distribution.iter().map(|(_, i)| i).sum();
1032
1033 let full_m0_fraction = if full_sum > 0.0 {
1034 full_result.distribution[0].1 / full_sum
1035 } else {
1036 0.0
1037 };
1038
1039 let m0_m0_fraction = if m0_sum > 0.0 {
1040 m0_result.distribution[0].1 / m0_sum
1041 } else {
1042 0.0
1043 };
1044
1045 assert!(
1048 m0_m0_fraction >= full_m0_fraction,
1049 "M0 fraction with M0-only transmission ({}) should be >= full transmission ({})",
1050 m0_m0_fraction,
1051 full_m0_fraction
1052 );
1053 }
1054
1055 #[test]
1057 fn test_max_isotope_limit() {
1058 let fragment_dist = vec![
1059 (500.0, 0.5),
1060 (501.0, 0.3),
1061 (502.0, 0.15),
1062 (503.0, 0.05),
1063 ];
1064
1065 let comp_dist = vec![
1066 (800.0, 0.6),
1067 (801.0, 0.25),
1068 (802.0, 0.1),
1069 (803.0, 0.05),
1070 ];
1071
1072 let all_isotopes: HashSet<usize> = [0, 1, 2, 3].iter().cloned().collect();
1073
1074 let result = calculate_transmission_dependent_distribution_with_factor(
1076 &fragment_dist,
1077 &comp_dist,
1078 &all_isotopes,
1079 2,
1080 );
1081
1082 assert_eq!(
1083 result.distribution.len(),
1084 2,
1085 "Distribution should be limited to 2 isotopes, got {}",
1086 result.distribution.len()
1087 );
1088 }
1089
1090 #[test]
1093 fn test_frame_building_intensity_scaling() {
1094 let fragment_dist = vec![
1097 (500.25, 0.65),
1098 (501.25, 0.25),
1099 (502.25, 0.08),
1100 (503.25, 0.02),
1101 ];
1102
1103 let comp_dist = vec![
1105 (800.40, 0.55),
1106 (801.40, 0.28),
1107 (802.40, 0.12),
1108 (803.40, 0.05),
1109 ];
1110
1111 let fraction_events: f64 = 1000.0; let all_isotopes: HashSet<usize> = (0..4).collect();
1116 let full_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
1117 &fragment_dist,
1118 &comp_dist,
1119 &all_isotopes,
1120 0,
1121 );
1122 let full_intensity_sum: f64 = full_dist.iter().map(|(_, i)| i * fraction_events).sum();
1123
1124 let m0_only: HashSet<usize> = [0].iter().cloned().collect();
1126 let m0_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
1127 &fragment_dist,
1128 &comp_dist,
1129 &m0_only,
1130 0,
1131 );
1132 let m0_intensity_sum: f64 = m0_dist.iter().map(|(_, i)| i * fraction_events).sum();
1133
1134 let m0_m1: HashSet<usize> = [0, 1].iter().cloned().collect();
1136 let m0_m1_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
1137 &fragment_dist,
1138 &comp_dist,
1139 &m0_m1,
1140 0,
1141 );
1142 let m0_m1_intensity_sum: f64 = m0_m1_dist.iter().map(|(_, i)| i * fraction_events).sum();
1143
1144 assert!(
1146 full_intensity_sum > m0_m1_intensity_sum,
1147 "Full transmission intensity ({}) should be > M0+M1 ({})",
1148 full_intensity_sum, m0_m1_intensity_sum
1149 );
1150 assert!(
1151 m0_m1_intensity_sum > m0_intensity_sum,
1152 "M0+M1 transmission intensity ({}) should be > M0 only ({})",
1153 m0_m1_intensity_sum, m0_intensity_sum
1154 );
1155
1156 let m0_m1_ratio = m0_m1_intensity_sum / full_intensity_sum;
1159 assert!(
1160 m0_m1_ratio > 0.5 && m0_m1_ratio < 1.0,
1161 "M0+M1 ratio ({}) should be between 0.5 and 1.0",
1162 m0_m1_ratio
1163 );
1164
1165 let m0_ratio = m0_intensity_sum / full_intensity_sum;
1167 assert!(
1168 m0_ratio > 0.2 && m0_ratio < 0.8,
1169 "M0 ratio ({}) should be between 0.2 and 0.8",
1170 m0_ratio
1171 );
1172
1173 let factor_result = calculate_transmission_dependent_distribution_with_factor(
1175 &fragment_dist,
1176 &comp_dist,
1177 &m0_m1,
1178 0,
1179 );
1180
1181 let manual_factor = m0_m1_intensity_sum / full_intensity_sum;
1183 assert!(
1184 approx_eq(factor_result.transmission_factor, manual_factor, 0.001),
1185 "Transmission factor ({}) should match manual calculation ({})",
1186 factor_result.transmission_factor, manual_factor
1187 );
1188
1189 println!("Frame building intensity test results:");
1190 println!(" Full transmission intensity: {:.2}", full_intensity_sum);
1191 println!(" M0+M1 transmission intensity: {:.2} (ratio: {:.3})", m0_m1_intensity_sum, m0_m1_ratio);
1192 println!(" M0 only transmission intensity: {:.2} (ratio: {:.3})", m0_intensity_sum, m0_ratio);
1193 println!(" Transmission factor (M0+M1): {:.4}", factor_result.transmission_factor);
1194 }
1195
1196 #[test]
1198 fn test_realistic_peptide_transmission() {
1199 let b_ion_dist = vec![
1204 (600.30, 0.58),
1205 (601.30, 0.28),
1206 (602.30, 0.10),
1207 (603.30, 0.03),
1208 (604.30, 0.01),
1209 ];
1210
1211 let comp_dist = vec![
1213 (900.45, 0.48),
1214 (901.45, 0.30),
1215 (902.45, 0.14),
1216 (903.45, 0.06),
1217 (904.45, 0.02),
1218 ];
1219
1220 let wide_window: HashSet<usize> = [0, 1, 2].iter().cloned().collect();
1224 let wide_result = calculate_transmission_dependent_distribution_with_factor(
1225 &b_ion_dist,
1226 &comp_dist,
1227 &wide_window,
1228 0,
1229 );
1230
1231 let narrow_window: HashSet<usize> = [0, 1].iter().cloned().collect();
1233 let narrow_result = calculate_transmission_dependent_distribution_with_factor(
1234 &b_ion_dist,
1235 &comp_dist,
1236 &narrow_window,
1237 0,
1238 );
1239
1240 let very_narrow: HashSet<usize> = [0].iter().cloned().collect();
1242 let very_narrow_result = calculate_transmission_dependent_distribution_with_factor(
1243 &b_ion_dist,
1244 &comp_dist,
1245 &very_narrow,
1246 0,
1247 );
1248
1249 assert!(
1251 wide_result.transmission_factor > narrow_result.transmission_factor,
1252 "Wide window should have higher transmission"
1253 );
1254 assert!(
1255 narrow_result.transmission_factor > very_narrow_result.transmission_factor,
1256 "Narrow window should have higher transmission than very narrow"
1257 );
1258
1259 assert!(wide_result.transmission_factor > 0.0 && wide_result.transmission_factor <= 1.0);
1261 assert!(narrow_result.transmission_factor > 0.0 && narrow_result.transmission_factor <= 1.0);
1262 assert!(very_narrow_result.transmission_factor > 0.0 && very_narrow_result.transmission_factor <= 1.0);
1263
1264 println!("Realistic peptide transmission test results:");
1265 println!(" Wide window (M0-M2) transmission factor: {:.4}", wide_result.transmission_factor);
1266 println!(" Narrow window (M0-M1) transmission factor: {:.4}", narrow_result.transmission_factor);
1267 println!(" Very narrow (M0 only) transmission factor: {:.4}", very_narrow_result.transmission_factor);
1268 }
1269}