1use mscore::simulation::noise_rng::noise_rng;
2use mscore::algorithm::isotope::calculate_precursor_transmission_factor;
3use crate::sim::containers::IsotopeTransmissionMode;
4use mscore::data::peptide::{PeptideIon, PeptideProductIonSeriesCollection};
5use mscore::data::spectrum::{IndexedMzSpectrum, MsType, MzSpectrum};
6use mscore::simulation::annotation::{
7 MzSpectrumAnnotated, PeakAnnotation, TimsFrameAnnotated, TimsSpectrumAnnotated,
8};
9use mscore::timstof::frame::TimsFrame;
10use mscore::timstof::quadrupole::{IonTransmission, TimsTransmissionDDA};
11use mscore::timstof::spectrum::TimsSpectrum;
12use std::collections::{BTreeMap, HashSet};
13use std::path::Path;
14use std::sync::Arc;
15
16use rand::Rng;
17use rayon::prelude::*;
18use crate::sim::containers::IsotopeTransmissionConfig;
19use crate::sim::scheme::InstrumentCapabilities;
20use crate::sim::handle::{FragmentIonsWithComplementary, TimsTofSyntheticsDataHandle};
21use crate::sim::precursor::TimsTofSyntheticsPrecursorFrameBuilder;
22
23pub struct TimsTofSyntheticsFrameBuilderDDA {
24 pub path: String,
25 pub precursor_frame_builder: TimsTofSyntheticsPrecursorFrameBuilder,
26 pub transmission_settings: TimsTransmissionDDA,
27 pub fragment_ions:
28 Option<BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrum>)>>,
29 pub fragment_ions_annotated: Option<
30 BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>)>,
31 >,
32 pub isotope_transmission_config: IsotopeTransmissionConfig,
35 pub fragment_ions_with_complementary: Option<BTreeMap<(u32, i8, i32), FragmentIonsWithComplementary>>,
37 pub capabilities: InstrumentCapabilities,
39 pub noise_seed: u64,
42}
43
44impl TimsTofSyntheticsFrameBuilderDDA {
45
46 pub fn set_noise_seed(&mut self, seed: u64) {
48 self.noise_seed = seed;
49 }
50 pub fn new(
59 path: &Path,
60 with_annotations: bool,
61 num_threads: usize,
62 isotope_config: Option<IsotopeTransmissionConfig>,
63 ) -> Self {
64 Self::new_with_capabilities(
65 path,
66 with_annotations,
67 num_threads,
68 isotope_config,
69 InstrumentCapabilities::default(),
70 )
71 }
72
73 pub fn new_with_capabilities(
78 path: &Path,
79 with_annotations: bool,
80 num_threads: usize,
81 isotope_config: Option<IsotopeTransmissionConfig>,
82 capabilities: InstrumentCapabilities,
83 ) -> Self {
84 let handle = TimsTofSyntheticsDataHandle::new(path).unwrap();
85 handle
89 .read_prediction_set()
90 .expect("read prediction set")
91 .assert_render_compatible()
92 .expect("incompatible fragment prediction set for this renderer");
93 let fragment_ions_raw = handle.read_fragment_ions().unwrap();
94 let transmission_settings = handle.get_transmission_dda();
95
96 let synthetics = TimsTofSyntheticsPrecursorFrameBuilder::new(path).unwrap();
97 let config = isotope_config.unwrap_or_default().gated_by(capabilities);
98
99 let fragment_ions_with_complementary = if config.is_enabled() {
101 Some(TimsTofSyntheticsDataHandle::build_fragment_ions_with_transmission_data(
102 &synthetics.peptides,
103 &fragment_ions_raw,
104 num_threads,
105 ))
106 } else {
107 None
108 };
109
110 match with_annotations {
111 true => {
112 let fragment_ions =
113 Some(TimsTofSyntheticsDataHandle::build_fragment_ions_annotated(
114 &synthetics.peptides,
115 &fragment_ions_raw,
116 num_threads,
117 ));
118 Self {
119 noise_seed: 0,
120 path: path.to_str().unwrap().to_string(),
121 precursor_frame_builder: synthetics,
122 transmission_settings,
123 fragment_ions: None,
124 fragment_ions_annotated: fragment_ions,
125 isotope_transmission_config: config,
126 fragment_ions_with_complementary,
127 capabilities,
128 }
129 }
130 false => {
131 let fragment_ions = Some(TimsTofSyntheticsDataHandle::build_fragment_ions(
132 &synthetics.peptides,
133 &fragment_ions_raw,
134 num_threads,
135 ));
136 Self {
137 noise_seed: 0,
138 path: path.to_str().unwrap().to_string(),
139 precursor_frame_builder: synthetics,
140 transmission_settings,
141 fragment_ions,
142 fragment_ions_annotated: None,
143 isotope_transmission_config: config,
144 fragment_ions_with_complementary,
145 capabilities,
146 }
147 }
148 }
149 }
150 pub fn from_entities(
160 precursor_frame_builder: TimsTofSyntheticsPrecursorFrameBuilder,
161 transmission_settings: TimsTransmissionDDA,
162 fragment_ions_raw: Vec<crate::sim::containers::FragmentIonSim>,
163 isotope_config: Option<IsotopeTransmissionConfig>,
164 fragmentation: bool,
165 num_threads: usize,
166 ) -> Self {
167 let config = isotope_config
170 .unwrap_or_default()
171 .gated_by(InstrumentCapabilities::default());
172
173 let (fragment_ions, fragment_ions_with_complementary) = if fragmentation {
178 let with_complementary = if config.is_enabled() {
179 Some(TimsTofSyntheticsDataHandle::build_fragment_ions_with_transmission_data(
180 &precursor_frame_builder.peptides,
181 &fragment_ions_raw,
182 num_threads,
183 ))
184 } else {
185 None
186 };
187 let fragment_ions = Some(TimsTofSyntheticsDataHandle::build_fragment_ions(
188 &precursor_frame_builder.peptides,
189 &fragment_ions_raw,
190 num_threads,
191 ));
192 (fragment_ions, with_complementary)
193 } else {
194 (None, None)
195 };
196
197 Self {
198 noise_seed: 0,
199 path: String::new(),
200 precursor_frame_builder,
201 transmission_settings,
202 fragment_ions,
203 fragment_ions_annotated: None,
204 isotope_transmission_config: config,
205 fragment_ions_with_complementary,
206 capabilities: InstrumentCapabilities::default(),
207 }
208 }
209
210 pub fn build_frame(
222 &self,
223 frame_id: u32,
224 fragmentation: bool,
225 mz_noise_precursor: bool,
226 uniform: bool,
227 precursor_noise_ppm: f64,
228 mz_noise_fragment: bool,
229 fragment_noise_ppm: f64,
230 right_drag: bool,
231 ) -> TimsFrame {
232 match self
234 .precursor_frame_builder
235 .precursor_frame_id_set
236 .contains(&frame_id)
237 {
238 true => self.build_ms1_frame(
239 frame_id,
240 mz_noise_precursor,
241 uniform,
242 precursor_noise_ppm,
243 right_drag,
244 ),
245 false => self.build_ms2_frame(
246 frame_id,
247 fragmentation,
248 mz_noise_fragment,
249 uniform,
250 fragment_noise_ppm,
251 right_drag,
252 ),
253 }
254 }
255
256 pub fn build_frame_annotated(
257 &self,
258 frame_id: u32,
259 fragmentation: bool,
260 mz_noise_precursor: bool,
261 uniform: bool,
262 precursor_noise_ppm: f64,
263 mz_noise_fragment: bool,
264 fragment_noise_ppm: f64,
265 right_drag: bool,
266 ) -> TimsFrameAnnotated {
267 match self
268 .precursor_frame_builder
269 .precursor_frame_id_set
270 .contains(&frame_id)
271 {
272 true => self.build_ms1_frame_annotated(
273 frame_id,
274 mz_noise_precursor,
275 uniform,
276 precursor_noise_ppm,
277 right_drag,
278 ),
279 false => self.build_ms2_frame_annotated(
280 frame_id,
281 fragmentation,
282 mz_noise_fragment,
283 uniform,
284 fragment_noise_ppm,
285 right_drag,
286 ),
287 }
288 }
289
290 pub fn get_fragment_ion_ids(&self, precursor_frame_ids: Vec<u32>) -> Vec<u32> {
291 let mut peptide_ids: HashSet<u32> = HashSet::new();
292 for frame_id in precursor_frame_ids {
294 for (peptide_id, peptide) in self.precursor_frame_builder.peptides.iter() {
295 if peptide.frame_start <= frame_id && peptide.frame_end >= frame_id {
296 peptide_ids.insert(*peptide_id);
297 }
298 }
299 }
300 let mut result: Vec<u32> = Vec::new();
302 for item in peptide_ids {
303 let ions = self.precursor_frame_builder.ions.get(&item).unwrap();
304 for ion in ions.iter() {
305 result.push(ion.ion_id);
306 }
307 }
308 result
309 }
310
311 pub fn build_frames(
312 &self,
313 frame_ids: Vec<u32>,
314 fragmentation: bool,
315 mz_noise_precursor: bool,
316 uniform: bool,
317 precursor_noise_ppm: f64,
318 mz_noise_fragment: bool,
319 fragment_noise_ppm: f64,
320 right_drag: bool,
321 num_threads: usize,
322 ) -> Vec<TimsFrame> {
323 let pool = rayon::ThreadPoolBuilder::new()
325 .num_threads(num_threads)
326 .build()
327 .unwrap();
328
329 pool.install(|| {
330 let mut tims_frames: Vec<TimsFrame> = Vec::with_capacity(frame_ids.len());
332 unsafe { tims_frames.set_len(frame_ids.len()); }
333
334 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
335 let frame = self.build_frame(
336 *frame_id,
337 fragmentation,
338 mz_noise_precursor,
339 uniform,
340 precursor_noise_ppm,
341 mz_noise_fragment,
342 fragment_noise_ppm,
343 right_drag,
344 );
345 unsafe {
346 let ptr = tims_frames.as_ptr() as *mut TimsFrame;
347 std::ptr::write(ptr.add(idx), frame);
348 }
349 });
350
351 tims_frames
352 })
353 }
354 pub fn build_frames_annotated(
355 &self,
356 frame_ids: Vec<u32>,
357 fragmentation: bool,
358 mz_noise_precursor: bool,
359 uniform: bool,
360 precursor_noise_ppm: f64,
361 mz_noise_fragment: bool,
362 fragment_noise_ppm: f64,
363 right_drag: bool,
364 num_threads: usize,
365 ) -> Vec<TimsFrameAnnotated> {
366 let pool = rayon::ThreadPoolBuilder::new()
368 .num_threads(num_threads)
369 .build()
370 .unwrap();
371
372 pool.install(|| {
373 let mut tims_frames: Vec<TimsFrameAnnotated> = Vec::with_capacity(frame_ids.len());
375 unsafe { tims_frames.set_len(frame_ids.len()); }
376
377 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
378 let frame = self.build_frame_annotated(
379 *frame_id,
380 fragmentation,
381 mz_noise_precursor,
382 uniform,
383 precursor_noise_ppm,
384 mz_noise_fragment,
385 fragment_noise_ppm,
386 right_drag,
387 );
388 unsafe {
389 let ptr = tims_frames.as_ptr() as *mut TimsFrameAnnotated;
390 std::ptr::write(ptr.add(idx), frame);
391 }
392 });
393
394 tims_frames
395 })
396 }
397
398 fn build_ms1_frame(
399 &self,
400 frame_id: u32,
401 mz_noise_precursor: bool,
402 uniform: bool,
403 precursor_ppm: f64,
404 right_drag: bool,
405 ) -> TimsFrame {
406 let mut tims_frame = self.precursor_frame_builder.build_precursor_frame(
407 frame_id,
408 mz_noise_precursor,
409 uniform,
410 precursor_ppm,
411 right_drag,
412 );
413 let intensities_rounded = tims_frame
414 .ims_frame
415 .intensity
416 .iter()
417 .map(|x| x.round())
418 .collect::<Vec<_>>();
419 tims_frame.ims_frame.intensity = Arc::new(intensities_rounded);
420 tims_frame
421 }
422
423 fn build_ms1_frame_annotated(
424 &self,
425 frame_id: u32,
426 mz_noise_precursor: bool,
427 uniform: bool,
428 precursor_ppm: f64,
429 right_drag: bool,
430 ) -> TimsFrameAnnotated {
431 let mut tims_frame = self
432 .precursor_frame_builder
433 .build_precursor_frame_annotated(
434 frame_id,
435 mz_noise_precursor,
436 uniform,
437 precursor_ppm,
438 right_drag,
439 );
440 let intensities_rounded = tims_frame
441 .intensity
442 .iter()
443 .map(|x| x.round())
444 .collect::<Vec<_>>();
445 tims_frame.intensity = intensities_rounded;
446 tims_frame
447 }
448
449 fn build_ms2_frame(
450 &self,
451 frame_id: u32,
452 fragmentation: bool,
453 mz_noise_fragment: bool,
454 uniform: bool,
455 fragment_ppm: f64,
456 right_drag: bool,
457 ) -> TimsFrame {
458 match fragmentation {
459 false => {
460 let mut frame = self.transmission_settings.transmit_tims_frame(
461 &self.build_ms1_frame(
462 frame_id,
463 mz_noise_fragment,
464 uniform,
465 fragment_ppm,
466 right_drag,
467 ),
468 None,
469 );
470 let intensities_rounded = frame
471 .ims_frame
472 .intensity
473 .iter()
474 .map(|x| x.round())
475 .collect::<Vec<_>>();
476 frame.ims_frame.intensity = Arc::new(intensities_rounded);
477 frame.ms_type = MsType::FragmentDda;
483 frame
484 }
485 true => {
486 let mut frame = self.build_fragment_frame(
487 frame_id,
488 &self.fragment_ions.as_ref().unwrap(),
489 mz_noise_fragment,
490 uniform,
491 fragment_ppm,
492 None,
493 None,
494 None,
495 Some(right_drag),
496 );
497 let intensities_rounded = frame
498 .ims_frame
499 .intensity
500 .iter()
501 .map(|x| x.round())
502 .collect::<Vec<_>>();
503 frame.ims_frame.intensity = Arc::new(intensities_rounded);
504 frame
505 }
506 }
507 }
508
509 fn build_ms2_frame_annotated(
510 &self,
511 frame_id: u32,
512 fragmentation: bool,
513 mz_noise_fragment: bool,
514 uniform: bool,
515 fragment_ppm: f64,
516 right_drag: bool,
517 ) -> TimsFrameAnnotated {
518 match fragmentation {
519 false => {
520 let mut frame = self.transmission_settings.transmit_tims_frame_annotated(
521 &self.build_ms1_frame_annotated(
522 frame_id,
523 mz_noise_fragment,
524 uniform,
525 fragment_ppm,
526 right_drag,
527 ),
528 None,
529 );
530 let intensities_rounded = frame
531 .intensity
532 .iter()
533 .map(|x| x.round())
534 .collect::<Vec<_>>();
535 frame.intensity = intensities_rounded;
536 frame.ms_type = MsType::FragmentDda;
538 frame
539 }
540 true => {
541 let mut frame = self.build_fragment_frame_annotated(
542 frame_id,
543 &self.fragment_ions_annotated.as_ref().unwrap(),
544 mz_noise_fragment,
545 uniform,
546 fragment_ppm,
547 None,
548 None,
549 None,
550 Some(right_drag),
551 );
552 let intensities_rounded = frame
553 .intensity
554 .iter()
555 .map(|x| x.round())
556 .collect::<Vec<_>>();
557 frame.intensity = intensities_rounded;
558 frame
559 }
560 }
561 }
562
563 fn build_fragment_frame(
577 &self,
578 frame_id: u32,
579 fragment_ions: &BTreeMap<
580 (u32, i8, i32),
581 (PeptideProductIonSeriesCollection, Vec<MzSpectrum>),
582 >,
583 mz_noise_fragment: bool,
584 uniform: bool,
585 fragment_ppm: f64,
586 mz_min: Option<f64>,
587 mz_max: Option<f64>,
588 intensity_min: Option<f64>,
589 right_drag: Option<bool>,
590 ) -> TimsFrame {
591 let mut rng = noise_rng(self.noise_seed, &[frame_id as u64]);
594 let ms_type = if self.precursor_frame_builder.precursor_frame_id_set.contains(&frame_id) {
596 MsType::Unknown
597 } else {
598 MsType::FragmentDda
599 };
600
601 let rt = *self.precursor_frame_builder.frame_to_rt.get(&frame_id)
602 .expect("frame_to_rt should always have this frame") as f64;
603 let right_drag_val = right_drag.unwrap_or(false);
604 let mz_min_val = mz_min.unwrap_or(100.0);
605 let mz_max_val = mz_max.unwrap_or(1700.0);
606 let intensity_min_val = intensity_min.unwrap_or(1.0);
607
608 let Some(pasef_meta) = self.transmission_settings.pasef_meta.get(&(frame_id as i32)) else {
610 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
611 };
612
613 let estimated_capacity = pasef_meta.len() * 4;
615 let mut tims_spectra: Vec<TimsSpectrum> = Vec::with_capacity(estimated_capacity);
616
617 for meta in pasef_meta.iter() {
619 let ion_id = meta.precursor as u32;
621
622 let Some(&(peptide_id, charge_state)) = self.precursor_frame_builder.ion_id_to_peptide_charge.get(&ion_id) else {
624 continue;
625 };
626
627 let Some(peptide) = self.precursor_frame_builder.peptides.get(&peptide_id) else {
629 continue;
630 };
631
632 if frame_id < peptide.frame_start || frame_id > peptide.frame_end {
634 continue;
635 }
636
637 let Some((ion_abundances, scan_occurrences, scan_abundances, charges, spectra)) = self
639 .precursor_frame_builder
640 .peptide_to_ions
641 .get(&peptide_id)
642 else {
643 continue;
644 };
645
646 let Some(ion_index) = charges.iter().position(|&c| c == charge_state) else {
648 continue;
649 };
650
651 let ion_abundance = ion_abundances[ion_index];
652 let all_scan_occurrence = &scan_occurrences[ion_index];
653 let all_scan_abundance = &scan_abundances[ion_index];
654 let spectrum = &spectra[ion_index];
655 let total_events = *self.precursor_frame_builder.peptide_to_events.get(&peptide_id).unwrap();
656
657 let frame_abundance = self.precursor_frame_builder.frame_to_abundances
659 .get(&frame_id)
660 .and_then(|(pep_ids, abundances)| {
661 pep_ids.iter().position(|&p| p == peptide_id)
662 .map(|idx| abundances[idx])
663 })
664 .unwrap_or(0.0);
665
666 if frame_abundance == 0.0 {
667 continue;
668 }
669
670 let collision_energy = meta.collision_energy;
672 let Some(collision_energy_quantized) = crate::sim::handle::resolve_fragment_ce_key(
675 fragment_ions, peptide_id, charge_state, collision_energy,
676 ) else {
677 if crate::sim::handle::fragment_prefix_exists(fragment_ions, peptide_id, charge_state) {
683 panic!(
684 "DDA fragment lookup miss: peptide {} charge {} applied CE {:.4} eV has \
685 predicted fragments, but none within 0.1 eV — the prediction set does \
686 not cover this instrument's collision energy",
687 peptide_id, charge_state, collision_energy,
688 );
689 }
690 continue;
691 };
692 let (_, fragment_series_vec) = fragment_ions
693 .get(&(peptide_id, charge_state, collision_energy_quantized))
694 .expect("resolve_fragment_ce_key returned a present key");
695
696 for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
698 let scan_i32 = *scan as i32;
700 if scan_i32 < meta.scan_start || scan_i32 > meta.scan_end {
701 continue;
702 }
703
704 let transmitted_indices = match self.isotope_transmission_config.mode {
706 IsotopeTransmissionMode::None => HashSet::new(),
707 IsotopeTransmissionMode::PrecursorScaling | IsotopeTransmissionMode::PerFragment => {
708 self.transmission_settings.get_transmission_set(
709 frame_id as i32,
710 scan_i32,
711 &spectrum.mz,
712 Some(self.isotope_transmission_config.min_probability),
713 )
714 },
715 };
716
717 let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
719
720 let scan_mobility = *self.precursor_frame_builder.scan_to_mobility.get(scan).unwrap() as f64;
722
723 let transmission_factor = if self.isotope_transmission_config.mode == IsotopeTransmissionMode::PrecursorScaling {
725 if let Some(comp_data) = self.fragment_ions_with_complementary.as_ref() {
726 if let Some(frag_data) = comp_data.get(&(peptide_id, charge_state, collision_energy_quantized)) {
727 calculate_precursor_transmission_factor(
728 &frag_data.precursor_isotope_distribution,
729 &transmitted_indices,
730 )
731 } else {
732 1.0
733 }
734 } else {
735 1.0
736 }
737 } else {
738 1.0
739 };
740
741 let frag_data = self
745 .fragment_ions_with_complementary
746 .as_ref()
747 .and_then(|c| c.get(&(peptide_id, charge_state, collision_energy_quantized)));
748
749 for (series_idx, fragment_ion_series) in fragment_series_vec.iter().enumerate() {
750 let final_spectrum = crate::sim::dia::fragment_series_spectrum(
751 self.isotope_transmission_config.mode,
752 fragment_ion_series,
753 series_idx,
754 fraction_events,
755 transmission_factor,
756 frag_data,
757 &transmitted_indices,
758 self.isotope_transmission_config.max_isotopes,
759 );
760
761 let mz_spectrum = if mz_noise_fragment {
762 if uniform {
763 final_spectrum.add_mz_noise_uniform_with_rng(fragment_ppm, right_drag_val, &mut rng)
764 } else {
765 final_spectrum.add_mz_noise_normal_with_rng(fragment_ppm, &mut rng)
766 }
767 } else {
768 final_spectrum
769 };
770
771 let spectrum_len = mz_spectrum.mz.len();
772 tims_spectra.push(TimsSpectrum::new(
773 frame_id as i32,
774 *scan as i32,
775 rt,
776 scan_mobility,
777 ms_type.clone(),
778 IndexedMzSpectrum::from_mz_spectrum(
779 vec![0; spectrum_len],
780 mz_spectrum,
781 ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
782 ));
783 }
784
785 if self.isotope_transmission_config.has_precursor_survival() {
787 let mut rng = rand::thread_rng();
788 let survival_fraction = rng.gen_range(
789 self.isotope_transmission_config.precursor_survival_min
790 ..=self.isotope_transmission_config.precursor_survival_max
791 );
792
793 if survival_fraction > 0.0 {
794 let precursor_transmitted = self.transmission_settings.transmit_spectrum(
796 frame_id as i32,
797 *scan as i32,
798 spectrum.clone(),
799 Some(self.isotope_transmission_config.min_probability),
800 );
801
802 if !precursor_transmitted.mz.is_empty() {
803 let precursor_scaled = precursor_transmitted * (fraction_events as f64 * survival_fraction);
805
806 let precursor_mz_spectrum = if mz_noise_fragment {
807 if uniform {
808 precursor_scaled.add_mz_noise_uniform_with_rng(fragment_ppm, right_drag_val, &mut rng)
809 } else {
810 precursor_scaled.add_mz_noise_normal_with_rng(fragment_ppm, &mut rng)
811 }
812 } else {
813 precursor_scaled
814 };
815
816 let precursor_len = precursor_mz_spectrum.mz.len();
817 tims_spectra.push(TimsSpectrum::new(
818 frame_id as i32,
819 *scan as i32,
820 rt,
821 scan_mobility,
822 ms_type.clone(),
823 IndexedMzSpectrum::from_mz_spectrum(
824 vec![0; precursor_len],
825 precursor_mz_spectrum,
826 ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
827 ));
828 }
829 }
830 }
831 }
832 }
833
834 if tims_spectra.is_empty() {
835 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
836 }
837
838 let tims_frame = TimsFrame::from_tims_spectra(tims_spectra);
839 tims_frame.filter_ranged(
840 mz_min_val,
841 mz_max_val,
842 0,
843 1000,
844 0.0,
845 10.0,
846 intensity_min_val,
847 1e9,
848 0,
849 i32::MAX,
850 )
851 }
852
853 pub fn build_fragment_frame_annotated(
854 &self,
855 frame_id: u32,
856 fragment_ions: &BTreeMap<
857 (u32, i8, i32),
858 (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>),
859 >,
860 mz_noise_fragment: bool,
861 uniform: bool,
862 fragment_ppm: f64,
863 mz_min: Option<f64>,
864 mz_max: Option<f64>,
865 intensity_min: Option<f64>,
866 right_drag: Option<bool>,
867 ) -> TimsFrameAnnotated {
868 let mut rng = noise_rng(self.noise_seed, &[frame_id as u64]);
871 let ms_type = if self.precursor_frame_builder.precursor_frame_id_set.contains(&frame_id) {
873 MsType::Unknown
874 } else {
875 MsType::FragmentDda
876 };
877
878 let rt = *self.precursor_frame_builder.frame_to_rt.get(&frame_id).unwrap() as f64;
879 let right_drag_val = right_drag.unwrap_or(false);
880 let mz_min_val = mz_min.unwrap_or(100.0);
881 let mz_max_val = mz_max.unwrap_or(1700.0);
882 let intensity_min_val = intensity_min.unwrap_or(1.0);
883
884 let Some(pasef_meta) = self.transmission_settings.pasef_meta.get(&(frame_id as i32)) else {
886 return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
887 };
888
889 let estimated_capacity = pasef_meta.len() * 4;
891 let mut tims_spectra: Vec<TimsSpectrumAnnotated> = Vec::with_capacity(estimated_capacity);
892
893 for meta in pasef_meta.iter() {
895 let ion_id = meta.precursor as u32;
897
898 let Some(&(peptide_id, charge_state)) = self.precursor_frame_builder.ion_id_to_peptide_charge.get(&ion_id) else {
900 continue;
901 };
902
903 let Some(peptide) = self.precursor_frame_builder.peptides.get(&peptide_id) else {
905 continue;
906 };
907
908 if frame_id < peptide.frame_start || frame_id > peptide.frame_end {
910 continue;
911 }
912
913 let Some((ion_abundances, scan_occurrences, scan_abundances, charges, _)) = self
915 .precursor_frame_builder
916 .peptide_to_ions
917 .get(&peptide_id)
918 else {
919 continue;
920 };
921
922 let Some(ion_index) = charges.iter().position(|&c| c == charge_state) else {
924 continue;
925 };
926
927 let ion_abundance = ion_abundances[ion_index];
928 let all_scan_occurrence = &scan_occurrences[ion_index];
929 let all_scan_abundance = &scan_abundances[ion_index];
930 let total_events = *self.precursor_frame_builder.peptide_to_events.get(&peptide_id).unwrap();
931
932 let frame_abundance = self.precursor_frame_builder.frame_to_abundances
934 .get(&frame_id)
935 .and_then(|(pep_ids, abundances)| {
936 pep_ids.iter().position(|&p| p == peptide_id)
937 .map(|idx| abundances[idx])
938 })
939 .unwrap_or(0.0);
940
941 if frame_abundance == 0.0 {
942 continue;
943 }
944
945 let collision_energy = meta.collision_energy;
947 let Some(collision_energy_quantized) = crate::sim::handle::resolve_fragment_ce_key(
950 fragment_ions, peptide_id, charge_state, collision_energy,
951 ) else {
952 if crate::sim::handle::fragment_prefix_exists(fragment_ions, peptide_id, charge_state) {
958 panic!(
959 "DDA fragment lookup miss: peptide {} charge {} applied CE {:.4} eV has \
960 predicted fragments, but none within 0.1 eV — the prediction set does \
961 not cover this instrument's collision energy",
962 peptide_id, charge_state, collision_energy,
963 );
964 }
965 continue;
966 };
967 let (_, fragment_series_vec) = fragment_ions
968 .get(&(peptide_id, charge_state, collision_energy_quantized))
969 .expect("resolve_fragment_ce_key returned a present key");
970
971 let ion = PeptideIon::new(
973 peptide.sequence.sequence.clone(),
974 charge_state as i32,
975 ion_abundance as f64,
976 Some(peptide_id as i32),
977 );
978 let precursor_spectrum_annotated = ion.calculate_isotopic_spectrum_annotated(1e-3, 1e-8, 200, 1e-4);
980
981 for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
983 let scan_i32 = *scan as i32;
985 if scan_i32 < meta.scan_start || scan_i32 > meta.scan_end {
986 continue;
987 }
988
989 let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
991
992 let scan_mobility = *self.precursor_frame_builder.scan_to_mobility.get(scan).unwrap() as f64;
994
995 for fragment_ion_series in fragment_series_vec.iter() {
996 let scaled_spec = fragment_ion_series.clone() * fraction_events as f64;
997
998 let mz_spectrum = if mz_noise_fragment {
999 if uniform {
1000 scaled_spec.add_mz_noise_uniform_with_rng(fragment_ppm, right_drag_val, &mut rng)
1001 } else {
1002 scaled_spec.add_mz_noise_normal_with_rng(fragment_ppm, &mut rng)
1003 }
1004 } else {
1005 scaled_spec
1006 };
1007
1008 let spectrum_len = mz_spectrum.mz.len();
1009 tims_spectra.push(TimsSpectrumAnnotated::new(
1010 frame_id as i32,
1011 *scan,
1012 rt,
1013 scan_mobility,
1014 ms_type.clone(),
1015 vec![0; spectrum_len],
1016 mz_spectrum,
1017 ));
1018 }
1019
1020 if self.isotope_transmission_config.has_precursor_survival() {
1022 let mut rng = rand::thread_rng();
1023 let survival_fraction = rng.gen_range(
1024 self.isotope_transmission_config.precursor_survival_min
1025 ..=self.isotope_transmission_config.precursor_survival_max
1026 );
1027
1028 if survival_fraction > 0.0 {
1029 let precursor_mz_spectrum = MzSpectrum::new(
1031 precursor_spectrum_annotated.mz.clone(),
1032 precursor_spectrum_annotated.intensity.clone(),
1033 );
1034
1035 let precursor_transmitted = self.transmission_settings.transmit_spectrum(
1037 frame_id as i32,
1038 scan_i32,
1039 precursor_mz_spectrum,
1040 Some(self.isotope_transmission_config.min_probability),
1041 );
1042
1043 if !precursor_transmitted.mz.is_empty() {
1044 let precursor_scaled = precursor_transmitted * (fraction_events as f64 * survival_fraction);
1046
1047 let precursor_final = if mz_noise_fragment {
1048 if uniform {
1049 precursor_scaled.add_mz_noise_uniform_with_rng(fragment_ppm, right_drag_val, &mut rng)
1050 } else {
1051 precursor_scaled.add_mz_noise_normal_with_rng(fragment_ppm, &mut rng)
1052 }
1053 } else {
1054 precursor_scaled
1055 };
1056
1057 let annotations: Vec<PeakAnnotation> = precursor_final.mz.iter()
1059 .map(|_| PeakAnnotation { contributions: vec![] })
1060 .collect();
1061 let precursor_annotated = MzSpectrumAnnotated::new(
1062 precursor_final.mz.to_vec(),
1063 precursor_final.intensity.to_vec(),
1064 annotations,
1065 );
1066
1067 let precursor_len = precursor_annotated.mz.len();
1068 tims_spectra.push(TimsSpectrumAnnotated::new(
1069 frame_id as i32,
1070 *scan,
1071 rt,
1072 scan_mobility,
1073 ms_type.clone(),
1074 vec![0; precursor_len],
1075 precursor_annotated,
1076 ));
1077 }
1078 }
1079 }
1080 }
1081 }
1082
1083 if tims_spectra.is_empty() {
1084 return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
1085 }
1086
1087 TimsFrameAnnotated::from_tims_spectra_annotated(tims_spectra).filter_ranged(
1088 mz_min_val, mz_max_val, 0.0, 10.0, 0, 1000, intensity_min_val, 1e9,
1089 )
1090 }
1091
1092 pub fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> f64 {
1093 self.transmission_settings.get_collision_energy(frame_id, scan_id).unwrap_or(0.0)
1094 }
1095
1096 pub fn get_collision_energies(&self, frame_ids: Vec<i32>, scan_ids: Vec<i32>) -> Vec<f64> {
1097 let mut collision_energies: Vec<f64> = Vec::new();
1098 for frame_id in frame_ids {
1099 for scan_id in &scan_ids {
1100 collision_energies.push(self.get_collision_energy(frame_id, *scan_id));
1101 }
1102 }
1103 collision_energies
1104 }
1105}