1use mscore::simulation::noise_rng::noise_rng;
2use mscore::data::peptide::PeptideIon;
3use mscore::data::spectrum::{IndexedMzSpectrum, MsType, MzSpectrum};
4use mscore::simulation::annotation::{
5 MzSpectrumAnnotated, TimsFrameAnnotated, TimsSpectrumAnnotated,
6};
7use mscore::timstof::frame::TimsFrame;
8use mscore::timstof::spectrum::TimsSpectrum;
9use rusqlite::Result;
10use std::collections::{BTreeMap, HashSet};
11use std::path::Path;
12
13use crate::sim::containers::{FramesSim, IonSim, PeptidesSim, ScansSim};
14use crate::sim::handle::TimsTofSyntheticsDataHandle;
15use crate::sim::projector::{IntensityStage, MzCoordSpace, RenderedEvent, RenderedSpectrum};
16use crate::sim::scheme::DataMode;
17use rayon::prelude::*;
18
19pub struct TimsTofSyntheticsPrecursorFrameBuilder {
20 pub ions: BTreeMap<u32, Vec<IonSim>>,
21 pub peptides: BTreeMap<u32, PeptidesSim>,
22 pub scans: Vec<ScansSim>,
23 pub frames: Vec<FramesSim>,
24 pub precursor_frame_id_set: HashSet<u32>,
25 pub frame_to_abundances: BTreeMap<u32, (Vec<u32>, Vec<f32>)>,
26 pub peptide_to_ions: BTreeMap<
27 u32,
28 (
29 Vec<f32>,
30 Vec<Vec<u32>>,
31 Vec<Vec<f32>>,
32 Vec<i8>,
33 Vec<MzSpectrum>,
34 ),
35 >,
36 pub frame_to_rt: BTreeMap<u32, f32>,
37 pub scan_to_mobility: BTreeMap<u32, f32>,
38 pub peptide_to_events: BTreeMap<u32, f32>,
39 pub ion_id_to_peptide_charge: BTreeMap<u32, (u32, i8)>,
41 pub noise_seed: u64,
44}
45
46impl TimsTofSyntheticsPrecursorFrameBuilder {
47
48 pub fn set_noise_seed(&mut self, seed: u64) {
50 self.noise_seed = seed;
51 }
52 pub fn new(path: &Path) -> Result<Self> {
63 Self::from_source(path, &crate::sim::projector::DistributionSource::Columns)
64 }
65
66 pub fn from_source(
71 path: &Path,
72 source: &crate::sim::projector::DistributionSource,
73 ) -> Result<Self> {
74 let handle = TimsTofSyntheticsDataHandle::new(path)?;
75 let ions = handle.read_ions_with_source(source)?;
76 let peptides = handle.read_peptides_with_source(source)?;
77 let scans = handle.read_scans()?;
78 let frames = handle.read_frames()?;
79
80 Ok(Self::from_entities(ions, peptides, scans, frames))
81 }
82
83 pub fn from_entities(
90 ions: Vec<IonSim>,
91 peptides: Vec<PeptidesSim>,
92 scans: Vec<ScansSim>,
93 frames: Vec<FramesSim>,
94 ) -> Self {
95 let mut ion_id_to_peptide_charge: BTreeMap<u32, (u32, i8)> = BTreeMap::new();
97 for ion in &ions {
98 ion_id_to_peptide_charge.insert(ion.ion_id, (ion.peptide_id, ion.charge));
99 }
100
101 Self {
102 noise_seed: 0,
103 ions: TimsTofSyntheticsDataHandle::build_peptide_to_ion_map(&ions),
104 peptides: TimsTofSyntheticsDataHandle::build_peptide_map(&peptides),
105 scans: scans.clone(),
106 frames: frames.clone(),
107 precursor_frame_id_set: TimsTofSyntheticsDataHandle::build_precursor_frame_id_set(
108 &frames,
109 ),
110 frame_to_abundances: TimsTofSyntheticsDataHandle::build_frame_to_abundances(&peptides),
111 peptide_to_ions: TimsTofSyntheticsDataHandle::build_peptide_to_ions(&ions),
112 frame_to_rt: TimsTofSyntheticsDataHandle::build_frame_to_rt(&frames),
113 scan_to_mobility: TimsTofSyntheticsDataHandle::build_scan_to_mobility(&scans),
114 peptide_to_events: TimsTofSyntheticsDataHandle::build_peptide_to_events(&peptides),
115 ion_id_to_peptide_charge,
116 }
117 }
118
119 pub fn precursor_frame_contributions(&self, frame_id: u32) -> Vec<(i32, MzSpectrum)> {
127 let mut out: Vec<(i32, MzSpectrum)> = Vec::new();
128 let Some((peptide_ids, abundances)) = self.frame_to_abundances.get(&frame_id) else {
129 return out;
130 };
131 out.reserve(peptide_ids.len() * 4);
132 for (peptide_id, abundance) in peptide_ids.iter().zip(abundances.iter()) {
133 let Some((ion_abundances, scan_occurrences, scan_abundances, _, spectra)) =
134 self.peptide_to_ions.get(peptide_id)
135 else {
136 continue;
137 };
138 let total_events = *self.peptide_to_events.get(peptide_id).unwrap();
139 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
140 let scan_occurrence = &scan_occurrences[index];
141 let scan_abundance = &scan_abundances[index];
142 let spectrum = &spectra[index];
143 for (scan, scan_abu) in scan_occurrence.iter().zip(scan_abundance.iter()) {
144 let abundance_factor = abundance * ion_abundance * scan_abu * total_events;
145 out.push((*scan as i32, spectrum.clone() * abundance_factor as f64));
146 }
147 }
148 }
149 out
150 }
151
152 pub fn precursor_scan_spectrum(&self, frame_id: u32) -> MzSpectrum {
166 let specs: Vec<MzSpectrum> = self
167 .precursor_frame_contributions(frame_id)
168 .into_iter()
169 .map(|(_scan, spectrum)| spectrum)
170 .collect();
171 MzSpectrum::from_collection(specs)
172 }
173
174 pub fn precursor_scan_marginal_spectrum(&self, frame_id: u32) -> MzSpectrum {
187 let mut specs: Vec<MzSpectrum> = Vec::new();
188 let Some((peptide_ids, abundances)) = self.frame_to_abundances.get(&frame_id) else {
189 return MzSpectrum::from_collection(specs);
190 };
191 specs.reserve(peptide_ids.len() * 4);
192 for (peptide_id, frame_abundance) in peptide_ids.iter().zip(abundances.iter()) {
193 let Some((ion_abundances, _scan_occurrences, _scan_abundances, _, spectra)) =
194 self.peptide_to_ions.get(peptide_id)
195 else {
196 continue;
197 };
198 let total_events = *self.peptide_to_events.get(peptide_id).unwrap();
199 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
200 let abundance_factor = frame_abundance * ion_abundance * total_events;
202 specs.push(spectra[index].clone() * abundance_factor as f64);
203 }
204 }
205 MzSpectrum::from_collection(specs)
206 }
207
208 pub fn render_precursor_scan(&self, frame_id: u32, data_mode: DataMode) -> RenderedEvent {
224 let rt = *self.frame_to_rt.get(&frame_id).unwrap() as f64;
225 let spectrum = self.precursor_scan_marginal_spectrum(frame_id);
226 RenderedEvent::Scan {
227 ms_level: 1,
228 retention_time_s: rt,
229 isolation: None,
230 spectrum: RenderedSpectrum {
231 mz: (*spectrum.mz).clone(),
232 intensity: (*spectrum.intensity).clone(),
233 coords: MzCoordSpace::Physical,
234 mode: data_mode,
235 detector_applied: false,
236 stage: IntensityStage::Mobility,
237 },
238 }
239 }
240
241 pub fn build_precursor_frame(
251 &self,
252 frame_id: u32,
253 mz_noise_precursor: bool,
254 uniform: bool,
255 precursor_noise_ppm: f64,
256 right_drag: bool,
257 ) -> TimsFrame {
258 let mut rng = noise_rng(self.noise_seed, &[frame_id as u64]);
261 let ms_type = if self.precursor_frame_id_set.contains(&frame_id) {
263 MsType::Precursor
264 } else {
265 MsType::Unknown
266 };
267 let rt = *self.frame_to_rt.get(&frame_id).unwrap() as f64;
268
269 let contributions = self.precursor_frame_contributions(frame_id);
274 let mut tims_spectra: Vec<TimsSpectrum> = Vec::with_capacity(contributions.len());
275 for (scan, scaled_spec) in contributions {
276 let mz_spectrum = if mz_noise_precursor {
277 if uniform {
278 scaled_spec.add_mz_noise_uniform_with_rng(precursor_noise_ppm, right_drag, &mut rng)
279 } else {
280 scaled_spec.add_mz_noise_normal_with_rng(precursor_noise_ppm, &mut rng)
281 }
282 } else {
283 scaled_spec
284 };
285
286 let scan_mobility = *self.scan_to_mobility.get(&(scan as u32)).unwrap() as f64;
287 let spectrum_len = mz_spectrum.mz.len();
288
289 tims_spectra.push(TimsSpectrum::new(
290 frame_id as i32,
291 scan,
292 rt,
293 scan_mobility,
294 ms_type.clone(),
295 IndexedMzSpectrum::from_mz_spectrum(vec![0; spectrum_len], mz_spectrum),
296 ));
297 }
298
299 if tims_spectra.is_empty() {
307 return TimsFrame::new(frame_id as i32, ms_type, rt, vec![], vec![], vec![], vec![], vec![]);
308 }
309 let tims_frame = TimsFrame::from_tims_spectra(tims_spectra);
310 tims_frame.filter_ranged(0.0, 10000.0, 0, 2000, 0.0, 10.0, 1.0, 1e9, 0, i32::MAX)
311 }
312
313 pub fn build_precursor_frames(
325 &self,
326 frame_ids: Vec<u32>,
327 mz_noise_precursor: bool,
328 uniform: bool,
329 precursor_noise_ppm: f64,
330 right_drag: bool,
331 num_threads: usize,
332 ) -> Vec<TimsFrame> {
333 let pool = rayon::ThreadPoolBuilder::new()
334 .num_threads(num_threads)
335 .build()
336 .unwrap();
337
338 pool.install(|| {
339 let mut tims_frames: Vec<TimsFrame> = Vec::with_capacity(frame_ids.len());
341 unsafe { tims_frames.set_len(frame_ids.len()); }
342
343 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
344 let frame = self.build_precursor_frame(
345 *frame_id,
346 mz_noise_precursor,
347 uniform,
348 precursor_noise_ppm,
349 right_drag,
350 );
351 unsafe {
352 let ptr = tims_frames.as_ptr() as *mut TimsFrame;
353 std::ptr::write(ptr.add(idx), frame);
354 }
355 });
356
357 tims_frames
358 })
359 }
360
361 pub fn build_precursor_frame_annotated(
362 &self,
363 frame_id: u32,
364 mz_noise_precursor: bool,
365 uniform: bool,
366 precursor_noise_ppm: f64,
367 right_drag: bool,
368 ) -> TimsFrameAnnotated {
369 let mut rng = noise_rng(self.noise_seed, &[frame_id as u64]);
372 let ms_type = if self.precursor_frame_id_set.contains(&frame_id) {
374 MsType::Precursor
375 } else {
376 MsType::Unknown
377 };
378 let rt = *self.frame_to_rt.get(&frame_id).unwrap_or(&0.0) as f64;
379
380 let Some((peptide_ids, abundances)) = self.frame_to_abundances.get(&frame_id) else {
382 return TimsFrameAnnotated::new(frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![]);
383 };
384
385 let estimated_capacity = peptide_ids.len() * 4;
387 let mut tims_spectra: Vec<TimsSpectrumAnnotated> = Vec::with_capacity(estimated_capacity);
388
389 for (peptide_id, abundance) in peptide_ids.iter().zip(abundances.iter()) {
390 let Some((ion_abundances, scan_occurrences, scan_abundances, charges, _)) =
392 self.peptide_to_ions.get(peptide_id)
393 else {
394 continue;
395 };
396
397 let total_events = *self.peptide_to_events.get(peptide_id).unwrap();
399 let peptide = self.peptides.get(peptide_id).unwrap();
400
401 for (index, ion_abundance) in ion_abundances.iter().enumerate() {
402 let scan_occurrence = &scan_occurrences[index];
403 let scan_abundance = &scan_abundances[index];
404 let charge = charges[index];
405
406 let ion = PeptideIon::new(
407 peptide.sequence.sequence.clone(),
408 charge as i32,
409 *ion_abundance as f64,
410 Some(*peptide_id as i32),
411 );
412 let spectrum = ion.calculate_isotopic_spectrum_annotated(1e-3, 1e-8, 200, 1e-4);
414
415 for (scan, scan_abu) in scan_occurrence.iter().zip(scan_abundance.iter()) {
416 let abundance_factor = abundance * ion_abundance * scan_abu * total_events;
417 let scaled_spec: MzSpectrumAnnotated = spectrum.clone() * abundance_factor as f64;
418
419 let mz_spectrum = if mz_noise_precursor {
420 if uniform {
421 scaled_spec.add_mz_noise_uniform_with_rng(precursor_noise_ppm, right_drag, &mut rng)
422 } else {
423 scaled_spec.add_mz_noise_normal_with_rng(precursor_noise_ppm, &mut rng)
424 }
425 } else {
426 scaled_spec
427 };
428
429 let scan_mobility = *self.scan_to_mobility.get(scan).unwrap() as f64;
431 let spectrum_len = mz_spectrum.mz.len();
432
433 tims_spectra.push(TimsSpectrumAnnotated::new(
434 frame_id as i32,
435 *scan,
436 rt,
437 scan_mobility,
438 ms_type.clone(),
439 vec![0; spectrum_len],
440 mz_spectrum,
441 ));
442 }
443 }
444 }
445
446 if tims_spectra.is_empty() {
450 return TimsFrameAnnotated::new(
451 frame_id as i32, rt, ms_type, vec![], vec![], vec![], vec![], vec![], vec![],
452 );
453 }
454 let tims_frame = TimsFrameAnnotated::from_tims_spectra_annotated(tims_spectra);
455 let filtered_frame = tims_frame.filter_ranged(0.0, 2000.0, 0.0, 2.0, 0, 1000, 1.0, 1e9);
456
457 TimsFrameAnnotated {
458 frame_id: filtered_frame.frame_id,
459 retention_time: filtered_frame.retention_time,
460 ms_type: filtered_frame.ms_type,
461 tof: filtered_frame.tof,
462 mz: filtered_frame.mz,
463 scan: filtered_frame.scan,
464 inv_mobility: filtered_frame.inv_mobility,
465 intensity: filtered_frame.intensity,
466 annotations: filtered_frame
467 .annotations
468 .into_iter()
469 .map(|mut x| {
470 x.contributions.sort_by(|a, b| {
471 a.intensity_contribution
472 .partial_cmp(&b.intensity_contribution)
473 .unwrap()
474 });
475 x
476 })
477 .collect(),
478 }
479 }
480
481 pub fn build_precursor_frames_annotated(
482 &self,
483 frame_ids: Vec<u32>,
484 mz_noise_precursor: bool,
485 uniform: bool,
486 precursor_noise_ppm: f64,
487 right_drag: bool,
488 num_threads: usize,
489 ) -> Vec<TimsFrameAnnotated> {
490 let pool = rayon::ThreadPoolBuilder::new()
491 .num_threads(num_threads)
492 .build()
493 .unwrap();
494
495 pool.install(|| {
496 let mut tims_frames: Vec<TimsFrameAnnotated> = Vec::with_capacity(frame_ids.len());
498 unsafe { tims_frames.set_len(frame_ids.len()); }
499
500 frame_ids.par_iter().enumerate().for_each(|(idx, frame_id)| {
501 let frame = self.build_precursor_frame_annotated(
502 *frame_id,
503 mz_noise_precursor,
504 uniform,
505 precursor_noise_ppm,
506 right_drag,
507 );
508 unsafe {
509 let ptr = tims_frames.as_ptr() as *mut TimsFrameAnnotated;
510 std::ptr::write(ptr.add(idx), frame);
511 }
512 });
513
514 tims_frames
515 })
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 fn one_frame_builder() -> TimsTofSyntheticsPrecursorFrameBuilder {
529 let mut frame_to_abundances = BTreeMap::new();
530 frame_to_abundances.insert(1u32, (vec![10u32, 11u32], vec![0.8f32, 0.8f32]));
531
532 let mut peptide_to_ions = BTreeMap::new();
533 peptide_to_ions.insert(
534 10u32,
535 (
536 vec![1.0f32],
537 vec![vec![100u32, 101u32]],
538 vec![vec![0.3f32, 0.2f32]], vec![1i8],
540 vec![MzSpectrum::new(vec![500.0], vec![1.0])],
541 ),
542 );
543 peptide_to_ions.insert(
544 11u32,
545 (
546 vec![1.0f32],
547 vec![vec![]], vec![vec![]],
549 vec![1i8],
550 vec![MzSpectrum::new(vec![700.0], vec![1.0])],
551 ),
552 );
553
554 let mut peptide_to_events = BTreeMap::new();
555 peptide_to_events.insert(10u32, 1000.0f32);
556 peptide_to_events.insert(11u32, 1000.0f32);
557
558 let mut frame_to_rt = BTreeMap::new();
559 frame_to_rt.insert(1u32, 60.0f32);
560
561 TimsTofSyntheticsPrecursorFrameBuilder {
562 noise_seed: 0,
563 ions: BTreeMap::new(),
564 peptides: BTreeMap::new(),
565 scans: Vec::new(),
566 frames: Vec::new(),
567 precursor_frame_id_set: HashSet::new(),
568 frame_to_abundances,
569 peptide_to_ions,
570 frame_to_rt,
571 scan_to_mobility: BTreeMap::new(),
572 peptide_to_events,
573 ion_id_to_peptide_charge: BTreeMap::new(),
574 }
575 }
576
577 fn total_intensity(s: &MzSpectrum) -> f64 {
578 s.intensity.iter().sum()
579 }
580
581 #[test]
582 fn astral_marginal_uses_full_mobility_and_recovers_grid_edge_ions() {
583 let b = one_frame_builder();
584
585 let grid = b.precursor_scan_spectrum(1);
588 assert!((total_intensity(&grid) - 400.0).abs() < 1e-2, "grid total {}", total_intensity(&grid));
589 assert_eq!(grid.mz.len(), 1, "grid drops the edge ion (no captured scans)");
590 assert!((grid.mz[0] - 500.0).abs() < 1e-9);
591
592 let marginal = b.precursor_scan_marginal_spectrum(1);
597 assert!((total_intensity(&marginal) - 1600.0).abs() < 1e-2, "marginal total {}", total_intensity(&marginal));
598 assert_eq!(marginal.mz.len(), 2, "marginal keeps both ions");
599 assert!(marginal.mz.iter().any(|&m| (m - 700.0).abs() < 1e-9), "edge ion recovered");
600
601 assert!(total_intensity(&marginal) > total_intensity(&grid));
605
606 let RenderedEvent::Scan { ms_level, isolation, spectrum, .. } =
608 b.render_precursor_scan(1, DataMode::Profile)
609 else {
610 panic!("MS1 render must be a Scan");
611 };
612 assert_eq!(ms_level, 1);
613 assert!(isolation.is_none());
614 assert_eq!(spectrum.intensity, *marginal.intensity);
615 assert_eq!(spectrum.mz, *marginal.mz);
616 }
617}