rustdf/sim/
dia.rs

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};
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::handle::{FragmentIonsWithComplementary, TimsTofSyntheticsDataHandle};
24use crate::sim::precursor::TimsTofSyntheticsPrecursorFrameBuilder;
25
26pub struct TimsTofSyntheticsFrameBuilderDIA {
27    pub path: String,
28    pub precursor_frame_builder: TimsTofSyntheticsPrecursorFrameBuilder,
29    pub transmission_settings: TimsTransmissionDIA,
30    pub fragmentation_settings: TimsTofCollisionEnergyDIA,
31    pub fragment_ions:
32        Option<BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrum>)>>,
33    pub fragment_ions_annotated: Option<
34        BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>)>,
35    >,
36    /// Configuration for quad-selection dependent isotope transmission
37    pub isotope_config: IsotopeTransmissionConfig,
38    /// Fragment ions with complementary data for transmission-dependent calculations
39    pub fragment_ions_with_transmission:
40        Option<BTreeMap<(u32, i8, i32), FragmentIonsWithComplementary>>,
41}
42
43impl TimsTofSyntheticsFrameBuilderDIA {
44    pub fn new(path: &Path, with_annotations: bool, num_threads: usize) -> rusqlite::Result<Self> {
45        Self::new_with_config(path, with_annotations, num_threads, IsotopeTransmissionConfig::default())
46    }
47
48    pub fn new_with_config(
49        path: &Path,
50        with_annotations: bool,
51        num_threads: usize,
52        isotope_config: IsotopeTransmissionConfig,
53    ) -> rusqlite::Result<Self> {
54        let synthetics = TimsTofSyntheticsPrecursorFrameBuilder::new(path)?;
55        let handle = TimsTofSyntheticsDataHandle::new(path)?;
56
57        let fragment_ions = handle.read_fragment_ions()?;
58
59        // get collision energy settings per window group
60        let fragmentation_settings = handle.get_collision_energy_dia();
61        // get ion transmission settings per window group
62        let transmission_settings = handle.get_transmission_dia();
63
64        // Build transmission data if isotope transmission is enabled
65        let fragment_ions_with_transmission = if isotope_config.is_enabled() {
66            Some(TimsTofSyntheticsDataHandle::build_fragment_ions_with_transmission_data(
67                &synthetics.peptides,
68                &fragment_ions,
69                num_threads,
70            ))
71        } else {
72            None
73        };
74
75        match with_annotations {
76            true => {
77                let fragment_ions_annotated =
78                    Some(TimsTofSyntheticsDataHandle::build_fragment_ions_annotated(
79                        &synthetics.peptides,
80                        &fragment_ions,
81                        num_threads,
82                    ));
83                Ok(Self {
84                    path: path.to_str().unwrap().to_string(),
85                    precursor_frame_builder: synthetics,
86                    transmission_settings,
87                    fragmentation_settings,
88                    fragment_ions: None,
89                    fragment_ions_annotated,
90                    isotope_config,
91                    fragment_ions_with_transmission,
92                })
93            }
94
95            false => {
96                let fragment_ions = Some(TimsTofSyntheticsDataHandle::build_fragment_ions(
97                    &synthetics.peptides,
98                    &fragment_ions,
99                    num_threads,
100                ));
101                Ok(Self {
102                    path: path.to_str().unwrap().to_string(),
103                    precursor_frame_builder: synthetics,
104                    transmission_settings,
105                    fragmentation_settings,
106                    fragment_ions,
107                    fragment_ions_annotated: None,
108                    isotope_config,
109                    fragment_ions_with_transmission,
110                })
111            }
112        }
113    }
114
115    /// Build a frame for DIA synthetic experiment
116    ///
117    /// # Arguments
118    ///
119    /// * `frame_id` - The frame id
120    /// * `fragmentation` - A boolean indicating if fragmentation is enabled, if false, the frame has same mz distribution as the precursor frame but will be quadrupole filtered
121    ///
122    /// # Returns
123    ///
124    /// A TimsFrame
125    ///
126    pub fn build_frame(
127        &self,
128        frame_id: u32,
129        fragmentation: bool,
130        mz_noise_precursor: bool,
131        uniform: bool,
132        precursor_noise_ppm: f64,
133        mz_noise_fragment: bool,
134        fragment_noise_ppm: f64,
135        right_drag: bool,
136    ) -> TimsFrame {
137        // determine if the frame is a precursor frame
138        match self
139            .precursor_frame_builder
140            .precursor_frame_id_set
141            .contains(&frame_id)
142        {
143            true => self.build_ms1_frame(
144                frame_id,
145                mz_noise_precursor,
146                uniform,
147                precursor_noise_ppm,
148                right_drag,
149            ),
150            false => self.build_ms2_frame(
151                frame_id,
152                fragmentation,
153                mz_noise_fragment,
154                uniform,
155                fragment_noise_ppm,
156                right_drag,
157            ),
158        }
159    }
160
161    pub fn build_frame_annotated(
162        &self,
163        frame_id: u32,
164        fragmentation: bool,
165        mz_noise_precursor: bool,
166        uniform: bool,
167        precursor_noise_ppm: f64,
168        mz_noise_fragment: bool,
169        fragment_noise_ppm: f64,
170        right_drag: bool,
171    ) -> TimsFrameAnnotated {
172        match self
173            .precursor_frame_builder
174            .precursor_frame_id_set
175            .contains(&frame_id)
176        {
177            true => self.build_ms1_frame_annotated(
178                frame_id,
179                mz_noise_precursor,
180                uniform,
181                precursor_noise_ppm,
182                right_drag,
183            ),
184            false => self.build_ms2_frame_annotated(
185                frame_id,
186                fragmentation,
187                mz_noise_fragment,
188                uniform,
189                fragment_noise_ppm,
190                right_drag,
191            ),
192        }
193    }
194
195    pub fn get_fragment_ion_ids(&self, precursor_frame_ids: Vec<u32>) -> Vec<u32> {
196        let mut peptide_ids: HashSet<u32> = HashSet::new();
197        // get all peptide ids for the precursor frame ids
198        for frame_id in precursor_frame_ids {
199            for (peptide_id, peptide) in self.precursor_frame_builder.peptides.iter() {
200                if peptide.frame_start <= frame_id && peptide.frame_end >= frame_id {
201                    peptide_ids.insert(*peptide_id);
202                }
203            }
204        }
205        // get all ion ids for the peptide ids
206        let mut result: Vec<u32> = Vec::new();
207        for item in peptide_ids {
208            let ions = self.precursor_frame_builder.ions.get(&item).unwrap();
209            for ion in ions.iter() {
210                result.push(ion.ion_id);
211            }
212        }
213        result
214    }
215
216    pub fn build_frames(
217        &self,
218        frame_ids: Vec<u32>,
219        fragmentation: bool,
220        mz_noise_precursor: bool,
221        uniform: bool,
222        precursor_noise_ppm: f64,
223        mz_noise_fragment: bool,
224        fragment_noise_ppm: f64,
225        right_drag: bool,
226        num_threads: usize,
227    ) -> Vec<TimsFrame> {
228        // Use global thread pool with custom parallelism instead of creating new pool each call
229        let pool = rayon::ThreadPoolBuilder::new()
230            .num_threads(num_threads)
231            .build()
232            .unwrap();
233
234        pool.install(|| {
235            // Use indexed parallel iteration to maintain order, avoiding post-sort
236            let mut tims_frames: Vec<TimsFrame> = Vec::with_capacity(frame_ids.len());
237            // Safety: we're about to fill all elements
238            unsafe { tims_frames.set_len(frame_ids.len()); }
239
240            frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
241                let frame = self.build_frame(
242                    *frame_id,
243                    fragmentation,
244                    mz_noise_precursor,
245                    uniform,
246                    precursor_noise_ppm,
247                    mz_noise_fragment,
248                    fragment_noise_ppm,
249                    right_drag,
250                );
251                // Safety: each index is unique due to enumerate
252                unsafe {
253                    let ptr = tims_frames.as_ptr() as *mut TimsFrame;
254                    std::ptr::write(ptr.add(idx), frame);
255                }
256            });
257
258            tims_frames
259        })
260    }
261
262    pub fn build_frames_annotated(
263        &self,
264        frame_ids: Vec<u32>,
265        fragmentation: bool,
266        mz_noise_precursor: bool,
267        uniform: bool,
268        precursor_noise_ppm: f64,
269        mz_noise_fragment: bool,
270        fragment_noise_ppm: f64,
271        right_drag: bool,
272        num_threads: usize,
273    ) -> Vec<TimsFrameAnnotated> {
274        // Use thread pool with custom parallelism
275        let pool = rayon::ThreadPoolBuilder::new()
276            .num_threads(num_threads)
277            .build()
278            .unwrap();
279
280        pool.install(|| {
281            // Use indexed parallel iteration to maintain order, avoiding post-sort
282            let mut tims_frames: Vec<TimsFrameAnnotated> = Vec::with_capacity(frame_ids.len());
283            unsafe { tims_frames.set_len(frame_ids.len()); }
284
285            frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
286                let frame = self.build_frame_annotated(
287                    *frame_id,
288                    fragmentation,
289                    mz_noise_precursor,
290                    uniform,
291                    precursor_noise_ppm,
292                    mz_noise_fragment,
293                    fragment_noise_ppm,
294                    right_drag,
295                );
296                unsafe {
297                    let ptr = tims_frames.as_ptr() as *mut TimsFrameAnnotated;
298                    std::ptr::write(ptr.add(idx), frame);
299                }
300            });
301
302            tims_frames
303        })
304    }
305
306    fn build_ms1_frame(
307        &self,
308        frame_id: u32,
309        mz_noise_precursor: bool,
310        uniform: bool,
311        precursor_ppm: f64,
312        right_drag: bool,
313    ) -> TimsFrame {
314        let mut tims_frame = self.precursor_frame_builder.build_precursor_frame(
315            frame_id,
316            mz_noise_precursor,
317            uniform,
318            precursor_ppm,
319            right_drag,
320        );
321        let intensities_rounded = tims_frame
322            .ims_frame
323            .intensity
324            .iter()
325            .map(|x| x.round())
326            .collect::<Vec<_>>();
327        tims_frame.ims_frame.intensity = Arc::new(intensities_rounded);
328        tims_frame
329    }
330
331    fn build_ms1_frame_annotated(
332        &self,
333        frame_id: u32,
334        mz_noise_precursor: bool,
335        uniform: bool,
336        precursor_ppm: f64,
337        right_drag: bool,
338    ) -> TimsFrameAnnotated {
339        let mut tims_frame = self
340            .precursor_frame_builder
341            .build_precursor_frame_annotated(
342                frame_id,
343                mz_noise_precursor,
344                uniform,
345                precursor_ppm,
346                right_drag,
347            );
348        let intensities_rounded = tims_frame
349            .intensity
350            .iter()
351            .map(|x| x.round())
352            .collect::<Vec<_>>();
353        tims_frame.intensity = intensities_rounded;
354        tims_frame
355    }
356
357    fn build_ms2_frame(
358        &self,
359        frame_id: u32,
360        fragmentation: bool,
361        mz_noise_fragment: bool,
362        uniform: bool,
363        fragment_ppm: f64,
364        right_drag: bool,
365    ) -> TimsFrame {
366        match fragmentation {
367            false => {
368                let mut frame = self.transmission_settings.transmit_tims_frame(
369                    &self.build_ms1_frame(
370                        frame_id,
371                        mz_noise_fragment,
372                        uniform,
373                        fragment_ppm,
374                        right_drag,
375                    ),
376                    None,
377                );
378                let intensities_rounded = frame
379                    .ims_frame
380                    .intensity
381                    .iter()
382                    .map(|x| x.round())
383                    .collect::<Vec<_>>();
384                frame.ims_frame.intensity = Arc::new(intensities_rounded);
385                frame.ms_type = MsType::FragmentDia;
386                frame
387            }
388            true => {
389                let mut frame = self.build_fragment_frame(
390                    frame_id,
391                    &self.fragment_ions.as_ref().unwrap(),
392                    mz_noise_fragment,
393                    uniform,
394                    fragment_ppm,
395                    None,
396                    None,
397                    None,
398                    Some(right_drag),
399                );
400                let intensities_rounded = frame
401                    .ims_frame
402                    .intensity
403                    .iter()
404                    .map(|x| x.round())
405                    .collect::<Vec<_>>();
406                frame.ims_frame.intensity = Arc::new(intensities_rounded);
407                frame
408            }
409        }
410    }
411
412    fn build_ms2_frame_annotated(
413        &self,
414        frame_id: u32,
415        fragmentation: bool,
416        mz_noise_fragment: bool,
417        uniform: bool,
418        fragment_ppm: f64,
419        right_drag: bool,
420    ) -> TimsFrameAnnotated {
421        match fragmentation {
422            false => {
423                let mut frame = self.transmission_settings.transmit_tims_frame_annotated(
424                    &self.build_ms1_frame_annotated(
425                        frame_id,
426                        mz_noise_fragment,
427                        uniform,
428                        fragment_ppm,
429                        right_drag,
430                    ),
431                    None,
432                );
433                let intensities_rounded = frame
434                    .intensity
435                    .iter()
436                    .map(|x| x.round())
437                    .collect::<Vec<_>>();
438                frame.intensity = intensities_rounded;
439                frame.ms_type = MsType::FragmentDia;
440                frame
441            }
442            true => {
443                let mut frame = self.build_fragment_frame_annotated(
444                    frame_id,
445                    &self.fragment_ions_annotated.as_ref().unwrap(),
446                    mz_noise_fragment,
447                    uniform,
448                    fragment_ppm,
449                    None,
450                    None,
451                    None,
452                    Some(right_drag),
453                );
454                let intensities_rounded = frame
455                    .intensity
456                    .iter()
457                    .map(|x| x.round())
458                    .collect::<Vec<_>>();
459                frame.intensity = intensities_rounded;
460                frame
461            }
462        }
463    }
464
465    /// Build a fragment frame
466    ///
467    /// # Arguments
468    ///
469    /// * `frame_id` - The frame id
470    /// * `mz_min` - The minimum m/z value in fragment spectrum
471    /// * `mz_max` - The maximum m/z value in fragment spectrum
472    /// * `intensity_min` - The minimum intensity value in fragment spectrum
473    ///
474    /// # Returns
475    ///
476    /// A TimsFrame
477    ///
478    fn build_fragment_frame(
479        &self,
480        frame_id: u32,
481        fragment_ions: &BTreeMap<
482            (u32, i8, i32),
483            (PeptideProductIonSeriesCollection, Vec<MzSpectrum>),
484        >,
485        mz_noise_fragment: bool,
486        uniform: bool,
487        fragment_ppm: f64,
488        mz_min: Option<f64>,
489        mz_max: Option<f64>,
490        intensity_min: Option<f64>,
491        right_drag: Option<bool>,
492    ) -> TimsFrame {
493        // Cache frame-level lookups once
494        let ms_type = if self.precursor_frame_builder.precursor_frame_id_set.contains(&frame_id) {
495            MsType::Unknown
496        } else {
497            MsType::FragmentDia
498        };
499
500        let rt = *self.precursor_frame_builder.frame_to_rt.get(&frame_id).unwrap() as f64;
501        let right_drag_val = right_drag.unwrap_or(false);
502        let mz_min_val = mz_min.unwrap_or(100.0);
503        let mz_max_val = mz_max.unwrap_or(1700.0);
504        let intensity_min_val = intensity_min.unwrap_or(1.0);
505
506        // Use single lookup instead of contains_key + get
507        let Some((peptide_ids, frame_abundances)) = self
508            .precursor_frame_builder
509            .frame_to_abundances
510            .get(&frame_id)
511        else {
512            return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
513        };
514
515        // Preallocate with estimated capacity
516        let estimated_capacity = peptide_ids.len() * 4;
517        let mut tims_spectra: Vec<TimsSpectrum> = Vec::with_capacity(estimated_capacity);
518
519        // Go over all peptides in the frame with their respective abundances
520        for (peptide_id, frame_abundance) in peptide_ids.iter().zip(frame_abundances.iter()) {
521            // Single lookup instead of contains_key + get
522            let Some((ion_abundances, scan_occurrences, scan_abundances, charges, spectra)) = self
523                .precursor_frame_builder
524                .peptide_to_ions
525                .get(peptide_id)
526            else {
527                continue;
528            };
529
530            // Cache peptide-level lookup
531            let total_events = *self.precursor_frame_builder.peptide_to_events.get(peptide_id).unwrap();
532
533            for (index, ion_abundance) in ion_abundances.iter().enumerate() {
534                let all_scan_occurrence = &scan_occurrences[index];
535                let all_scan_abundance = &scan_abundances[index];
536                let spectrum = &spectra[index];
537                let charge_state = charges[index];
538
539                for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
540                    // Get transmitted isotope indices based on config mode
541                    let (is_transmitted, transmitted_indices) = match self.isotope_config.mode {
542                        IsotopeTransmissionMode::None => {
543                            // Standard check without indices
544                            let any = self.transmission_settings.any_transmitted(
545                                frame_id as i32,
546                                *scan as i32,
547                                &spectrum.mz,
548                                None,
549                            );
550                            (any, HashSet::new())
551                        },
552                        IsotopeTransmissionMode::PrecursorScaling | IsotopeTransmissionMode::PerFragment => {
553                            let indices = self.transmission_settings.get_transmission_set(
554                                frame_id as i32,
555                                *scan as i32,
556                                &spectrum.mz,
557                                Some(self.isotope_config.min_probability),
558                            );
559                            (!indices.is_empty(), indices)
560                        },
561                    };
562
563                    if !is_transmitted {
564                        continue;
565                    }
566
567                    // Calculate abundance factor (total_events cached above)
568                    let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
569
570                    // Get collision energy for the ion
571                    let collision_energy = self.fragmentation_settings.get_collision_energy(frame_id as i32, *scan as i32);
572                    let collision_energy_quantized = (collision_energy * 1e1).round() as i32;
573
574                    // Single lookup with let-else
575                    let Some((_, fragment_series_vec)) = fragment_ions.get(&(*peptide_id, charge_state, collision_energy_quantized)) else {
576                        continue;
577                    };
578
579                    // Cache scan mobility lookup
580                    let scan_mobility = *self.precursor_frame_builder.scan_to_mobility.get(scan).unwrap() as f64;
581
582                    // Calculate transmission factor for PrecursorScaling mode
583                    let transmission_factor = if self.isotope_config.mode == IsotopeTransmissionMode::PrecursorScaling {
584                        if let Some(comp_data) = self.fragment_ions_with_transmission.as_ref() {
585                            if let Some(frag_data) = comp_data.get(&(*peptide_id, charge_state, collision_energy_quantized)) {
586                                calculate_precursor_transmission_factor(
587                                    &frag_data.precursor_isotope_distribution,
588                                    &transmitted_indices,
589                                )
590                            } else {
591                                1.0
592                            }
593                        } else {
594                            1.0
595                        }
596                    } else {
597                        1.0
598                    };
599
600                    for (series_idx, fragment_ion_series) in fragment_series_vec.iter().enumerate() {
601                        let final_spectrum = match self.isotope_config.mode {
602                            IsotopeTransmissionMode::None => {
603                                // Standard spectrum scaling
604                                fragment_ion_series.clone() * fraction_events as f64
605                            },
606                            IsotopeTransmissionMode::PrecursorScaling => {
607                                // Apply precursor-based transmission factor
608                                fragment_ion_series.clone() * (fraction_events as f64 * transmission_factor)
609                            },
610                            IsotopeTransmissionMode::PerFragment => {
611                                // Per-fragment transmission-dependent calculation
612                                if let Some(comp_data) = self.fragment_ions_with_transmission.as_ref() {
613                                    if let Some(frag_data) = comp_data.get(&(*peptide_id, charge_state, collision_energy_quantized)) {
614                                        if series_idx < frag_data.per_fragment_data.len() {
615                                            // Aggregate adjusted spectra from all fragment ions in this series
616                                            let series_data = &frag_data.per_fragment_data[series_idx];
617                                            let mut aggregated_mz: Vec<f64> = Vec::new();
618                                            let mut aggregated_intensity: Vec<f64> = Vec::new();
619
620                                            for frag_ion_data in series_data {
621                                                // Apply transmission-dependent calculation for this fragment
622                                                let adjusted_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
623                                                    &frag_ion_data.fragment_distribution,
624                                                    &frag_ion_data.complementary_distribution,
625                                                    &transmitted_indices,
626                                                    self.isotope_config.max_isotopes,
627                                                );
628
629                                                // Scale by predicted intensity and fraction_events
630                                                for (mz, abundance) in adjusted_dist {
631                                                    aggregated_mz.push(mz);
632                                                    aggregated_intensity.push(abundance * frag_ion_data.predicted_intensity * fraction_events as f64);
633                                                }
634                                            }
635
636                                            if !aggregated_mz.is_empty() {
637                                                MzSpectrum::new(aggregated_mz, aggregated_intensity)
638                                            } else {
639                                                fragment_ion_series.clone() * fraction_events as f64
640                                            }
641                                        } else {
642                                            fragment_ion_series.clone() * fraction_events as f64
643                                        }
644                                    } else {
645                                        fragment_ion_series.clone() * fraction_events as f64
646                                    }
647                                } else {
648                                    fragment_ion_series.clone() * fraction_events as f64
649                                }
650                            },
651                        };
652
653                        let mz_spectrum = if mz_noise_fragment {
654                            if uniform {
655                                final_spectrum.add_mz_noise_uniform(fragment_ppm, right_drag_val)
656                            } else {
657                                final_spectrum.add_mz_noise_normal(fragment_ppm)
658                            }
659                        } else {
660                            final_spectrum
661                        };
662
663                        let spectrum_len = mz_spectrum.mz.len();
664                        tims_spectra.push(TimsSpectrum::new(
665                            frame_id as i32,
666                            *scan as i32,
667                            rt,
668                            scan_mobility,
669                            ms_type.clone(),
670                            IndexedMzSpectrum::from_mz_spectrum(
671                                vec![0; spectrum_len],
672                                mz_spectrum,
673                            ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
674                        ));
675                    }
676
677                    // Add unfragmented precursor ions (survival) if configured
678                    if self.isotope_config.has_precursor_survival() {
679                        let mut rng = rand::thread_rng();
680                        let survival_fraction = rng.gen_range(
681                            self.isotope_config.precursor_survival_min
682                            ..=self.isotope_config.precursor_survival_max
683                        );
684
685                        if survival_fraction > 0.0 {
686                            // Transmit the precursor spectrum through the quadrupole
687                            let precursor_transmitted = self.transmission_settings.transmit_spectrum(
688                                frame_id as i32,
689                                *scan as i32,
690                                spectrum.clone(),
691                                Some(self.isotope_config.min_probability),
692                            );
693
694                            if !precursor_transmitted.mz.is_empty() {
695                                // Scale by survival fraction and event count
696                                let precursor_scaled = precursor_transmitted * (fraction_events as f64 * survival_fraction);
697
698                                let precursor_mz_spectrum = if mz_noise_fragment {
699                                    if uniform {
700                                        precursor_scaled.add_mz_noise_uniform(fragment_ppm, right_drag_val)
701                                    } else {
702                                        precursor_scaled.add_mz_noise_normal(fragment_ppm)
703                                    }
704                                } else {
705                                    precursor_scaled
706                                };
707
708                                let precursor_len = precursor_mz_spectrum.mz.len();
709                                tims_spectra.push(TimsSpectrum::new(
710                                    frame_id as i32,
711                                    *scan as i32,
712                                    rt,
713                                    scan_mobility,
714                                    ms_type.clone(),
715                                    IndexedMzSpectrum::from_mz_spectrum(
716                                        vec![0; precursor_len],
717                                        precursor_mz_spectrum,
718                                    ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
719                                ));
720                            }
721                        }
722                    }
723                }
724            }
725        }
726
727        if tims_spectra.is_empty() {
728            return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
729        }
730
731        let tims_frame = TimsFrame::from_tims_spectra(tims_spectra);
732        tims_frame.filter_ranged(
733            mz_min_val,
734            mz_max_val,
735            0,
736            1000,
737            0.0,
738            10.0,
739            intensity_min_val,
740            1e9,
741            0,
742            i32::MAX,
743        )
744    }
745
746    pub fn build_fragment_frame_annotated(
747        &self,
748        frame_id: u32,
749        fragment_ions: &BTreeMap<
750            (u32, i8, i32),
751            (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>),
752        >,
753        mz_noise_fragment: bool,
754        uniform: bool,
755        fragment_ppm: f64,
756        mz_min: Option<f64>,
757        mz_max: Option<f64>,
758        intensity_min: Option<f64>,
759        right_drag: Option<bool>,
760    ) -> TimsFrameAnnotated {
761        // Cache frame-level lookups
762        let ms_type = if self.precursor_frame_builder.precursor_frame_id_set.contains(&frame_id) {
763            MsType::Unknown
764        } else {
765            MsType::FragmentDia
766        };
767
768        let rt = *self.precursor_frame_builder.frame_to_rt.get(&frame_id).unwrap() as f64;
769        let right_drag_val = right_drag.unwrap_or(false);
770        let mz_min_val = mz_min.unwrap_or(100.0);
771        let mz_max_val = mz_max.unwrap_or(1700.0);
772        let intensity_min_val = intensity_min.unwrap_or(1.0);
773
774        // Single lookup instead of contains_key + get
775        let Some((peptide_ids, frame_abundances)) = self
776            .precursor_frame_builder
777            .frame_to_abundances
778            .get(&frame_id)
779        else {
780            return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
781        };
782
783        // Preallocate with estimated capacity
784        let estimated_capacity = peptide_ids.len() * 4;
785        let mut tims_spectra: Vec<TimsSpectrumAnnotated> = Vec::with_capacity(estimated_capacity);
786
787        for (peptide_id, frame_abundance) in peptide_ids.iter().zip(frame_abundances.iter()) {
788            // Single lookup
789            let Some((ion_abundances, scan_occurrences, scan_abundances, charges, _)) = self
790                .precursor_frame_builder
791                .peptide_to_ions
792                .get(peptide_id)
793            else {
794                continue;
795            };
796
797            // Cache peptide-level lookups
798            let total_events = *self.precursor_frame_builder.peptide_to_events.get(peptide_id).unwrap();
799            let peptide = self.precursor_frame_builder.peptides.get(peptide_id).unwrap();
800
801            for (index, ion_abundance) in ion_abundances.iter().enumerate() {
802                let all_scan_occurrence = &scan_occurrences[index];
803                let all_scan_abundance = &scan_abundances[index];
804                let charge_state = charges[index];
805
806                let ion = PeptideIon::new(
807                    peptide.sequence.sequence.clone(),
808                    charge_state as i32,
809                    *ion_abundance as f64,
810                    Some(*peptide_id as i32),
811                );
812                // TODO: make this configurable
813                let spectrum = ion.calculate_isotopic_spectrum_annotated(1e-3, 1e-8, 200, 1e-4);
814
815                for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
816                    if !self.transmission_settings.any_transmitted(
817                        frame_id as i32,
818                        *scan as i32,
819                        &spectrum.mz,
820                        None,
821                    ) {
822                        continue;
823                    }
824
825                    let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
826
827                    let collision_energy = self.fragmentation_settings.get_collision_energy(frame_id as i32, *scan as i32);
828                    let collision_energy_quantized = (collision_energy * 1e1).round() as i32;
829
830                    let Some((_, fragment_series_vec)) = fragment_ions.get(&(*peptide_id, charge_state, collision_energy_quantized)) else {
831                        continue;
832                    };
833
834                    // Cache scan mobility
835                    let scan_mobility = *self.precursor_frame_builder.scan_to_mobility.get(scan).unwrap() as f64;
836
837                    for fragment_ion_series in fragment_series_vec.iter() {
838                        let scaled_spec = fragment_ion_series.clone() * fraction_events as f64;
839
840                        let mz_spectrum = if mz_noise_fragment {
841                            if uniform {
842                                scaled_spec.add_mz_noise_uniform(fragment_ppm, right_drag_val)
843                            } else {
844                                scaled_spec.add_mz_noise_normal(fragment_ppm)
845                            }
846                        } else {
847                            scaled_spec
848                        };
849
850                        let spectrum_len = mz_spectrum.mz.len();
851                        tims_spectra.push(TimsSpectrumAnnotated::new(
852                            frame_id as i32,
853                            *scan,
854                            rt,
855                            scan_mobility,
856                            ms_type.clone(),
857                            vec![0; spectrum_len],
858                            mz_spectrum,
859                        ));
860                    }
861
862                    // Add unfragmented precursor ions (survival) if configured
863                    if self.isotope_config.has_precursor_survival() {
864                        let mut rng = rand::thread_rng();
865                        let survival_fraction = rng.gen_range(
866                            self.isotope_config.precursor_survival_min
867                            ..=self.isotope_config.precursor_survival_max
868                        );
869
870                        if survival_fraction > 0.0 {
871                            // Create a non-annotated spectrum for transmission
872                            let precursor_mz_spectrum = MzSpectrum::new(spectrum.mz.clone(), spectrum.intensity.clone());
873
874                            // Transmit through the quadrupole
875                            let precursor_transmitted = self.transmission_settings.transmit_spectrum(
876                                frame_id as i32,
877                                *scan as i32,
878                                precursor_mz_spectrum,
879                                Some(self.isotope_config.min_probability),
880                            );
881
882                            if !precursor_transmitted.mz.is_empty() {
883                                // Scale by survival fraction and event count
884                                let precursor_scaled = precursor_transmitted * (fraction_events as f64 * survival_fraction);
885
886                                let precursor_final = if mz_noise_fragment {
887                                    if uniform {
888                                        precursor_scaled.add_mz_noise_uniform(fragment_ppm, right_drag_val)
889                                    } else {
890                                        precursor_scaled.add_mz_noise_normal(fragment_ppm)
891                                    }
892                                } else {
893                                    precursor_scaled
894                                };
895
896                                // Convert to annotated spectrum (with precursor annotations)
897                                let annotations: Vec<PeakAnnotation> = precursor_final.mz.iter()
898                                    .map(|_| PeakAnnotation { contributions: vec![] })
899                                    .collect();
900                                let precursor_annotated = MzSpectrumAnnotated::new(
901                                    precursor_final.mz.to_vec(),
902                                    precursor_final.intensity.to_vec(),
903                                    annotations,
904                                );
905
906                                let precursor_len = precursor_annotated.mz.len();
907                                tims_spectra.push(TimsSpectrumAnnotated::new(
908                                    frame_id as i32,
909                                    *scan,
910                                    rt,
911                                    scan_mobility,
912                                    ms_type.clone(),
913                                    vec![0; precursor_len],
914                                    precursor_annotated,
915                                ));
916                            }
917                        }
918                    }
919                }
920            }
921        }
922
923        if tims_spectra.is_empty() {
924            return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
925        }
926
927        TimsFrameAnnotated::from_tims_spectra_annotated(tims_spectra).filter_ranged(
928            mz_min_val, mz_max_val, 0.0, 10.0, 0, 1000, intensity_min_val, 1e9,
929        )
930    }
931
932    pub fn get_ion_transmission_matrix(
933        &self,
934        peptide_id: u32,
935        charge: i8,
936        include_precursor_frames: bool,
937    ) -> Vec<Vec<f32>> {
938        let maybe_peptide_sim = self.precursor_frame_builder.peptides.get(&peptide_id);
939
940        let mut frame_ids = match maybe_peptide_sim {
941            Some(maybe_peptide_sim) => maybe_peptide_sim.frame_distribution.occurrence.clone(),
942            _ => vec![],
943        };
944
945        if !include_precursor_frames {
946            frame_ids = frame_ids
947                .iter()
948                .filter(|frame_id| {
949                    !self
950                        .precursor_frame_builder
951                        .precursor_frame_id_set
952                        .contains(frame_id)
953                })
954                .cloned()
955                .collect();
956        }
957
958        let ion = self
959            .precursor_frame_builder
960            .ions
961            .get(&peptide_id)
962            .unwrap()
963            .iter()
964            .find(|ion| ion.charge == charge)
965            .unwrap();
966        let spectrum = ion.simulated_spectrum.clone();
967        let scan_distribution = &ion.scan_distribution;
968
969        let mut transmission_matrix =
970            vec![vec![0.0; frame_ids.len()]; scan_distribution.occurrence.len()];
971
972        for (frame_index, frame) in frame_ids.iter().enumerate() {
973            for (scan_index, scan) in scan_distribution.occurrence.iter().enumerate() {
974                if self.transmission_settings.all_transmitted(
975                    *frame as i32,
976                    *scan as i32,
977                    &spectrum.mz,
978                    None,
979                ) {
980                    transmission_matrix[scan_index][frame_index] = 1.0;
981                } else if self.transmission_settings.any_transmitted(
982                    *frame as i32,
983                    *scan as i32,
984                    &spectrum.mz,
985                    None,
986                ) {
987                    let transmitted_spectrum = self.transmission_settings.transmit_spectrum(
988                        *frame as i32,
989                        *scan as i32,
990                        spectrum.clone(),
991                        None,
992                    );
993                    let percentage_transmitted = transmitted_spectrum.intensity.iter().sum::<f64>()
994                        / spectrum.intensity.iter().sum::<f64>();
995                    transmission_matrix[scan_index][frame_index] = percentage_transmitted as f32;
996                }
997            }
998        }
999
1000        transmission_matrix
1001    }
1002
1003    pub fn count_number_transmissions(&self, peptide_id: u32, charge: i8) -> (usize, usize) {
1004        let frame_ids: Vec<_> = self
1005            .precursor_frame_builder
1006            .peptides
1007            .get(&peptide_id)
1008            .unwrap()
1009            .frame_distribution
1010            .occurrence
1011            .clone()
1012            .iter()
1013            .filter(|frame_id| {
1014                !self
1015                    .precursor_frame_builder
1016                    .precursor_frame_id_set
1017                    .contains(frame_id)
1018            })
1019            .cloned()
1020            .collect();
1021        let ion = self
1022            .precursor_frame_builder
1023            .ions
1024            .get(&peptide_id)
1025            .unwrap()
1026            .iter()
1027            .find(|ion| ion.charge == charge)
1028            .unwrap();
1029        let spectrum = ion.simulated_spectrum.clone();
1030        let scan_distribution = &ion.scan_distribution;
1031
1032        let mut frame_count = 0;
1033        let mut scan_count = 0;
1034
1035        for frame in frame_ids.iter() {
1036            let mut frame_transmitted = false;
1037            for scan in scan_distribution.occurrence.iter() {
1038                if self.transmission_settings.any_transmitted(
1039                    *frame as i32,
1040                    *scan as i32,
1041                    &spectrum.mz,
1042                    None,
1043                ) {
1044                    frame_transmitted = true;
1045                    scan_count += 1;
1046                }
1047            }
1048            if frame_transmitted {
1049                frame_count += 1;
1050            }
1051        }
1052
1053        (frame_count, scan_count)
1054    }
1055
1056    pub fn count_number_transmissions_parallel(
1057        &self,
1058        peptide_ids: Vec<u32>,
1059        charge: Vec<i8>,
1060        num_threads: usize,
1061    ) -> Vec<(usize, usize)> {
1062        let thread_pool = ThreadPoolBuilder::new()
1063            .num_threads(num_threads)
1064            .build()
1065            .unwrap();
1066        let result: Vec<(usize, usize)> = thread_pool.install(|| {
1067            peptide_ids
1068                .par_iter()
1069                .zip(charge.par_iter())
1070                .map(|(peptide_id, charge)| self.count_number_transmissions(*peptide_id, *charge))
1071                .collect()
1072        });
1073
1074        result
1075    }
1076}
1077
1078impl TimsTofCollisionEnergy for TimsTofSyntheticsFrameBuilderDIA {
1079    fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> f64 {
1080        self.fragmentation_settings
1081            .get_collision_energy(frame_id, scan_id)
1082    }
1083}