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 let mut elements: Vec<(&String, i32)> =
183 atomic_composition.iter().map(|(k, v)| (k, *v)).collect();
184 elements.sort_by(|a, b| a.0.cmp(b.0));
185
186 for (element, count) in elements {
187 let elemental_isotope_weights = atoms_isotopic_weights
188 .get(element)
189 .expect("Element not found in isotopic weights table")
190 .clone();
191 let elemental_isotope_abundance = atomic_isotope_abundance
192 .get(element)
193 .expect("Element not found in isotopic abundance table")
194 .clone();
195
196 let element_distribution: Vec<(f64, f64)> = elemental_isotope_weights
197 .iter()
198 .zip(elemental_isotope_abundance.iter())
199 .map(|(&mass, &abundance)| (mass, abundance))
200 .collect();
201
202 let element_power_distribution = if count > 1 {
203 convolve_pow(&element_distribution, count)
204 } else {
205 element_distribution
206 };
207
208 cumulative_distribution = match cumulative_distribution {
209 Some(cum_dist) => Some(convolve(
210 &cum_dist,
211 &element_power_distribution,
212 mass_tolerance,
213 abundance_threshold,
214 max_result as usize,
215 )),
216 None => Some(element_power_distribution),
217 };
218 }
219
220 let final_distribution = cumulative_distribution.expect("Peptide has no elements");
221 let total_abundance: f64 = final_distribution
223 .iter()
224 .map(|&(_, abundance)| abundance)
225 .sum();
226 let result: Vec<_> = final_distribution
227 .into_iter()
228 .map(|(mass, abundance)| (mass, abundance / total_abundance))
229 .collect();
230
231 let mut sort_map: BTreeMap<i64, f64> = BTreeMap::new();
232 let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
233
234 for (mz, intensity) in result {
235 let key = quantize(mz);
236 sort_map
237 .entry(key)
238 .and_modify(|e| *e += intensity)
239 .or_insert(intensity);
240 }
241
242 let mz: Vec<f64> = sort_map
243 .keys()
244 .map(|&key| key as f64 / 1_000_000.0)
245 .collect();
246 let intensity: Vec<f64> = sort_map.values().map(|&intensity| intensity).collect();
247 mz.iter()
248 .zip(intensity.iter())
249 .map(|(&mz, &intensity)| (mz, intensity))
250 .collect()
251}
252
253pub fn normal_pdf(x: f64, mean: f64, std_dev: f64) -> f64 {
274 let normal = Normal::new(mean, std_dev).unwrap();
275 normal.pdf(x)
276}
277
278pub fn factorial(n: i32) -> f64 {
297 (1..=n).fold(1.0, |acc, x| acc * x as f64)
298}
299
300pub fn weight(mass: f64, peak_nums: Vec<i32>, normalize: bool) -> Vec<f64> {
301 let lam_val = lam(mass, 0.000594, -0.03091);
302 let factorials: Vec<f64> = peak_nums.iter().map(|&k| factorial(k)).collect();
303 let mut weights: Vec<f64> = peak_nums
304 .iter()
305 .map(|&k| {
306 let pow = lam_val.powi(k);
307 let exp = (-lam_val).exp();
308 exp * pow / factorials[k as usize]
309 })
310 .collect();
311
312 if normalize {
313 let sum: f64 = weights.iter().sum();
314 weights = weights.iter().map(|&w| w / sum).collect();
315 }
316
317 weights
318}
319
320pub fn lam(mass: f64, slope: f64, intercept: f64) -> f64 {
340 slope * mass + intercept
341}
342
343pub fn iso(
361 x: &Vec<f64>,
362 mass: f64,
363 charge: f64,
364 sigma: f64,
365 amp: f64,
366 k: usize,
367 step_size: f64,
368) -> Vec<f64> {
369 let k_range: Vec<usize> = (0..k).collect();
370 let means: Vec<f64> = k_range
371 .iter()
372 .map(|&k_val| (mass + MASS_NEUTRON * k_val as f64) / charge)
373 .collect();
374 let weights = weight(
375 mass,
376 k_range
377 .iter()
378 .map(|&k_val| k_val as i32)
379 .collect::<Vec<i32>>(),
380 true,
381 );
382
383 let mut intensities = vec![0.0; x.len()];
384 for (i, x_val) in x.iter().enumerate() {
385 for (j, &mean) in means.iter().enumerate() {
386 intensities[i] += weights[j] * normal_pdf(*x_val, mean, sigma);
387 }
388 intensities[i] *= step_size;
389 }
390 intensities
391 .iter()
392 .map(|&intensity| intensity * amp)
393 .collect()
394}
395
396pub fn generate_isotope_pattern(
421 lower_bound: f64,
422 upper_bound: f64,
423 mass: f64,
424 charge: f64,
425 amp: f64,
426 k: usize,
427 sigma: f64,
428 resolution: i32,
429) -> (Vec<f64>, Vec<f64>) {
430 let step_size = f64::min(sigma / 10.0, 1.0 / 10f64.powi(resolution));
431 let size = ((upper_bound - lower_bound) / step_size).ceil() as usize;
432 let mzs: Vec<f64> = (0..size)
433 .map(|i| lower_bound + step_size * i as f64)
434 .collect();
435 let intensities = iso(&mzs, mass, charge, sigma, amp, k, step_size);
436
437 (
438 mzs.iter().map(|&mz| mz + MASS_PROTON).collect(),
439 intensities,
440 )
441}
442
443pub fn generate_averagine_spectrum(
467 mass: f64,
468 charge: i32,
469 min_intensity: i32,
470 k: i32,
471 resolution: i32,
472 centroid: bool,
473 amp: Option<f64>,
474) -> MzSpectrum {
475 let amp = amp.unwrap_or(1e4);
476 let lb = mass / charge as f64 - 0.2;
477 let ub = mass / charge as f64 + k as f64 + 0.2;
478
479 let (mz, intensities) = generate_isotope_pattern(
480 lb,
481 ub,
482 mass,
483 charge as f64,
484 amp,
485 k as usize,
486 0.008492569002123142,
487 resolution,
488 );
489
490 let spectrum = MzSpectrum::new(mz, intensities)
491 .to_resolution(resolution)
492 .filter_ranged(lb, ub, min_intensity as f64, 1e9);
493
494 if centroid {
495 spectrum.to_centroid(
496 std::cmp::max(min_intensity, 1),
497 1.0 / 10f64.powi(resolution - 1),
498 true,
499 )
500 } else {
501 spectrum
502 }
503}
504
505pub fn generate_averagine_spectra(
533 masses: Vec<f64>,
534 charges: Vec<i32>,
535 min_intensity: i32,
536 k: i32,
537 resolution: i32,
538 centroid: bool,
539 num_threads: usize,
540 amp: Option<f64>,
541) -> Vec<MzSpectrum> {
542 let amp = amp.unwrap_or(1e5);
543 let mut spectra: Vec<MzSpectrum> = Vec::new();
544 let thread_pool = ThreadPoolBuilder::new()
545 .num_threads(num_threads)
546 .build()
547 .unwrap();
548
549 thread_pool.install(|| {
550 spectra = masses
551 .par_iter()
552 .zip(charges.par_iter())
553 .map(|(&mass, &charge)| {
554 generate_averagine_spectrum(
555 mass,
556 charge,
557 min_intensity,
558 k,
559 resolution,
560 centroid,
561 Some(amp),
562 )
563 })
564 .collect();
565 });
566
567 spectra
568}
569
570pub fn generate_precursor_spectrum(
583 sequence: &str,
584 charge: i32,
585 peptide_id: Option<i32>,
586) -> MzSpectrum {
587 let peptide_ion = PeptideIon::new(sequence.to_string(), charge, 1.0, peptide_id);
588 peptide_ion.calculate_isotopic_spectrum(1e-3, 1e-9, 200, 1e-6)
589}
590
591pub fn generate_precursor_spectra(
604 sequences: &Vec<&str>,
605 charges: &Vec<i32>,
606 num_threads: usize,
607 peptide_ids: Vec<Option<i32>>,
608) -> Vec<MzSpectrum> {
609 let thread_pool = ThreadPoolBuilder::new()
610 .num_threads(num_threads)
611 .build()
612 .unwrap();
613 let result = thread_pool.install(|| {
615 sequences
616 .par_iter()
617 .zip(charges.par_iter())
618 .zip(peptide_ids.par_iter())
619 .map(|((&sequence, &charge), &peptide_id)| {
620 generate_precursor_spectrum(sequence, charge, peptide_id)
621 })
622 .collect()
623 });
624 result
625}
626
627#[derive(Debug, Clone)]
630pub struct TransmissionDependentIsotopeDistribution {
631 pub distribution: Vec<(f64, f64)>,
633 pub transmission_factor: f64,
637}
638
639pub fn calculate_transmission_dependent_fragment_ion_isotope_distribution(
642 fragment_isotope_dist: &Vec<(f64, f64)>,
643 comp_fragment_isotope_dist: &Vec<(f64, f64)>,
644 precursor_isotopes: &HashSet<usize>,
645 max_isotope: usize,
646) -> Vec<(f64, f64)> {
647 if fragment_isotope_dist.is_empty() || comp_fragment_isotope_dist.is_empty() {
648 return Vec::new();
649 }
650
651 let mut r_max = fragment_isotope_dist.len();
652 if max_isotope != 0 && r_max > max_isotope {
653 r_max = max_isotope;
654 }
655
656 let mut result = (0..r_max)
657 .map(|i| (fragment_isotope_dist[0].0 + i as f64, 0.0))
658 .collect::<Vec<(f64, f64)>>();
659
660 for (i, &(_mz, intensity)) in fragment_isotope_dist.iter().enumerate().take(r_max) {
662 for &precursor in precursor_isotopes {
663 if precursor >= i && (precursor - i) < comp_fragment_isotope_dist.len() {
664 let comp_intensity = comp_fragment_isotope_dist[precursor - i].1;
665 result[i].1 += comp_intensity;
666 }
667 }
668 result[i].1 *= intensity;
669 }
670
671 result
672}
673
674pub fn calculate_transmission_dependent_distribution_with_factor(
703 fragment_isotope_dist: &Vec<(f64, f64)>,
704 comp_fragment_isotope_dist: &Vec<(f64, f64)>,
705 precursor_isotopes: &HashSet<usize>,
706 max_isotope: usize,
707) -> TransmissionDependentIsotopeDistribution {
708 if fragment_isotope_dist.is_empty() || comp_fragment_isotope_dist.is_empty() {
709 return TransmissionDependentIsotopeDistribution {
710 distribution: Vec::new(),
711 transmission_factor: 0.0,
712 };
713 }
714
715 let all_isotopes: HashSet<usize> = (0..fragment_isotope_dist.len().max(comp_fragment_isotope_dist.len())).collect();
717 let full_distribution = calculate_transmission_dependent_fragment_ion_isotope_distribution(
718 fragment_isotope_dist,
719 comp_fragment_isotope_dist,
720 &all_isotopes,
721 max_isotope,
722 );
723 let full_sum: f64 = full_distribution.iter().map(|(_, i)| i).sum();
724
725 let transmitted_distribution = calculate_transmission_dependent_fragment_ion_isotope_distribution(
727 fragment_isotope_dist,
728 comp_fragment_isotope_dist,
729 precursor_isotopes,
730 max_isotope,
731 );
732 let transmitted_sum: f64 = transmitted_distribution.iter().map(|(_, i)| i).sum();
733
734 let transmission_factor = if full_sum > 0.0 {
736 transmitted_sum / full_sum
737 } else {
738 0.0
739 };
740
741 TransmissionDependentIsotopeDistribution {
742 distribution: transmitted_distribution,
743 transmission_factor,
744 }
745}
746
747pub fn calculate_precursor_transmission_factor(
761 precursor_isotope_dist: &[(f64, f64)],
762 transmitted_indices: &HashSet<usize>,
763) -> f64 {
764 if precursor_isotope_dist.is_empty() || transmitted_indices.is_empty() {
765 return 0.0;
766 }
767
768 let total_intensity: f64 = precursor_isotope_dist.iter().map(|(_, i)| i).sum();
769 if total_intensity <= 0.0 {
770 return 0.0;
771 }
772
773 let transmitted_intensity: f64 = precursor_isotope_dist
774 .iter()
775 .enumerate()
776 .filter(|(idx, _)| transmitted_indices.contains(idx))
777 .map(|(_, (_, i))| i)
778 .sum();
779
780 transmitted_intensity / total_intensity
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
788 (a - b).abs() < epsilon
789 }
790
791 #[test]
794 fn test_transmission_all_isotopes() {
795 let fragment_dist = vec![
797 (500.0, 0.6),
798 (501.0, 0.3),
799 (502.0, 0.1),
800 ];
801
802 let comp_dist = vec![
804 (800.0, 0.7),
805 (801.0, 0.2),
806 (802.0, 0.1),
807 ];
808
809 let all_isotopes: HashSet<usize> = (0..3).collect();
812
813 let result = calculate_transmission_dependent_distribution_with_factor(
814 &fragment_dist,
815 &comp_dist,
816 &all_isotopes,
817 0,
818 );
819
820 assert!(
822 approx_eq(result.transmission_factor, 1.0, 0.001),
823 "Transmission factor should be 1.0 when same isotopes as reference transmitted, got {}",
824 result.transmission_factor
825 );
826
827 assert!(!result.distribution.is_empty());
829 }
830
831 #[test]
834 fn test_transmission_extra_isotopes() {
835 let fragment_dist = vec![
836 (500.0, 0.6),
837 (501.0, 0.3),
838 (502.0, 0.1),
839 ];
840
841 let comp_dist = vec![
842 (800.0, 0.7),
843 (801.0, 0.2),
844 (802.0, 0.1),
845 ];
846
847 let extra_isotopes: HashSet<usize> = [0, 1, 2, 3, 4, 5].iter().cloned().collect();
849
850 let result = calculate_transmission_dependent_distribution_with_factor(
851 &fragment_dist,
852 &comp_dist,
853 &extra_isotopes,
854 0,
855 );
856
857 assert!(
860 result.transmission_factor >= 1.0,
861 "Extra isotopes should give transmission factor >= 1.0, got {}",
862 result.transmission_factor
863 );
864 }
865
866 #[test]
868 fn test_transmission_partial_isotopes() {
869 let fragment_dist = vec![
871 (500.0, 0.5),
872 (501.0, 0.3),
873 (502.0, 0.15),
874 (503.0, 0.05),
875 ];
876
877 let comp_dist = vec![
879 (800.0, 0.6),
880 (801.0, 0.25),
881 (802.0, 0.1),
882 (803.0, 0.05),
883 ];
884
885 let all_isotopes: HashSet<usize> = [0, 1, 2, 3].iter().cloned().collect();
887 let full_result = calculate_transmission_dependent_distribution_with_factor(
888 &fragment_dist,
889 &comp_dist,
890 &all_isotopes,
891 0,
892 );
893
894 let partial_isotopes: HashSet<usize> = [0, 1].iter().cloned().collect();
896 let partial_result = calculate_transmission_dependent_distribution_with_factor(
897 &fragment_dist,
898 &comp_dist,
899 &partial_isotopes,
900 0,
901 );
902
903 assert!(
905 partial_result.transmission_factor < full_result.transmission_factor,
906 "Partial transmission ({}) should be less than full transmission ({})",
907 partial_result.transmission_factor,
908 full_result.transmission_factor
909 );
910
911 assert!(
913 partial_result.transmission_factor > 0.0 && partial_result.transmission_factor < 1.0,
914 "Partial transmission factor should be between 0 and 1, got {}",
915 partial_result.transmission_factor
916 );
917 }
918
919 #[test]
921 fn test_transmission_m0_only() {
922 let fragment_dist = vec![
923 (500.0, 0.5),
924 (501.0, 0.3),
925 (502.0, 0.15),
926 (503.0, 0.05),
927 ];
928
929 let comp_dist = vec![
930 (800.0, 0.6),
931 (801.0, 0.25),
932 (802.0, 0.1),
933 (803.0, 0.05),
934 ];
935
936 let m0_only: HashSet<usize> = [0].iter().cloned().collect();
938 let m0_result = calculate_transmission_dependent_distribution_with_factor(
939 &fragment_dist,
940 &comp_dist,
941 &m0_only,
942 0,
943 );
944
945 let m0_m1: HashSet<usize> = [0, 1].iter().cloned().collect();
947 let m0_m1_result = calculate_transmission_dependent_distribution_with_factor(
948 &fragment_dist,
949 &comp_dist,
950 &m0_m1,
951 0,
952 );
953
954 assert!(
956 m0_result.transmission_factor < m0_m1_result.transmission_factor,
957 "M0 only ({}) should transmit less than M0+M1 ({})",
958 m0_result.transmission_factor,
959 m0_m1_result.transmission_factor
960 );
961
962 let m0_intensity = m0_result.distribution.get(0).map(|(_, i)| *i).unwrap_or(0.0);
965 let m1_intensity = m0_result.distribution.get(1).map(|(_, i)| *i).unwrap_or(0.0);
966
967 assert!(
968 m0_intensity > 0.0,
969 "M0 fragment should have intensity when M0 precursor transmitted"
970 );
971 assert!(
972 approx_eq(m1_intensity, 0.0, 1e-10),
973 "M1 fragment should have zero intensity when only M0 precursor transmitted, got {}",
974 m1_intensity
975 );
976 }
977
978 #[test]
980 fn test_transmission_empty_inputs() {
981 let empty: Vec<(f64, f64)> = vec![];
982 let non_empty = vec![(500.0, 0.5)];
983 let isotopes: HashSet<usize> = [0].iter().cloned().collect();
984
985 let result1 = calculate_transmission_dependent_distribution_with_factor(
987 &empty,
988 &non_empty,
989 &isotopes,
990 0,
991 );
992 assert!(result1.distribution.is_empty());
993 assert!(approx_eq(result1.transmission_factor, 0.0, 1e-10));
994
995 let result2 = calculate_transmission_dependent_distribution_with_factor(
997 &non_empty,
998 &empty,
999 &isotopes,
1000 0,
1001 );
1002 assert!(result2.distribution.is_empty());
1003 assert!(approx_eq(result2.transmission_factor, 0.0, 1e-10));
1004 }
1005
1006 #[test]
1008 fn test_isotope_pattern_shift() {
1009 let fragment_dist = vec![
1011 (500.0, 0.6),
1012 (501.0, 0.3),
1013 (502.0, 0.1),
1014 ];
1015
1016 let comp_dist = vec![
1017 (800.0, 0.7),
1018 (801.0, 0.2),
1019 (802.0, 0.1),
1020 ];
1021
1022 let all_isotopes: HashSet<usize> = [0, 1, 2].iter().cloned().collect();
1024 let full_result = calculate_transmission_dependent_distribution_with_factor(
1025 &fragment_dist,
1026 &comp_dist,
1027 &all_isotopes,
1028 0,
1029 );
1030
1031 let m0_only: HashSet<usize> = [0].iter().cloned().collect();
1033 let m0_result = calculate_transmission_dependent_distribution_with_factor(
1034 &fragment_dist,
1035 &comp_dist,
1036 &m0_only,
1037 0,
1038 );
1039
1040 let full_sum: f64 = full_result.distribution.iter().map(|(_, i)| i).sum();
1042 let m0_sum: f64 = m0_result.distribution.iter().map(|(_, i)| i).sum();
1043
1044 let full_m0_fraction = if full_sum > 0.0 {
1045 full_result.distribution[0].1 / full_sum
1046 } else {
1047 0.0
1048 };
1049
1050 let m0_m0_fraction = if m0_sum > 0.0 {
1051 m0_result.distribution[0].1 / m0_sum
1052 } else {
1053 0.0
1054 };
1055
1056 assert!(
1059 m0_m0_fraction >= full_m0_fraction,
1060 "M0 fraction with M0-only transmission ({}) should be >= full transmission ({})",
1061 m0_m0_fraction,
1062 full_m0_fraction
1063 );
1064 }
1065
1066 #[test]
1068 fn test_max_isotope_limit() {
1069 let fragment_dist = vec![
1070 (500.0, 0.5),
1071 (501.0, 0.3),
1072 (502.0, 0.15),
1073 (503.0, 0.05),
1074 ];
1075
1076 let comp_dist = vec![
1077 (800.0, 0.6),
1078 (801.0, 0.25),
1079 (802.0, 0.1),
1080 (803.0, 0.05),
1081 ];
1082
1083 let all_isotopes: HashSet<usize> = [0, 1, 2, 3].iter().cloned().collect();
1084
1085 let result = calculate_transmission_dependent_distribution_with_factor(
1087 &fragment_dist,
1088 &comp_dist,
1089 &all_isotopes,
1090 2,
1091 );
1092
1093 assert_eq!(
1094 result.distribution.len(),
1095 2,
1096 "Distribution should be limited to 2 isotopes, got {}",
1097 result.distribution.len()
1098 );
1099 }
1100
1101 #[test]
1104 fn test_frame_building_intensity_scaling() {
1105 let fragment_dist = vec![
1108 (500.25, 0.65),
1109 (501.25, 0.25),
1110 (502.25, 0.08),
1111 (503.25, 0.02),
1112 ];
1113
1114 let comp_dist = vec![
1116 (800.40, 0.55),
1117 (801.40, 0.28),
1118 (802.40, 0.12),
1119 (803.40, 0.05),
1120 ];
1121
1122 let fraction_events: f64 = 1000.0; let all_isotopes: HashSet<usize> = (0..4).collect();
1127 let full_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
1128 &fragment_dist,
1129 &comp_dist,
1130 &all_isotopes,
1131 0,
1132 );
1133 let full_intensity_sum: f64 = full_dist.iter().map(|(_, i)| i * fraction_events).sum();
1134
1135 let m0_only: HashSet<usize> = [0].iter().cloned().collect();
1137 let m0_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
1138 &fragment_dist,
1139 &comp_dist,
1140 &m0_only,
1141 0,
1142 );
1143 let m0_intensity_sum: f64 = m0_dist.iter().map(|(_, i)| i * fraction_events).sum();
1144
1145 let m0_m1: HashSet<usize> = [0, 1].iter().cloned().collect();
1147 let m0_m1_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
1148 &fragment_dist,
1149 &comp_dist,
1150 &m0_m1,
1151 0,
1152 );
1153 let m0_m1_intensity_sum: f64 = m0_m1_dist.iter().map(|(_, i)| i * fraction_events).sum();
1154
1155 assert!(
1157 full_intensity_sum > m0_m1_intensity_sum,
1158 "Full transmission intensity ({}) should be > M0+M1 ({})",
1159 full_intensity_sum, m0_m1_intensity_sum
1160 );
1161 assert!(
1162 m0_m1_intensity_sum > m0_intensity_sum,
1163 "M0+M1 transmission intensity ({}) should be > M0 only ({})",
1164 m0_m1_intensity_sum, m0_intensity_sum
1165 );
1166
1167 let m0_m1_ratio = m0_m1_intensity_sum / full_intensity_sum;
1170 assert!(
1171 m0_m1_ratio > 0.5 && m0_m1_ratio < 1.0,
1172 "M0+M1 ratio ({}) should be between 0.5 and 1.0",
1173 m0_m1_ratio
1174 );
1175
1176 let m0_ratio = m0_intensity_sum / full_intensity_sum;
1178 assert!(
1179 m0_ratio > 0.2 && m0_ratio < 0.8,
1180 "M0 ratio ({}) should be between 0.2 and 0.8",
1181 m0_ratio
1182 );
1183
1184 let factor_result = calculate_transmission_dependent_distribution_with_factor(
1186 &fragment_dist,
1187 &comp_dist,
1188 &m0_m1,
1189 0,
1190 );
1191
1192 let manual_factor = m0_m1_intensity_sum / full_intensity_sum;
1194 assert!(
1195 approx_eq(factor_result.transmission_factor, manual_factor, 0.001),
1196 "Transmission factor ({}) should match manual calculation ({})",
1197 factor_result.transmission_factor, manual_factor
1198 );
1199
1200 println!("Frame building intensity test results:");
1201 println!(" Full transmission intensity: {:.2}", full_intensity_sum);
1202 println!(" M0+M1 transmission intensity: {:.2} (ratio: {:.3})", m0_m1_intensity_sum, m0_m1_ratio);
1203 println!(" M0 only transmission intensity: {:.2} (ratio: {:.3})", m0_intensity_sum, m0_ratio);
1204 println!(" Transmission factor (M0+M1): {:.4}", factor_result.transmission_factor);
1205 }
1206
1207 #[test]
1209 fn test_realistic_peptide_transmission() {
1210 let b_ion_dist = vec![
1215 (600.30, 0.58),
1216 (601.30, 0.28),
1217 (602.30, 0.10),
1218 (603.30, 0.03),
1219 (604.30, 0.01),
1220 ];
1221
1222 let comp_dist = vec![
1224 (900.45, 0.48),
1225 (901.45, 0.30),
1226 (902.45, 0.14),
1227 (903.45, 0.06),
1228 (904.45, 0.02),
1229 ];
1230
1231 let wide_window: HashSet<usize> = [0, 1, 2].iter().cloned().collect();
1235 let wide_result = calculate_transmission_dependent_distribution_with_factor(
1236 &b_ion_dist,
1237 &comp_dist,
1238 &wide_window,
1239 0,
1240 );
1241
1242 let narrow_window: HashSet<usize> = [0, 1].iter().cloned().collect();
1244 let narrow_result = calculate_transmission_dependent_distribution_with_factor(
1245 &b_ion_dist,
1246 &comp_dist,
1247 &narrow_window,
1248 0,
1249 );
1250
1251 let very_narrow: HashSet<usize> = [0].iter().cloned().collect();
1253 let very_narrow_result = calculate_transmission_dependent_distribution_with_factor(
1254 &b_ion_dist,
1255 &comp_dist,
1256 &very_narrow,
1257 0,
1258 );
1259
1260 assert!(
1262 wide_result.transmission_factor > narrow_result.transmission_factor,
1263 "Wide window should have higher transmission"
1264 );
1265 assert!(
1266 narrow_result.transmission_factor > very_narrow_result.transmission_factor,
1267 "Narrow window should have higher transmission than very narrow"
1268 );
1269
1270 assert!(wide_result.transmission_factor > 0.0 && wide_result.transmission_factor <= 1.0);
1272 assert!(narrow_result.transmission_factor > 0.0 && narrow_result.transmission_factor <= 1.0);
1273 assert!(very_narrow_result.transmission_factor > 0.0 && very_narrow_result.transmission_factor <= 1.0);
1274
1275 println!("Realistic peptide transmission test results:");
1276 println!(" Wide window (M0-M2) transmission factor: {:.4}", wide_result.transmission_factor);
1277 println!(" Narrow window (M0-M1) transmission factor: {:.4}", narrow_result.transmission_factor);
1278 println!(" Very narrow (M0 only) transmission factor: {:.4}", very_narrow_result.transmission_factor);
1279 }
1280}