1use std::collections::HashMap;
2use std::f64::consts::SQRT_2;
3use rayon::prelude::*;
4use rayon::ThreadPoolBuilder;
5
6use std::collections::VecDeque;
7
8fn gauss_kronrod(f: &dyn Fn(f64) -> f64, a: f64, b: f64) -> (f64, f64) {
9 let nodes = [
10 0.0, 0.20778495500789848, 0.40584515137739717, 0.58608723546769113,
11 0.74153118559939444, 0.86486442335976907, 0.94910791234275852, 0.99145537112081264,
12 ];
13 let weights_gauss = [
14 0.41795918367346939, 0.38183005050511894, 0.27970539148927667, 0.12948496616886969,
15 ];
16 let weights_kronrod = [
17 0.20948214108472783, 0.20443294007529889, 0.19035057806478541, 0.16900472663926790,
18 0.14065325971552592, 0.10479001032225018, 0.06309209262997855, 0.02293532201052922,
19 ];
20
21 let c1 = (b - a) / 2.0;
22 let c2 = (b + a) / 2.0;
23
24 let mut integral_gauss = 0.0;
25 let mut integral_kronrod = 0.0;
26
27 for i in 0..4 {
28 let x = c1 * nodes[i] + c2;
29 integral_gauss += weights_gauss[i] * (f(x) + f(2.0 * c2 - x));
30 }
31
32 for i in 0..8 {
33 let x = c1 * nodes[i] + c2;
34 integral_kronrod += weights_kronrod[i] * (f(x) + f(2.0 * c2 - x));
35 }
36
37 integral_gauss *= c1;
38 integral_kronrod *= c1;
39
40 (integral_kronrod, (integral_kronrod - integral_gauss).abs())
41}
42
43pub fn adaptive_integration(f: &dyn Fn(f64) -> f64, a: f64, b: f64, epsabs: f64, epsrel: f64) -> (f64, f64) {
44 let mut intervals = VecDeque::new();
45 intervals.push_back((a, b));
46
47 let mut result = 0.0;
48 let mut total_error = 0.0;
49
50 while let Some((a, b)) = intervals.pop_front() {
51 let (integral, error) = gauss_kronrod(f, a, b);
52 if error < epsabs || error < epsrel * integral.abs() {
53 result += integral;
54 total_error += error;
55 } else {
56 let mid = (a + b) / 2.0;
57 intervals.push_back((a, mid));
58 intervals.push_back((mid, b));
59 }
60 }
61
62 (result, total_error)
63}
64
65
66
67
68fn integrate<F>(f: F, a: f64, b: f64, n: usize) -> f64
70 where
71 F: Fn(f64) -> f64,
72{
73 let dx = (b - a) / n as f64;
74 let mut sum = 0.0;
75 for i in 0..n {
76 let x = a + i as f64 * dx;
77 sum += f(x);
78 }
79 sum * dx
80}
81
82fn erfc(x: f64) -> f64 {
84 1.0 - erf(x)
85}
86
87fn erf(x: f64) -> f64 {
89 let t = 1.0 / (1.0 + 0.5 * x.abs());
90 let tau = t * (-x * x - 1.26551223 + t * (1.00002368 +
91 t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 +
92 t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 +
93 t * (-0.82215223 + t * 0.17087277)))))))))
94 .exp();
95 if x >= 0.0 {
96 1.0 - tau
97 } else {
98 tau - 1.0
99 }
100}
101
102fn emg(x: f64, mu: f64, sigma: f64, lambda: f64) -> f64 {
104 let part1 = lambda / 2.0 * (-lambda * (x - mu) + lambda * lambda * sigma * sigma / 2.0).exp();
105 let part2 = erfc((mu + lambda * sigma * sigma - x) / (sigma * 2.0_f64.sqrt()));
106 part1 * part2
107}
108
109pub fn custom_cdf_normal(x: f64, mean: f64, std_dev: f64) -> f64 {
110 let z = (x - mean) / std_dev;
111 0.5 * (1.0 + erf(z / SQRT_2))
112}
113
114pub fn accumulated_intensity_cdf_normal(sample_start: f64, sample_end: f64, mean: f64, std_dev: f64) -> f64 {
115 let cdf_start = custom_cdf_normal(sample_start, mean, std_dev);
116 let cdf_end = custom_cdf_normal(sample_end, mean, std_dev);
117 cdf_end - cdf_start
118}
119
120pub fn calculate_bounds_normal(mean: f64, std: f64, z_score: f64) -> (f64, f64) {
121 (mean - z_score * std, mean + z_score * std)
122}
123
124pub fn emg_function(x: f64, mu: f64, sigma: f64, lambda: f64) -> f64 {
125 let prefactor = lambda / 2.0 * ((lambda / 2.0) * (2.0 * mu + lambda * sigma.powi(2) - 2.0 * x)).exp();
126 let erfc_part = erfc((mu + lambda * sigma.powi(2) - x) / (SQRT_2 * sigma));
127 prefactor * erfc_part
128}
129
130pub fn emg_cdf_range(lower_limit: f64, upper_limit: f64, mu: f64, sigma: f64, lambda: f64, n_steps: Option<usize>) -> f64 {
131 let n_steps = n_steps.unwrap_or(1000);
132 integrate(|x| emg(x, mu, sigma, lambda), lower_limit, upper_limit, n_steps)
133}
134
135pub fn calculate_bounds_emg(mu: f64, sigma: f64, lambda: f64, step_size: f64, target: f64, lower_start: f64, upper_start: f64, n_steps: Option<usize>) -> (f64, f64) {
136 assert!(0.0 <= target && target <= 1.0, "target must be in [0, 1]");
137
138 let lower_initial = mu - lower_start * sigma - 2.0;
139 let upper_initial = mu + upper_start * sigma;
140
141 let steps = ((upper_initial - lower_initial) / step_size).round() as usize;
142 let search_space: Vec<f64> = (0..=steps).map(|i| lower_initial + i as f64 * step_size).collect();
143
144 let calc_cdf = |low: usize, high: usize| -> f64 {
145 emg_cdf_range(search_space[low], search_space[high], mu, sigma, lambda, n_steps)
146 };
147
148 let (mut low, mut high) = (0, steps);
150 while low < high {
151 let mid = low + (high - low) / 2;
152 if calc_cdf(0, mid) < target {
153 low = mid + 1;
154 } else {
155 high = mid;
156 }
157 }
158 let upper_cutoff_index = low;
159
160 low = 0;
161 high = upper_cutoff_index;
162 while low < high {
163 let mid = high - (high - low) / 2;
164 let prob_mid_to_upper = calc_cdf(mid, upper_cutoff_index);
165
166 if prob_mid_to_upper < target {
167 high = mid - 1;
168 } else {
169 low = mid;
170 }
171 }
172 let lower_cutoff_index = high;
173
174 (search_space[lower_cutoff_index], search_space[upper_cutoff_index])
175}
176
177pub fn calculate_frame_occurrence_emg(retention_times: &[f64], rt: f64, sigma: f64, lambda_: f64, target_p: f64, step_size: f64, n_steps: Option<usize>) -> Vec<i32> {
178 let (rt_min, rt_max) = calculate_bounds_emg(rt, sigma, lambda_, step_size, target_p, 20.0, 60.0, n_steps);
179
180 let first_frame = retention_times.iter()
182 .enumerate()
183 .min_by(|(_, &a), (_, &b)| (a - rt_min).abs().partial_cmp(&(b - rt_min).abs()).unwrap())
184 .map(|(idx, _)| idx + 1) .unwrap_or(0); let last_frame = retention_times.iter()
189 .enumerate()
190 .min_by(|(_, &a), (_, &b)| (a - rt_max).abs().partial_cmp(&(b - rt_max).abs()).unwrap())
191 .map(|(idx, _)| idx + 1) .unwrap_or(0); (first_frame..=last_frame).map(|x| x as i32).collect()
196}
197
198pub fn calculate_frame_abundance_emg(time_map: &HashMap<i32, f64>, occurrences: &[i32], rt: f64, sigma: f64, lambda_: f64, rt_cycle_length: f64, n_steps: Option<usize>) -> Vec<f64> {
199 let mut frame_abundance = Vec::new();
200
201 for &occurrence in occurrences {
202 if let Some(&time) = time_map.get(&occurrence) {
203 let start = time - rt_cycle_length;
204 let i = emg_cdf_range(start, time, rt, sigma, lambda_, n_steps);
205 frame_abundance.push(i);
206 }
207 }
208
209 frame_abundance
210}
211
212pub fn calculate_frame_occurrences_emg_par(retention_times: &[f64], rts: Vec<f64>, sigmas: Vec<f64>, lambdas: Vec<f64>, target_p: f64, step_size: f64, num_threads: usize, n_steps: Option<usize>) -> Vec<Vec<i32>> {
214 let thread_pool = ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap();
215 let result = thread_pool.install(|| {
216 rts.into_par_iter().zip(sigmas.into_par_iter()).zip(lambdas.into_par_iter())
217 .map(|((rt, sigma), lambda)| {
218 calculate_frame_occurrence_emg(retention_times, rt, sigma, lambda, target_p, step_size, n_steps)
219 })
220 .collect()
221 });
222 result
223}
224
225pub fn calculate_frame_abundances_emg_par(time_map: &HashMap<i32, f64>, occurrences: Vec<Vec<i32>>, rts: Vec<f64>, sigmas: Vec<f64>, lambdas: Vec<f64>, rt_cycle_length: f64, num_threads: usize, n_steps: Option<usize>) -> Vec<Vec<f64>> {
226 let thread_pool = ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap();
227 let result = thread_pool.install(|| {
228 occurrences.into_par_iter().zip(rts.into_par_iter()).zip(sigmas.into_par_iter()).zip(lambdas.into_par_iter())
229 .map(|(((occurrences, rt), sigma), lambda)| {
230 calculate_frame_abundance_emg(time_map, &occurrences, rt, sigma, lambda, rt_cycle_length, n_steps)
231 })
232 .collect()
233 });
234 result
235}
236
237pub fn project_emg_over_events(
258 event_intervals: &[(f64, f64)],
259 mu: f64,
260 sigma: f64,
261 lambda: f64,
262 target_p: f64,
263 step_size: f64,
264 n_steps: Option<usize>,
265) -> Vec<(usize, f64)> {
266 let (lower, upper) = calculate_bounds_emg(mu, sigma, lambda, step_size, target_p, 20.0, 60.0, n_steps);
269 event_intervals
270 .iter()
271 .enumerate()
272 .filter(|(_, &(start, end))| end >= lower && start <= upper)
273 .filter_map(|(idx, &(start, end))| {
274 let abundance = emg_cdf_range(start, end, mu, sigma, lambda, n_steps);
275 if abundance > 0.0 {
276 Some((idx, abundance))
277 } else {
278 None
279 }
280 })
281 .collect()
282}
283
284pub fn project_emg_over_events_par(
288 event_intervals: &[(f64, f64)],
289 rts: Vec<f64>,
290 sigmas: Vec<f64>,
291 lambdas: Vec<f64>,
292 target_p: f64,
293 step_size: f64,
294 num_threads: usize,
295 n_steps: Option<usize>,
296) -> Vec<Vec<(usize, f64)>> {
297 let thread_pool = ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap();
298 thread_pool.install(|| {
299 rts.into_par_iter()
300 .zip(sigmas.into_par_iter())
301 .zip(lambdas.into_par_iter())
302 .map(|((rt, sigma), lambda)| {
303 project_emg_over_events(event_intervals, rt, sigma, lambda, target_p, step_size, n_steps)
304 })
305 .collect()
306 })
307}
308
309pub fn normal_cdf_range(lower_limit: f64, upper_limit: f64, mean: f64, std_dev: f64) -> f64 {
311 let cdf_start = custom_cdf_normal(lower_limit, mean, std_dev);
312 let cdf_end = custom_cdf_normal(upper_limit, mean, std_dev);
313 cdf_end - cdf_start
314}
315
316pub fn calculate_bounds_gaussian(
319 mean: f64,
320 sigma: f64,
321 step_size: f64,
322 target: f64,
323 lower_start: f64,
324 upper_start: f64
325) -> (f64, f64) {
326 assert!((0.0..=1.0).contains(&target), "target must be in [0, 1]");
327
328 let lower_initial = mean - lower_start * sigma;
329 let upper_initial = mean + upper_start * sigma;
330
331 let steps = ((upper_initial - lower_initial) / step_size).ceil() as usize;
332 let search_space: Vec<f64> = (0..=steps)
333 .map(|i| lower_initial + i as f64 * step_size)
334 .collect();
335
336 let calc_cdf = |low: usize, high: usize| -> f64 {
337 normal_cdf_range(search_space[low], search_space[high], mean, sigma)
338 };
339
340 let (mut low, mut high) = (0, steps);
342 while low < high {
343 let mid = low + (high - low) / 2;
344 if calc_cdf(0, mid) < target {
345 low = mid + 1;
346 } else {
347 high = mid;
348 }
349 }
350 let upper_cutoff_index = low;
351
352 low = 0;
354 high = upper_cutoff_index;
355 while low < high {
356 let mid = high - (high - low) / 2;
357 if calc_cdf(mid, upper_cutoff_index) < target {
358 high = mid - 1;
359 } else {
360 low = mid;
361 }
362 }
363 let lower_cutoff_index = high;
364
365 (search_space[lower_cutoff_index], search_space[upper_cutoff_index])
366}
367
368pub fn calculate_scan_occurrence_gaussian(
406 inverse_ion_mobility: &[f64],
407 mean: f64,
408 sigma: f64,
409 target_p: f64,
410 step_size: f64,
411 n_lower_start: f64,
412 n_upper_start: f64,
413) -> Vec<i32> {
414 let (ims_lower, ims_upper) = calculate_bounds_gaussian(mean, sigma, step_size, target_p, n_lower_start, n_upper_start);
416
417 let indexed_values: Vec<(f64, usize)> = inverse_ion_mobility
419 .iter()
420 .rev()
421 .enumerate()
422 .map(|(i, &val)| (val, i))
423 .collect();
424
425 let upper_idx = indexed_values
427 .iter()
428 .enumerate()
429 .min_by(|(_, (val_a, _)), (_, (val_b, _))| {
430 (val_a - ims_lower).abs().partial_cmp(&(val_b - ims_lower).abs()).unwrap()
431 })
432 .map(|(idx, _)| idx)
433 .unwrap_or(0);
434
435 let lower_idx = indexed_values
437 .iter()
438 .enumerate()
439 .min_by(|(_, (val_a, _)), (_, (val_b, _))| {
440 (val_a - ims_upper).abs().partial_cmp(&(val_b - ims_upper).abs()).unwrap()
441 })
442 .map(|(idx, _)| idx)
443 .unwrap_or(indexed_values.len() - 1);
444
445 if lower_idx <= upper_idx {
447 indexed_values[lower_idx..=upper_idx]
448 .iter()
449 .map(|&(_, idx)| idx as i32)
450 .collect()
451 } else {
452 Vec::new()
453 }
454}
455
456
457pub fn calculate_abundance_gaussian(
460 time_map: &HashMap<i32, f64>,
461 occurrences: &[i32],
462 mean: f64,
463 sigma: f64,
464 cycle_length: f64,
465) -> Vec<f64> {
466 let mut frame_abundance = Vec::new();
467
468 for &occurrence in occurrences {
469 if let Some(&time) = time_map.get(&occurrence) {
470 let start = time - cycle_length;
471 let val = normal_cdf_range(start, time, mean, sigma);
472 frame_abundance.push(val);
473 }
474 }
475
476 frame_abundance
477}
478
479pub fn calculate_scan_occurrences_gaussian_par(
480 times: &[f64],
481 means: Vec<f64>,
482 sigmas: Vec<f64>,
483 target_p: f64,
484 step_size: f64,
485 n_lower_start: f64,
486 n_upper_start: f64,
487 num_threads: usize
488) -> Vec<Vec<i32>> {
489 let thread_pool = ThreadPoolBuilder::new()
490 .num_threads(num_threads)
491 .build()
492 .unwrap();
493
494 thread_pool.install(|| {
495 means.into_par_iter()
496 .zip(sigmas.into_par_iter())
497 .map(|(m, s)| {
498 calculate_scan_occurrence_gaussian(
499 times,
500 m,
501 s,
502 target_p,
503 step_size,
504 n_lower_start,
505 n_upper_start
506 )
507 })
508 .collect()
509 })
510}
511
512pub fn calculate_scan_abundances_gaussian_par(
514 time_map: &HashMap<i32, f64>,
515 occurrences: Vec<Vec<i32>>,
516 means: Vec<f64>,
517 sigmas: Vec<f64>,
518 cycle_length: f64,
519 num_threads: usize
520) -> Vec<Vec<f64>> {
521 let thread_pool = ThreadPoolBuilder::new()
522 .num_threads(num_threads)
523 .build()
524 .unwrap();
525
526 thread_pool.install(|| {
527 occurrences.into_par_iter()
528 .zip(means.into_par_iter())
529 .zip(sigmas.into_par_iter())
530 .map(|((occ, m), s)| {
531 calculate_abundance_gaussian(
532 time_map,
533 &occ,
534 m,
535 s,
536 cycle_length
537 )
538 })
539 .collect()
540 })
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
548 (a - b).abs() < epsilon
549 }
550
551 #[test]
552 fn test_project_emg_reduces_to_legacy_under_uniform_intervals() {
553 let (mu, sigma, lambda) = (25.0, 1.5, 0.3);
556 let cycle = 1.0_f64;
557 let times: Vec<f64> = (1..=60).map(|i| i as f64).collect();
558 let intervals: Vec<(f64, f64)> = times.iter().map(|&t| (t - cycle, t)).collect();
559
560 let occ = calculate_frame_occurrence_emg(×, mu, sigma, lambda, 0.9999, 0.01, None);
562 let mut time_map = std::collections::HashMap::new();
563 for (i, &t) in times.iter().enumerate() {
564 time_map.insert((i + 1) as i32, t); }
566 let legacy_abund =
567 calculate_frame_abundance_emg(&time_map, &occ, mu, sigma, lambda, cycle, None);
568
569 let projected = project_emg_over_events(&intervals, mu, sigma, lambda, 0.9999, 0.01, None);
571
572 for (&frame_id, &abund) in occ.iter().zip(legacy_abund.iter()) {
575 if abund <= 0.0 {
576 continue;
577 }
578 let event_idx = (frame_id - 1) as usize;
579 let found = projected.iter().find(|(idx, _)| *idx == event_idx);
580 assert!(found.is_some(), "event {event_idx} (frame {frame_id}) missing from projection");
581 let (_, p_abund) = found.unwrap();
582 assert!(
583 (p_abund - abund).abs() < 1e-9,
584 "abundance mismatch at frame {frame_id}: legacy {abund} vs projected {p_abund}"
585 );
586 }
587 }
588
589 #[test]
590 fn test_project_emg_respects_unequal_event_durations() {
591 let (mu, sigma, lambda) = (10.0, 1.0, 0.2);
594 let wide = vec![(8.0, 12.0)];
595 let split = vec![(8.0, 10.0), (10.0, 12.0)];
596 let wide_p = project_emg_over_events(&wide, mu, sigma, lambda, 0.9999, 0.01, None);
597 let split_p = project_emg_over_events(&split, mu, sigma, lambda, 0.9999, 0.01, None);
598 let wide_sum: f64 = wide_p.iter().map(|(_, a)| a).sum();
599 let split_sum: f64 = split_p.iter().map(|(_, a)| a).sum();
600 assert!((wide_sum - split_sum).abs() < 1e-3, "split must conserve mass: {wide_sum} vs {split_sum}");
603 assert_eq!(split_p.len(), 2, "both sub-intervals should carry mass");
604 }
605
606 #[test]
607 fn test_normal_cdf_range() {
608 let mean = 0.0;
609 let std_dev = 1.0;
610
611 let prob_all = normal_cdf_range(-10.0, 10.0, mean, std_dev);
614 assert!(approx_eq(prob_all, 1.0, 1e-6),
615 "CDF range from -10 to 10 should capture nearly all probability, got {prob_all}");
616
617 let prob_1sigma = normal_cdf_range(-1.0, 1.0, mean, std_dev);
619 assert!(
620 (prob_1sigma - 0.68).abs() < 0.02,
621 "Expected ~0.68 within ±1σ, got {prob_1sigma}"
622 );
623 }
624
625 #[test]
626 fn test_calculate_bounds_gaussian() {
627 let mean = 0.0;
628 let sigma = 1.0;
629 let target = 0.68;
630 let (low, high) = calculate_bounds_gaussian(mean, sigma, 0.01, target, 5.0, 5.0);
632
633 let coverage = normal_cdf_range(low, high, mean, sigma);
635 assert!(
636 (coverage - target).abs() < 0.1,
637 "Expected coverage ~0.68, got {coverage} for interval [{low}, {high}]"
638 );
639 }
640
641 #[test]
642 fn test_calculate_frame_occurrence_gaussian() {
643 let retention_times: Vec<f64> = (0..10).map(|x| x as f64).collect();
645 let mean = 5.0; let sigma = 1.0;
647 let target_p = 0.68;
648 let step_size = 0.1;
649
650 let frames = calculate_scan_occurrence_gaussian(
653 &retention_times,
654 mean,
655 sigma,
656 target_p,
657 step_size,
658 5.0,
659 5.0
660 );
661
662 assert!(
665 !frames.is_empty(),
666 "We expect at least a few frames around 5.0"
667 );
668 assert!(
669 frames.contains(&5),
670 "We definitely expect the central frame (index=5 in 1-based indexing) to be included"
671 );
672 }
673
674 #[test]
675 fn test_calculate_frame_abundance_gaussian() {
676 let mut time_map = HashMap::new();
679 for i in 1..=5 {
680 time_map.insert(i as i32, i as f64);
681 }
682
683 let occurrences = vec![1, 3];
685 let mean = 3.0;
686 let sigma = 1.0;
687 let im_cycle_length = 1.0;
688
689 let abundances = calculate_abundance_gaussian(
690 &time_map,
691 &occurrences,
692 mean,
693 sigma,
694 im_cycle_length
695 );
696
697 assert_eq!(abundances.len(), 2, "We should have 2 abundance values");
701 let (a1, a2) = (abundances[0], abundances[1]);
702
703 assert!(
706 a2 > a1,
707 "Expected frame near t=3 to have higher abundance than t=1"
708 );
709 }
710
711 #[test]
712 fn test_parallel_functions() {
713 let retention_times: Vec<f64> = (0..10).map(|x| x as f64).collect();
715 let means = vec![3.0, 5.0];
716 let sigmas = vec![1.0, 1.5];
717
718 let target_p = 0.68;
719 let step_size = 0.1;
720 let num_threads = 2;
721
722 let res_occurrences = calculate_scan_occurrences_gaussian_par(
723 &retention_times,
724 means.clone(),
725 sigmas.clone(),
726 target_p,
727 step_size,
728 5.0,
729 5.0,
730 num_threads
731 );
732 assert_eq!(res_occurrences.len(), 2, "Should produce 2 sets of occurrences");
733
734 let mut time_map = HashMap::new();
736 for i in 1..=10 {
737 time_map.insert(i, i as f64);
738 }
739
740 let res_abundances = calculate_scan_abundances_gaussian_par(
741 &time_map,
742 res_occurrences,
743 means,
744 sigmas,
745 1.0, num_threads
747 );
748 assert_eq!(res_abundances.len(), 2, "Should produce 2 sets of abundances");
749 }
750}