1use mscore::algorithm::isotope::{
2 calculate_precursor_transmission_factor,
3 calculate_transmission_dependent_fragment_ion_isotope_distribution,
4};
5use mscore::data::peptide::{PeptideIon, PeptideProductIonSeriesCollection};
6use mscore::data::spectrum::{IndexedMzSpectrum, MsType, MzSpectrum};
7use mscore::simulation::annotation::{
8 MzSpectrumAnnotated, PeakAnnotation, TimsFrameAnnotated, TimsSpectrumAnnotated,
9};
10use mscore::timstof::collision::{TimsTofCollisionEnergy, TimsTofCollisionEnergyDIA};
11use mscore::timstof::frame::TimsFrame;
12use mscore::timstof::quadrupole::{IonTransmission, TimsTransmissionDIA, WindowTransmission};
13use mscore::timstof::spectrum::TimsSpectrum;
14use std::collections::{BTreeMap, HashSet};
15use std::path::Path;
16use std::sync::Arc;
17
18use rand::Rng;
19use rayon::prelude::*;
20use rayon::ThreadPoolBuilder;
21
22use crate::sim::containers::{IsotopeTransmissionConfig, IsotopeTransmissionMode};
23use crate::sim::projector::{IntensityStage, MzCoordSpace, RenderedEvent, RenderedSpectrum};
24use crate::sim::scheme::{DataMode, InstrumentCapabilities, IsolationWindow};
25use crate::sim::handle::{FragmentIonsWithComplementary, TimsTofSyntheticsDataHandle};
26use crate::sim::precursor::TimsTofSyntheticsPrecursorFrameBuilder;
27
28#[allow(clippy::too_many_arguments)]
38pub(crate) fn fragment_series_spectrum(
39 mode: IsotopeTransmissionMode,
40 fragment_ion_series: &MzSpectrum,
41 series_idx: usize,
42 fraction_events: f32,
43 transmission_factor: f64,
44 frag_data: Option<&FragmentIonsWithComplementary>,
45 transmitted_indices: &HashSet<usize>,
46 max_isotopes: usize,
47) -> MzSpectrum {
48 match mode {
49 IsotopeTransmissionMode::None => fragment_ion_series.clone() * fraction_events as f64,
50 IsotopeTransmissionMode::PrecursorScaling => {
51 fragment_ion_series.clone() * (fraction_events as f64 * transmission_factor)
52 }
53 IsotopeTransmissionMode::PerFragment => {
54 if let Some(frag_data) = frag_data {
55 if series_idx < frag_data.per_fragment_data.len() {
56 let series_data = &frag_data.per_fragment_data[series_idx];
57 let mut aggregated_mz: Vec<f64> = Vec::new();
58 let mut aggregated_intensity: Vec<f64> = Vec::new();
59 for frag_ion_data in series_data {
60 let adjusted_dist =
61 calculate_transmission_dependent_fragment_ion_isotope_distribution(
62 &frag_ion_data.fragment_distribution,
63 &frag_ion_data.complementary_distribution,
64 transmitted_indices,
65 max_isotopes,
66 );
67 for (mz, abundance) in adjusted_dist {
68 aggregated_mz.push(mz);
69 aggregated_intensity
70 .push(abundance * frag_ion_data.predicted_intensity * fraction_events as f64);
71 }
72 }
73 if !aggregated_mz.is_empty() {
74 MzSpectrum::new(aggregated_mz, aggregated_intensity)
75 } else {
76 fragment_ion_series.clone() * fraction_events as f64
77 }
78 } else {
79 fragment_ion_series.clone() * fraction_events as f64
80 }
81 } else {
82 fragment_ion_series.clone() * fraction_events as f64
83 }
84 }
85 }
86}
87
88pub struct TimsTofSyntheticsFrameBuilderDIA {
89 pub path: String,
90 pub precursor_frame_builder: TimsTofSyntheticsPrecursorFrameBuilder,
91 pub transmission_settings: TimsTransmissionDIA,
92 pub fragmentation_settings: TimsTofCollisionEnergyDIA,
93 pub fragment_ions:
94 Option<BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrum>)>>,
95 pub fragment_ions_annotated: Option<
96 BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>)>,
97 >,
98 pub isotope_config: IsotopeTransmissionConfig,
101 pub fragment_ions_with_transmission:
103 Option<BTreeMap<(u32, i8, i32), FragmentIonsWithComplementary>>,
104 pub capabilities: InstrumentCapabilities,
106}
107
108impl TimsTofSyntheticsFrameBuilderDIA {
109 pub fn new(path: &Path, with_annotations: bool, num_threads: usize) -> rusqlite::Result<Self> {
110 Self::new_with_config(path, with_annotations, num_threads, IsotopeTransmissionConfig::default())
111 }
112
113 pub fn new_with_config(
114 path: &Path,
115 with_annotations: bool,
116 num_threads: usize,
117 isotope_config: IsotopeTransmissionConfig,
118 ) -> rusqlite::Result<Self> {
119 Self::new_with_config_and_source(
120 path,
121 with_annotations,
122 num_threads,
123 isotope_config,
124 &crate::sim::projector::DistributionSource::Columns,
125 )
126 }
127
128 pub fn new_with_config_and_source(
131 path: &Path,
132 with_annotations: bool,
133 num_threads: usize,
134 isotope_config: IsotopeTransmissionConfig,
135 source: &crate::sim::projector::DistributionSource,
136 ) -> rusqlite::Result<Self> {
137 let synthetics = TimsTofSyntheticsPrecursorFrameBuilder::from_source(path, source)?;
138 let handle = TimsTofSyntheticsDataHandle::new(path)?;
139
140 handle
143 .read_prediction_set()?
144 .assert_render_compatible()
145 .map_err(|_| rusqlite::Error::InvalidQuery)?;
146
147 let fragment_ions = handle.read_fragment_ions()?;
148
149 let capabilities = InstrumentCapabilities::default();
152 let isotope_config = isotope_config.gated_by(capabilities);
153
154 let fragmentation_settings = handle.get_collision_energy_dia();
156 let transmission_settings = handle.get_transmission_dia();
158
159 let fragment_ions_with_transmission = if isotope_config.is_enabled() {
161 Some(TimsTofSyntheticsDataHandle::build_fragment_ions_with_transmission_data(
162 &synthetics.peptides,
163 &fragment_ions,
164 num_threads,
165 ))
166 } else {
167 None
168 };
169
170 match with_annotations {
171 true => {
172 let fragment_ions_annotated =
173 Some(TimsTofSyntheticsDataHandle::build_fragment_ions_annotated(
174 &synthetics.peptides,
175 &fragment_ions,
176 num_threads,
177 ));
178 Ok(Self {
179 path: path.to_str().unwrap().to_string(),
180 precursor_frame_builder: synthetics,
181 transmission_settings,
182 fragmentation_settings,
183 fragment_ions: None,
184 fragment_ions_annotated,
185 isotope_config,
186 fragment_ions_with_transmission,
187 capabilities,
188 })
189 }
190
191 false => {
192 let fragment_ions = Some(TimsTofSyntheticsDataHandle::build_fragment_ions(
193 &synthetics.peptides,
194 &fragment_ions,
195 num_threads,
196 ));
197 Ok(Self {
198 path: path.to_str().unwrap().to_string(),
199 precursor_frame_builder: synthetics,
200 transmission_settings,
201 fragmentation_settings,
202 fragment_ions,
203 fragment_ions_annotated: None,
204 isotope_config,
205 fragment_ions_with_transmission,
206 capabilities,
207 })
208 }
209 }
210 }
211
212 pub fn build_frame(
224 &self,
225 frame_id: u32,
226 fragmentation: bool,
227 mz_noise_precursor: bool,
228 uniform: bool,
229 precursor_noise_ppm: f64,
230 mz_noise_fragment: bool,
231 fragment_noise_ppm: f64,
232 right_drag: bool,
233 ) -> TimsFrame {
234 match self
236 .precursor_frame_builder
237 .precursor_frame_id_set
238 .contains(&frame_id)
239 {
240 true => self.build_ms1_frame(
241 frame_id,
242 mz_noise_precursor,
243 uniform,
244 precursor_noise_ppm,
245 right_drag,
246 ),
247 false => self.build_ms2_frame(
248 frame_id,
249 fragmentation,
250 mz_noise_fragment,
251 uniform,
252 fragment_noise_ppm,
253 right_drag,
254 ),
255 }
256 }
257
258 pub fn build_frame_annotated(
259 &self,
260 frame_id: u32,
261 fragmentation: bool,
262 mz_noise_precursor: bool,
263 uniform: bool,
264 precursor_noise_ppm: f64,
265 mz_noise_fragment: bool,
266 fragment_noise_ppm: f64,
267 right_drag: bool,
268 ) -> TimsFrameAnnotated {
269 match self
270 .precursor_frame_builder
271 .precursor_frame_id_set
272 .contains(&frame_id)
273 {
274 true => self.build_ms1_frame_annotated(
275 frame_id,
276 mz_noise_precursor,
277 uniform,
278 precursor_noise_ppm,
279 right_drag,
280 ),
281 false => self.build_ms2_frame_annotated(
282 frame_id,
283 fragmentation,
284 mz_noise_fragment,
285 uniform,
286 fragment_noise_ppm,
287 right_drag,
288 ),
289 }
290 }
291
292 pub fn get_fragment_ion_ids(&self, precursor_frame_ids: Vec<u32>) -> Vec<u32> {
293 let mut peptide_ids: HashSet<u32> = HashSet::new();
294 for frame_id in precursor_frame_ids {
296 for (peptide_id, peptide) in self.precursor_frame_builder.peptides.iter() {
297 if peptide.frame_start <= frame_id && peptide.frame_end >= frame_id {
298 peptide_ids.insert(*peptide_id);
299 }
300 }
301 }
302 let mut result: Vec<u32> = Vec::new();
304 for item in peptide_ids {
305 let ions = self.precursor_frame_builder.ions.get(&item).unwrap();
306 for ion in ions.iter() {
307 result.push(ion.ion_id);
308 }
309 }
310 result
311 }
312
313 pub fn build_frames(
314 &self,
315 frame_ids: Vec<u32>,
316 fragmentation: bool,
317 mz_noise_precursor: bool,
318 uniform: bool,
319 precursor_noise_ppm: f64,
320 mz_noise_fragment: bool,
321 fragment_noise_ppm: f64,
322 right_drag: bool,
323 num_threads: usize,
324 ) -> Vec<TimsFrame> {
325 let pool = rayon::ThreadPoolBuilder::new()
327 .num_threads(num_threads)
328 .build()
329 .unwrap();
330
331 pool.install(|| {
332 let mut tims_frames: Vec<TimsFrame> = Vec::with_capacity(frame_ids.len());
334 unsafe { tims_frames.set_len(frame_ids.len()); }
336
337 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
338 let frame = self.build_frame(
339 *frame_id,
340 fragmentation,
341 mz_noise_precursor,
342 uniform,
343 precursor_noise_ppm,
344 mz_noise_fragment,
345 fragment_noise_ppm,
346 right_drag,
347 );
348 unsafe {
350 let ptr = tims_frames.as_ptr() as *mut TimsFrame;
351 std::ptr::write(ptr.add(idx), frame);
352 }
353 });
354
355 tims_frames
356 })
357 }
358
359 pub fn build_frames_annotated(
360 &self,
361 frame_ids: Vec<u32>,
362 fragmentation: bool,
363 mz_noise_precursor: bool,
364 uniform: bool,
365 precursor_noise_ppm: f64,
366 mz_noise_fragment: bool,
367 fragment_noise_ppm: f64,
368 right_drag: bool,
369 num_threads: usize,
370 ) -> Vec<TimsFrameAnnotated> {
371 let pool = rayon::ThreadPoolBuilder::new()
373 .num_threads(num_threads)
374 .build()
375 .unwrap();
376
377 pool.install(|| {
378 let mut tims_frames: Vec<TimsFrameAnnotated> = Vec::with_capacity(frame_ids.len());
380 unsafe { tims_frames.set_len(frame_ids.len()); }
381
382 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
383 let frame = self.build_frame_annotated(
384 *frame_id,
385 fragmentation,
386 mz_noise_precursor,
387 uniform,
388 precursor_noise_ppm,
389 mz_noise_fragment,
390 fragment_noise_ppm,
391 right_drag,
392 );
393 unsafe {
394 let ptr = tims_frames.as_ptr() as *mut TimsFrameAnnotated;
395 std::ptr::write(ptr.add(idx), frame);
396 }
397 });
398
399 tims_frames
400 })
401 }
402
403 fn build_ms1_frame(
404 &self,
405 frame_id: u32,
406 mz_noise_precursor: bool,
407 uniform: bool,
408 precursor_ppm: f64,
409 right_drag: bool,
410 ) -> TimsFrame {
411 let mut tims_frame = self.precursor_frame_builder.build_precursor_frame(
412 frame_id,
413 mz_noise_precursor,
414 uniform,
415 precursor_ppm,
416 right_drag,
417 );
418 let intensities_rounded = tims_frame
419 .ims_frame
420 .intensity
421 .iter()
422 .map(|x| x.round())
423 .collect::<Vec<_>>();
424 tims_frame.ims_frame.intensity = Arc::new(intensities_rounded);
425 tims_frame
426 }
427
428 fn build_ms1_frame_annotated(
429 &self,
430 frame_id: u32,
431 mz_noise_precursor: bool,
432 uniform: bool,
433 precursor_ppm: f64,
434 right_drag: bool,
435 ) -> TimsFrameAnnotated {
436 let mut tims_frame = self
437 .precursor_frame_builder
438 .build_precursor_frame_annotated(
439 frame_id,
440 mz_noise_precursor,
441 uniform,
442 precursor_ppm,
443 right_drag,
444 );
445 let intensities_rounded = tims_frame
446 .intensity
447 .iter()
448 .map(|x| x.round())
449 .collect::<Vec<_>>();
450 tims_frame.intensity = intensities_rounded;
451 tims_frame
452 }
453
454 fn build_ms2_frame(
455 &self,
456 frame_id: u32,
457 fragmentation: bool,
458 mz_noise_fragment: bool,
459 uniform: bool,
460 fragment_ppm: f64,
461 right_drag: bool,
462 ) -> TimsFrame {
463 match fragmentation {
464 false => {
465 let mut frame = self.transmission_settings.transmit_tims_frame(
466 &self.build_ms1_frame(
467 frame_id,
468 mz_noise_fragment,
469 uniform,
470 fragment_ppm,
471 right_drag,
472 ),
473 None,
474 );
475 let intensities_rounded = frame
476 .ims_frame
477 .intensity
478 .iter()
479 .map(|x| x.round())
480 .collect::<Vec<_>>();
481 frame.ims_frame.intensity = Arc::new(intensities_rounded);
482 frame.ms_type = MsType::FragmentDia;
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::FragmentDia;
537 frame
538 }
539 true => {
540 let mut frame = self.build_fragment_frame_annotated(
541 frame_id,
542 &self.fragment_ions_annotated.as_ref().unwrap(),
543 mz_noise_fragment,
544 uniform,
545 fragment_ppm,
546 None,
547 None,
548 None,
549 Some(right_drag),
550 );
551 let intensities_rounded = frame
552 .intensity
553 .iter()
554 .map(|x| x.round())
555 .collect::<Vec<_>>();
556 frame.intensity = intensities_rounded;
557 frame
558 }
559 }
560 }
561
562 fn build_fragment_frame(
576 &self,
577 frame_id: u32,
578 fragment_ions: &BTreeMap<
579 (u32, i8, i32),
580 (PeptideProductIonSeriesCollection, Vec<MzSpectrum>),
581 >,
582 mz_noise_fragment: bool,
583 uniform: bool,
584 fragment_ppm: f64,
585 mz_min: Option<f64>,
586 mz_max: Option<f64>,
587 intensity_min: Option<f64>,
588 right_drag: Option<bool>,
589 ) -> TimsFrame {
590 let ms_type = if self.precursor_frame_builder.precursor_frame_id_set.contains(&frame_id) {
592 MsType::Unknown
593 } else {
594 MsType::FragmentDia
595 };
596
597 let rt = *self.precursor_frame_builder.frame_to_rt.get(&frame_id).unwrap() as f64;
598 let right_drag_val = right_drag.unwrap_or(false);
599 let mz_min_val = mz_min.unwrap_or(100.0);
600 let mz_max_val = mz_max.unwrap_or(1700.0);
601 let intensity_min_val = intensity_min.unwrap_or(1.0);
602
603 let Some((peptide_ids, frame_abundances)) = self
605 .precursor_frame_builder
606 .frame_to_abundances
607 .get(&frame_id)
608 else {
609 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
610 };
611
612 let estimated_capacity = peptide_ids.len() * 4;
614 let mut tims_spectra: Vec<TimsSpectrum> = Vec::with_capacity(estimated_capacity);
615
616 for (peptide_id, frame_abundance) in peptide_ids.iter().zip(frame_abundances.iter()) {
618 let Some((ion_abundances, scan_occurrences, scan_abundances, charges, spectra)) = self
620 .precursor_frame_builder
621 .peptide_to_ions
622 .get(peptide_id)
623 else {
624 continue;
625 };
626
627 let total_events = *self.precursor_frame_builder.peptide_to_events.get(peptide_id).unwrap();
629
630 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
631 let all_scan_occurrence = &scan_occurrences[index];
632 let all_scan_abundance = &scan_abundances[index];
633 let spectrum = &spectra[index];
634 let charge_state = charges[index];
635
636 for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
637 let (is_transmitted, transmitted_indices) = match self.isotope_config.mode {
639 IsotopeTransmissionMode::None => {
640 let any = self.transmission_settings.any_transmitted(
642 frame_id as i32,
643 *scan as i32,
644 &spectrum.mz,
645 None,
646 );
647 (any, HashSet::new())
648 },
649 IsotopeTransmissionMode::PrecursorScaling | IsotopeTransmissionMode::PerFragment => {
650 let indices = self.transmission_settings.get_transmission_set(
651 frame_id as i32,
652 *scan as i32,
653 &spectrum.mz,
654 Some(self.isotope_config.min_probability),
655 );
656 (!indices.is_empty(), indices)
657 },
658 };
659
660 if !is_transmitted {
661 continue;
662 }
663
664 let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
666
667 let collision_energy = self.fragmentation_settings.get_collision_energy(frame_id as i32, *scan as i32);
669 let Some(collision_energy_quantized) = crate::sim::handle::resolve_fragment_ce_key(
672 fragment_ions, *peptide_id, charge_state, collision_energy,
673 ) else {
674 if crate::sim::handle::fragment_prefix_exists(fragment_ions, *peptide_id, charge_state) {
678 panic!(
679 "DIA fragment lookup miss: peptide {} charge {} applied CE {:.4} eV \
680 has predicted fragments, but none within 0.1 eV — the prediction \
681 set does not cover this instrument's collision energy",
682 *peptide_id, charge_state, collision_energy,
683 );
684 }
685 continue;
686 };
687 let (_, fragment_series_vec) = fragment_ions
688 .get(&(*peptide_id, charge_state, collision_energy_quantized))
689 .expect("resolve_fragment_ce_key returned a present key");
690
691 let scan_mobility = *self.precursor_frame_builder.scan_to_mobility.get(scan).unwrap() as f64;
693
694 let transmission_factor = if self.isotope_config.mode == IsotopeTransmissionMode::PrecursorScaling {
696 if let Some(comp_data) = self.fragment_ions_with_transmission.as_ref() {
697 if let Some(frag_data) = comp_data.get(&(*peptide_id, charge_state, collision_energy_quantized)) {
698 calculate_precursor_transmission_factor(
699 &frag_data.precursor_isotope_distribution,
700 &transmitted_indices,
701 )
702 } else {
703 1.0
704 }
705 } else {
706 1.0
707 }
708 } else {
709 1.0
710 };
711
712 let frag_data = self
715 .fragment_ions_with_transmission
716 .as_ref()
717 .and_then(|c| c.get(&(*peptide_id, charge_state, collision_energy_quantized)));
718
719 for (series_idx, fragment_ion_series) in fragment_series_vec.iter().enumerate() {
720 let final_spectrum = fragment_series_spectrum(
721 self.isotope_config.mode,
722 fragment_ion_series,
723 series_idx,
724 fraction_events,
725 transmission_factor,
726 frag_data,
727 &transmitted_indices,
728 self.isotope_config.max_isotopes,
729 );
730
731 let mz_spectrum = if mz_noise_fragment {
732 if uniform {
733 final_spectrum.add_mz_noise_uniform(fragment_ppm, right_drag_val)
734 } else {
735 final_spectrum.add_mz_noise_normal(fragment_ppm)
736 }
737 } else {
738 final_spectrum
739 };
740
741 let spectrum_len = mz_spectrum.mz.len();
742 tims_spectra.push(TimsSpectrum::new(
743 frame_id as i32,
744 *scan as i32,
745 rt,
746 scan_mobility,
747 ms_type.clone(),
748 IndexedMzSpectrum::from_mz_spectrum(
749 vec![0; spectrum_len],
750 mz_spectrum,
751 ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
752 ));
753 }
754
755 if self.isotope_config.has_precursor_survival() {
757 let mut rng = rand::thread_rng();
758 let survival_fraction = rng.gen_range(
759 self.isotope_config.precursor_survival_min
760 ..=self.isotope_config.precursor_survival_max
761 );
762
763 if survival_fraction > 0.0 {
764 let precursor_transmitted = self.transmission_settings.transmit_spectrum(
766 frame_id as i32,
767 *scan as i32,
768 spectrum.clone(),
769 Some(self.isotope_config.min_probability),
770 );
771
772 if !precursor_transmitted.mz.is_empty() {
773 let precursor_scaled = precursor_transmitted * (fraction_events as f64 * survival_fraction);
775
776 let precursor_mz_spectrum = if mz_noise_fragment {
777 if uniform {
778 precursor_scaled.add_mz_noise_uniform(fragment_ppm, right_drag_val)
779 } else {
780 precursor_scaled.add_mz_noise_normal(fragment_ppm)
781 }
782 } else {
783 precursor_scaled
784 };
785
786 let precursor_len = precursor_mz_spectrum.mz.len();
787 tims_spectra.push(TimsSpectrum::new(
788 frame_id as i32,
789 *scan as i32,
790 rt,
791 scan_mobility,
792 ms_type.clone(),
793 IndexedMzSpectrum::from_mz_spectrum(
794 vec![0; precursor_len],
795 precursor_mz_spectrum,
796 ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
797 ));
798 }
799 }
800 }
801 }
802 }
803 }
804
805 if tims_spectra.is_empty() {
806 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
807 }
808
809 let tims_frame = TimsFrame::from_tims_spectra(tims_spectra);
810 tims_frame.filter_ranged(
811 mz_min_val,
812 mz_max_val,
813 0,
814 1000,
815 0.0,
816 10.0,
817 intensity_min_val,
818 1e9,
819 0,
820 i32::MAX,
821 )
822 }
823
824 pub fn render_fragment_scan(
851 &self,
852 frame_id: u32,
853 window: &WindowTransmission,
854 nce: f64,
855 data_mode: DataMode,
856 ) -> RenderedEvent {
857 let rt = *self
858 .precursor_frame_builder
859 .frame_to_rt
860 .get(&frame_id)
861 .unwrap_or(&0.0) as f64;
862 let isolation = Some(IsolationWindow {
863 center_mz: window.center_mz,
864 width_mz: window.width_mz,
865 });
866 let make_scan = |peaks: MzSpectrum| RenderedEvent::Scan {
867 ms_level: 2,
868 retention_time_s: rt,
869 isolation,
870 spectrum: RenderedSpectrum {
871 mz: (*peaks.mz).clone(),
872 intensity: (*peaks.intensity).clone(),
873 coords: MzCoordSpace::Physical,
874 mode: data_mode,
875 detector_applied: false,
876 stage: IntensityStage::Transmitted,
877 },
878 };
879
880 let (Some(fragment_ions), Some((peptide_ids, frame_abundances))) = (
881 self.fragment_ions.as_ref(),
882 self.precursor_frame_builder.frame_to_abundances.get(&frame_id),
883 ) else {
884 return make_scan(MzSpectrum::from_collection(vec![]));
885 };
886
887 let mut specs: Vec<MzSpectrum> = Vec::new();
888 for (peptide_id, frame_abundance) in peptide_ids.iter().zip(frame_abundances.iter()) {
889 let Some((ion_abundances, _scan_occ, _scan_abu, charges, spectra)) =
890 self.precursor_frame_builder.peptide_to_ions.get(peptide_id)
891 else {
892 continue;
893 };
894 let total_events = *self
895 .precursor_frame_builder
896 .peptide_to_events
897 .get(peptide_id)
898 .unwrap();
899 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
900 let spectrum = &spectra[index];
901 let charge_state = charges[index];
902 if !window.any_transmitted(&spectrum.mz, None) {
904 continue;
905 }
906 let Some(ce_key) = crate::sim::handle::resolve_fragment_ce_key(
914 fragment_ions,
915 *peptide_id,
916 charge_state,
917 nce,
918 ) else {
919 continue;
920 };
921 let (_, fragment_series_vec) = fragment_ions
922 .get(&(*peptide_id, charge_state, ce_key))
923 .expect("resolve_fragment_ce_key returned a present key");
924
925 let fraction_events = frame_abundance * ion_abundance * total_events;
927 for (series_idx, fragment_ion_series) in fragment_series_vec.iter().enumerate() {
928 specs.push(fragment_series_spectrum(
929 IsotopeTransmissionMode::None,
930 fragment_ion_series,
931 series_idx,
932 fraction_events,
933 1.0,
934 None,
935 &HashSet::new(),
936 self.isotope_config.max_isotopes,
937 ));
938 }
939 }
940 }
941 make_scan(MzSpectrum::from_collection(specs))
942 }
943
944 pub fn build_fragment_frame_annotated(
945 &self,
946 frame_id: u32,
947 fragment_ions: &BTreeMap<
948 (u32, i8, i32),
949 (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>),
950 >,
951 mz_noise_fragment: bool,
952 uniform: bool,
953 fragment_ppm: f64,
954 mz_min: Option<f64>,
955 mz_max: Option<f64>,
956 intensity_min: Option<f64>,
957 right_drag: Option<bool>,
958 ) -> TimsFrameAnnotated {
959 let ms_type = if self.precursor_frame_builder.precursor_frame_id_set.contains(&frame_id) {
961 MsType::Unknown
962 } else {
963 MsType::FragmentDia
964 };
965
966 let rt = *self.precursor_frame_builder.frame_to_rt.get(&frame_id).unwrap() as f64;
967 let right_drag_val = right_drag.unwrap_or(false);
968 let mz_min_val = mz_min.unwrap_or(100.0);
969 let mz_max_val = mz_max.unwrap_or(1700.0);
970 let intensity_min_val = intensity_min.unwrap_or(1.0);
971
972 let Some((peptide_ids, frame_abundances)) = self
974 .precursor_frame_builder
975 .frame_to_abundances
976 .get(&frame_id)
977 else {
978 return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
979 };
980
981 let estimated_capacity = peptide_ids.len() * 4;
983 let mut tims_spectra: Vec<TimsSpectrumAnnotated> = Vec::with_capacity(estimated_capacity);
984
985 for (peptide_id, frame_abundance) in peptide_ids.iter().zip(frame_abundances.iter()) {
986 let Some((ion_abundances, scan_occurrences, scan_abundances, charges, _)) = self
988 .precursor_frame_builder
989 .peptide_to_ions
990 .get(peptide_id)
991 else {
992 continue;
993 };
994
995 let total_events = *self.precursor_frame_builder.peptide_to_events.get(peptide_id).unwrap();
997 let peptide = self.precursor_frame_builder.peptides.get(peptide_id).unwrap();
998
999 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
1000 let all_scan_occurrence = &scan_occurrences[index];
1001 let all_scan_abundance = &scan_abundances[index];
1002 let charge_state = charges[index];
1003
1004 let ion = PeptideIon::new(
1005 peptide.sequence.sequence.clone(),
1006 charge_state as i32,
1007 *ion_abundance as f64,
1008 Some(*peptide_id as i32),
1009 );
1010 let spectrum = ion.calculate_isotopic_spectrum_annotated(1e-3, 1e-8, 200, 1e-4);
1012
1013 for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
1014 if !self.transmission_settings.any_transmitted(
1015 frame_id as i32,
1016 *scan as i32,
1017 &spectrum.mz,
1018 None,
1019 ) {
1020 continue;
1021 }
1022
1023 let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
1024
1025 let collision_energy = self.fragmentation_settings.get_collision_energy(frame_id as i32, *scan as i32);
1026 let Some(collision_energy_quantized) = crate::sim::handle::resolve_fragment_ce_key(
1028 fragment_ions, *peptide_id, charge_state, collision_energy,
1029 ) else {
1030 if crate::sim::handle::fragment_prefix_exists(fragment_ions, *peptide_id, charge_state) {
1031 panic!(
1032 "DIA (annotated) fragment lookup miss: peptide {} charge {} applied CE \
1033 {:.4} eV has predicted fragments, but none within 0.1 eV — the \
1034 prediction set does not cover this instrument's collision energy",
1035 *peptide_id, charge_state, collision_energy,
1036 );
1037 }
1038 continue;
1039 };
1040 let (_, fragment_series_vec) = fragment_ions
1041 .get(&(*peptide_id, charge_state, collision_energy_quantized))
1042 .expect("resolve_fragment_ce_key returned a present key");
1043
1044 let scan_mobility = *self.precursor_frame_builder.scan_to_mobility.get(scan).unwrap() as f64;
1046
1047 for fragment_ion_series in fragment_series_vec.iter() {
1048 let scaled_spec = fragment_ion_series.clone() * fraction_events as f64;
1049
1050 let mz_spectrum = if mz_noise_fragment {
1051 if uniform {
1052 scaled_spec.add_mz_noise_uniform(fragment_ppm, right_drag_val)
1053 } else {
1054 scaled_spec.add_mz_noise_normal(fragment_ppm)
1055 }
1056 } else {
1057 scaled_spec
1058 };
1059
1060 let spectrum_len = mz_spectrum.mz.len();
1061 tims_spectra.push(TimsSpectrumAnnotated::new(
1062 frame_id as i32,
1063 *scan,
1064 rt,
1065 scan_mobility,
1066 ms_type.clone(),
1067 vec![0; spectrum_len],
1068 mz_spectrum,
1069 ));
1070 }
1071
1072 if self.isotope_config.has_precursor_survival() {
1074 let mut rng = rand::thread_rng();
1075 let survival_fraction = rng.gen_range(
1076 self.isotope_config.precursor_survival_min
1077 ..=self.isotope_config.precursor_survival_max
1078 );
1079
1080 if survival_fraction > 0.0 {
1081 let precursor_mz_spectrum = MzSpectrum::new(spectrum.mz.clone(), spectrum.intensity.clone());
1083
1084 let precursor_transmitted = self.transmission_settings.transmit_spectrum(
1086 frame_id as i32,
1087 *scan as i32,
1088 precursor_mz_spectrum,
1089 Some(self.isotope_config.min_probability),
1090 );
1091
1092 if !precursor_transmitted.mz.is_empty() {
1093 let precursor_scaled = precursor_transmitted * (fraction_events as f64 * survival_fraction);
1095
1096 let precursor_final = if mz_noise_fragment {
1097 if uniform {
1098 precursor_scaled.add_mz_noise_uniform(fragment_ppm, right_drag_val)
1099 } else {
1100 precursor_scaled.add_mz_noise_normal(fragment_ppm)
1101 }
1102 } else {
1103 precursor_scaled
1104 };
1105
1106 let annotations: Vec<PeakAnnotation> = precursor_final.mz.iter()
1108 .map(|_| PeakAnnotation { contributions: vec![] })
1109 .collect();
1110 let precursor_annotated = MzSpectrumAnnotated::new(
1111 precursor_final.mz.to_vec(),
1112 precursor_final.intensity.to_vec(),
1113 annotations,
1114 );
1115
1116 let precursor_len = precursor_annotated.mz.len();
1117 tims_spectra.push(TimsSpectrumAnnotated::new(
1118 frame_id as i32,
1119 *scan,
1120 rt,
1121 scan_mobility,
1122 ms_type.clone(),
1123 vec![0; precursor_len],
1124 precursor_annotated,
1125 ));
1126 }
1127 }
1128 }
1129 }
1130 }
1131 }
1132
1133 if tims_spectra.is_empty() {
1134 return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
1135 }
1136
1137 TimsFrameAnnotated::from_tims_spectra_annotated(tims_spectra).filter_ranged(
1138 mz_min_val, mz_max_val, 0.0, 10.0, 0, 1000, intensity_min_val, 1e9,
1139 )
1140 }
1141
1142 pub fn get_ion_transmission_matrix(
1143 &self,
1144 peptide_id: u32,
1145 charge: i8,
1146 include_precursor_frames: bool,
1147 ) -> Vec<Vec<f32>> {
1148 let maybe_peptide_sim = self.precursor_frame_builder.peptides.get(&peptide_id);
1149
1150 let mut frame_ids = match maybe_peptide_sim {
1151 Some(maybe_peptide_sim) => maybe_peptide_sim.frame_distribution.occurrence.clone(),
1152 _ => vec![],
1153 };
1154
1155 if !include_precursor_frames {
1156 frame_ids = frame_ids
1157 .iter()
1158 .filter(|frame_id| {
1159 !self
1160 .precursor_frame_builder
1161 .precursor_frame_id_set
1162 .contains(frame_id)
1163 })
1164 .cloned()
1165 .collect();
1166 }
1167
1168 let ion = self
1169 .precursor_frame_builder
1170 .ions
1171 .get(&peptide_id)
1172 .unwrap()
1173 .iter()
1174 .find(|ion| ion.charge == charge)
1175 .unwrap();
1176 let spectrum = ion.simulated_spectrum.clone();
1177 let scan_distribution = &ion.scan_distribution;
1178
1179 let mut transmission_matrix =
1180 vec![vec![0.0; frame_ids.len()]; scan_distribution.occurrence.len()];
1181
1182 for (frame_index, frame) in frame_ids.iter().enumerate() {
1183 for (scan_index, scan) in scan_distribution.occurrence.iter().enumerate() {
1184 if self.transmission_settings.all_transmitted(
1185 *frame as i32,
1186 *scan as i32,
1187 &spectrum.mz,
1188 None,
1189 ) {
1190 transmission_matrix[scan_index][frame_index] = 1.0;
1191 } else if self.transmission_settings.any_transmitted(
1192 *frame as i32,
1193 *scan as i32,
1194 &spectrum.mz,
1195 None,
1196 ) {
1197 let transmitted_spectrum = self.transmission_settings.transmit_spectrum(
1198 *frame as i32,
1199 *scan as i32,
1200 spectrum.clone(),
1201 None,
1202 );
1203 let percentage_transmitted = transmitted_spectrum.intensity.iter().sum::<f64>()
1204 / spectrum.intensity.iter().sum::<f64>();
1205 transmission_matrix[scan_index][frame_index] = percentage_transmitted as f32;
1206 }
1207 }
1208 }
1209
1210 transmission_matrix
1211 }
1212
1213 pub fn count_number_transmissions(&self, peptide_id: u32, charge: i8) -> (usize, usize) {
1214 let frame_ids: Vec<_> = self
1215 .precursor_frame_builder
1216 .peptides
1217 .get(&peptide_id)
1218 .unwrap()
1219 .frame_distribution
1220 .occurrence
1221 .clone()
1222 .iter()
1223 .filter(|frame_id| {
1224 !self
1225 .precursor_frame_builder
1226 .precursor_frame_id_set
1227 .contains(frame_id)
1228 })
1229 .cloned()
1230 .collect();
1231 let ion = self
1232 .precursor_frame_builder
1233 .ions
1234 .get(&peptide_id)
1235 .unwrap()
1236 .iter()
1237 .find(|ion| ion.charge == charge)
1238 .unwrap();
1239 let spectrum = ion.simulated_spectrum.clone();
1240 let scan_distribution = &ion.scan_distribution;
1241
1242 let mut frame_count = 0;
1243 let mut scan_count = 0;
1244
1245 for frame in frame_ids.iter() {
1246 let mut frame_transmitted = false;
1247 for scan in scan_distribution.occurrence.iter() {
1248 if self.transmission_settings.any_transmitted(
1249 *frame as i32,
1250 *scan as i32,
1251 &spectrum.mz,
1252 None,
1253 ) {
1254 frame_transmitted = true;
1255 scan_count += 1;
1256 }
1257 }
1258 if frame_transmitted {
1259 frame_count += 1;
1260 }
1261 }
1262
1263 (frame_count, scan_count)
1264 }
1265
1266 pub fn count_number_transmissions_parallel(
1267 &self,
1268 peptide_ids: Vec<u32>,
1269 charge: Vec<i8>,
1270 num_threads: usize,
1271 ) -> Vec<(usize, usize)> {
1272 let thread_pool = ThreadPoolBuilder::new()
1273 .num_threads(num_threads)
1274 .build()
1275 .unwrap();
1276 let result: Vec<(usize, usize)> = thread_pool.install(|| {
1277 peptide_ids
1278 .par_iter()
1279 .zip(charge.par_iter())
1280 .map(|(peptide_id, charge)| self.count_number_transmissions(*peptide_id, *charge))
1281 .collect()
1282 });
1283
1284 result
1285 }
1286}
1287
1288impl TimsTofCollisionEnergy for TimsTofSyntheticsFrameBuilderDIA {
1289 fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> f64 {
1290 self.fragmentation_settings
1291 .get_collision_energy(frame_id, scan_id)
1292 }
1293}
1294
1295#[cfg(test)]
1296mod p6a_fragment_kernel_tests {
1297 use super::*;
1298 use std::collections::HashSet;
1299
1300 #[test]
1305 fn fragment_series_spectrum_scaling_branches() {
1306 let series = MzSpectrum::new(vec![200.0, 300.0], vec![10.0, 20.0]);
1307 let empty: HashSet<usize> = HashSet::new();
1308
1309 let none = fragment_series_spectrum(
1311 IsotopeTransmissionMode::None, &series, 0, 2.0, 99.0 , None, &empty, 10,
1312 );
1313 assert_eq!(*none.mz, vec![200.0, 300.0]);
1314 assert_eq!(*none.intensity, vec![20.0, 40.0]);
1315
1316 let ps = fragment_series_spectrum(
1318 IsotopeTransmissionMode::PrecursorScaling, &series, 0, 2.0, 0.5, None, &empty, 10,
1319 );
1320 assert_eq!(*ps.intensity, vec![10.0, 20.0]); let pf = fragment_series_spectrum(
1324 IsotopeTransmissionMode::PerFragment, &series, 0, 2.0, 0.5, None, &empty, 10,
1325 );
1326 assert_eq!(*pf.intensity, vec![20.0, 40.0]);
1327 }
1328
1329 use crate::sim::precursor::TimsTofSyntheticsPrecursorFrameBuilder;
1330
1331 fn one_fragment_frame_dia() -> TimsTofSyntheticsFrameBuilderDIA {
1336 let mut frame_to_abundances = BTreeMap::new();
1337 frame_to_abundances.insert(2u32, (vec![10u32], vec![0.8f32]));
1338 let mut peptide_to_ions = BTreeMap::new();
1339 peptide_to_ions.insert(
1340 10u32,
1341 (
1342 vec![1.0f32],
1343 vec![vec![100u32, 101u32]],
1344 vec![vec![0.3f32, 0.2f32]], vec![2i8],
1346 vec![MzSpectrum::new(vec![600.0], vec![1.0])], ),
1348 );
1349 let mut peptide_to_events = BTreeMap::new();
1350 peptide_to_events.insert(10u32, 1000.0f32);
1351 let mut frame_to_rt = BTreeMap::new();
1352 frame_to_rt.insert(2u32, 30.0f32);
1353
1354 let precursor = TimsTofSyntheticsPrecursorFrameBuilder {
1355 ions: BTreeMap::new(),
1356 peptides: BTreeMap::new(),
1357 scans: Vec::new(),
1358 frames: Vec::new(),
1359 precursor_frame_id_set: HashSet::new(),
1360 frame_to_abundances,
1361 peptide_to_ions,
1362 frame_to_rt,
1363 scan_to_mobility: BTreeMap::new(),
1364 peptide_to_events,
1365 ion_id_to_peptide_charge: BTreeMap::new(),
1366 };
1367
1368 let mut fragment_ions = BTreeMap::new();
1369 fragment_ions.insert(
1370 (10u32, 2i8, 270i32), (
1372 PeptideProductIonSeriesCollection::new(vec![]),
1373 vec![MzSpectrum::new(vec![200.0], vec![1.0])],
1374 ),
1375 );
1376
1377 TimsTofSyntheticsFrameBuilderDIA {
1378 path: String::new(),
1379 precursor_frame_builder: precursor,
1380 transmission_settings: TimsTransmissionDIA::new(
1381 vec![], vec![], vec![], vec![], vec![], vec![], vec![], None,
1382 ),
1383 fragmentation_settings: TimsTofCollisionEnergyDIA::new(
1384 vec![], vec![], vec![], vec![], vec![], vec![],
1385 ),
1386 fragment_ions: Some(fragment_ions),
1387 fragment_ions_annotated: None,
1388 isotope_config: IsotopeTransmissionConfig::default(),
1389 fragment_ions_with_transmission: None,
1390 capabilities: InstrumentCapabilities::astral(),
1391 }
1392 }
1393
1394 #[test]
1395 fn astral_ms2_render_full_marginal_and_window_gating() {
1396 let b = one_fragment_frame_dia();
1397
1398 let win = WindowTransmission::new(600.0, 50.0, 15.0);
1402 let RenderedEvent::Scan { ms_level, isolation, spectrum, .. } =
1403 b.render_fragment_scan(2, &win, 27.0, DataMode::Centroid)
1404 else {
1405 panic!("MS2 render must be a Scan");
1406 };
1407 assert_eq!(ms_level, 2);
1408 let iso = isolation.expect("MS2 scan carries an isolation window");
1409 assert!((iso.center_mz - 600.0).abs() < 1e-9 && (iso.width_mz - 50.0).abs() < 1e-9);
1410 assert_eq!(spectrum.mz, vec![200.0]);
1411 assert!((spectrum.intensity[0] - 800.0).abs() < 1e-2, "full marginal, got {}", spectrum.intensity[0]);
1412
1413 let off = WindowTransmission::new(900.0, 50.0, 15.0);
1416 let RenderedEvent::Scan { spectrum: empty, .. } =
1417 b.render_fragment_scan(2, &off, 27.0, DataMode::Centroid)
1418 else {
1419 panic!("must be a Scan");
1420 };
1421 assert!(empty.mz.is_empty(), "no precursor transmitted -> empty MS2 scan");
1422
1423 let RenderedEvent::Scan { spectrum: s2, .. } =
1425 b.render_fragment_scan(2, &win, 27.0, DataMode::Centroid)
1426 else { panic!() };
1427 assert_eq!(s2.mz, spectrum.mz);
1428 assert_eq!(s2.intensity, spectrum.intensity);
1429 }
1430}