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