1use rustc_hash::{FxHashMap, FxHashSet};
11use std::cmp::Ordering;
12use std::collections::BinaryHeap;
13
14struct GreedyEntry<'a> {
20 score: f64,
21 key: (bool, &'a str),
22 n_obs: usize,
23}
24impl PartialEq for GreedyEntry<'_> {
25 fn eq(&self, other: &Self) -> bool {
26 self.cmp(other) == Ordering::Equal
27 }
28}
29impl Eq for GreedyEntry<'_> {}
30impl Ord for GreedyEntry<'_> {
31 fn cmp(&self, other: &Self) -> Ordering {
32 self.score
35 .total_cmp(&other.score)
36 .then_with(|| self.key.cmp(&other.key))
37 }
38}
39impl PartialOrd for GreedyEntry<'_> {
40 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
41 Some(self.cmp(other))
42 }
43}
44
45#[derive(Debug, Clone, serde::Deserialize)]
46#[serde(default)]
47pub struct ProteinConfig {
48 pub rt_window: f64,
52 pub im_window: f64,
57 pub mass_z_window: f64,
61 pub fdr: f64,
63 pub optimize_thresholds: bool,
67 pub max_feature_isotopes_gate: f64,
73 pub ml_rescore: bool,
78}
79
80impl Default for ProteinConfig {
81 fn default() -> Self {
82 ProteinConfig {
83 rt_window: 3.0,
84 im_window: 0.03,
85 mass_z_window: 3.0,
86 fdr: 0.01,
87 optimize_thresholds: true,
88 max_feature_isotopes_gate: 0.0,
89 ml_rescore: false,
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
95pub struct ProteinId {
96 pub accession: String,
97 pub decoy: bool,
98 pub observed_peptides: usize,
99 pub theoretical_peptides: usize,
100 pub coverage: f64,
101 pub binomial_score: f64,
102 pub mean_rt_diff: f64,
105 pub mean_mass_z: f64,
108 pub mean_pep_score: f64,
109 pub q_value: f64,
110}
111
112fn ln_gamma(x: f64) -> f64 {
114 const G: [f64; 8] = [
115 676.5203681218851,
116 -1259.1392167224028,
117 771.323_428_777_653_1,
118 -176.615_029_162_140_6,
119 12.507343278686905,
120 -0.13857109526572012,
121 9.984_369_578_019_572e-6,
122 1.5056327351493116e-7,
123 ];
124 if x < 0.5 {
125 std::f64::consts::PI / ((std::f64::consts::PI * x).sin() * ln_gamma(1.0 - x).exp())
126 } else {
127 let x = x - 1.0;
128 let mut a = 0.999_999_999_999_809_9;
129 let t = x + 7.5;
130 for (i, &g) in G.iter().enumerate() {
131 a += g / (x + (i as f64) + 1.0);
132 }
133 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
134 }
135}
136
137fn betacf(a: f64, b: f64, x: f64) -> f64 {
139 let tiny = 1e-30;
140 let qab = a + b;
141 let qap = a + 1.0;
142 let qam = a - 1.0;
143 let mut c = 1.0;
144 let mut d = 1.0 - qab * x / qap;
145 if d.abs() < tiny {
146 d = tiny;
147 }
148 d = 1.0 / d;
149 let mut h = d;
150 for m in 1..200 {
151 let m = m as f64;
152 let m2 = 2.0 * m;
153 let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
154 d = 1.0 + aa * d;
155 if d.abs() < tiny {
156 d = tiny;
157 }
158 c = 1.0 + aa / c;
159 if c.abs() < tiny {
160 c = tiny;
161 }
162 d = 1.0 / d;
163 h *= d * c;
164 let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
165 d = 1.0 + aa * d;
166 if d.abs() < tiny {
167 d = tiny;
168 }
169 c = 1.0 + aa / c;
170 if c.abs() < tiny {
171 c = tiny;
172 }
173 d = 1.0 / d;
174 let del = d * c;
175 h *= del;
176 if (del - 1.0).abs() < 1e-12 {
177 break;
178 }
179 }
180 h
181}
182
183pub(crate) fn betai(a: f64, b: f64, x: f64) -> f64 {
185 if x <= 0.0 {
186 return 0.0;
187 }
188 if x >= 1.0 {
189 return 1.0;
190 }
191 let bt = (ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln()).exp();
192 if x < (a + 1.0) / (a + b + 2.0) {
193 bt * betacf(a, b, x) / a
194 } else {
195 1.0 - bt * betacf(b, a, 1.0 - x) / b
196 }
197}
198
199pub(crate) fn binom_sf_ge_k(k: u32, n: u32, p: f64) -> f64 {
205 if k == 0 {
206 return 1.0;
207 }
208 if k as u64 > n as u64 {
209 return 0.0;
210 }
211 betai(k as f64, (n - k + 1) as f64, p).clamp(1e-300, 1.0)
212}
213
214pub(crate) fn binom_sf_ge_k_cont(k: f64, n: f64, p: f64) -> f64 {
220 if k <= 0.0 {
221 return 1.0;
222 }
223 if k >= n + 1.0 {
224 return 0.0;
225 }
226 betai(k, (n - k + 1.0).max(1e-9), p).clamp(1e-300, 1.0)
227}
228
229pub(crate) fn score_proteins_weighted(
238 observed: FxHashMap<(bool, &str), FxHashSet<usize>>,
239 pep_feat: &FxHashMap<usize, Vec<usize>>,
240 weight: &FxHashMap<usize, f64>,
241 theoretical: &FxHashMap<(bool, &str), u32>,
242 decoy_theo: u64,
243) -> Vec<ProteinId> {
244 let theo_of = |key: &(bool, &str)| theoretical.get(key).copied().unwrap_or(0) as usize;
245 let w_of = |pi: usize| weight.get(&pi).copied().unwrap_or(0.0);
246 let prot_peps = observed;
247 let mut feat_peps: FxHashMap<usize, FxHashSet<usize>> = FxHashMap::default();
248 let mut all_peps: FxHashSet<usize> = FxHashSet::default();
249 for peps in prot_peps.values() {
250 for &pi in peps {
251 all_peps.insert(pi);
252 }
253 }
254 let mut rem_feat: FxHashMap<usize, usize> = FxHashMap::default();
255 for &pi in &all_peps {
256 if let Some(fs) = pep_feat.get(&pi) {
257 for &f in fs {
258 feat_peps.entry(f).or_default().insert(pi);
259 }
260 rem_feat.insert(pi, fs.len().max(1));
261 }
262 }
263 let mut pep_prots: FxHashMap<usize, Vec<(bool, &str)>> = FxHashMap::default();
264 for (&key, peps) in &prot_peps {
265 for &pi in peps {
266 pep_prots.entry(pi).or_default().push(key);
267 }
268 }
269 let mut prot_peps = prot_peps;
271 let mut obs_w: FxHashMap<(bool, &str), f64> = prot_peps
272 .iter()
273 .map(|(&k, v)| (k, v.iter().map(|&pi| w_of(pi)).sum()))
274 .collect();
275 let mut by_n: FxHashMap<usize, Vec<(bool, &str)>> = FxHashMap::default();
276 for &k in prot_peps.keys() {
277 let n = theo_of(&k);
278 if n > 0 {
279 by_n.entry(n).or_default().push(k);
280 }
281 }
282 let decoy_theo_f = decoy_theo.max(1) as f64;
283 let mut decoy_obs_sum: f64 = obs_w.iter().filter(|((d, _), _)| *d).map(|(_, &c)| c).sum();
284
285 fn best_for_n<'a>(
288 n: usize,
289 by_n: &FxHashMap<usize, Vec<(bool, &'a str)>>,
290 obs_w: &FxHashMap<(bool, &'a str), f64>,
291 ) -> Option<(bool, &'a str)> {
292 by_n.get(&n).and_then(|ks| {
293 ks.iter()
294 .filter(|k| obs_w.get(*k).copied().unwrap_or(0.0) > 1e-9)
295 .max_by(|a, b| {
296 let (wa, wb) = (obs_w[*a], obs_w[*b]);
297 wa.partial_cmp(&wb).unwrap().then_with(|| b.cmp(a))
298 })
299 .copied()
300 })
301 }
302 let mut best_per_n: FxHashMap<usize, (bool, &str)> = FxHashMap::default();
303 for &n in by_n.keys() {
304 if let Some(k) = best_for_n(n, &by_n, &obs_w) {
305 best_per_n.insert(n, k);
306 }
307 }
308
309 let mut proteins: Vec<ProteinId> = Vec::new();
310 let (mut t, mut d) = (0usize, 0usize);
311 let mut dead: FxHashSet<usize> = FxHashSet::default();
312 let mut banned_feat: FxHashSet<usize> = FxHashSet::default();
313 loop {
314 let p = (decoy_obs_sum / decoy_theo_f).clamp(1e-9, 0.999);
315 let mut best: Option<((bool, &str), f64, f64, usize)> = None;
316 for (&n, &k) in &best_per_n {
317 let kobs = obs_w.get(&k).copied().unwrap_or(0.0);
318 if kobs <= 1e-9 {
319 continue;
320 }
321 let s = -binom_sf_ge_k_cont(kobs, n as f64, p).log10();
322 let better = match best {
323 None => true,
324 Some((bk, bs, _, _)) => s > bs || (s == bs && k < bk),
325 };
326 if better {
327 best = Some((k, s, kobs, n));
328 }
329 }
330 let (key, score, kobs, n) = match best {
331 Some(x) => x,
332 None => break,
333 };
334 let (decoy, acc) = key;
335 if decoy {
336 d += 1;
337 } else {
338 t += 1;
339 }
340 let q = if t == 0 { 1.0 } else { d as f64 / t as f64 };
341 proteins.push(ProteinId {
342 accession: acc.to_string(),
343 decoy,
344 observed_peptides: kobs.round() as usize,
345 theoretical_peptides: n,
346 coverage: if n > 0 { kobs / n as f64 } else { 0.0 },
347 binomial_score: score,
348 mean_rt_diff: 0.0,
349 mean_mass_z: 0.0,
350 mean_pep_score: 0.0,
351 q_value: q,
352 });
353 if t > 20 && q > 0.5 {
354 break;
355 }
356 let won_peps: Vec<usize> = prot_peps
357 .get(&key)
358 .map(|s| s.iter().copied().collect())
359 .unwrap_or_default();
360 let mut to_kill: Vec<usize> = Vec::new();
361 for pi in &won_peps {
362 match pep_feat.get(pi) {
363 Some(fs) if !fs.is_empty() => {
364 for &f in fs {
365 if banned_feat.insert(f) {
366 if let Some(ps) = feat_peps.get(&f) {
367 for &pj in ps {
368 if let Some(r) = rem_feat.get_mut(&pj) {
369 if *r > 0 {
370 *r -= 1;
371 if *r == 0 {
372 to_kill.push(pj);
373 }
374 }
375 }
376 }
377 }
378 }
379 }
380 }
381 _ => to_kill.push(*pi),
382 }
383 }
384 let mut dirty_n: FxHashSet<usize> = FxHashSet::default();
385 for pj in to_kill {
386 if !dead.insert(pj) {
387 continue;
388 }
389 let wj = w_of(pj);
390 if let Some(keys) = pep_prots.get(&pj) {
391 for k2 in keys.clone() {
392 if let Some(s) = prot_peps.get_mut(&k2) {
393 if s.remove(&pj) {
394 if let Some(c) = obs_w.get_mut(&k2) {
395 *c -= wj;
396 }
397 if k2.0 {
398 decoy_obs_sum -= wj;
399 }
400 dirty_n.insert(theo_of(&k2));
401 }
402 }
403 }
404 }
405 }
406 obs_w.insert(key, 0.0);
407 if let Some(s) = prot_peps.get_mut(&key) {
408 s.clear();
409 }
410 dirty_n.insert(n);
411 for dn in dirty_n {
412 match best_for_n(dn, &by_n, &obs_w) {
413 Some(k) => {
414 best_per_n.insert(dn, k);
415 }
416 None => {
417 best_per_n.remove(&dn);
418 }
419 }
420 }
421 }
422 proteins.sort_by(|a, b| {
423 b.binomial_score
424 .partial_cmp(&a.binomial_score)
425 .unwrap()
426 .then_with(|| a.accession.cmp(&b.accession))
427 .then(a.decoy.cmp(&b.decoy))
428 });
429 let score_by: FxHashMap<(bool, String), f64> = proteins
430 .iter()
431 .map(|p| ((p.decoy, p.accession.clone()), p.binomial_score))
432 .collect();
433 proteins.retain(|p| match score_by.get(&(!p.decoy, p.accession.clone())) {
434 Some(&cs) => p.binomial_score > cs || (p.binomial_score == cs && !p.decoy),
435 None => true,
436 });
437 let plus1 = if std::env::var("UNO_NO_FDR_PLUS1").is_ok() {
438 0.0
439 } else {
440 1.0
441 };
442 let (mut t, mut d) = (0usize, 0usize);
443 for p in proteins.iter_mut() {
444 if p.decoy {
445 d += 1;
446 } else {
447 t += 1;
448 }
449 p.q_value = if t == 0 {
450 1.0
451 } else {
452 (d as f64 + plus1) / t as f64
453 };
454 }
455 let mut run = f64::INFINITY;
456 for i in (0..proteins.len()).rev() {
457 run = run.min(proteins[i].q_value);
458 proteins[i].q_value = run.min(1.0);
459 }
460 proteins
461}
462
463pub(crate) fn score_proteins_greedy(
477 observed: FxHashMap<(bool, &str), FxHashSet<usize>>,
478 theoretical: &FxHashMap<(bool, &str), u32>,
479 decoy_theo: u64,
480 decoy_seen: usize,
481 fdr: f64,
482) -> Vec<ProteinId> {
483 let permute = std::env::var("UNO_PROT_PERMUTE").is_ok();
490 let hit_rate = if decoy_theo == 0 {
491 1e-9
492 } else {
493 (decoy_seen as f64 / decoy_theo as f64).max(1e-9)
494 };
495 if std::env::var("UNO_LOG_HITRATE").is_ok() {
496 log::info!(
497 "score_proteins_greedy: hit_rate(p)={:.4} decoy_seen={} decoy_theo={}",
498 hit_rate,
499 decoy_seen,
500 decoy_theo
501 );
502 }
503 let mut score_cache: FxHashMap<(u32, usize), f64> = FxHashMap::default();
504 let mut score_of = |key: &(bool, &str), n_obs: usize| -> f64 {
505 if n_obs == 0 {
506 return 0.0;
507 }
508 let theo = theoretical.get(key).copied().unwrap_or(0);
509 *score_cache
510 .entry((theo, n_obs))
511 .or_insert_with(|| -binom_sf_ge_k(n_obs as u32, theo, hit_rate).log10())
512 };
513 let mut prot_peps = observed;
514 let mut pep_prots: FxHashMap<usize, Vec<(bool, &str)>> = FxHashMap::default();
515 for (&key, peps) in &prot_peps {
516 for &pi in peps {
517 pep_prots.entry(pi).or_default().push(key);
518 }
519 }
520
521 if std::env::var("UNO_DYNAMIC_P").is_ok() {
535 let theo_of = |key: &(bool, &str)| theoretical.get(key).copied().unwrap_or(0) as usize;
536 let mut obs: FxHashMap<(bool, &str), usize> =
541 prot_peps.iter().map(|(&k, v)| (k, v.len())).collect();
542 let mut by_n: FxHashMap<usize, Vec<(bool, &str)>> = FxHashMap::default();
543 for &k in prot_peps.keys() {
544 let n = theo_of(&k);
545 if n > 0 {
546 by_n.entry(n).or_default().push(k);
547 }
548 }
549 let decoy_theo_f = decoy_theo.max(1) as f64;
550 let mut decoy_obs_sum: f64 = obs
551 .iter()
552 .filter(|((d, _), _)| *d)
553 .map(|(_, &c)| c as f64)
554 .sum();
555 fn best_for_n<'a>(
556 n: usize,
557 by_n: &FxHashMap<usize, Vec<(bool, &'a str)>>,
558 obs: &FxHashMap<(bool, &'a str), usize>,
559 ) -> Option<(bool, &'a str)> {
560 by_n.get(&n).and_then(|ks| {
561 ks.iter()
562 .filter(|k| obs.get(*k).copied().unwrap_or(0) > 0)
563 .max_by_key(|k| (obs[*k], std::cmp::Reverse(**k)))
564 .copied()
565 })
566 }
567 let mut best_per_n: FxHashMap<usize, (bool, &str)> = FxHashMap::default();
568 for &n in by_n.keys() {
569 if let Some(k) = best_for_n(n, &by_n, &obs) {
570 best_per_n.insert(n, k);
571 }
572 }
573
574 let mut proteins: Vec<ProteinId> = Vec::new();
575 let (mut t, mut d) = (0usize, 0usize);
576 loop {
577 let p = (decoy_obs_sum / decoy_theo_f).clamp(1e-9, 0.999);
578 let mut best: Option<((bool, &str), f64, usize, usize)> = None;
580 for (&n, &k) in &best_per_n {
581 let kobs = obs.get(&k).copied().unwrap_or(0);
582 if kobs == 0 {
583 continue;
584 }
585 let s = -binom_sf_ge_k(kobs as u32, n as u32, p).log10();
586 let better = match best {
587 None => true,
588 Some((bk, bs, _, _)) => s > bs || (s == bs && k < bk),
589 };
590 if better {
591 best = Some((k, s, kobs, n));
592 }
593 }
594 let (key, score, kobs, n) = match best {
595 Some(x) => x,
596 None => break,
597 };
598 let (decoy, acc) = key;
599 if decoy {
600 d += 1;
601 } else {
602 t += 1;
603 }
604 let q = if t == 0 { 1.0 } else { d as f64 / t as f64 };
605 proteins.push(ProteinId {
606 accession: acc.to_string(),
607 decoy,
608 observed_peptides: kobs,
609 theoretical_peptides: n,
610 coverage: if n > 0 { kobs as f64 / n as f64 } else { 0.0 },
611 binomial_score: score,
612 mean_rt_diff: 0.0,
613 mean_mass_z: 0.0,
614 mean_pep_score: 0.0,
615 q_value: q,
616 });
617 if t > 20 && q > (fdr * 25.0).max(0.5) {
618 break;
619 }
620 let won: Vec<usize> = prot_peps
624 .get(&key)
625 .map(|s| s.iter().copied().collect())
626 .unwrap_or_default();
627 let mut dirty_n: FxHashSet<usize> = FxHashSet::default();
628 for pi in won {
629 if let Some(keys) = pep_prots.get(&pi) {
630 for k2 in keys.clone() {
631 if let Some(s) = prot_peps.get_mut(&k2) {
632 if s.remove(&pi) {
633 if let Some(c) = obs.get_mut(&k2) {
634 *c -= 1;
635 }
636 if k2.0 {
637 decoy_obs_sum -= 1.0;
638 }
639 dirty_n.insert(theo_of(&k2));
640 }
641 }
642 }
643 }
644 }
645 obs.insert(key, 0);
647 dirty_n.insert(n);
648 for dn in dirty_n {
649 match best_for_n(dn, &by_n, &obs) {
650 Some(k) => {
651 best_per_n.insert(dn, k);
652 }
653 None => {
654 best_per_n.remove(&dn);
655 }
656 }
657 }
658 }
659 proteins.sort_by(|a, b| {
660 b.binomial_score
661 .partial_cmp(&a.binomial_score)
662 .unwrap()
663 .then_with(|| a.accession.cmp(&b.accession))
664 .then(a.decoy.cmp(&b.decoy))
665 });
666 let (mut t, mut d) = (0usize, 0usize);
668 for p in proteins.iter_mut() {
669 if p.decoy {
670 d += 1;
671 } else {
672 t += 1;
673 }
674 p.q_value = if t == 0 { 1.0 } else { d as f64 / t as f64 };
675 }
676 let mut run = f64::INFINITY;
677 for i in (0..proteins.len()).rev() {
678 run = run.min(proteins[i].q_value);
679 proteins[i].q_value = run.min(1.0);
680 }
681 return proteins;
682 }
683
684 let mut heap: BinaryHeap<GreedyEntry> = prot_peps
685 .iter()
686 .filter(|(_, peps)| !peps.is_empty())
687 .map(|(&key, peps)| GreedyEntry {
688 score: score_of(&key, peps.len()),
689 key,
690 n_obs: peps.len(),
691 })
692 .collect();
693 let mut proteins: Vec<ProteinId> = Vec::new();
694 let (mut t, mut d) = (0usize, 0usize);
695 loop {
696 let (key, obs, score) = loop {
697 let top = match heap.pop() {
698 Some(e) => e,
699 None => break (None, 0usize, 0.0f64),
700 };
701 match prot_peps.get(&top.key) {
702 Some(peps) if !peps.is_empty() && peps.len() == top.n_obs => {
703 break (Some(top.key), top.n_obs, top.score);
704 }
705 _ => continue,
706 }
707 };
708 let key = match key {
709 Some(k) => k,
710 None => break,
711 };
712 let (decoy, acc) = key;
713 let theo = theoretical.get(&key).copied().unwrap_or(0) as usize;
714 if decoy {
715 d += 1;
716 } else {
717 t += 1;
718 }
719 let q = if t == 0 { 1.0 } else { d as f64 / t as f64 };
720 proteins.push(ProteinId {
721 accession: acc.to_string(),
722 decoy,
723 observed_peptides: obs,
724 theoretical_peptides: theo,
725 coverage: if theo > 0 {
726 obs as f64 / theo as f64
727 } else {
728 0.0
729 },
730 binomial_score: score,
731 mean_rt_diff: 0.0,
732 mean_mass_z: 0.0,
733 mean_pep_score: 0.0,
734 q_value: q,
735 });
736 if t > 20 && q > (fdr * 25.0).max(0.5) {
737 break;
738 }
739 let won: Vec<usize> = prot_peps
740 .get(&key)
741 .map(|s| s.iter().copied().collect())
742 .unwrap_or_default();
743 let mut affected: FxHashSet<(bool, &str)> = FxHashSet::default();
744 for pi in won {
745 if let Some(keys) = pep_prots.get(&pi) {
746 for k in keys.clone() {
747 if let Some(s) = prot_peps.get_mut(&k) {
748 s.remove(&pi);
749 affected.insert(k);
750 }
751 }
752 }
753 }
754 for k in affected {
755 if let Some(peps) = prot_peps.get(&k) {
756 if !peps.is_empty() {
757 heap.push(GreedyEntry {
758 score: score_of(&k, peps.len()),
759 key: k,
760 n_obs: peps.len(),
761 });
762 }
763 }
764 }
765 }
766 proteins.sort_by(|a, b| {
767 b.binomial_score
768 .partial_cmp(&a.binomial_score)
769 .unwrap()
770 .then_with(|| a.accession.cmp(&b.accession))
771 .then(a.decoy.cmp(&b.decoy))
772 });
773 if permute {
774 let m = proteins.iter().filter(|p| !p.decoy).count().max(1) as f64;
778 let mut rank = 0usize;
779 for p in proteins.iter_mut() {
780 if p.decoy {
781 p.q_value = 1.0;
782 continue;
783 }
784 rank += 1;
785 let pv = 10f64.powf(-p.binomial_score);
786 p.q_value = (pv * m / rank as f64).min(1.0);
787 }
788 let mut run = 1.0f64;
790 for p in proteins.iter_mut().rev() {
791 if p.decoy {
792 continue;
793 }
794 run = run.min(p.q_value);
795 p.q_value = run;
796 }
797 return proteins;
798 }
799 let score_by: FxHashMap<(bool, String), f64> = proteins
801 .iter()
802 .map(|p| ((p.decoy, p.accession.clone()), p.binomial_score))
803 .collect();
804 proteins.retain(|p| match score_by.get(&(!p.decoy, p.accession.clone())) {
805 Some(&cs) => p.binomial_score > cs || (p.binomial_score == cs && !p.decoy),
806 None => true,
807 });
808 let (mut t, mut d) = (0usize, 0usize);
809 for p in proteins.iter_mut() {
810 if p.decoy {
811 d += 1;
812 } else {
813 t += 1;
814 }
815 p.q_value = if t == 0 { 1.0 } else { d as f64 / t as f64 };
816 }
817 let mut run = f64::INFINITY;
818 for i in (0..proteins.len()).rev() {
819 run = run.min(proteins[i].q_value);
820 proteins[i].q_value = run.min(1.0);
821 }
822 proteins
823}
824
825pub(crate) fn score_proteins_feature_compete(
833 observed: FxHashMap<(bool, &str), FxHashSet<usize>>,
834 pep_feat: &FxHashMap<usize, Vec<usize>>,
835 theoretical: &FxHashMap<(bool, &str), u32>,
836 decoy_theo: u64,
837 fdr: f64,
838) -> Vec<ProteinId> {
839 let theo_of = |key: &(bool, &str)| theoretical.get(key).copied().unwrap_or(0) as usize;
840 let mut prot_peps = observed;
841 let mut feat_peps: FxHashMap<usize, FxHashSet<usize>> = FxHashMap::default();
847 let mut all_peps: FxHashSet<usize> = FxHashSet::default();
848 for peps in prot_peps.values() {
849 for &pi in peps {
850 all_peps.insert(pi);
851 }
852 }
853 let mut rem_feat: FxHashMap<usize, usize> = FxHashMap::default();
855 for &pi in &all_peps {
856 if let Some(fs) = pep_feat.get(&pi) {
857 for &f in fs {
858 feat_peps.entry(f).or_default().insert(pi);
859 }
860 rem_feat.insert(pi, fs.len().max(1));
861 }
862 }
863 let mut pep_prots: FxHashMap<usize, Vec<(bool, &str)>> = FxHashMap::default();
864 for (&key, peps) in &prot_peps {
865 for &pi in peps {
866 pep_prots.entry(pi).or_default().push(key);
867 }
868 }
869 let mut obs: FxHashMap<(bool, &str), usize> =
870 prot_peps.iter().map(|(&k, v)| (k, v.len())).collect();
871 let decoy_theo_f = decoy_theo.max(1) as f64;
872 let mut decoy_obs_sum: f64 = obs
873 .iter()
874 .filter(|((d, _), _)| *d)
875 .map(|(_, &c)| c as f64)
876 .sum();
877
878 type BucketHeap<'a> = std::collections::BinaryHeap<(usize, std::cmp::Reverse<(bool, &'a str)>)>;
893 #[inline]
894 fn peek_best<'a>(
895 heap: &mut BucketHeap<'a>,
896 obs: &FxHashMap<(bool, &'a str), usize>,
897 ) -> Option<(usize, (bool, &'a str))> {
898 while let Some(&(snap, std::cmp::Reverse(key))) = heap.peek() {
899 let cur = obs.get(&key).copied().unwrap_or(0);
900 if cur == 0 {
901 heap.pop();
902 continue;
903 }
904 if cur < snap {
905 heap.pop();
906 heap.push((cur, std::cmp::Reverse(key)));
907 continue;
908 }
909 return Some((cur, key));
910 }
911 None
912 }
913 let mut heaps: FxHashMap<usize, BucketHeap> = FxHashMap::default();
914 for (&k, &c) in &obs {
915 let n = theo_of(&k);
916 if n > 0 && c > 0 {
917 heaps.entry(n).or_default().push((c, std::cmp::Reverse(k)));
918 }
919 }
920
921 let mut proteins: Vec<ProteinId> = Vec::new();
922 let (mut t, mut d) = (0usize, 0usize);
923 let mut dead: FxHashSet<usize> = FxHashSet::default();
924 let mut banned_feat: FxHashSet<usize> = FxHashSet::default();
925 let mut empty_buckets: Vec<usize> = Vec::new();
926 loop {
927 let p = (decoy_obs_sum / decoy_theo_f).clamp(1e-9, 0.999);
928 let mut best: Option<((bool, &str), f64, usize, usize)> = None;
929 empty_buckets.clear();
930 for (&n, heap) in heaps.iter_mut() {
931 let (kobs, k) = match peek_best(heap, &obs) {
932 Some(x) => x,
933 None => {
934 empty_buckets.push(n);
935 continue;
936 }
937 };
938 let s = -binom_sf_ge_k(kobs as u32, n as u32, p).log10();
939 let better = match best {
940 None => true,
941 Some((bk, bs, _, _)) => s > bs || (s == bs && k < bk),
942 };
943 if better {
944 best = Some((k, s, kobs, n));
945 }
946 }
947 for n in empty_buckets.drain(..) {
948 heaps.remove(&n);
949 }
950 let (key, score, kobs, n) = match best {
951 Some(x) => x,
952 None => break,
953 };
954 let (decoy, acc) = key;
955 if decoy {
956 d += 1;
957 } else {
958 t += 1;
959 }
960 let q = if t == 0 { 1.0 } else { d as f64 / t as f64 };
961 proteins.push(ProteinId {
962 accession: acc.to_string(),
963 decoy,
964 observed_peptides: kobs,
965 theoretical_peptides: n,
966 coverage: if n > 0 { kobs as f64 / n as f64 } else { 0.0 },
967 binomial_score: score,
968 mean_rt_diff: 0.0,
969 mean_mass_z: 0.0,
970 mean_pep_score: 0.0,
971 q_value: q,
972 });
973 if t > 20 && q > (fdr * 25.0).max(0.5) {
974 break;
975 }
976 let won_peps: Vec<usize> = prot_peps
979 .get(&key)
980 .map(|s| s.iter().copied().collect())
981 .unwrap_or_default();
982 let mut to_kill: Vec<usize> = Vec::new();
985 for pi in &won_peps {
986 match pep_feat.get(pi) {
987 Some(fs) if !fs.is_empty() => {
988 for &f in fs {
989 if banned_feat.insert(f) {
990 if let Some(ps) = feat_peps.get(&f) {
991 for &pj in ps {
992 if let Some(r) = rem_feat.get_mut(&pj) {
993 if *r > 0 {
994 *r -= 1;
995 if *r == 0 {
996 to_kill.push(pj);
997 }
998 }
999 }
1000 }
1001 }
1002 }
1003 }
1004 }
1005 _ => to_kill.push(*pi),
1006 }
1007 }
1008 for pj in to_kill {
1011 if !dead.insert(pj) {
1012 continue;
1013 }
1014 if let Some(keys) = pep_prots.get(&pj) {
1015 for k2 in keys.clone() {
1016 if let Some(s) = prot_peps.get_mut(&k2) {
1017 if s.remove(&pj) {
1018 if let Some(c) = obs.get_mut(&k2) {
1019 *c -= 1;
1020 }
1021 if k2.0 {
1022 decoy_obs_sum -= 1.0;
1023 }
1024 }
1025 }
1026 }
1027 }
1028 }
1029 obs.insert(key, 0);
1031 if let Some(s) = prot_peps.get_mut(&key) {
1032 s.clear();
1033 }
1034 }
1035 proteins.sort_by(|a, b| {
1036 b.binomial_score
1037 .partial_cmp(&a.binomial_score)
1038 .unwrap()
1039 .then_with(|| a.accession.cmp(&b.accession))
1040 .then(a.decoy.cmp(&b.decoy))
1041 });
1042 let score_by: FxHashMap<(bool, String), f64> = proteins
1048 .iter()
1049 .map(|p| ((p.decoy, p.accession.clone()), p.binomial_score))
1050 .collect();
1051 proteins.retain(|p| match score_by.get(&(!p.decoy, p.accession.clone())) {
1052 Some(&cs) => p.binomial_score > cs || (p.binomial_score == cs && !p.decoy),
1053 None => true,
1054 });
1055 let plus1 = if std::env::var("UNO_NO_FDR_PLUS1").is_ok() {
1061 0.0
1062 } else {
1063 1.0
1064 };
1065 let (mut t, mut d) = (0usize, 0usize);
1066 for p in proteins.iter_mut() {
1067 if p.decoy {
1068 d += 1;
1069 } else {
1070 t += 1;
1071 }
1072 p.q_value = if t == 0 {
1073 1.0
1074 } else {
1075 (d as f64 + plus1) / t as f64
1076 };
1077 }
1078 let mut run = f64::INFINITY;
1079 for i in (0..proteins.len()).rev() {
1080 run = run.min(proteins[i].q_value);
1081 proteins[i].q_value = run.min(1.0);
1082 }
1083 proteins
1084}
1085
1086#[cfg(test)]
1087mod joint_alternative_tests {
1088 use super::*;
1089 #[test]
1090 fn alternative_peak_preserves_one_peptide_after_primary_peak_is_claimed() {
1091 let observed: FxHashMap<(bool, &str), FxHashSet<usize>> = [
1092 ((false, "A"), [0, 1, 2].into_iter().collect()),
1093 ((false, "B"), [3, 4].into_iter().collect()),
1094 ((true, "D"), [5, 6].into_iter().collect()),
1095 ]
1096 .into_iter()
1097 .collect();
1098 let theo = [((false, "A"), 10), ((false, "B"), 20), ((true, "D"), 1000)]
1099 .into_iter()
1100 .collect();
1101 let mut peaks: FxHashMap<usize, Vec<usize>> = [
1102 (0, vec![0]),
1103 (1, vec![1]),
1104 (2, vec![2]),
1105 (3, vec![0]),
1106 (4, vec![4]),
1107 (5, vec![5]),
1108 (6, vec![6]),
1109 ]
1110 .into_iter()
1111 .collect();
1112 let single = score_proteins_feature_compete(observed.clone(), &peaks, &theo, 1000, 0.01);
1113 peaks.insert(3, vec![0, 7, 8]);
1114 let multiple = score_proteins_feature_compete(observed, &peaks, &theo, 1000, 0.01);
1115 assert_eq!(single[0].accession, "A");
1116 assert_eq!(multiple[0].accession, "A");
1117 assert_eq!(
1118 single
1119 .iter()
1120 .find(|p| p.accession == "B")
1121 .unwrap()
1122 .observed_peptides,
1123 1
1124 );
1125 assert_eq!(
1127 multiple
1128 .iter()
1129 .find(|p| p.accession == "B")
1130 .unwrap()
1131 .observed_peptides,
1132 2
1133 );
1134 }
1135}