1use mscore::simulation::noise_rng::noise_rng;
8use mscore::data::peptide::PeptideProductIonSeriesCollection;
9use mscore::data::spectrum::{IndexedMzSpectrum, MsType, MzSpectrum};
10use mscore::timstof::collision::{TimsTofCollisionEnergy, TimsTofCollisionEnergyDIA};
11use mscore::timstof::frame::TimsFrame;
12use mscore::timstof::quadrupole::{IonTransmission, TimsTransmissionDDA, TimsTransmissionDIA};
13use mscore::timstof::spectrum::TimsSpectrum;
14use std::collections::{BTreeMap, HashSet};
15use std::path::Path;
16use std::sync::Arc;
17
18use rayon::prelude::*;
19
20use crate::sim::containers::{FragmentIonSim, FramesSim, IonSim, PeptidesSim, ScansSim};
21use crate::sim::dda::TimsTofSyntheticsFrameBuilderDDA;
22use crate::sim::handle::TimsTofSyntheticsDataHandle;
23use crate::sim::precursor::TimsTofSyntheticsPrecursorFrameBuilder;
24use crate::sim::projector::DistributionSource;
25
26pub struct TimsTofLazyFrameBuilderDIA {
35 pub db_path: String,
37 pub frames: Vec<FramesSim>,
39 pub scans: Vec<ScansSim>,
41 pub precursor_frame_id_set: HashSet<u32>,
43 pub frame_to_rt: BTreeMap<u32, f32>,
45 pub scan_to_mobility: BTreeMap<u32, f32>,
47 pub transmission_settings: TimsTransmissionDIA,
49 pub fragmentation_settings: TimsTofCollisionEnergyDIA,
51 pub num_threads: usize,
53 pub source: DistributionSource,
55 pub noise_seed: u64,
58}
59
60impl TimsTofLazyFrameBuilderDIA {
61
62 pub fn set_noise_seed(&mut self, seed: u64) {
64 self.noise_seed = seed;
65 }
66 pub fn new(path: &Path, num_threads: usize) -> rusqlite::Result<Self> {
80 Self::new_with_source(path, num_threads, DistributionSource::Columns)
81 }
82
83 pub fn new_with_source(
86 path: &Path,
87 num_threads: usize,
88 source: DistributionSource,
89 ) -> rusqlite::Result<Self> {
90 let handle = TimsTofSyntheticsDataHandle::new(path)?;
91 handle
93 .read_prediction_set()?
94 .assert_render_compatible()
95 .map_err(|_| rusqlite::Error::InvalidQuery)?;
96
97 let frames = handle.read_frames()?;
98 let scans = handle.read_scans()?;
99
100 let precursor_frame_id_set = TimsTofSyntheticsDataHandle::build_precursor_frame_id_set(&frames);
101 let frame_to_rt = TimsTofSyntheticsDataHandle::build_frame_to_rt(&frames);
102 let scan_to_mobility = TimsTofSyntheticsDataHandle::build_scan_to_mobility(&scans);
103
104 let transmission_settings = handle.get_transmission_dia();
105 let fragmentation_settings = handle.get_collision_energy_dia();
106
107 Ok(Self {
108 noise_seed: 0,
109 db_path: path.to_str().unwrap().to_string(),
110 frames,
111 scans,
112 precursor_frame_id_set,
113 frame_to_rt,
114 scan_to_mobility,
115 transmission_settings,
116 fragmentation_settings,
117 num_threads,
118 source,
119 })
120 }
121
122 fn load_data_for_frame_range(
126 &self,
127 frame_min: u32,
128 frame_max: u32,
129 ) -> rusqlite::Result<(Vec<PeptidesSim>, Vec<IonSim>, Vec<FragmentIonSim>)> {
130 let path = Path::new(&self.db_path);
131 let handle = TimsTofSyntheticsDataHandle::new(path)?;
132
133 let peptides = handle.read_peptides_for_frame_range_with_source(frame_min, frame_max, &self.source)?;
135
136 if peptides.is_empty() {
137 return Ok((Vec::new(), Vec::new(), Vec::new()));
138 }
139
140 let peptide_ids: Vec<u32> = peptides.iter().map(|p| p.peptide_id).collect();
142
143 let ions = handle.read_ions_for_peptides_with_source(&peptide_ids, &self.source)?;
145 let fragment_ions = handle.read_fragment_ions_for_peptides(&peptide_ids)?;
146
147 Ok((peptides, ions, fragment_ions))
148 }
149
150 pub fn build_frames_lazy(
170 &self,
171 frame_ids: Vec<u32>,
172 fragmentation: bool,
173 mz_noise_precursor: bool,
174 uniform: bool,
175 precursor_noise_ppm: f64,
176 mz_noise_fragment: bool,
177 fragment_noise_ppm: f64,
178 right_drag: bool,
179 ) -> Vec<TimsFrame> {
180 if frame_ids.is_empty() {
181 return Vec::new();
182 }
183
184 let frame_min = *frame_ids.iter().min().unwrap();
186 let frame_max = *frame_ids.iter().max().unwrap();
187
188 let (peptides, ions, fragment_ions) = match self.load_data_for_frame_range(frame_min, frame_max) {
190 Ok(data) => data,
191 Err(_) => return Vec::new(),
192 };
193
194 let peptide_map = TimsTofSyntheticsDataHandle::build_peptide_map(&peptides);
196 let peptide_to_ions = TimsTofSyntheticsDataHandle::build_peptide_to_ions(&ions);
197 let frame_to_abundances = TimsTofSyntheticsDataHandle::build_frame_to_abundances(&peptides);
198 let peptide_to_events = TimsTofSyntheticsDataHandle::build_peptide_to_events(&peptides);
199
200 let fragment_ions_map = if fragmentation {
202 Some(TimsTofSyntheticsDataHandle::build_fragment_ions(
203 &peptide_map,
204 &fragment_ions,
205 self.num_threads,
206 ))
207 } else {
208 None
209 };
210
211 let pool = rayon::ThreadPoolBuilder::new()
213 .num_threads(self.num_threads)
214 .build()
215 .unwrap();
216
217 pool.install(|| {
218 let mut tims_frames: Vec<TimsFrame> = Vec::with_capacity(frame_ids.len());
219 unsafe { tims_frames.set_len(frame_ids.len()); }
220
221 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
222 let frame = self.build_single_frame(
223 *frame_id,
224 fragmentation,
225 mz_noise_precursor,
226 uniform,
227 precursor_noise_ppm,
228 mz_noise_fragment,
229 fragment_noise_ppm,
230 right_drag,
231 &peptide_map,
232 &peptide_to_ions,
233 &frame_to_abundances,
234 &peptide_to_events,
235 &fragment_ions_map,
236 );
237 unsafe {
238 let ptr = tims_frames.as_ptr() as *mut TimsFrame;
239 std::ptr::write(ptr.add(idx), frame);
240 }
241 });
242
243 tims_frames
244 })
245 }
246
247 #[allow(clippy::too_many_arguments)]
249 fn build_single_frame(
250 &self,
251 frame_id: u32,
252 fragmentation: bool,
253 mz_noise_precursor: bool,
254 uniform: bool,
255 precursor_noise_ppm: f64,
256 mz_noise_fragment: bool,
257 fragment_noise_ppm: f64,
258 right_drag: bool,
259 _peptide_map: &BTreeMap<u32, PeptidesSim>,
260 peptide_to_ions: &BTreeMap<u32, (Vec<f32>, Vec<Vec<u32>>, Vec<Vec<f32>>, Vec<i8>, Vec<MzSpectrum>)>,
261 frame_to_abundances: &BTreeMap<u32, (Vec<u32>, Vec<f32>)>,
262 peptide_to_events: &BTreeMap<u32, f32>,
263 fragment_ions_map: &Option<BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrum>)>>,
264 ) -> TimsFrame {
265 let is_precursor = self.precursor_frame_id_set.contains(&frame_id);
267
268 if is_precursor {
269 self.build_precursor_frame(
270 frame_id,
271 mz_noise_precursor,
272 uniform,
273 precursor_noise_ppm,
274 right_drag,
275 peptide_to_ions,
276 frame_to_abundances,
277 peptide_to_events,
278 )
279 } else {
280 self.build_fragment_frame(
281 frame_id,
282 fragmentation,
283 mz_noise_fragment,
284 uniform,
285 fragment_noise_ppm,
286 right_drag,
287 peptide_to_ions,
288 frame_to_abundances,
289 peptide_to_events,
290 fragment_ions_map,
291 )
292 }
293 }
294
295 #[allow(clippy::too_many_arguments)]
297 fn build_precursor_frame(
298 &self,
299 frame_id: u32,
300 mz_noise_precursor: bool,
301 uniform: bool,
302 precursor_noise_ppm: f64,
303 right_drag: bool,
304 peptide_to_ions: &BTreeMap<u32, (Vec<f32>, Vec<Vec<u32>>, Vec<Vec<f32>>, Vec<i8>, Vec<MzSpectrum>)>,
305 frame_to_abundances: &BTreeMap<u32, (Vec<u32>, Vec<f32>)>,
306 peptide_to_events: &BTreeMap<u32, f32>,
307 ) -> TimsFrame {
308 let mut rng = noise_rng(self.noise_seed, &[frame_id as u64]);
311 let ms_type = MsType::Precursor;
312 let rt = *self.frame_to_rt.get(&frame_id).unwrap_or(&0.0) as f64;
313
314 let Some((peptide_ids, abundances)) = frame_to_abundances.get(&frame_id) else {
316 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
317 };
318
319 let estimated_capacity = peptide_ids.len() * 4;
321 let mut tims_spectra: Vec<TimsSpectrum> = Vec::with_capacity(estimated_capacity);
322
323 for (peptide_id, abundance) in peptide_ids.iter().zip(abundances.iter()) {
324 let Some((ion_abundances, scan_occurrences, scan_abundances, _, spectra)) =
325 peptide_to_ions.get(peptide_id)
326 else {
327 continue;
328 };
329
330 let total_events = *peptide_to_events.get(peptide_id).unwrap_or(&1.0);
332
333 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
334 let scan_occurrence = &scan_occurrences[index];
335 let scan_abundance = &scan_abundances[index];
336 let spectrum = &spectra[index];
337
338 for (scan, scan_abu) in scan_occurrence.iter().zip(scan_abundance.iter()) {
339 let abundance_factor = abundance * ion_abundance * scan_abu * total_events;
340 let scaled_spec: MzSpectrum = spectrum.clone() * abundance_factor as f64;
341
342 let mz_spectrum = if mz_noise_precursor {
343 if uniform {
344 scaled_spec.add_mz_noise_uniform_with_rng(precursor_noise_ppm, right_drag, &mut rng)
345 } else {
346 scaled_spec.add_mz_noise_normal_with_rng(precursor_noise_ppm, &mut rng)
347 }
348 } else {
349 scaled_spec
350 };
351
352 let scan_mobility = *self.scan_to_mobility.get(scan).unwrap_or(&0.0) as f64;
353 let spectrum_len = mz_spectrum.mz.len();
354
355 tims_spectra.push(TimsSpectrum::new(
356 frame_id as i32,
357 *scan as i32,
358 rt,
359 scan_mobility,
360 ms_type.clone(),
361 IndexedMzSpectrum::from_mz_spectrum(
362 vec![0; spectrum_len],
363 mz_spectrum,
364 ),
365 ));
366 }
367 }
368 }
369
370 let mut filtered = TimsFrame::from_tims_spectra_filtered(
371 tims_spectra, 0.0, 10000.0, 0, 2000, 0.0, 10.0, 1.0, 1e9,
372 );
373
374 let intensities_rounded: Vec<f64> = filtered
376 .ims_frame
377 .intensity
378 .iter()
379 .map(|x| x.round())
380 .collect();
381 filtered.ims_frame.intensity = Arc::new(intensities_rounded);
382
383 filtered
384 }
385
386 #[allow(clippy::too_many_arguments)]
388 fn build_fragment_frame(
389 &self,
390 frame_id: u32,
391 fragmentation: bool,
392 mz_noise_fragment: bool,
393 uniform: bool,
394 fragment_noise_ppm: f64,
395 right_drag: bool,
396 peptide_to_ions: &BTreeMap<u32, (Vec<f32>, Vec<Vec<u32>>, Vec<Vec<f32>>, Vec<i8>, Vec<MzSpectrum>)>,
397 frame_to_abundances: &BTreeMap<u32, (Vec<u32>, Vec<f32>)>,
398 peptide_to_events: &BTreeMap<u32, f32>,
399 fragment_ions_map: &Option<BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrum>)>>,
400 ) -> TimsFrame {
401 let mut rng = noise_rng(self.noise_seed, &[frame_id as u64]);
404 let ms_type = MsType::FragmentDia;
405 let rt = *self.frame_to_rt.get(&frame_id).unwrap_or(&0.0) as f64;
406
407 if !fragmentation || fragment_ions_map.is_none() {
408 let precursor_frame = self.build_precursor_frame(
410 frame_id,
411 mz_noise_fragment,
412 uniform,
413 fragment_noise_ppm,
414 right_drag,
415 peptide_to_ions,
416 frame_to_abundances,
417 peptide_to_events,
418 );
419 let mut frame = self.transmission_settings.transmit_tims_frame(&precursor_frame, None);
420 frame.ms_type = MsType::FragmentDia;
421 return frame;
422 }
423
424 let fragment_ions = fragment_ions_map.as_ref().unwrap();
425
426 let Some((peptide_ids, frame_abundances)) = frame_to_abundances.get(&frame_id) else {
428 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
429 };
430
431 let estimated_capacity = peptide_ids.len() * 4;
433 let mut tims_spectra: Vec<TimsSpectrum> = Vec::with_capacity(estimated_capacity);
434
435 for (peptide_id, frame_abundance) in peptide_ids.iter().zip(frame_abundances.iter()) {
436 let Some((ion_abundances, scan_occurrences, scan_abundances, charges, spectra)) =
437 peptide_to_ions.get(peptide_id)
438 else {
439 continue;
440 };
441
442 let total_events = *peptide_to_events.get(peptide_id).unwrap_or(&1.0);
444
445 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
446 let all_scan_occurrence = &scan_occurrences[index];
447 let all_scan_abundance = &scan_abundances[index];
448 let spectrum = &spectra[index];
449 let charge_state = charges[index];
450
451 for (scan, scan_abundance) in all_scan_occurrence.iter().zip(all_scan_abundance.iter()) {
452 if !self.transmission_settings.any_transmitted(
454 frame_id as i32,
455 *scan as i32,
456 &spectrum.mz,
457 None,
458 ) {
459 continue;
460 }
461
462 let fraction_events = frame_abundance * scan_abundance * ion_abundance * total_events;
464
465 let collision_energy = self.fragmentation_settings.get_collision_energy(
467 frame_id as i32,
468 *scan as i32,
469 );
470 let Some(collision_energy_quantized) = crate::sim::handle::resolve_fragment_ce_key(
473 fragment_ions, *peptide_id, charge_state, collision_energy,
474 ) else {
475 if crate::sim::handle::fragment_prefix_exists(fragment_ions, *peptide_id, charge_state) {
476 panic!(
477 "lazy DIA fragment lookup miss: peptide {} charge {} applied CE {:.4} eV \
478 has predicted fragments, but none within 0.1 eV — the prediction set \
479 does not cover this instrument's collision energy",
480 *peptide_id, charge_state, collision_energy,
481 );
482 }
483 continue;
484 };
485 let (_, fragment_series_vec) = fragment_ions
486 .get(&(*peptide_id, charge_state, collision_energy_quantized))
487 .expect("resolve_fragment_ce_key returned a present key");
488
489 let scan_mobility = *self.scan_to_mobility.get(scan).unwrap_or(&0.0) as f64;
491
492 for fragment_ion_series in fragment_series_vec.iter() {
493 let scaled_spec = fragment_ion_series.clone() * fraction_events as f64;
494
495 let mz_spectrum = if mz_noise_fragment {
496 if uniform {
497 scaled_spec.add_mz_noise_uniform_with_rng(fragment_noise_ppm, right_drag, &mut rng)
498 } else {
499 scaled_spec.add_mz_noise_normal_with_rng(fragment_noise_ppm, &mut rng)
500 }
501 } else {
502 scaled_spec
503 };
504
505 let spectrum_len = mz_spectrum.mz.len();
506 tims_spectra.push(TimsSpectrum::new(
507 frame_id as i32,
508 *scan as i32,
509 rt,
510 scan_mobility,
511 ms_type.clone(),
512 IndexedMzSpectrum::from_mz_spectrum(
513 vec![0; spectrum_len],
514 mz_spectrum,
515 ).filter_ranged(100.0, 1700.0, 1.0, 1e9),
516 ));
517 }
518 }
519 }
520 }
521
522 if tims_spectra.is_empty() {
523 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
524 }
525
526 let mut filtered = TimsFrame::from_tims_spectra_filtered(
527 tims_spectra, 100.0, 1700.0, 0, 1000, 0.0, 10.0, 1.0, 1e9,
528 );
529
530 let intensities_rounded: Vec<f64> = filtered
532 .ims_frame
533 .intensity
534 .iter()
535 .map(|x| x.round())
536 .collect();
537 filtered.ims_frame.intensity = Arc::new(intensities_rounded);
538
539 filtered
540 }
541
542 pub fn num_frames(&self) -> usize {
544 self.frames.len()
545 }
546
547 pub fn frame_ids(&self) -> Vec<u32> {
549 self.frames.iter().map(|f| f.frame_id).collect()
550 }
551
552 pub fn precursor_frame_ids(&self) -> Vec<u32> {
554 self.precursor_frame_id_set.iter().cloned().collect()
555 }
556
557 pub fn fragment_frame_ids(&self) -> Vec<u32> {
559 self.frames
560 .iter()
561 .filter(|f| !self.precursor_frame_id_set.contains(&f.frame_id))
562 .map(|f| f.frame_id)
563 .collect()
564 }
565}
566
567impl TimsTofCollisionEnergy for TimsTofLazyFrameBuilderDIA {
568 fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> f64 {
569 self.fragmentation_settings.get_collision_energy(frame_id, scan_id)
570 }
571}
572
573pub struct TimsTofLazyFrameBuilderDDA {
582 pub db_path: String,
584 pub frames: Vec<FramesSim>,
586 pub scans: Vec<ScansSim>,
588 pub precursor_frame_id_set: HashSet<u32>,
590 pub frame_to_rt: BTreeMap<u32, f32>,
592 pub scan_to_mobility: BTreeMap<u32, f32>,
594 pub transmission_settings: TimsTransmissionDDA,
596 pub num_threads: usize,
598 pub source: DistributionSource,
600}
601
602impl TimsTofLazyFrameBuilderDDA {
603 pub fn new(path: &Path, num_threads: usize) -> rusqlite::Result<Self> {
617 Self::new_with_source(path, num_threads, DistributionSource::Columns)
618 }
619
620 pub fn new_with_source(
623 path: &Path,
624 num_threads: usize,
625 source: DistributionSource,
626 ) -> rusqlite::Result<Self> {
627 let handle = TimsTofSyntheticsDataHandle::new(path)?;
628 handle
630 .read_prediction_set()?
631 .assert_render_compatible()
632 .map_err(|_| rusqlite::Error::InvalidQuery)?;
633
634 let frames = handle.read_frames()?;
635 let scans = handle.read_scans()?;
636
637 let precursor_frame_id_set = TimsTofSyntheticsDataHandle::build_precursor_frame_id_set(&frames);
638 let frame_to_rt = TimsTofSyntheticsDataHandle::build_frame_to_rt(&frames);
639 let scan_to_mobility = TimsTofSyntheticsDataHandle::build_scan_to_mobility(&scans);
640
641 let transmission_settings = handle.get_transmission_dda();
642
643 Ok(Self {
644 db_path: path.to_str().unwrap().to_string(),
645 frames,
646 scans,
647 precursor_frame_id_set,
648 frame_to_rt,
649 scan_to_mobility,
650 transmission_settings,
651 num_threads,
652 source,
653 })
654 }
655
656 fn load_data_for_frame_range(
660 &self,
661 frame_min: u32,
662 frame_max: u32,
663 ) -> rusqlite::Result<(Vec<PeptidesSim>, Vec<IonSim>, Vec<FragmentIonSim>)> {
664 let path = Path::new(&self.db_path);
665 let handle = TimsTofSyntheticsDataHandle::new(path)?;
666
667 let peptides = handle.read_peptides_for_frame_range_with_source(frame_min, frame_max, &self.source)?;
669
670 if peptides.is_empty() {
671 return Ok((Vec::new(), Vec::new(), Vec::new()));
672 }
673
674 let peptide_ids: Vec<u32> = peptides.iter().map(|p| p.peptide_id).collect();
676
677 let ions = handle.read_ions_for_peptides_with_source(&peptide_ids, &self.source)?;
679 let fragment_ions = handle.read_fragment_ions_for_peptides(&peptide_ids)?;
680
681 Ok((peptides, ions, fragment_ions))
682 }
683
684 pub fn build_frames_lazy(
704 &self,
705 frame_ids: Vec<u32>,
706 fragmentation: bool,
707 mz_noise_precursor: bool,
708 uniform: bool,
709 precursor_noise_ppm: f64,
710 mz_noise_fragment: bool,
711 fragment_noise_ppm: f64,
712 right_drag: bool,
713 ) -> Vec<TimsFrame> {
714 if frame_ids.is_empty() {
715 return Vec::new();
716 }
717
718 let frame_min = *frame_ids.iter().min().unwrap();
720 let frame_max = *frame_ids.iter().max().unwrap();
721
722 let (peptides, ions, fragment_ions) = match self.load_data_for_frame_range(frame_min, frame_max) {
724 Ok(data) => data,
725 Err(_) => return Vec::new(),
726 };
727
728 let precursor_builder = TimsTofSyntheticsPrecursorFrameBuilder::from_entities(
735 ions,
736 peptides,
737 self.scans.clone(),
738 self.frames.clone(),
739 );
740 let dda_builder = TimsTofSyntheticsFrameBuilderDDA::from_entities(
741 precursor_builder,
742 self.transmission_settings.clone(),
743 fragment_ions,
744 None,
745 fragmentation,
746 self.num_threads,
747 );
748
749 dda_builder.build_frames(
750 frame_ids,
751 fragmentation,
752 mz_noise_precursor,
753 uniform,
754 precursor_noise_ppm,
755 mz_noise_fragment,
756 fragment_noise_ppm,
757 right_drag,
758 self.num_threads,
759 )
760 }
761
762 pub fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> f64 {
764 self.transmission_settings.get_collision_energy(frame_id, scan_id).unwrap_or(0.0)
765 }
766
767 pub fn num_frames(&self) -> usize {
769 self.frames.len()
770 }
771
772 pub fn frame_ids(&self) -> Vec<u32> {
774 self.frames.iter().map(|f| f.frame_id).collect()
775 }
776
777 pub fn precursor_frame_ids(&self) -> Vec<u32> {
779 self.precursor_frame_id_set.iter().cloned().collect()
780 }
781
782 pub fn fragment_frame_ids(&self) -> Vec<u32> {
784 self.frames
785 .iter()
786 .filter(|f| !self.precursor_frame_id_set.contains(&f.frame_id))
787 .map(|f| f.frame_id)
788 .collect()
789 }
790}