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 {
165            mz: filtered_mz,
166            intensity: filtered_intensity,
167        }
168    }
169
170    /// Transmit an annotated spectrum given a frame id and scan id
171    ///
172    /// Arguments:
173    ///
174    /// * `frame_id` - frame id
175    /// * `scan_id` - scan id
176    /// * `spectrum` - MzSpectrumAnnotated
177    /// * `min_probability` - minimum probability for transmission
178    ///
179    /// Returns:
180    ///
181    /// * `MzSpectrumAnnotated` - transmitted spectrum
182    ///
183    fn transmit_annotated_spectrum(&self, frame_id: i32, scan_id: i32, spectrum: MzSpectrumAnnotated, min_probability: Option<f64>) -> MzSpectrumAnnotated {
184        let probability_cutoff = min_probability.unwrap_or(0.5);
185        let transmission_probability = self.apply_transmission(frame_id, scan_id, &spectrum.mz);
186
187        let mut filtered_mz = Vec::new();
188        let mut filtered_intensity = Vec::new();
189        let mut filtered_annotation = Vec::new();
190
191        // zip mz and intensity with transmission probability and filter out all mz values with transmission probability 0.5
192        for (i, (mz, intensity, annotation)) in izip!(spectrum.mz.iter(), spectrum.intensity.iter(), spectrum.annotations.iter()).enumerate() {
193            if transmission_probability[i] > probability_cutoff {
194                filtered_mz.push(*mz);
195                filtered_intensity.push(*intensity* transmission_probability[i]);
196                filtered_annotation.push(annotation.clone());
197            }
198        }
199
200        MzSpectrumAnnotated {
201            mz: filtered_mz,
202            intensity: filtered_intensity,
203            annotations: filtered_annotation,
204        }
205    }
206
207    fn transmit_ion(&self, frame_ids: Vec<i32>, scan_ids: Vec<i32>, spec: MzSpectrum, min_proba: Option<f64>) -> Vec<Vec<MzSpectrum>> {
208
209        let mut result: Vec<Vec<MzSpectrum>> = Vec::new();
210
211        for frame_id in frame_ids.iter() {
212            let mut frame_result: Vec<MzSpectrum> = Vec::new();
213            for scan_id in scan_ids.iter() {
214                let transmitted_spectrum = self.transmit_spectrum(*frame_id, *scan_id, spec.clone(), min_proba);
215                frame_result.push(transmitted_spectrum);
216            }
217            result.push(frame_result);
218        }
219        result
220    }
221
222    /// Get all ions in a frame that are transmitted
223    ///
224    /// Arguments:
225    ///
226    /// * `frame_id` - frame id
227    /// * `scan_id` - scan id
228    /// * `mz` - mz values
229    /// * `min_proba` - minimum probability for transmission
230    ///
231    /// Returns:
232    ///
233    /// * `HashSet<usize>` - indices of transmitted mz values
234    ///
235    fn get_transmission_set(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>, min_proba: Option<f64>) -> HashSet<usize> {
236        // go over enumerated mz and push all indices with transmission probability > min_proba to a set
237        let probability_cutoff = min_proba.unwrap_or(0.5);
238        let transmission_probability = self.apply_transmission(frame_id, scan_id, mz);
239        mz.iter().enumerate().filter(|&(i, _)| transmission_probability[i] > probability_cutoff).map(|(i, _)| i).collect()
240    }
241
242    /// Check if all mz values in a given collection are transmitted
243    ///
244    /// Arguments:
245    ///
246    /// * `frame_id` - frame id
247    /// * `scan_id` - scan id
248    /// * `mz` - mz values
249    /// * `min_proba` - minimum probability for transmission
250    ///
251    /// Returns:
252    ///
253    /// * `bool` - true if all mz values are transmitted
254    ///
255    fn all_transmitted(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>, min_proba: Option<f64>) -> bool {
256        let probability_cutoff = min_proba.unwrap_or(0.5);
257        let transmission_probability = self.apply_transmission(frame_id, scan_id, mz);
258        transmission_probability.iter().all(|&p| p > probability_cutoff)
259    }
260
261    /// Check if a single mz value is transmitted
262    ///
263    /// Arguments:
264    ///
265    /// * `frame_id` - frame id
266    /// * `scan_id` - scan id
267    /// * `mz` - mz value
268    /// * `min_proba` - minimum probability for transmission
269    ///
270    /// Returns:
271    ///
272    /// * `bool` - true if mz value is transmitted
273    ///
274    fn is_transmitted(&self, frame_id: i32, scan_id: i32, mz: f64, min_proba: Option<f64>) -> bool {
275        let probability_cutoff = min_proba.unwrap_or(0.5);
276        let transmission_probability = self.apply_transmission(frame_id, scan_id, &vec![mz]);
277        transmission_probability[0] > probability_cutoff
278    }
279
280    /// Check if any mz value is transmitted, can be used to check if one peak of isotopic envelope is transmitted
281    ///
282    /// Arguments:
283    ///
284    /// * `frame_id` - frame id
285    /// * `scan_id` - scan id
286    /// * `mz` - mz values
287    /// * `min_proba` - minimum probability for transmission
288    ///
289    /// Returns:
290    ///
291    /// * `bool` - true if any mz value is transmitted
292    ///
293    fn any_transmitted(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>, min_proba: Option<f64>) -> bool {
294        let probability_cutoff = min_proba.unwrap_or(0.5);
295        let transmission_probability = self.apply_transmission(frame_id, scan_id, mz);
296        transmission_probability.iter().any(|&p| p > probability_cutoff)
297    }
298
299    /// Transmit a frame given a diaPASEF transmission layout
300    fn transmit_tims_frame(&self, frame: &TimsFrame, min_probability: Option<f64>) -> TimsFrame {
301        let spectra = frame.to_tims_spectra();
302        let mut filtered_spectra = Vec::new();
303
304        for mut spectrum in spectra {
305            let filtered_spectrum = self.transmit_spectrum(frame.frame_id, spectrum.scan, spectrum.spectrum.mz_spectrum, min_probability);
306            if filtered_spectrum.mz.len() > 0 {
307                spectrum.spectrum.mz_spectrum = filtered_spectrum;
308                filtered_spectra.push(spectrum);
309            }
310        }
311
312        if  filtered_spectra.len() > 0 {
313            TimsFrame::from_tims_spectra(filtered_spectra)
314        } else {
315            TimsFrame::new(
316                frame.frame_id,
317                frame.ms_type.clone(),
318                0.0,
319                vec![],
320                vec![],
321                vec![],
322                vec![],
323                vec![]
324            )
325        }
326    }
327
328    /// Transmit a frame given a diaPASEF transmission layout with annotations
329    ///
330    /// Arguments:
331    ///
332    /// * `frame` - TimsFrameAnnotated
333    /// * `min_probability` - minimum probability for transmission
334    ///
335    /// Returns:
336    ///
337    /// * `TimsFrameAnnotated` - transmitted frame
338    ///
339    fn transmit_tims_frame_annotated(&self, frame: &TimsFrameAnnotated, min_probability: Option<f64>) -> TimsFrameAnnotated {
340        let spectra = frame.to_tims_spectra_annotated();
341        let mut filtered_spectra = Vec::new();
342
343        for mut spectrum in spectra {
344            let filtered_spectrum = self.transmit_annotated_spectrum(frame.frame_id, spectrum.scan as i32, spectrum.spectrum.clone(), min_probability);
345            if filtered_spectrum.mz.len() > 0 {
346                spectrum.spectrum = filtered_spectrum;
347                filtered_spectra.push(spectrum);
348            }
349        }
350
351        if  filtered_spectra.len() > 0 {
352            TimsFrameAnnotated::from_tims_spectra_annotated(filtered_spectra)
353        } else {
354            TimsFrameAnnotated::new(
355                frame.frame_id,
356                frame.retention_time,
357                frame.ms_type.clone(),
358                vec![],
359                vec![],
360                vec![],
361                vec![],
362                vec![],
363                vec![]
364            )
365        }
366    }
367
368    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)>) {
369
370        let probability_cutoff = min_probability.unwrap_or(0.5);
371        let transmission_probability = self.apply_transmission(frame_id, scan_id, &isotopic_envelope);
372        let mut result: Vec<(f64, f64)> = Vec::new();
373
374        for (mz, p) in isotopic_envelope.iter().zip(transmission_probability.iter()) {
375            if *p > probability_cutoff {
376                result.push((*mz - mz_mono, *p));
377            }
378        }
379
380        (mz_mono, result)
381    }
382}
383
384#[derive(Clone, Debug)]
385pub struct TimsTransmissionDIA {
386    frame_to_window_group: HashMap<i32, i32>,
387    window_group_settings: HashMap<(i32, i32), (f64, f64)>,
388    k: f64,
389}
390
391impl TimsTransmissionDIA {
392    pub fn new(
393        frame: Vec<i32>,
394        frame_window_group: Vec<i32>,
395        window_group: Vec<i32>,
396        scan_start: Vec<i32>,
397        scan_end: Vec<i32>,
398        isolation_mz: Vec<f64>,
399        isolation_width: Vec<f64>,
400        k: Option<f64>,
401    ) -> Self {
402        // hashmap from frame to window group
403        let frame_to_window_group = frame.iter().zip(frame_window_group.iter()).map(|(&f, &wg)| (f, wg)).collect::<HashMap<i32, i32>>();
404        let mut window_group_settings: HashMap<(i32, i32), (f64, f64)> = HashMap::new();
405
406        for (index, &wg) in window_group.iter().enumerate() {
407            let scan_start = scan_start[index];
408            let scan_end = scan_end[index];
409            let isolation_mz = isolation_mz[index];
410            let isolation_width = isolation_width[index];
411
412            let value = (isolation_mz, isolation_width);
413
414            for scan in scan_start..scan_end + 1 {
415                let key = (wg, scan);
416                window_group_settings.insert(key, value);
417            }
418        }
419
420        Self {
421            frame_to_window_group,
422            window_group_settings,
423            k: k.unwrap_or(15.0),
424        }
425    }
426
427    pub fn frame_to_window_group(&self, frame_id: i32) -> i32 {
428        let window_group = self.frame_to_window_group.get(&frame_id);
429        match window_group {
430            Some(&wg) => wg,
431            None => -1,
432        }
433    }
434
435    pub fn get_setting(&self, window_group: i32, scan_id: i32) -> Option<&(f64, f64)> {
436        let setting = self.window_group_settings.get(&(window_group, scan_id));
437        match setting {
438            Some(s) => Some(s),
439            None => None,
440        }
441    }
442
443    // check if a frame is a precursor frame
444    pub fn is_precursor(&self, frame_id: i32) -> bool {
445        // if frame id is in the hashmap, it is not a precursor frame
446        match self.frame_to_window_group.contains_key(&frame_id) {
447            true => false,
448            false => true,
449        }
450    }
451}
452
453impl IonTransmission for TimsTransmissionDIA {
454    fn apply_transmission(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>) -> Vec<f64> {
455
456        let setting = self.get_setting(self.frame_to_window_group(frame_id), scan_id);
457        let is_precursor = self.is_precursor(frame_id);
458
459        match setting {
460            Some((isolation_mz, isolation_width)) => {
461                apply_transmission(*isolation_mz, *isolation_width, self.k, mz.clone())
462            },
463            None => match is_precursor {
464                true => vec![1.0; mz.len()],
465                false => vec![0.0; mz.len()],
466            }
467        }
468    }
469}
470
471#[derive(Clone, Debug)]
472pub struct PASEFMeta {
473    pub frame: i32,
474    pub scan_start: i32,
475    pub scan_end: i32,
476    pub isolation_mz: f64,
477    pub isolation_width: f64,
478    pub collision_energy: f64,
479    pub precursor: i32,
480}
481
482impl PASEFMeta {
483    pub fn new(frame: i32, scan_start: i32, scan_end: i32, isolation_mz: f64, isolation_width: f64, collision_energy: f64, precursor: i32) -> Self {
484        Self {
485            frame,
486            scan_start,
487            scan_end,
488            isolation_mz,
489            isolation_width,
490            collision_energy,
491            precursor,
492        }
493    }
494}
495
496#[derive(Clone, Debug)]
497pub struct TimsTransmissionDDA {
498    // frame id to corresponding pasef meta data
499    pub pasef_meta: BTreeMap<i32, Vec<PASEFMeta>>,
500    pub k: f64,
501}
502
503impl TimsTransmissionDDA {
504    pub fn new(pasef_meta: Vec<PASEFMeta>, k: Option<f64>) -> Self {
505        let mut pasef_map: BTreeMap<i32, Vec<PASEFMeta>> = BTreeMap::new();
506        for meta in pasef_meta {
507            let entry = pasef_map.entry(meta.frame).or_insert(Vec::new());
508            entry.push(meta);
509        }
510        Self {
511            pasef_meta: pasef_map,
512            k: k.unwrap_or(15.0),
513        }
514    }
515
516    pub fn get_collision_energy(&self, frame_id: i32, scan_id: i32) -> Option<f64> {
517        let frame_meta = self.pasef_meta.get(&frame_id);
518        match frame_meta {
519            Some(meta) => {
520                for m in meta {
521                    if scan_id >= m.scan_start && scan_id <= m.scan_end {
522                        return Some(m.collision_energy);
523                    }
524                }
525                None
526            },
527            None => None,
528        }
529    }
530}
531
532impl IonTransmission for TimsTransmissionDDA {
533    fn apply_transmission(&self, frame_id: i32, scan_id: i32, mz: &Vec<f64>) -> Vec<f64> {
534
535        // get all selections for a frame, if frame is not in the PASEF metadata, no ions are transmitted
536        let meta = self.pasef_meta.get(&frame_id);
537
538        match meta {
539            Some(meta) => {
540                let mut transmission = vec![0.0; mz.len()];
541
542                for m in meta {
543                    // check if scan id is in the range of the selection
544                    if scan_id >= m.scan_start && scan_id <= m.scan_end {
545                        // apply transmission function to mz values
546                        let transmission_prob = apply_transmission(m.isolation_mz, m.isolation_width, self.k, mz.clone());
547                        // make sure that the transmission probability is not lower than the previous one
548                        for (i, p) in transmission_prob.iter().enumerate() {
549                            transmission[i] = p.max(transmission[i]);
550                        }
551                    }
552                }
553                transmission
554            },
555            // if frame is not in the metadata, no ions are transmitted
556            None => vec![0.0; mz.len()],
557        }
558    }
559}