mscore/timstof/
quadrupole.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::f64;
3use std::f64::consts::E;
4use itertools::izip;
5use crate::data::spectrum::MzSpectrum;
6use crate::simulation::annotation::{MzSpectrumAnnotated, TimsFrameAnnotated};
7use crate::timstof::frame::TimsFrame;
8
9/// Sigmoid step function for quadrupole selection simulation
10///
11/// Arguments:
12///
13/// * `x` - mz values
14/// * `up_start` - start of the step
15/// * `up_end` - end of the step
16/// * `k` - steepness of the step
17///
18/// Returns:
19///
20/// * `Vec<f64>` - transmission probability for each mz value
21///
22/// # Examples
23///
24/// ```
25/// use mscore::timstof::quadrupole::smooth_step;
26///
27/// let mz = vec![100.0, 200.0, 300.0];
28/// let transmission = smooth_step(&mz, 150.0, 250.0, 0.5).iter().map(
29/// |&x| (x * 100.0).round() / 100.0).collect::<Vec<f64>>();
30/// assert_eq!(transmission, vec![0.0, 0.5, 1.0]);
31/// ```
32pub fn smooth_step(x: &Vec<f64>, up_start: f64, up_end: f64, k: f64) -> Vec<f64> {
33    let m = (up_start + up_end) / 2.0;
34    x.iter().map(|&xi| 1.0 / (1.0 + E.powf(-k * (xi - m)))).collect()
35}
36
37/// Sigmoide step function for quadrupole selection simulation
38///
39/// Arguments:
40///
41/// * `x` - mz values
42/// * `up_start` - start of the step up
43/// * `up_end` - end of the step up
44/// * `down_start` - start of the step down
45/// * `down_end` - end of the step down
46/// * `k` - steepness of the step
47///
48/// Returns:
49///
50/// * `Vec<f64>` - transmission probability for each mz value
51///
52/// # Examples
53///
54/// ```
55/// use mscore::timstof::quadrupole::smooth_step_up_down;
56///
57/// let mz = vec![100.0, 200.0, 300.0];
58/// let transmission = smooth_step_up_down(&mz, 150.0, 200.0, 250.0, 300.0, 0.5).iter().map(
59/// |&x| (x * 100.0).round() / 100.0).collect::<Vec<f64>>();
60/// assert_eq!(transmission, vec![0.0, 1.0, 0.0]);
61/// ```
62pub fn smooth_step_up_down(x: &Vec<f64>, up_start: f64, up_end: f64, down_start: f64, down_end: f64, k: f64) -> Vec<f64> {
63    let step_up = smooth_step(x, up_start, up_end, k);
64    let step_down = smooth_step(x, down_start, down_end, k);
65    step_up.iter().zip(step_down.iter()).map(|(&u, &d)| u - d).collect()
66}
67
68/// Ion transmission function for quadrupole selection simulation
69///
70/// Arguments:
71///
72/// * `midpoint` - center of the step
73/// * `window_length` - length of the step
74/// * `k` - steepness of the step
75///
76/// Returns:
77///
78/// * `impl Fn(Vec<f64>) -> Vec<f64>` - ion transmission function
79///
80/// # Examples
81///
82/// ```
83/// use mscore::timstof::quadrupole::ion_transition_function_midpoint;
84///
85/// let ion_transmission = ion_transition_function_midpoint(150.0, 50.0, 1.0);
86/// let mz = vec![100.0, 150.0, 160.0];
87/// let transmission = ion_transmission(mz).iter().map(
88/// |&x| (x * 100.0).round() / 100.0).collect::<Vec<f64>>();
89/// assert_eq!(transmission, vec![0.0, 1.0, 1.0]);
90/// ```
91pub fn ion_transition_function_midpoint(midpoint: f64, window_length: f64, k: f64) -> impl Fn(Vec<f64>) -> Vec<f64> {
92    let half_window = window_length / 2.0;
93
94    let up_start = midpoint - half_window - 0.5;
95    let up_end = midpoint - half_window;
96    let down_start = midpoint + half_window;
97    let down_end = midpoint + half_window + 0.5;
98
99    // take a vector of mz values to their transmission probability
100    move |mz: Vec<f64>| -> Vec<f64> {
101        smooth_step_up_down(&mz, up_start, up_end, down_start, down_end, k)
102    }
103}
104
105/// Apply ion transmission function to mz values
106///
107/// Arguments:
108///
109/// * `midpoint` - center of the step
110/// * `window_length` - length of the step
111/// * `k` - steepness of the step
112/// * `mz` - mz values
113///
114/// Returns:
115///
116/// * `Vec<f64>` - transmission probability for each mz value
117///
118/// # Examples
119///
120/// ```
121/// use mscore::timstof::quadrupole::apply_transmission;
122///
123/// let mz = vec![100.0, 150.0, 160.0];
124/// let transmission = apply_transmission(150.0, 50.0, 1.0, mz).iter().map(
125/// |&x| (x * 100.0).round() / 100.0).collect::<Vec<f64>>();
126/// assert_eq!(transmission, vec![0.0, 1.0, 1.0]);
127/// ```
128pub fn apply_transmission(midpoint: f64, window_length: f64, k: f64, mz: Vec<f64>) -> Vec<f64> {
129    ion_transition_function_midpoint(midpoint, window_length, k)(mz)
130}
131
132pub trait IonTransmission {
133    fn apply_transmission(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>) -> Vec<f64>;
134
135    /// Transmit a spectrum given a frame id and scan id
136    ///
137    /// Arguments:
138    ///
139    /// * `frame_id` - frame id
140    /// * `scan_id` - scan id
141    /// * `spectrum` - MzSpectrum
142    /// * `min_probability` - minimum probability for transmission
143    ///
144    /// Returns:
145    ///
146    /// * `MzSpectrum` - transmitted spectrum
147    ///
148    fn transmit_spectrum(&self, frame_id: i32, scan_id: i32, spectrum: MzSpectrum, min_probability: Option<f64>) -> MzSpectrum {
149
150        let probability_cutoff = min_probability.unwrap_or(0.5);
151        let transmission_probability = self.apply_transmission(frame_id, scan_id, &spectrum.mz);
152
153        let mut filtered_mz = Vec::new();
154        let mut filtered_intensity = Vec::new();
155
156        // zip mz and intensity with transmission probability and filter out all mz values with transmission probability 0.001
157        for (i, (mz, intensity)) in spectrum.mz.iter().zip(spectrum.intensity.iter()).enumerate() {
158            if transmission_probability[i] > probability_cutoff {
159                filtered_mz.push(*mz);
160                filtered_intensity.push(*intensity* transmission_probability[i]);
161            }
162        }
163
164        MzSpectrum::new(filtered_mz, filtered_intensity)
165    }
166
167    /// Transmit an annotated spectrum given a frame id and scan id
168    ///
169    /// Arguments:
170    ///
171    /// * `frame_id` - frame id
172    /// * `scan_id` - scan id
173    /// * `spectrum` - MzSpectrumAnnotated
174    /// * `min_probability` - minimum probability for transmission
175    ///
176    /// Returns:
177    ///
178    /// * `MzSpectrumAnnotated` - transmitted spectrum
179    ///
180    fn transmit_annotated_spectrum(&self, frame_id: i32, scan_id: i32, spectrum: MzSpectrumAnnotated, min_probability: Option<f64>) -> MzSpectrumAnnotated {
181        let probability_cutoff = min_probability.unwrap_or(0.5);
182        let transmission_probability = self.apply_transmission(frame_id, scan_id, &spectrum.mz);
183
184        let mut filtered_mz = Vec::new();
185        let mut filtered_intensity = Vec::new();
186        let mut filtered_annotation = Vec::new();
187
188        // zip mz and intensity with transmission probability and filter out all mz values with transmission probability 0.5
189        for (i, (mz, intensity, annotation)) in izip!(spectrum.mz.iter(), spectrum.intensity.iter(), spectrum.annotations.iter()).enumerate() {
190            if transmission_probability[i] > probability_cutoff {
191                filtered_mz.push(*mz);
192                filtered_intensity.push(*intensity* transmission_probability[i]);
193                filtered_annotation.push(annotation.clone());
194            }
195        }
196
197        MzSpectrumAnnotated {
198            mz: filtered_mz,
199            intensity: filtered_intensity,
200            annotations: filtered_annotation,
201        }
202    }
203
204    fn transmit_ion(&self, frame_ids: Vec<i32>, scan_ids: Vec<i32>, spec: MzSpectrum, min_proba: Option<f64>) -> Vec<Vec<MzSpectrum>> {
205
206        let mut result: Vec<Vec<MzSpectrum>> = Vec::new();
207
208        for frame_id in frame_ids.iter() {
209            let mut frame_result: Vec<MzSpectrum> = Vec::new();
210            for scan_id in scan_ids.iter() {
211                let transmitted_spectrum = self.transmit_spectrum(*frame_id, *scan_id, spec.clone(), min_proba);
212                frame_result.push(transmitted_spectrum);
213            }
214            result.push(frame_result);
215        }
216        result
217    }
218
219    /// Get all ions in a frame that are transmitted
220    ///
221    /// Arguments:
222    ///
223    /// * `frame_id` - frame id
224    /// * `scan_id` - scan id
225    /// * `mz` - mz values
226    /// * `min_proba` - minimum probability for transmission
227    ///
228    /// Returns:
229    ///
230    /// * `HashSet<usize>` - indices of transmitted mz values
231    ///
232    fn get_transmission_set(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>, min_proba: Option<f64>) -> HashSet<usize> {
233        // go over enumerated mz and push all indices with transmission probability > min_proba to a set
234        let probability_cutoff = min_proba.unwrap_or(0.5);
235        let transmission_probability = self.apply_transmission(frame_id, scan_id, mz);
236        mz.iter().enumerate().filter(|&(i, _)| transmission_probability[i] > probability_cutoff).map(|(i, _)| i).collect()
237    }
238
239    /// Check if all mz values in a given collection are transmitted
240    ///
241    /// Arguments:
242    ///
243    /// * `frame_id` - frame id
244    /// * `scan_id` - scan id
245    /// * `mz` - mz values
246    /// * `min_proba` - minimum probability for transmission
247    ///
248    /// Returns:
249    ///
250    /// * `bool` - true if all mz values are transmitted
251    ///
252    fn all_transmitted(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>, min_proba: Option<f64>) -> bool {
253        let probability_cutoff = min_proba.unwrap_or(0.5);
254        let transmission_probability = self.apply_transmission(frame_id, scan_id, mz);
255        transmission_probability.iter().all(|&p| p > probability_cutoff)
256    }
257
258    /// Check if a single mz value is transmitted
259    ///
260    /// Arguments:
261    ///
262    /// * `frame_id` - frame id
263    /// * `scan_id` - scan id
264    /// * `mz` - mz value
265    /// * `min_proba` - minimum probability for transmission
266    ///
267    /// Returns:
268    ///
269    /// * `bool` - true if mz value is transmitted
270    ///
271    fn is_transmitted(&self, frame_id: i32, scan_id: i32, mz: f64, min_proba: Option<f64>) -> bool {
272        let probability_cutoff = min_proba.unwrap_or(0.5);
273        let transmission_probability = self.apply_transmission(frame_id, scan_id, &vec![mz]);
274        transmission_probability[0] > probability_cutoff
275    }
276
277    /// Check if any mz value is transmitted, can be used to check if one peak of isotopic envelope is transmitted
278    ///
279    /// Arguments:
280    ///
281    /// * `frame_id` - frame id
282    /// * `scan_id` - scan id
283    /// * `mz` - mz values
284    /// * `min_proba` - minimum probability for transmission
285    ///
286    /// Returns:
287    ///
288    /// * `bool` - true if any mz value is transmitted
289    ///
290    fn any_transmitted(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>, min_proba: Option<f64>) -> bool {
291        let probability_cutoff = min_proba.unwrap_or(0.5);
292        let transmission_probability = self.apply_transmission(frame_id, scan_id, mz);
293        transmission_probability.iter().any(|&p| p > probability_cutoff)
294    }
295
296    /// Transmit a frame given a diaPASEF transmission layout
297    fn transmit_tims_frame(&self, frame: &TimsFrame, min_probability: Option<f64>) -> TimsFrame {
298        let spectra = frame.to_tims_spectra();
299        let mut filtered_spectra = Vec::new();
300
301        for mut spectrum in spectra {
302            let filtered_spectrum = self.transmit_spectrum(frame.frame_id, spectrum.scan, spectrum.spectrum.mz_spectrum, min_probability);
303            if filtered_spectrum.mz.len() > 0 {
304                spectrum.spectrum.mz_spectrum = filtered_spectrum;
305                filtered_spectra.push(spectrum);
306            }
307        }
308
309        if  filtered_spectra.len() > 0 {
310            TimsFrame::from_tims_spectra(filtered_spectra)
311        } else {
312            TimsFrame::new(
313                frame.frame_id,
314                frame.ms_type.clone(),
315                0.0,
316                vec![],
317                vec![],
318                vec![],
319                vec![],
320                vec![]
321            )
322        }
323    }
324
325    /// Transmit a frame given a diaPASEF transmission layout with annotations
326    ///
327    /// Arguments:
328    ///
329    /// * `frame` - TimsFrameAnnotated
330    /// * `min_probability` - minimum probability for transmission
331    ///
332    /// Returns:
333    ///
334    /// * `TimsFrameAnnotated` - transmitted frame
335    ///
336    fn transmit_tims_frame_annotated(&self, frame: &TimsFrameAnnotated, min_probability: Option<f64>) -> TimsFrameAnnotated {
337        let spectra = frame.to_tims_spectra_annotated();
338        let mut filtered_spectra = Vec::new();
339
340        for mut spectrum in spectra {
341            let filtered_spectrum = self.transmit_annotated_spectrum(frame.frame_id, spectrum.scan as i32, spectrum.spectrum.clone(), min_probability);
342            if filtered_spectrum.mz.len() > 0 {
343                spectrum.spectrum = filtered_spectrum;
344                filtered_spectra.push(spectrum);
345            }
346        }
347
348        if  filtered_spectra.len() > 0 {
349            TimsFrameAnnotated::from_tims_spectra_annotated(filtered_spectra)
350        } else {
351            TimsFrameAnnotated::new(
352                frame.frame_id,
353                frame.retention_time,
354                frame.ms_type.clone(),
355                vec![],
356                vec![],
357                vec![],
358                vec![],
359                vec![],
360                vec![]
361            )
362        }
363    }
364
365    fn isotopes_transmitted(&self, frame_id: i32, scan_id: i32, mz_mono: f64, isotopic_envelope: &Vec<f64>, min_probability: Option<f64>) -> (f64, Vec<(f64, f64)>) {
366
367        let probability_cutoff = min_probability.unwrap_or(0.5);
368        let transmission_probability = self.apply_transmission(frame_id, scan_id, &isotopic_envelope);
369        let mut result: Vec<(f64, f64)> = Vec::new();
370
371        for (mz, p) in isotopic_envelope.iter().zip(transmission_probability.iter()) {
372            if *p > probability_cutoff {
373                result.push((*mz - mz_mono, *p));
374            }
375        }
376
377        (mz_mono, result)
378    }
379}
380
381#[derive(Clone, Debug)]
382pub struct TimsTransmissionDIA {
383    frame_to_window_group: HashMap<i32, i32>,
384    window_group_settings: HashMap<(i32, i32), (f64, f64)>,
385    k: f64,
386}
387
388impl TimsTransmissionDIA {
389    pub fn new(
390        frame: Vec<i32>,
391        frame_window_group: Vec<i32>,
392        window_group: Vec<i32>,
393        scan_start: Vec<i32>,
394        scan_end: Vec<i32>,
395        isolation_mz: Vec<f64>,
396        isolation_width: Vec<f64>,
397        k: Option<f64>,
398    ) -> Self {
399        // hashmap from frame to window group
400        let frame_to_window_group = frame.iter().zip(frame_window_group.iter()).map(|(&f, &wg)| (f, wg)).collect::<HashMap<i32, i32>>();
401        let mut window_group_settings: HashMap<(i32, i32), (f64, f64)> = HashMap::new();
402
403        for (index, &wg) in window_group.iter().enumerate() {
404            let scan_start = scan_start[index];
405            let scan_end = scan_end[index];
406            let isolation_mz = isolation_mz[index];
407            let isolation_width = isolation_width[index];
408
409            let value = (isolation_mz, isolation_width);
410
411            for scan in scan_start..scan_end + 1 {
412                let key = (wg, scan);
413                window_group_settings.insert(key, value);
414            }
415        }
416
417        Self {
418            frame_to_window_group,
419            window_group_settings,
420            k: k.unwrap_or(15.0),
421        }
422    }
423
424    pub fn frame_to_window_group(&self, frame_id: i32) -> i32 {
425        let window_group = self.frame_to_window_group.get(&frame_id);
426        match window_group {
427            Some(&wg) => wg,
428            None => -1,
429        }
430    }
431
432    pub fn get_setting(&self, window_group: i32, scan_id: i32) -> Option<&(f64, f64)> {
433        let setting = self.window_group_settings.get(&(window_group, scan_id));
434        match setting {
435            Some(s) => Some(s),
436            None => None,
437        }
438    }
439
440    // check if a frame is a precursor frame
441    pub fn is_precursor(&self, frame_id: i32) -> bool {
442        // if frame id is in the hashmap, it is not a precursor frame
443        match self.frame_to_window_group.contains_key(&frame_id) {
444            true => false,
445            false => true,
446        }
447    }
448}
449
450impl IonTransmission for TimsTransmissionDIA {
451    fn apply_transmission(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>) -> Vec<f64> {
452
453        let setting = self.get_setting(self.frame_to_window_group(frame_id), scan_id);
454        let is_precursor = self.is_precursor(frame_id);
455
456        match setting {
457            Some((isolation_mz, isolation_width)) => {
458                apply_transmission(*isolation_mz, *isolation_width, self.k, mz.clone())
459            },
460            None => match is_precursor {
461                true => vec![1.0; mz.len()],
462                false => vec![0.0; mz.len()],
463            }
464        }
465    }
466}
467
468#[derive(Clone, Debug)]
469pub struct PASEFMeta {
470    pub frame: i32,
471    pub scan_start: i32,
472    pub scan_end: i32,
473    pub isolation_mz: f64,
474    pub isolation_width: f64,
475    pub collision_energy: f64,
476    pub precursor: i32,
477}
478
479impl PASEFMeta {
480    pub fn new(frame: i32, scan_start: i32, scan_end: i32, isolation_mz: f64, isolation_width: f64, collision_energy: f64, precursor: i32) -> Self {
481        Self {
482            frame,
483            scan_start,
484            scan_end,
485            isolation_mz,
486            isolation_width,
487            collision_energy,
488            precursor,
489        }
490    }
491}
492
493#[derive(Clone, Debug)]
494pub struct TimsTransmissionDDA {
495    // frame id to corresponding pasef meta data
496    pub pasef_meta: BTreeMap<i32, Vec<PASEFMeta>>,
497    pub k: f64,
498}
499
500impl TimsTransmissionDDA {
501    pub fn new(pasef_meta: Vec<PASEFMeta>, k: Option<f64>) -> Self {
502        let mut pasef_map: BTreeMap<i32, Vec<PASEFMeta>> = BTreeMap::new();
503        for meta in pasef_meta {
504            let entry = pasef_map.entry(meta.frame).or_insert(Vec::new());
505            entry.push(meta);
506        }
507        Self {
508            pasef_meta: pasef_map,
509            k: k.unwrap_or(15.0),
510        }
511    }
512
513    pub fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> Option<f64> {
514        let frame_meta = self.pasef_meta.get(&frame_id);
515        match frame_meta {
516            Some(meta) => {
517                for m in meta {
518                    if scan_id >= m.scan_start && scan_id <= m.scan_end {
519                        return Some(m.collision_energy);
520                    }
521                }
522                None
523            },
524            None => None,
525        }
526    }
527
528    /// Get all (frame_id, collision_energy) pairs where a specific precursor (ion_id) was selected.
529    /// This uses the explicit precursor selection from pasef_meta rather than m/z matching.
530    pub fn get_selections_for_precursor(&self, precursor_id: i32) -> Vec<(i32, f64)> {
531        let mut selections = Vec::new();
532        for (frame_id, meta_list) in &self.pasef_meta {
533            for meta in meta_list {
534                if meta.precursor == precursor_id {
535                    selections.push((*frame_id, meta.collision_energy));
536                }
537            }
538        }
539        selections
540    }
541}
542
543impl IonTransmission for TimsTransmissionDDA {
544    fn apply_transmission(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>) -> Vec<f64> {
545
546        // get all selections for a frame, if frame is not in the PASEF metadata, no ions are transmitted
547        let meta = self.pasef_meta.get(&frame_id);
548
549        match meta {
550            Some(meta) => {
551                let mut transmission = vec![0.0; mz.len()];
552
553                for m in meta {
554                    // check if scan id is in the range of the selection
555                    if scan_id >= m.scan_start && scan_id <= m.scan_end {
556                        // apply transmission function to mz values
557                        let transmission_prob = apply_transmission(m.isolation_mz, m.isolation_width, self.k, mz.clone());
558                        // make sure that the transmission probability is not lower than the previous one
559                        for (i, p) in transmission_prob.iter().enumerate() {
560                            transmission[i] = p.max(transmission[i]);
561                        }
562                    }
563                }
564                transmission
565            },
566            // if frame is not in the metadata, no ions are transmitted
567            None => vec![0.0; mz.len()],
568        }
569    }
570}