mzpeaks/
peak.rs

1//! A peak is the most atomic unit of a (processed) mass spectrum. It represents
2//! a location in one (or more) coordinate spaces with an measured intensity value
3//!
4//! All peak-like types implement [`PartialEq`], [`PartialOrd`], and [`CoordinateLike`]
5//!
6
7use std::cmp::Ordering;
8use std::fmt;
9
10use crate::coordinate::{CoordinateLike, IndexType, IndexedCoordinate, Mass, MZ};
11use crate::{implement_centroidlike_inner, implement_deconvoluted_centroidlike_inner, CoordinateLikeMut, IonMobility};
12use crate::{MZLocated, MassLocated};
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16/// An intensity measurement is an entity that has a measured intensity
17/// of whateger it is.
18pub trait IntensityMeasurement {
19    fn intensity(&self) -> f32;
20}
21
22pub trait IntensityMeasurementMut: IntensityMeasurement {
23    fn intensity_mut(&mut self) -> &mut f32;
24}
25
26impl<T: IntensityMeasurement> IntensityMeasurement for &T {
27    fn intensity(&self) -> f32 {
28        (*self).intensity()
29    }
30}
31
32impl<T: IntensityMeasurement> IntensityMeasurement for &mut T {
33    fn intensity(&self) -> f32 {
34        IntensityMeasurement::intensity(*self)
35    }
36}
37
38impl<T: IntensityMeasurementMut> IntensityMeasurementMut for &mut T {
39    fn intensity_mut(&mut self) -> &mut f32 {
40        IntensityMeasurementMut::intensity_mut(*self)
41    }
42}
43
44/// A [`CentroidLike`] entity is indexed in m/z coordinate space and
45/// is an [`IntensityMeasurement`]
46pub trait CentroidLike: IndexedCoordinate<MZ> + IntensityMeasurement {
47    #[inline]
48    fn as_centroid(&self) -> CentroidPeak {
49        CentroidPeak {
50            mz: self.coordinate(),
51            intensity: self.intensity(),
52            index: self.get_index(),
53        }
54    }
55}
56
57/// A known charge has a determined charge state value
58pub trait KnownCharge {
59    fn charge(&self) -> i32;
60}
61
62impl<T: KnownCharge> KnownCharge for &T {
63    fn charge(&self) -> i32 {
64        (*self).charge()
65    }
66}
67
68pub trait KnownChargeMut: KnownCharge {
69    fn charge_mut(&mut self) -> &mut i32;
70}
71
72impl<T: KnownCharge> KnownCharge for &mut T {
73    fn charge(&self) -> i32 {
74        KnownCharge::charge(*self)
75    }
76}
77
78impl<T: KnownChargeMut> KnownChargeMut for &mut T {
79    fn charge_mut(&mut self) -> &mut i32 {
80        KnownChargeMut::charge_mut(*self)
81    }
82}
83
84/// A [`DeconvolutedCentroidLike`] entity is indexed in the neutral mass
85/// coordinate space, has known charge state and an aggregated intensity
86/// measurement. Any [`DeconvolutedCentroidLike`] can be converted into
87/// a [`DeconvolutedPeak`]
88pub trait DeconvolutedCentroidLike:
89    IndexedCoordinate<Mass> + IntensityMeasurement + KnownCharge
90{
91    fn as_centroid(&self) -> DeconvolutedPeak {
92        DeconvolutedPeak {
93            neutral_mass: self.coordinate(),
94            intensity: self.intensity(),
95            charge: self.charge(),
96            index: self.get_index(),
97        }
98    }
99}
100
101/// Represent a single m/z coordinate with an
102/// intensity and an index. Nearly the most basic
103/// peak representation for peak-picked data.
104#[derive(Default, Clone, Debug)]
105#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
106pub struct CentroidPeak {
107    pub mz: f64,
108    pub intensity: f32,
109    pub index: IndexType,
110}
111
112impl CentroidPeak {
113    #[inline]
114    pub fn new(mz: f64, intensity: f32, index: IndexType) -> CentroidPeak {
115        CentroidPeak {
116            mz,
117            intensity,
118            index,
119        }
120    }
121}
122
123impl fmt::Display for CentroidPeak {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        write!(
126            f,
127            "CentroidPeak({}, {}, {})",
128            self.mz, self.intensity, self.index
129        )
130    }
131}
132
133implement_centroidlike_inner!(CentroidPeak, true, false);
134
135impl<T: IndexedCoordinate<MZ> + IntensityMeasurement> CentroidLike for T {}
136
137impl<T: IndexedCoordinate<Mass> + IntensityMeasurement + KnownCharge> DeconvolutedCentroidLike
138    for T
139{
140}
141
142#[derive(Debug, Clone, Default, PartialEq, PartialOrd)]
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144pub struct MZPoint {
145    pub mz: f64,
146    pub intensity: f32,
147}
148
149impl MZPoint {
150    #[inline]
151    pub fn new(mz: f64, intensity: f32) -> MZPoint {
152        MZPoint { mz, intensity }
153    }
154}
155
156impl CoordinateLike<MZ> for MZPoint {
157    fn coordinate(&self) -> f64 {
158        self.mz
159    }
160}
161
162impl IntensityMeasurement for MZPoint {
163    #[inline]
164    fn intensity(&self) -> f32 {
165        self.intensity
166    }
167}
168
169impl IntensityMeasurementMut for MZPoint {
170    #[inline]
171    fn intensity_mut(&mut self) -> &mut f32 {
172        &mut self.intensity
173    }
174}
175
176impl IndexedCoordinate<MZ> for MZPoint {
177    #[inline]
178    fn get_index(&self) -> IndexType {
179        0
180    }
181    fn set_index(&mut self, _index: IndexType) {}
182}
183
184impl From<MZPoint> for CentroidPeak {
185    fn from(peak: MZPoint) -> Self {
186        CentroidPeak {
187            mz: peak.mz,
188            intensity: peak.intensity,
189            index: 0,
190        }
191    }
192}
193
194impl From<CentroidPeak> for MZPoint {
195    fn from(peak: CentroidPeak) -> Self {
196        let mut inst = Self {
197            mz: peak.coordinate(),
198            intensity: peak.intensity(),
199        };
200        inst.set_index(peak.index);
201        inst
202    }
203}
204
205#[derive(Default, Clone, Debug)]
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
207/// Represent a single neutral mass coordinate with an
208/// intensity, a known charge and an index.
209pub struct DeconvolutedPeak {
210    pub neutral_mass: f64,
211    pub intensity: f32,
212    pub charge: i32,
213    pub index: IndexType,
214}
215
216impl DeconvolutedPeak {
217    pub fn new(neutral_mass: f64, intensity: f32, charge: i32, index: IndexType) -> Self {
218        Self {
219            neutral_mass,
220            intensity,
221            charge,
222            index,
223        }
224    }
225
226    pub fn mz(&self) -> f64 {
227        let charge_carrier: f64 = 1.007276;
228        let charge = self.charge as f64;
229        (self.neutral_mass + charge_carrier * charge) / charge
230    }
231}
232
233implement_deconvoluted_centroidlike_inner!(DeconvolutedPeak, true, false);
234
235impl fmt::Display for DeconvolutedPeak {
236    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
237        write!(
238            f,
239            "DeconvolutedPeak({}, {}, {}, {})",
240            self.neutral_mass, self.intensity, self.charge, self.index
241        )
242    }
243}
244
245impl CoordinateLike<MZ> for DeconvolutedPeak {
246    fn coordinate(&self) -> f64 {
247        self.mz()
248    }
249}
250
251/// A reference wrapper for [`CentroidLike`] peaks.
252///
253/// The wrapper has its own [`IndexedCoordinate`] implementation
254/// managed separately from the source. This allows them to be re-sorted
255/// and indexed independently.
256#[derive(Debug, Clone, Copy)]
257pub struct CentroidRef<'a, C: CentroidLike> {
258    inner: &'a C,
259    index: IndexType,
260}
261
262impl<C: CentroidLike> PartialEq for CentroidRef<'_, C> {
263    fn eq(&self, other: &Self) -> bool {
264        if (self.mz() - other.coordinate()).abs() > 1e-3
265            || (self.intensity() - other.intensity()).abs() > 1e-3
266        {
267            return false;
268        }
269        true
270    }
271}
272
273impl<C: CentroidLike> PartialOrd for CentroidRef<'_, C> {
274    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
275        Some(
276            self.mz()
277                .total_cmp(&other.mz())
278                .then_with(|| self.intensity().total_cmp(&other.intensity())),
279        )
280    }
281}
282
283impl<'a, C: CentroidLike> CentroidRef<'a, C> {
284    pub fn new(inner: &'a C, index: IndexType) -> Self {
285        Self { inner, index }
286    }
287}
288
289impl<C: CentroidLike> CoordinateLike<MZ> for CentroidRef<'_, C> {
290    fn coordinate(&self) -> f64 {
291        self.inner.mz()
292    }
293}
294
295impl<C: CentroidLike> IntensityMeasurement for CentroidRef<'_, C> {
296    #[inline]
297    fn intensity(&self) -> f32 {
298        self.inner.intensity()
299    }
300}
301
302impl<C: CentroidLike> IndexedCoordinate<MZ> for CentroidRef<'_, C> {
303    #[inline]
304    fn get_index(&self) -> IndexType {
305        self.index
306    }
307    fn set_index(&mut self, index: IndexType) {
308        self.index = index
309    }
310}
311
312/// A reference wrapper for [`DeconvolutedCentroidLike`] peaks.
313///
314/// The wrapper has its own [`IndexedCoordinate`] implementation
315/// managed separately from the source. This allows them to be re-sorted
316/// and indexed independently.
317#[derive(Debug, Clone, Copy)]
318pub struct DeconvolutedCentroidRef<'a, D: DeconvolutedCentroidLike> {
319    inner: &'a D,
320    index: IndexType,
321}
322
323impl<D: DeconvolutedCentroidLike> PartialEq for DeconvolutedCentroidRef<'_, D> {
324    fn eq(&self, other: &Self) -> bool {
325        if (self.neutral_mass() - other.coordinate()).abs() > 1e-3
326            || self.charge() != other.charge()
327            || (self.intensity() - other.intensity()).abs() > 1e-3
328        {
329            return false;
330        }
331        true
332    }
333}
334
335impl<D: DeconvolutedCentroidLike> PartialOrd for DeconvolutedCentroidRef<'_, D> {
336    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
337        Some(
338            self.neutral_mass()
339                .total_cmp(&other.neutral_mass())
340                .then_with(|| self.intensity().total_cmp(&other.intensity())),
341        )
342    }
343}
344
345impl<'a, D: DeconvolutedCentroidLike> DeconvolutedCentroidRef<'a, D> {
346    pub fn new(inner: &'a D, index: IndexType) -> Self {
347        Self { inner, index }
348    }
349}
350
351impl<D: DeconvolutedCentroidLike> CoordinateLike<Mass> for DeconvolutedCentroidRef<'_, D> {
352    fn coordinate(&self) -> f64 {
353        self.inner.neutral_mass()
354    }
355}
356
357impl<D: DeconvolutedCentroidLike> KnownCharge for DeconvolutedCentroidRef<'_, D> {
358    fn charge(&self) -> i32 {
359        self.inner.charge()
360    }
361}
362
363impl<D: DeconvolutedCentroidLike> IntensityMeasurement for DeconvolutedCentroidRef<'_, D> {
364    #[inline]
365    fn intensity(&self) -> f32 {
366        self.inner.intensity()
367    }
368}
369
370impl<D: DeconvolutedCentroidLike> IndexedCoordinate<Mass> for DeconvolutedCentroidRef<'_, D> {
371    #[inline]
372    fn get_index(&self) -> IndexType {
373        self.index
374    }
375    fn set_index(&mut self, index: IndexType) {
376        self.index = index
377    }
378}
379
380impl<D: DeconvolutedCentroidLike> CoordinateLike<MZ> for DeconvolutedCentroidRef<'_, D>
381where
382    D: CoordinateLike<MZ>,
383{
384    fn coordinate(&self) -> f64 {
385        <D as CoordinateLike<MZ>>::coordinate(self.inner)
386    }
387}
388
389/// Represent a single m/z coordinate and ion mobility coordinate with an
390/// intensity and an index.
391#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
392pub struct IonMobilityAwareCentroidPeak {
393    pub mz: f64,
394    pub ion_mobility: f64,
395    pub intensity: f32,
396    pub index: u32,
397}
398
399impl CoordinateLike<MZ> for IonMobilityAwareCentroidPeak {
400    fn coordinate(&self) -> f64 {
401        self.mz
402    }
403}
404
405impl CoordinateLike<IonMobility> for IonMobilityAwareCentroidPeak {
406    fn coordinate(&self) -> f64 {
407        self.ion_mobility
408    }
409}
410
411impl IntensityMeasurement for IonMobilityAwareCentroidPeak {
412    fn intensity(&self) -> f32 {
413        self.intensity
414    }
415}
416
417impl CoordinateLikeMut<MZ> for IonMobilityAwareCentroidPeak {
418    fn coordinate_mut(&mut self) -> &mut f64 {
419        &mut self.mz
420    }
421}
422
423impl CoordinateLikeMut<IonMobility> for IonMobilityAwareCentroidPeak {
424    fn coordinate_mut(&mut self) -> &mut f64 {
425        &mut self.ion_mobility
426    }
427}
428
429impl IndexedCoordinate<MZ> for IonMobilityAwareCentroidPeak {
430    fn get_index(&self) -> IndexType {
431        self.index
432    }
433
434    fn set_index(&mut self, index: IndexType) {
435        self.index = index
436    }
437}
438
439impl IntensityMeasurementMut for IonMobilityAwareCentroidPeak {
440    fn intensity_mut(&mut self) -> &mut f32 {
441        &mut self.intensity
442    }
443}
444
445impl IonMobilityAwareCentroidPeak {
446    pub fn new(mz: f64, ion_mobility: f64, intensity: f32, index: u32) -> Self {
447        Self { mz, ion_mobility, intensity, index }
448    }
449}
450
451
452/// Represent a single neutral mass coordinate and ion mobility coordinate with an
453/// intensity, a known charge and an index.
454#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
455pub struct IonMobilityAwareDeconvolutedPeak {
456    pub neutral_mass: f64,
457    pub ion_mobility: f64,
458    pub charge: i32,
459    pub intensity: f32,
460    pub index: u32,
461}
462
463impl IonMobilityAwareDeconvolutedPeak {
464    pub fn new(neutral_mass: f64, ion_mobility: f64, charge: i32, intensity: f32, index: u32) -> Self {
465        Self {
466            neutral_mass,
467            ion_mobility,
468            charge,
469            intensity,
470            index,
471        }
472    }
473}
474
475impl CoordinateLike<Mass> for IonMobilityAwareDeconvolutedPeak {
476    fn coordinate(&self) -> f64 {
477        self.neutral_mass
478    }
479}
480
481impl CoordinateLike<MZ> for IonMobilityAwareDeconvolutedPeak {
482    fn coordinate(&self) -> f64 {
483        self.as_centroid().mz()
484    }
485}
486
487impl CoordinateLike<IonMobility> for IonMobilityAwareDeconvolutedPeak {
488    fn coordinate(&self) -> f64 {
489        self.ion_mobility
490    }
491}
492
493impl CoordinateLikeMut<Mass> for IonMobilityAwareDeconvolutedPeak {
494    fn coordinate_mut(&mut self) -> &mut f64 {
495        &mut self.neutral_mass
496    }
497}
498
499impl CoordinateLikeMut<IonMobility> for IonMobilityAwareDeconvolutedPeak {
500    fn coordinate_mut(&mut self) -> &mut f64 {
501        &mut self.ion_mobility
502    }
503}
504
505impl From<IonMobilityAwareDeconvolutedPeak> for DeconvolutedPeak {
506    fn from(value: IonMobilityAwareDeconvolutedPeak) -> Self {
507        Self::new(
508            value.neutral_mass,
509            value.intensity,
510            value.charge,
511            value.index,
512        )
513    }
514}
515
516impl IntensityMeasurement for IonMobilityAwareDeconvolutedPeak {
517    fn intensity(&self) -> f32 {
518        self.intensity
519    }
520}
521
522impl IntensityMeasurementMut for IonMobilityAwareDeconvolutedPeak {
523    fn intensity_mut(&mut self) -> &mut f32 {
524        &mut self.intensity
525    }
526}
527
528impl KnownCharge for IonMobilityAwareDeconvolutedPeak {
529    fn charge(&self) -> i32 {
530        self.charge
531    }
532}
533
534impl KnownChargeMut for IonMobilityAwareDeconvolutedPeak {
535    fn charge_mut(&mut self) -> &mut i32 {
536        &mut self.charge
537    }
538}
539
540impl IndexedCoordinate<Mass> for IonMobilityAwareDeconvolutedPeak {
541    fn get_index(&self) -> IndexType {
542        self.index
543    }
544
545    fn set_index(&mut self, index: IndexType) {
546        self.index = index;
547    }
548}
549
550
551#[cfg(test)]
552mod test {
553    use super::*;
554    use crate::coordinate::*;
555
556    #[test]
557    fn test_conversion() {
558        let x = CentroidPeak::new(204.07, 5000f32, 19);
559        let y: MZPoint = x.clone().into();
560        assert_eq!(y.mz, x.mz);
561        assert_eq!(y.coordinate(), x.coordinate());
562        assert_eq!(y.intensity(), x.intensity());
563        // MZPoint doesn't use index
564        let mut z: CentroidPeak = y.clone().into();
565        assert_eq!(z, y);
566        *z.intensity_mut() += 500.0;
567        assert!(x < z);
568        assert!(z > x);
569        assert!(x == x);
570        assert!(z != x);
571
572        let xr = CentroidRef::new(&x, 0);
573        let zr = CentroidRef::new(&z, 0);
574        assert!(xr < zr);
575        assert!(zr > xr);
576        assert!(xr == xr);
577        assert!(zr != xr);
578    }
579
580    #[test]
581    fn test_to_string() {
582        let x = CentroidPeak::new(204.07, 5000f32, 19);
583        assert!(x.to_string().starts_with("CentroidPeak"));
584
585        let x = DeconvolutedPeak {
586            neutral_mass: 799.359964027,
587            charge: 2,
588            intensity: 5000f32,
589            index: 1,
590        };
591
592        assert!(x.to_string().starts_with("DeconvolutedPeak"));
593    }
594
595    #[test]
596    fn test_coordinate_context() {
597        let x = DeconvolutedPeak {
598            neutral_mass: 799.359964027,
599            charge: 2,
600            intensity: 5000f32,
601            index: 1,
602        };
603        assert_eq!(x.neutral_mass, 799.359964027);
604        assert_eq!(CoordinateLike::<Mass>::coordinate(&x), 799.359964027);
605        assert_eq!(Mass::coordinate(&x), 799.359964027);
606        assert!((x.mz() - 400.68725848027003).abs() < 1e-6);
607        assert!((MZ::coordinate(&x) - 400.68725848027003).abs() < 1e-6);
608
609        let mut y = x.as_centroid();
610        *y.intensity_mut() += 500.0;
611        assert!(x < y);
612        assert!(y > x);
613        assert!(x == x);
614        assert!(y != x);
615
616        let xr = DeconvolutedCentroidRef::new(&x, 0);
617        let yr = DeconvolutedCentroidRef::new(&y, 0);
618        assert!(xr < yr);
619        assert!(yr > xr);
620        assert!(xr == xr);
621        assert!(yr != xr);
622    }
623
624    #[cfg(feature = "serde")]
625    #[test]
626    fn test_serialize() -> std::io::Result<()> {
627        use serde_json;
628        use std::io;
629        use std::io::prelude::*;
630
631        let mut buff = Vec::new();
632        let buffer_writer = io::Cursor::new(&mut buff);
633        let mut writer = io::BufWriter::new(buffer_writer);
634
635        let x = CentroidPeak::new(204.07, 5000f32, 19);
636        // let y: MZPoint = x.clone().into();
637        serde_json::to_writer_pretty(&mut writer, &x)?;
638        writer.flush()?;
639        let view = String::from_utf8_lossy(writer.get_ref().get_ref());
640        let peak: CentroidPeak = serde_json::from_str(&view)?;
641        assert!((peak.mz() - x.mz()).abs() < 1e-6);
642        Ok(())
643    }
644}