Skip to main content

mscore/simulation/
annotation.rs

1use std::collections::{BTreeMap, HashMap};
2use std::fmt::Display;
3use itertools::{izip, multizip};
4use rand::distributions::{Uniform, Distribution};
5use rand::Rng;
6use statrs::distribution::Normal;
7use crate::data::spectrum::{MsType, ToResolution, Vectorized};
8use crate::data::peptide::FragmentType;
9
10#[derive(Clone, Debug)]
11pub struct PeakAnnotation {
12    pub contributions: Vec<ContributionSource>,
13}
14
15impl PeakAnnotation {
16    pub fn new_random_noise(intensity: f64) -> Self {
17        let contribution_source = ContributionSource {
18            intensity_contribution: intensity,
19            source_type: SourceType::RandomNoise,
20            signal_attributes: None,
21        };
22
23        PeakAnnotation {
24            contributions: vec![contribution_source],
25        }
26    }
27}
28
29
30#[derive(Clone, Debug)]
31pub struct ContributionSource {
32    pub intensity_contribution: f64,
33    pub source_type: SourceType,
34    pub signal_attributes: Option<SignalAttributes>,
35}
36
37#[derive(Clone, Debug, PartialEq)]
38pub enum SourceType {
39    Signal,
40    ChemicalNoise,
41    RandomNoise,
42    Unknown,
43}
44
45impl SourceType {
46    pub fn new(source_type: i32) -> Self {
47        match source_type {
48            0 => SourceType::Signal,
49            1 => SourceType::ChemicalNoise,
50            2 => SourceType::RandomNoise,
51            3 => SourceType::Unknown,
52            _ => panic!("Invalid source type"),
53        }
54    }
55}
56
57impl Display for SourceType {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            SourceType::Signal => write!(f, "Signal"),
61            SourceType::ChemicalNoise => write!(f, "ChemicalNoise"),
62            SourceType::RandomNoise => write!(f, "RandomNoise"),
63            SourceType::Unknown => write!(f, "Unknown"),
64        }
65    }
66}
67
68#[derive(Clone, Debug)]
69pub struct SignalAttributes {
70    pub charge_state: i32,
71    pub peptide_id: i32,
72    pub isotope_peak: i32,
73    /// Fragment-ion kind (b/y/...) this peak originates from, when known.
74    /// Stored as a small `Copy` enum rather than baked into a per-peak string,
75    /// which is what makes the annotated builder cheap while keeping the peak's
76    /// full fragment identity (see `description`).
77    pub fragment_kind: Option<FragmentType>,
78    /// 1-based fragment-ion ordinal (the ion number, e.g. 5 for b5/y5), when known.
79    pub fragment_ordinal: Option<i32>,
80}
81
82impl SignalAttributes {
83    /// Human-readable fragment label `"{kind}_{ordinal}_{isotope}"` (e.g. `b_5_1`),
84    /// derived on demand from the structured fields. Returns `None` unless both the
85    /// fragment kind and ordinal are known. This reproduces the string that used to
86    /// be eagerly allocated per peak, without paying for it during frame building.
87    pub fn description(&self) -> Option<String> {
88        match (self.fragment_kind, self.fragment_ordinal) {
89            (Some(kind), Some(ordinal)) => {
90                Some(format!("{}_{}_{}", kind, ordinal, self.isotope_peak))
91            }
92            _ => None,
93        }
94    }
95}
96
97#[derive(Clone, Debug)]
98pub struct MzSpectrumAnnotated {
99    pub mz: Vec<f64>,
100    pub intensity: Vec<f64>,
101    pub annotations: Vec<PeakAnnotation>,
102}
103
104impl MzSpectrumAnnotated {
105    pub fn new(mz: Vec<f64>, intensity: Vec<f64>, annotations: Vec<PeakAnnotation>) -> Self {
106        assert!(mz.len() == intensity.len() && intensity.len() == annotations.len());
107        // Zip by value (moving the annotations) and sort by mz. The previous
108        // implementation cloned every PeakAnnotation twice here (once into the
109        // sort buffer, once back out); moving avoids both clones, which matters
110        // for the annotated frame builder that constructs millions of these.
111        // `sort_by` is stable, so the ordering of equal-mz peaks is unchanged.
112        let mut triples: Vec<(f64, f64, PeakAnnotation)> = izip!(mz, intensity, annotations).collect();
113        triples.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
114
115        let mut mz = Vec::with_capacity(triples.len());
116        let mut intensity = Vec::with_capacity(triples.len());
117        let mut annotations = Vec::with_capacity(triples.len());
118        for (m, i, a) in triples {
119            mz.push(m);
120            intensity.push(i);
121            annotations.push(a);
122        }
123
124        MzSpectrumAnnotated { mz, intensity, annotations }
125    }
126
127    pub fn filter_ranged(&self, mz_min: f64, mz_max: f64, intensity_min: f64, intensity_max: f64) -> Self {
128        let mut mz_filtered: Vec<f64> = Vec::new();
129        let mut intensity_filtered: Vec<f64> = Vec::new();
130        let mut annotations_filtered: Vec<PeakAnnotation> = Vec::new();
131
132        for (mz, intensity, annotation) in izip!(self.mz.iter(), self.intensity.iter(), self.annotations.iter()) {
133            if *mz >= mz_min && *mz <= mz_max && *intensity >= intensity_min && *intensity <= intensity_max {
134                mz_filtered.push(*mz);
135                intensity_filtered.push(*intensity);
136                annotations_filtered.push(annotation.clone());
137            }
138        }
139        // after filtering, the length of the mz, intensity and annotations vectors should be the same
140        assert!(mz_filtered.len() == intensity_filtered.len() && intensity_filtered.len() == annotations_filtered.len());
141
142        MzSpectrumAnnotated {
143            mz: mz_filtered,
144            intensity: intensity_filtered,
145            annotations: annotations_filtered,
146        }
147    }
148
149    pub fn add_mz_noise_uniform(&self, ppm: f64, right_drag: bool) -> Self {
150        let mut rng = rand::thread_rng();
151        self.add_mz_noise_uniform_with_rng(ppm, right_drag, &mut rng)
152    }
153
154    /// Reproducible counterpart of `add_mz_noise_uniform`; see `MzSpectrum`.
155    pub fn add_mz_noise_uniform_with_rng<R: Rng>(&self, ppm: f64, right_drag: bool, rng: &mut R) -> Self {
156        self.add_mz_noise(ppm, rng, |rng, mz, ppm| {
157
158            let ppm_mz = match right_drag {
159                true => mz * ppm / 1e6 / 2.0,
160                false => mz * ppm / 1e6,
161            };
162
163            let dist = match right_drag {
164                true => Uniform::from(mz - (ppm_mz / 3.0)..=mz + ppm_mz),
165                false => Uniform::from(mz - ppm_mz..=mz + ppm_mz),
166            };
167
168            dist.sample(rng)
169        })
170    }
171
172    pub fn add_mz_noise_normal(&self, ppm: f64) -> Self {
173        let mut rng = rand::thread_rng();
174        self.add_mz_noise_normal_with_rng(ppm, &mut rng)
175    }
176
177    /// Reproducible counterpart of `add_mz_noise_normal`; see `MzSpectrum`.
178    pub fn add_mz_noise_normal_with_rng<R: Rng>(&self, ppm: f64, rng: &mut R) -> Self {
179        self.add_mz_noise(ppm, rng, |rng, mz, ppm| {
180            let ppm_mz = mz * ppm / 1e6;
181            let dist = Normal::new(mz, ppm_mz / 3.0).unwrap(); // 3 sigma ? good enough?
182            dist.sample(rng)
183        })
184    }
185
186    fn add_mz_noise<R: Rng, F>(&self, ppm: f64, rng: &mut R, noise_fn: F) -> Self
187        where
188            F: Fn(&mut R, f64, f64) -> f64,
189    {
190        let mz: Vec<f64> = self.mz.iter().map(|&mz_value| noise_fn(rng, mz_value, ppm)).collect();
191        let spectrum = MzSpectrumAnnotated { mz, intensity: self.intensity.clone(), annotations: self.annotations.clone()};
192
193        // Sort the spectrum by m/z values and potentially sum up intensities and extend annotations at the same m/z value
194        spectrum.to_resolution(6)
195    }
196
197    pub fn to_windows(&self, window_length: f64, overlapping: bool, min_peaks: usize, min_intensity: f64) -> BTreeMap<i32, MzSpectrumAnnotated> {
198        let mut splits = BTreeMap::new();
199
200        for (i, &mz) in self.mz.iter().enumerate() {
201            let intensity = self.intensity[i];
202            let annotation = self.annotations[i].clone();
203
204            let tmp_key = (mz / window_length).floor() as i32;
205
206            splits.entry(tmp_key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).mz.push(mz);
207            splits.entry(tmp_key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).intensity.push(intensity);
208            splits.entry(tmp_key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).annotations.push(annotation);
209        }
210
211        if overlapping {
212            let mut splits_offset = BTreeMap::new();
213
214            for (i, &mmz) in self.mz.iter().enumerate() {
215                let intensity = self.intensity[i];
216                let annotation = self.annotations[i].clone();
217
218                let tmp_key = -((mmz + window_length / 2.0) / window_length).floor() as i32;
219
220                splits_offset.entry(tmp_key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).mz.push(mmz);
221                splits_offset.entry(tmp_key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).intensity.push(intensity);
222                splits_offset.entry(tmp_key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).annotations.push(annotation);
223            }
224
225            for (key, val) in splits_offset {
226                splits.entry(key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).mz.extend(val.mz);
227                splits.entry(key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).intensity.extend(val.intensity);
228                splits.entry(key).or_insert_with(|| MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())).annotations.extend(val.annotations);
229            }
230        }
231
232        splits.retain(|_, spectrum| {
233            spectrum.mz.len() >= min_peaks && spectrum.intensity.iter().cloned().max_by(
234                |a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(0.0) >= min_intensity
235        });
236
237        splits
238    }
239}
240
241impl std::ops::Add for MzSpectrumAnnotated {
242    type Output = Self;
243    fn add(self, other: Self) -> Self {
244
245        let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
246        let mut spec_map: BTreeMap<i64, (f64, PeakAnnotation)> = BTreeMap::new();
247
248        for ((mz, intensity), annotation) in self.mz.iter().zip(self.intensity.iter()).zip(self.annotations.iter()) {
249            let key = quantize(*mz);
250            spec_map.insert(key, (*intensity, annotation.clone()));
251        }
252
253        for ((mz, intensity), annotation) in other.mz.iter().zip(other.intensity.iter()).zip(other.annotations.iter()) {
254            let key = quantize(*mz);
255            spec_map.entry(key).and_modify(|e| {
256                e.0 += *intensity;
257                e.1.contributions.extend(annotation.contributions.clone());
258            }).or_insert((*intensity, annotation.clone()));
259        }
260
261        let mz: Vec<f64> = spec_map.keys().map(|&key| key as f64 / 1_000_000.0).collect();
262        let intensity: Vec<f64> = spec_map.values().map(|(intensity, _)| *intensity).collect();
263        let annotations: Vec<PeakAnnotation> = spec_map.values().map(|(_, annotation)| annotation.clone()).collect();
264
265        assert!(mz.len() == intensity.len() && intensity.len() == annotations.len());
266
267        MzSpectrumAnnotated {
268            mz,
269            intensity,
270            annotations,
271        }
272    }
273}
274
275impl ToResolution for MzSpectrumAnnotated {
276    fn to_resolution(&self, resolution: i32) -> Self {
277        let mut spec_map: BTreeMap<i64, (f64, PeakAnnotation)> = BTreeMap::new();
278        let quantize = |mz: f64| -> i64 { (mz * 10.0_f64.powi(resolution)).round() as i64 };
279
280        for ((mz, intensity), annotation) in self.mz.iter().zip(self.intensity.iter()).zip(self.annotations.iter()) {
281            let key = quantize(*mz);
282            spec_map.entry(key).and_modify(|e| {
283                e.0 += *intensity;
284                e.1.contributions.extend(annotation.contributions.clone());
285            }).or_insert((*intensity, annotation.clone()));
286        }
287
288        let mz: Vec<f64> = spec_map.keys().map(|&key| key as f64 / 10.0_f64.powi(resolution)).collect();
289        let intensity: Vec<f64> = spec_map.values().map(|(intensity, _)| *intensity).collect();
290        let annotations: Vec<PeakAnnotation> = spec_map.values().map(|(_, annotation)| annotation.clone()).collect();
291
292        assert!(mz.len() == intensity.len() && intensity.len() == annotations.len());
293
294        MzSpectrumAnnotated {
295            mz,
296            intensity,
297            annotations,
298        }
299    }
300}
301
302impl std::ops::Mul<f64> for MzSpectrumAnnotated {
303    type Output = Self;
304    fn mul(self, scale: f64) -> Self::Output{
305
306        let mut scaled_intensities: Vec<f64> = vec![0.0; self.intensity.len()];
307
308        for (idx,intensity) in self.intensity.iter().enumerate(){
309            scaled_intensities[idx] = scale*intensity;
310        }
311
312        let mut scaled_annotations: Vec<PeakAnnotation> = Vec::new();
313
314        for annotation in self.annotations.iter(){
315            let mut scaled_contributions: Vec<ContributionSource> = Vec::new();
316            for contribution in annotation.contributions.iter(){
317                let scaled_intensity = (contribution.intensity_contribution*scale).round();
318                let scaled_contribution = ContributionSource{
319                    intensity_contribution: scaled_intensity,
320                    source_type: contribution.source_type.clone(),
321                    signal_attributes: contribution.signal_attributes.clone(),
322                };
323                scaled_contributions.push(scaled_contribution);
324            }
325            let scaled_annotation = PeakAnnotation{
326                contributions: scaled_contributions,
327            };
328            scaled_annotations.push(scaled_annotation);
329        }
330
331        MzSpectrumAnnotated { mz: self.mz.clone(), intensity: scaled_intensities, annotations: scaled_annotations }
332    }
333}
334
335impl Vectorized<MzSpectrumAnnotatedVectorized> for MzSpectrumAnnotated {
336    fn vectorized(&self, resolution: i32) -> MzSpectrumAnnotatedVectorized {
337
338        let quantize = |mz: f64| -> i64 { (mz * 10.0_f64.powi(resolution)).round() as i64 };
339
340        let binned_spec = self.to_resolution(resolution);
341        let mut indices: Vec<u32> = Vec::with_capacity(binned_spec.mz.len());
342        let mut values: Vec<f64> = Vec::with_capacity(binned_spec.mz.len());
343        let mut annotations: Vec<PeakAnnotation> = Vec::with_capacity(binned_spec.mz.len());
344
345        for (mz, intensity, annotation) in izip!(binned_spec.mz.iter(), binned_spec.intensity.iter(), binned_spec.annotations.iter()) {
346            indices.push(quantize(*mz) as u32);
347            values.push(*intensity);
348            annotations.push(annotation.clone());
349        }
350
351        MzSpectrumAnnotatedVectorized {
352            indices,
353            values,
354            annotations,
355        }
356    }
357}
358
359#[derive(Clone, Debug)]
360pub struct MzSpectrumAnnotatedVectorized {
361    pub indices: Vec<u32>,
362    pub values: Vec<f64>,
363    pub annotations: Vec<PeakAnnotation>,
364}
365
366#[derive(Clone, Debug)]
367pub struct TimsSpectrumAnnotated {
368    pub frame_id: i32,
369    pub scan: u32,
370    pub retention_time: f64,
371    pub mobility: f64,
372    pub ms_type: MsType,
373    pub tof: Vec<u32>,
374    pub spectrum: MzSpectrumAnnotated,
375}
376
377impl TimsSpectrumAnnotated {
378    pub fn new(frame_id: i32, scan: u32, retention_time: f64, mobility: f64, ms_type: MsType, tof: Vec<u32>, spectrum: MzSpectrumAnnotated) -> Self {
379        assert!(tof.len() == spectrum.mz.len() && spectrum.mz.len() == spectrum.intensity.len() && spectrum.intensity.len() == spectrum.annotations.len());
380        // zip and sort by mz
381        let mut mz_intensity_annotations: Vec<(u32, f64, f64, PeakAnnotation)> = izip!(tof.iter(), spectrum.mz.iter(), spectrum.intensity.iter(),
382            spectrum.annotations.iter()).map(|(tof, mz, intensity, annotation)| (*tof, *mz, *intensity, annotation.clone())).collect();
383        mz_intensity_annotations.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
384        TimsSpectrumAnnotated {
385            frame_id,
386            scan,
387            retention_time,
388            mobility,
389            ms_type,
390            tof: mz_intensity_annotations.iter().map(|(tof, _, _, _)| *tof).collect(),
391            spectrum: MzSpectrumAnnotated {
392                mz: mz_intensity_annotations.iter().map(|(_, mz, _, _)| *mz).collect(),
393                intensity: mz_intensity_annotations.iter().map(|(_, _, intensity, _)| *intensity).collect(),
394                annotations: mz_intensity_annotations.iter().map(|(_, _, _, annotation)| annotation.clone()).collect(),
395            },
396        }
397    }
398
399    pub fn filter_ranged(&self, mz_min: f64, mz_max: f64, intensity_min: f64, intensity_max: f64) -> Self {
400        let mut tof_filtered: Vec<u32> = Vec::new();
401        let mut mz_filtered: Vec<f64> = Vec::new();
402        let mut intensity_filtered: Vec<f64> = Vec::new();
403        let mut annotations_filtered: Vec<PeakAnnotation> = Vec::new();
404
405        for (tof, mz, intensity, annotation) in izip!(self.tof.iter(), self.spectrum.mz.iter(), self.spectrum.intensity.iter(), self.spectrum.annotations.iter()) {
406            if *mz >= mz_min && *mz <= mz_max && *intensity >= intensity_min && *intensity <= intensity_max {
407                tof_filtered.push(*tof);
408                mz_filtered.push(*mz);
409                intensity_filtered.push(*intensity);
410                annotations_filtered.push(annotation.clone());
411            }
412        }
413
414        assert!(tof_filtered.len() == mz_filtered.len() && mz_filtered.len() == intensity_filtered.len() && intensity_filtered.len() == annotations_filtered.len());
415
416        TimsSpectrumAnnotated {
417            frame_id: self.frame_id,
418            scan: self.scan,
419            retention_time: self.retention_time,
420            mobility: self.mobility,
421            ms_type: self.ms_type.clone(),
422            tof: tof_filtered,
423            spectrum: MzSpectrumAnnotated::new(mz_filtered, intensity_filtered, annotations_filtered),
424        }
425    }
426
427    pub fn add_mz_noise_uniform(&self, ppm: f64, right_drag: bool) -> Self {
428        TimsSpectrumAnnotated {
429            frame_id: self.frame_id,
430            scan: self.scan,
431            retention_time: self.retention_time,
432            mobility: self.mobility,
433            ms_type: self.ms_type.clone(),
434            // TODO: adding noise to mz means that TOF values need to be re-calculated
435            tof: self.tof.clone(),
436            spectrum: self.spectrum.add_mz_noise_uniform(ppm, right_drag),
437        }
438    }
439
440    pub fn add_mz_noise_normal(&self, ppm: f64) -> Self {
441        TimsSpectrumAnnotated {
442            frame_id: self.frame_id,
443            scan: self.scan,
444            retention_time: self.retention_time,
445            mobility: self.mobility,
446            ms_type: self.ms_type.clone(),
447            // TODO: adding noise to mz means that TOF values need to be re-calculated
448            tof: self.tof.clone(),
449            spectrum: self.spectrum.add_mz_noise_normal(ppm),
450        }
451    }
452
453    pub fn to_windows(
454        &self,
455        window_length: f64,
456        overlapping: bool,
457        min_peaks: usize,
458        min_intensity: f64,
459    ) -> BTreeMap<i32, TimsSpectrumAnnotated> {
460        // 1) base‐window buckets
461        let mut buckets: BTreeMap<i32, Vec<(u32, f64, f64, PeakAnnotation)>> = BTreeMap::new();
462        for (&tof, &mz, &intensity, annotation) in
463            izip!(
464                &self.tof,
465                &self.spectrum.mz,
466                &self.spectrum.intensity,
467                &self.spectrum.annotations
468            )
469        {
470            let idx = (mz / window_length).floor() as i32;
471            buckets.entry(idx)
472                .or_default()
473                .push((tof, mz, intensity, annotation.clone()));
474        }
475
476        // 2) overlapping half‐shifted buckets
477        if overlapping {
478            let mut off: BTreeMap<i32, Vec<(u32, f64, f64, PeakAnnotation)>> = BTreeMap::new();
479            let half = window_length / 2.0;
480            for (&tof, &mz, &intensity, annotation) in
481                izip!(
482                    &self.tof,
483                    &self.spectrum.mz,
484                    &self.spectrum.intensity,
485                    &self.spectrum.annotations
486                )
487            {
488                let idx = -(((mz + half) / window_length).floor() as i32);
489                off.entry(idx)
490                    .or_default()
491                    .push((tof, mz, intensity, annotation.clone()));
492            }
493            for (k, v) in off {
494                buckets.entry(k).or_default().extend(v);
495            }
496        }
497
498        // 3) filter & rebuild
499        let mut out = BTreeMap::new();
500        for (idx, group) in buckets {
501            if group.len() < min_peaks {
502                continue;
503            }
504            let max_i = group.iter().map(|&(_, _, i, _)| i).fold(0.0, f64::max);
505            if max_i < min_intensity {
506                continue;
507            }
508
509            // manual “unzip4”
510            let mut tofs   = Vec::with_capacity(group.len());
511            let mut mzs    = Vec::with_capacity(group.len());
512            let mut ints   = Vec::with_capacity(group.len());
513            let mut annots = Vec::with_capacity(group.len());
514            for (tof, mz, intensity, annotation) in group {
515                tofs.push(tof);
516                mzs.push(mz);
517                ints.push(intensity);
518                annots.push(annotation);
519            }
520
521            // sort+rebuild the annotated spectrum
522            let window_spec = MzSpectrumAnnotated::new(mzs, ints, annots);
523
524            let sub = TimsSpectrumAnnotated {
525                frame_id:       self.frame_id,
526                scan:           self.scan,
527                retention_time: self.retention_time,
528                mobility:       self.mobility,
529                ms_type:        self.ms_type.clone(),
530                tof:            tofs,
531                spectrum:       window_spec,
532            };
533
534            out.insert(idx, sub);
535        }
536
537        out
538    }
539}
540
541impl std::ops::Add for TimsSpectrumAnnotated {
542    type Output = Self;
543    fn add(self, other: Self) -> Self {
544
545        let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
546        let mut spec_map: BTreeMap<i64, (u32, f64, PeakAnnotation, i64)> = BTreeMap::new();
547        let mean_scan_floor = ((self.scan as f64 + other.scan as f64) / 2.0) as u32;
548
549        for (tof, mz, intensity, annotation) in izip!(self.tof.iter(), self.spectrum.mz.iter(), self.spectrum.intensity.iter(), self.spectrum.annotations.iter()) {
550            let key = quantize(*mz);
551            spec_map.insert(key, (*tof, *intensity, annotation.clone(), 1));
552        }
553
554        for (tof, mz, intensity, annotation) in izip!(other.tof.iter(), other.spectrum.mz.iter(), other.spectrum.intensity.iter(), other.spectrum.annotations.iter()) {
555            let key = quantize(*mz);
556            spec_map.entry(key).and_modify(|e| {
557                e.0 += *tof;
558                e.1 += *intensity;
559                e.2.contributions.extend(annotation.contributions.clone());
560                e.3 += 1;
561            }).or_insert((*tof, *intensity, annotation.clone(), 1));
562        }
563
564        let mut tof_vec: Vec<u32> = Vec::with_capacity(spec_map.len());
565        let mut mz_vec: Vec<f64> = Vec::with_capacity(spec_map.len());
566        let mut intensity_vec: Vec<f64> = Vec::with_capacity(spec_map.len());
567        let mut annotations_vec: Vec<PeakAnnotation> = Vec::with_capacity(spec_map.len());
568
569        for (key, (tof, intensity, annotation, count)) in spec_map.iter() {
570            tof_vec.push((*tof as f64 / *count as f64) as u32);
571            mz_vec.push(*key as f64 / 1_000_000.0);
572            intensity_vec.push(*intensity / *count as f64);
573            annotations_vec.push(annotation.clone());
574        }
575
576        assert!(tof_vec.len() == mz_vec.len() && mz_vec.len() == intensity_vec.len() && intensity_vec.len() == annotations_vec.len());
577
578        TimsSpectrumAnnotated {
579            frame_id: self.frame_id,
580            scan: mean_scan_floor,
581            retention_time: self.retention_time,
582            mobility: self.mobility,
583            ms_type: if self.ms_type == other.ms_type { self.ms_type.clone() } else { MsType::Unknown },
584            tof: tof_vec,
585            spectrum: MzSpectrumAnnotated::new(mz_vec, intensity_vec, annotations_vec),
586        }
587    }
588}
589
590#[derive(Clone, Debug)]
591pub struct TimsFrameAnnotated {
592    pub frame_id: i32,
593    pub retention_time: f64,
594    pub ms_type: MsType,
595    pub tof: Vec<u32>,
596    pub mz: Vec<f64>,
597    pub scan: Vec<u32>,
598    pub inv_mobility: Vec<f64>,
599    pub intensity: Vec<f64>,
600    pub annotations: Vec<PeakAnnotation>,
601}
602
603impl TimsFrameAnnotated {
604    pub fn new(frame_id: i32, retention_time: f64, ms_type: MsType, tof: Vec<u32>, mz: Vec<f64>, scan: Vec<u32>, inv_mobility: Vec<f64>, intensity: Vec<f64>, annotations: Vec<PeakAnnotation>) -> Self {
605        assert!(tof.len() == mz.len() && mz.len() == scan.len() && scan.len() == inv_mobility.len() && inv_mobility.len() == intensity.len() && intensity.len() == annotations.len());
606        TimsFrameAnnotated {
607            frame_id,
608            retention_time,
609            ms_type,
610            tof,
611            mz,
612            scan,
613            inv_mobility,
614            intensity,
615            annotations,
616        }
617    }
618    pub fn filter_ranged(&self, mz_min: f64, mz_max: f64, inv_mobility_min: f64, inv_mobility_max: f64, scan_min: u32, scan_max: u32, intensity_min: f64, intensity_max: f64) -> Self {
619        let mut tof_filtered: Vec<u32> = Vec::new();
620        let mut mz_filtered: Vec<f64> = Vec::new();
621        let mut scan_filtered: Vec<u32> = Vec::new();
622        let mut inv_mobility_filtered: Vec<f64> = Vec::new();
623        let mut intensity_filtered: Vec<f64> = Vec::new();
624        let mut annotations_filtered: Vec<PeakAnnotation> = Vec::new();
625
626        for (tof, mz, scan, inv_mobility, intensity, annotation) in izip!(self.tof.iter(), self.mz.iter(), self.scan.iter(), self.inv_mobility.iter(), self.intensity.iter(), self.annotations.iter()) {
627            if *mz >= mz_min && *mz <= mz_max && *inv_mobility >= inv_mobility_min && *inv_mobility <= inv_mobility_max && *scan >= scan_min && *scan <= scan_max && *intensity >= intensity_min && *intensity <= intensity_max {
628                tof_filtered.push(*tof);
629                mz_filtered.push(*mz);
630                scan_filtered.push(*scan);
631                inv_mobility_filtered.push(*inv_mobility);
632                intensity_filtered.push(*intensity);
633                annotations_filtered.push(annotation.clone());
634            }
635        }
636
637        assert!(tof_filtered.len() == mz_filtered.len() && mz_filtered.len() == scan_filtered.len() && scan_filtered.len() == inv_mobility_filtered.len() && inv_mobility_filtered.len() == intensity_filtered.len() && intensity_filtered.len() == annotations_filtered.len());
638
639        TimsFrameAnnotated {
640            frame_id: self.frame_id,
641            retention_time: self.retention_time,
642            ms_type: self.ms_type.clone(),
643            tof: tof_filtered,
644            mz: mz_filtered,
645            scan: scan_filtered,
646            inv_mobility: inv_mobility_filtered,
647            intensity: intensity_filtered,
648            annotations: annotations_filtered,
649        }
650    }
651
652    pub fn to_tims_spectra_annotated(&self) -> Vec<TimsSpectrumAnnotated> {
653        // use a sorted map where scan is used as key
654        let mut spectra = BTreeMap::<i32, (f64, Vec<u32>, MzSpectrumAnnotated)>::new();
655
656        // all indices and the intensity values are sorted by scan and stored in the map as a tuple (mobility, tof, mz, intensity)
657        for (scan, mobility, tof, mz, intensity, annotations) in izip!(self.scan.iter(), self.inv_mobility.iter(), self.tof.iter(), self.mz.iter(), self.intensity.iter(), self.annotations.iter()) {
658            let entry = spectra.entry(*scan as i32).or_insert_with(|| (*mobility, Vec::new(), MzSpectrumAnnotated::new(Vec::new(), Vec::new(), Vec::new())));
659            entry.1.push(*tof);
660            entry.2.mz.push(*mz);
661            entry.2.intensity.push(*intensity);
662            entry.2.annotations.push(annotations.clone());
663        }
664
665        // convert the map to a vector of TimsSpectrumAnnotated
666        let mut tims_spectra: Vec<TimsSpectrumAnnotated> = Vec::new();
667
668        for (scan, (mobility, tof, spectrum)) in spectra {
669            tims_spectra.push(TimsSpectrumAnnotated::new(self.frame_id, scan as u32, self.retention_time, mobility, self.ms_type.clone(), tof, spectrum));
670        }
671
672        tims_spectra
673    }
674
675    pub fn from_tims_spectra_annotated(spectra: Vec<TimsSpectrumAnnotated>) -> TimsFrameAnnotated {
676        let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
677        let mut spec_map: BTreeMap<(u32, i64), (f64, u32, f64, PeakAnnotation, i64)> = BTreeMap::new();
678        let mut capacity_count = 0;
679
680        for spectrum in &spectra {
681            let inv_mobility = spectrum.mobility;
682            for (i, mz) in spectrum.spectrum.mz.iter().enumerate() {
683                let scan = spectrum.scan;
684                let tof = spectrum.tof[i];
685                let intensity = spectrum.spectrum.intensity[i];
686                let annotation = spectrum.spectrum.annotations[i].clone();
687                let key = (scan, quantize(*mz));
688                spec_map.entry(key).and_modify(|e| {
689                    e.0 += intensity;
690                    e.1 += tof;
691                    e.2 += inv_mobility;
692                    e.3.contributions.extend(annotation.contributions.clone());
693                    e.4 += 1;
694                }).or_insert((intensity, tof, inv_mobility, annotation, 1));
695                capacity_count += 1;
696            }
697        }
698
699        let mut scan_vec: Vec<u32> = Vec::with_capacity(capacity_count);
700        let mut inv_mobility_vec: Vec<f64> = Vec::with_capacity(capacity_count);
701        let mut tof_vec: Vec<u32> = Vec::with_capacity(capacity_count);
702        let mut mz_vec: Vec<f64> = Vec::with_capacity(capacity_count);
703        let mut intensity_vec: Vec<f64> = Vec::with_capacity(capacity_count);
704        let mut annotations_vec: Vec<PeakAnnotation> = Vec::with_capacity(capacity_count);
705
706        for ((scan, mz), (intensity, tof, inv_mobility, annotation, count)) in spec_map.iter() {
707            scan_vec.push(*scan);
708            inv_mobility_vec.push(*inv_mobility / *count as f64);
709            tof_vec.push((*tof as f64 / *count as f64) as u32);
710            mz_vec.push(*mz as f64 / 1_000_000.0);
711            intensity_vec.push(*intensity);
712            annotations_vec.push(annotation.clone());
713        }
714
715        assert!(tof_vec.len() == mz_vec.len() && mz_vec.len() == scan_vec.len() && scan_vec.len() == inv_mobility_vec.len() && inv_mobility_vec.len() == intensity_vec.len() && intensity_vec.len() == annotations_vec.len());
716
717        TimsFrameAnnotated {
718            frame_id: spectra.first().unwrap().frame_id,
719            retention_time: spectra.first().unwrap().retention_time,
720            ms_type: spectra.first().unwrap().ms_type.clone(),
721            tof: tof_vec,
722            mz: mz_vec,
723            scan: scan_vec,
724            inv_mobility: inv_mobility_vec,
725            intensity: intensity_vec,
726            annotations: annotations_vec,
727        }
728    }
729    pub fn to_windows_indexed(
730        &self,
731        window_length: f64,
732        overlapping: bool,
733        min_peaks: usize,
734        min_intensity: f64
735    ) -> (Vec<u32>, Vec<i32>, Vec<TimsSpectrumAnnotated>) {
736        // 1) explode into spectra by scan/mobility
737        let spectra = self.to_tims_spectra_annotated();
738
739        // 2) window each one
740        let windows_per_scan: Vec<_> = spectra
741            .iter()
742            .map(|s| s.to_windows(window_length, overlapping, min_peaks, min_intensity))
743            .collect();
744
745        // 3) flatten out into three parallel vectors
746        let mut scan_indices   = Vec::new();
747        let mut window_indices = Vec::new();
748        let mut out_spectra    = Vec::new();
749
750        for (spec, window_map) in spectra.iter().zip(windows_per_scan.iter()) {
751            for (&win_idx, win_spec) in window_map {
752                scan_indices.push(spec.scan);
753                window_indices.push(win_idx);
754                out_spectra.push(win_spec.clone());
755            }
756        }
757
758        (scan_indices, window_indices, out_spectra)
759    }
760
761    pub fn to_windows(
762        &self,
763        window_length: f64,
764        overlapping: bool,
765        min_peaks: usize,
766        min_intensity: f64
767    ) -> Vec<TimsSpectrumAnnotated> {
768        // 1) explode into spectra by scan/mobility
769        let spectra = self.to_tims_spectra_annotated();
770
771        // 2) window each one
772        let windows_per_scan: Vec<_> = spectra
773            .iter()
774            .map(|s| s.to_windows(window_length, overlapping, min_peaks, min_intensity))
775            .collect();
776
777        // 3) flatten out into a single vector of TimsSpectrumAnnotated
778        let mut out_spectra = Vec::new();
779        for (_, window_map) in spectra.iter().zip(windows_per_scan.iter()) {
780            for (_, win_spec) in window_map {
781                out_spectra.push(win_spec.clone());
782            }
783        }
784
785        out_spectra
786    }
787
788    pub fn to_dense_windows(
789        &self,
790        window_length: f64,
791        overlapping: bool,
792        min_peaks: usize,
793        min_intensity: f64,
794        resolution: i32
795    ) -> (Vec<f64>, Vec<i32>, Vec<i32>, usize, usize) {
796        let factor    = 10f64.powi(resolution);
797        let n_cols    = ((window_length * factor).round() + 1.0) as usize;
798
799        // 1) get indexed windows
800        let (scan_indices, window_indices, spectra) =
801            self.to_windows_indexed(window_length, overlapping, min_peaks, min_intensity);
802
803        // 2) vectorize each window’s MzSpectrumAnnotated
804        let vec_specs: Vec<_> = spectra
805            .iter()
806            .map(|ts| ts.spectrum.vectorized(resolution))
807            .collect();
808
809        // 3) prepare flat matrix
810        let n_rows      = spectra.len();
811        let mut matrix = vec![0.0; n_rows * n_cols];
812
813        // 4) fill in each row
814        for (row, ( &win_idx, vec_spec)) in
815            multizip((&window_indices, &vec_specs))
816                .enumerate()
817        {
818            // compute the "vectorized" start index of this window
819            let start_i = if win_idx >= 0 {
820                ((win_idx as f64 * window_length) * factor).round() as i32
821            } else {
822                // negative key → half‐shifted
823                ((((-win_idx) as f64 * window_length) - 0.5 * window_length) * factor)
824                    .round() as i32
825            };
826
827            // now place each nonzero bin
828            for (&idx, &val) in vec_spec.indices.iter().zip(&vec_spec.values) {
829                let col = (idx as i32 - start_i) as usize;
830                let flat_idx = row * n_cols + col;
831                matrix[flat_idx] = val;
832            }
833        }
834
835        // cast scan indices to i32 for consistency
836        let scan_indices: Vec<i32> = scan_indices.iter().map(|&scan| scan as i32).collect();
837
838        (matrix, scan_indices, window_indices, n_rows, n_cols)
839    }
840
841    /// Returns:
842    ///  - intensity_matrix,
843    ///  - scan_indices,
844    ///  - window_indices,
845    ///  - mz_start for each window,
846    ///  - ion_mobility_start for each window,
847    ///  - n_rows, n_cols,
848    ///  - isotope_peak labels,
849    ///  - charge_state labels,
850    ///  - peptide_id labels (0..5)
851    pub fn to_dense_windows_with_labels(
852        &self,
853        window_length: f64,
854        overlapping: bool,
855        min_peaks: usize,
856        min_intensity: f64,
857        resolution: i32,
858    ) -> (
859        Vec<f64>,    // intensities
860        Vec<u32>,    // scan index per row
861        Vec<i32>,    // window key per row
862        Vec<f64>,    // mz_start per row
863        Vec<f64>,    // ion_mobility_start per row
864        usize,       // n_rows
865        usize,       // n_cols
866        Vec<i32>,    // isotope_peak labels
867        Vec<i32>,    // charge_state labels
868        Vec<i32>,    // peptide_id labels
869    ) {
870        let factor = 10f64.powi(resolution);
871        let n_cols = ((window_length * factor).round() + 1.0) as usize;
872
873        // 1) explode into per-scan windows
874        let (scan_indices, window_indices, spectra) =
875            self.to_windows_indexed(window_length, overlapping, min_peaks, min_intensity);
876        let vec_specs: Vec<_> = spectra
877            .iter()
878            .map(|ts| ts.spectrum.vectorized(resolution))
879            .collect();
880
881        let n_rows     = vec_specs.len();
882        let matrix_sz  = n_rows * n_cols;
883
884        // 2) allocate output arrays
885        let mut intensities       = vec![0.0_f64; matrix_sz];
886        let mut iso_labels        = vec![-1_i32; matrix_sz];
887        let mut charge_labels     = vec![-1_i32; matrix_sz];
888        let mut peptide_labels    = vec![-1_i32; matrix_sz];
889        let mut mz_start          = Vec::with_capacity(n_rows);
890        let mut ion_mobility_start = Vec::with_capacity(n_rows);
891
892        // 3) fill each row
893        for (row, ((&win_idx, vec_spec), ts)) in
894            multizip((&window_indices, &vec_specs))
895                .zip(spectra.iter())
896                .enumerate()
897        {
898            // record the first‐peak m/z and mobility
899            let first_mz = ts.spectrum.mz.first().cloned().unwrap_or(0.0);
900            mz_start.push(first_mz);
901            ion_mobility_start.push(ts.mobility);
902
903            // per‐window map for peptide_id → 0..5
904            let mut feat_map  = HashMap::<i32,i32>::new();
905            let mut next_feat = 0;
906
907            // window start index
908            let start_i = if win_idx >= 0 {
909                ((win_idx as f64 * window_length) * factor).round() as i32
910            } else {
911                (((-win_idx) as f64 * window_length - 0.5 * window_length) * factor)
912                    .round() as i32
913            };
914
915            // fill columns
916            for (&bin_idx, &val, annotation) in
917                izip!(&vec_spec.indices, &vec_spec.values, &vec_spec.annotations)
918            {
919                let col  = (bin_idx as i32 - start_i) as usize;
920                let flat = row * n_cols + col;
921                intensities[flat] = val;
922
923                // choose best contributor
924                if let Some(best) = annotation
925                    .contributions
926                    .iter()
927                    .max_by(|a, b| {
928                        a.intensity_contribution
929                            .partial_cmp(&b.intensity_contribution)
930                            .unwrap()
931                    })
932                {
933                    match best.source_type {
934                        SourceType::Signal => {
935                            if let Some(sa) = &best.signal_attributes {
936                                iso_labels[flat]    = sa.isotope_peak;
937                                charge_labels[flat] = sa.charge_state;
938                                // re-index peptide_id
939                                let old = sa.peptide_id;
940                                let new = *feat_map.entry(old).or_insert_with(|| {
941                                    let i = next_feat; next_feat += 1; i.min(5)
942                                });
943                                peptide_labels[flat] = new;
944                            }
945                        }
946                        SourceType::RandomNoise => {
947                            iso_labels[flat]    = -2;
948                            charge_labels[flat] = -2;
949                            peptide_labels[flat] = -2;
950                        }
951                        _ => { /* leave as -1 */ }
952                    }
953                }
954            }
955        }
956
957        (
958            intensities,
959            scan_indices,
960            window_indices,
961            mz_start,
962            ion_mobility_start,
963            n_rows,
964            n_cols,
965            iso_labels,
966            charge_labels,
967            peptide_labels,
968        )
969    }
970
971    pub fn fold_along_scan_axis(self, fold_width: usize) -> TimsFrameAnnotated {
972        // extract tims spectra from frame
973        let spectra = self.to_tims_spectra_annotated();
974
975        // create a new collection of merged spectra,where spectra are first grouped by the key they create when divided by fold_width
976        // and then merge them by addition
977        let mut merged_spectra: BTreeMap<u32, TimsSpectrumAnnotated> = BTreeMap::new();
978        for spectrum in spectra {
979            let key = spectrum.scan / fold_width as u32;
980
981            // if the key already exists, merge the spectra
982            if let Some(existing_spectrum) = merged_spectra.get_mut(&key) {
983
984                let merged_spectrum = existing_spectrum.clone() + spectrum;
985                // update the existing spectrum with the merged one
986                *existing_spectrum = merged_spectrum;
987
988            } else {
989                // otherwise, insert the new spectrum
990                merged_spectra.insert(key, spectrum);
991            }
992        }
993        // convert the merged spectra back to a TimsFrame
994        TimsFrameAnnotated::from_tims_spectra_annotated(merged_spectra.into_values().collect())
995    }
996}
997
998impl std::ops::Add for TimsFrameAnnotated {
999    type Output = Self;
1000    fn add(self, other: Self) -> Self {
1001
1002        let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
1003        let mut spec_map: BTreeMap<(u32, i64), (f64, u32, f64, PeakAnnotation, i64)> = BTreeMap::new();
1004
1005        for (scan, mz, tof, inv_mobility, intensity, annotation) in
1006        izip!(self.scan.iter(), self.mz.iter(), self.tof.iter(), self.inv_mobility.iter(), self.intensity.iter(), self.annotations.iter()) {
1007            let key = (*scan, quantize(*mz));
1008            spec_map.insert(key, (*intensity, *tof, *inv_mobility, annotation.clone(), 1));
1009        }
1010
1011        for (scan, mz, tof, inv_mobility, intensity, annotation) in
1012        izip!(other.scan.iter(), other.mz.iter(), other.tof.iter(), other.inv_mobility.iter(), other.intensity.iter(), other.annotations.iter()) {
1013            let key = (*scan, quantize(*mz));
1014            spec_map.entry(key).and_modify(|e| {
1015                e.0 += *intensity;
1016                e.1 += *tof;
1017                e.2 += *inv_mobility;
1018                e.3.contributions.extend(annotation.contributions.clone());
1019                e.4 += 1;
1020            }).or_insert((*intensity, *tof, *inv_mobility, annotation.clone(), 1));
1021        }
1022
1023        let mut tof_vec: Vec<u32> = Vec::with_capacity(spec_map.len());
1024        let mut mz_vec: Vec<f64> = Vec::with_capacity(spec_map.len());
1025        let mut scan_vec: Vec<u32> = Vec::with_capacity(spec_map.len());
1026        let mut inv_mobility_vec: Vec<f64> = Vec::with_capacity(spec_map.len());
1027        let mut intensity_vec: Vec<f64> = Vec::with_capacity(spec_map.len());
1028        let mut annotations_vec: Vec<PeakAnnotation> = Vec::with_capacity(spec_map.len());
1029
1030        for ((scan, mz), (intensity, tof, inv_mobility, annotation, count)) in spec_map.iter() {
1031            scan_vec.push(*scan);
1032            mz_vec.push(*mz as f64 / 1_000_000.0);
1033            intensity_vec.push(*intensity);
1034            tof_vec.push((*tof as f64 / *count as f64) as u32);
1035            inv_mobility_vec.push(*inv_mobility / *count as f64);
1036            annotations_vec.push(annotation.clone());
1037        }
1038
1039        assert!(tof_vec.len() == mz_vec.len() && mz_vec.len() == scan_vec.len() && scan_vec.len() == inv_mobility_vec.len() && inv_mobility_vec.len() == intensity_vec.len() && intensity_vec.len() == annotations_vec.len());
1040
1041        TimsFrameAnnotated {
1042            frame_id: self.frame_id,
1043            retention_time: self.retention_time,
1044            ms_type: if self.ms_type == other.ms_type { self.ms_type.clone() } else { MsType::Unknown },
1045            tof: tof_vec,
1046            mz: mz_vec,
1047            scan: scan_vec,
1048            inv_mobility: inv_mobility_vec,
1049            intensity: intensity_vec,
1050            annotations: annotations_vec,
1051        }
1052    }
1053}
1054
1055#[cfg(test)]
1056mod new_sort_tests {
1057    use super::*;
1058
1059    // Tag a peak's annotation with a distinct peptide_id so we can verify the
1060    // sort permutation keeps each annotation attached to its original peak.
1061    fn ann(peptide_id: i32) -> PeakAnnotation {
1062        PeakAnnotation {
1063            contributions: vec![ContributionSource {
1064                intensity_contribution: 1.0,
1065                source_type: SourceType::Signal,
1066                signal_attributes: Some(SignalAttributes {
1067                    charge_state: 1,
1068                    peptide_id,
1069                    isotope_peak: 0,
1070                    fragment_kind: None,
1071                    fragment_ordinal: None,
1072                }),
1073            }],
1074        }
1075    }
1076
1077    fn pids(spec: &MzSpectrumAnnotated) -> Vec<i32> {
1078        spec.annotations
1079            .iter()
1080            .map(|a| a.contributions[0].signal_attributes.as_ref().unwrap().peptide_id)
1081            .collect()
1082    }
1083
1084    // Guards optimization "D": MzSpectrumAnnotated::new now moves annotations
1085    // instead of cloning them twice. Behavior (sort by mz, annotation stays
1086    // attached to its peak) must be identical.
1087    #[test]
1088    fn new_sorts_by_mz_and_keeps_annotation_attached() {
1089        // peptide_id == mz * 10 so we can detect any mis-permutation
1090        let spec = MzSpectrumAnnotated::new(
1091            vec![300.0, 100.0, 200.0],
1092            vec![3.0, 1.0, 2.0],
1093            vec![ann(3000), ann(1000), ann(2000)],
1094        );
1095
1096        assert_eq!(spec.mz, vec![100.0, 200.0, 300.0]);
1097        assert_eq!(spec.intensity, vec![1.0, 2.0, 3.0]);
1098        assert_eq!(pids(&spec), vec![1000, 2000, 3000]);
1099    }
1100
1101    // The sort must be stable for equal mz (matters for merge order / which
1102    // contribution ends up "first" downstream).
1103    #[test]
1104    fn new_is_stable_for_equal_mz() {
1105        let spec = MzSpectrumAnnotated::new(
1106            vec![150.0, 150.0],
1107            vec![10.0, 20.0],
1108            vec![ann(111), ann(222)],
1109        );
1110        assert_eq!(spec.intensity, vec![10.0, 20.0]);
1111        assert_eq!(pids(&spec), vec![111, 222]);
1112    }
1113}