Skip to main content

mzdata_spectrum/
spectrum_types.rs

1use std::convert::TryFrom;
2use std::{borrow::Cow, ops::Index};
3
4use mzdata_param::curie;
5use mzpeaks::Mass;
6use thiserror::Error;
7
8use mzpeaks::{
9    peak_set::PeakSetVec, prelude::*, CentroidLike, CentroidPeak, DeconvolutedCentroidLike,
10    DeconvolutedPeak, MZPeakSetType, MassPeakSetType, PeakCollection, PeakSet, MZ,
11};
12
13#[cfg(feature = "mzsignal")]
14use mzsignal::{
15    denoise::{denoise, DenoisingError},
16    peak_picker::{PeakFitType, PeakPicker, PeakPickerError},
17    reprofile::{self, PeakShape, PeakShapeModel},
18    FittedPeak,
19};
20
21use mzdata_param::{ParamDescribed, Unit, Value};
22
23#[allow(unused)]
24use mzdata_bindata::{
25    ArrayRetrievalError, ArrayType, BinaryArrayMap, BinaryDataArrayType, BuildArrayMapFrom,
26    BuildFromArrayMap, DataArray,
27};
28
29use crate::peaks::{PeakDataLevel, RefPeakDataLevel, SpectrumSummary};
30use crate::scan_properties::{
31    Acquisition, IonMobilityMeasure, Precursor, ScanPolarity, SignalContinuity, SpectrumDescription,
32};
33
34use crate::utils::HasIonMobility;
35
36/// A blanket trait that ties together all the assumed behaviors of an m/z coordinate centroid peak
37pub trait CentroidPeakAdapting: CentroidLike + From<CentroidPeak> {}
38impl<C: CentroidLike + From<CentroidPeak>> CentroidPeakAdapting for C {}
39
40/// A blanket trait that ties together all the assumed behaviors of an neutral mass coordinate centroid peak
41pub trait DeconvolutedPeakAdapting: DeconvolutedCentroidLike + From<DeconvolutedPeak> {}
42impl<D: DeconvolutedCentroidLike + From<DeconvolutedPeak>> DeconvolutedPeakAdapting for D {}
43
44/// A trait for providing a uniform delegated access to spectrum metadata
45pub trait SpectrumLike<
46    C: CentroidLike = CentroidPeak,
47    D: DeconvolutedCentroidLike = DeconvolutedPeak,
48>: ParamDescribed
49{
50    /// The method to access the spectrum description itself, which supplies
51    /// the data for most other methods on this trait.
52    fn description(&self) -> &SpectrumDescription;
53
54    /// The method to access the spectrum descript itself, mutably.
55    fn description_mut(&mut self) -> &mut SpectrumDescription;
56
57    /// Access the acquisition information for this spectrum.
58    #[inline]
59    fn acquisition(&self) -> &Acquisition {
60        &self.description().acquisition
61    }
62
63    /// Access the (first) precursor information, if it exists.
64    #[inline]
65    fn precursor(&self) -> Option<&Precursor> {
66        let desc = self.description();
67        desc.precursor.first()
68    }
69
70    /// Iterate over all precursors of the spectrum
71    fn precursor_iter(&self) -> impl Iterator<Item = &Precursor> + ExactSizeIterator {
72        let desc = self.description();
73        desc.precursor.iter()
74    }
75
76    /// The number of precursor levels recorded
77    fn precursor_count(&self) -> usize {
78        self.description().precursor.len()
79    }
80
81    /// Mutably access the (first) precursor information, if it exists
82    fn precursor_mut(&mut self) -> Option<&mut Precursor> {
83        let desc = self.description_mut();
84        desc.precursor.first_mut()
85    }
86
87    /// Iterate over all precursors of the spectrum mutably
88    fn precursor_iter_mut(&mut self) -> impl Iterator<Item = &mut Precursor> + ExactSizeIterator {
89        let desc = self.description_mut();
90        desc.precursor.iter_mut()
91    }
92
93    /// Add a precursor to the list of precursors for this spectrum.
94    ///
95    /// Precursors beyond the first one correspond to lower exponentiated spectra, e.g. for an MS3 spectrum
96    /// the first precursor is the MS2 product ion that was selected, and the second precursor corresponds
97    /// to the original MS1 ion that was chosen for MS2.
98    ///
99    /// Higher order precursors may be accessed with [`SpectrumLike::precursor_iter`].
100    fn add_precursor(&mut self, precursor: Precursor) {
101        self.description_mut().precursor.push(precursor);
102    }
103
104    /// Remove the precursor entry at `index` in the precursor list.
105    ///
106    /// Care should be taken if you are attempting to re-order precursors.
107    /// It may be simpler to use [`SpectrumLike::precursor_iter_mut`] to
108    /// re-arrange entries.
109    fn remove_precursor(&mut self, index: usize) -> Precursor {
110        self.description_mut().precursor.remove(index)
111    }
112
113    /// A shortcut method to retrieve the scan start time of a spectrum
114    #[inline]
115    fn start_time(&self) -> f64 {
116        let acq = self.acquisition();
117        match acq.scans.first() {
118            Some(evt) => evt.start_time,
119            None => 0.0,
120        }
121    }
122
123    /// Access the MS exponentiation level
124    #[inline]
125    fn ms_level(&self) -> u8 {
126        self.description().ms_level
127    }
128
129    /// Access the native ID string for the spectrum
130    #[inline]
131    fn id(&self) -> &str {
132        &self.description().id
133    }
134
135    /// Access the index of the spectrum in the source file
136    #[inline]
137    fn index(&self) -> usize {
138        self.description().index
139    }
140
141    /// Access a description of how raw the signal is, whether a
142    /// profile spectrum is available or only centroids are present.
143    #[inline]
144    fn signal_continuity(&self) -> SignalContinuity {
145        self.description().signal_continuity
146    }
147
148    /// Access a description of the spectrum polarity
149    #[inline]
150    fn polarity(&self) -> ScanPolarity {
151        self.description().polarity
152    }
153
154    /// Access the point measure of ion mobility associated with the scan if present. This is distinct from
155    /// having a frame-level scan across the ion mobility dimension.
156    fn ion_mobility(&self) -> Option<f64> {
157        self.acquisition()
158            .iter()
159            .flat_map(|s| s.ion_mobility())
160            .next()
161    }
162
163    /// Check if this spectrum has a point measure of ion mobility. This is distinct from
164    /// having a frame-level scan across the ion mobility dimension.
165    fn has_ion_mobility(&self) -> bool {
166        self.ion_mobility().is_some()
167    }
168
169    /// Retrieve the most processed representation of the mass spectrum's signal
170    fn peaks(&'_ self) -> RefPeakDataLevel<'_, C, D>;
171
172    /// Consume the spectrum, decomposing it into the [`SpectrumDescription`] and an owning
173    /// [`PeakDataLevel`].
174    ///
175    /// # See also
176    /// [`SpectrumLike::peaks`]
177    /// [`SpectrumLike::description`]
178    fn into_peaks_and_description(self) -> (PeakDataLevel<C, D>, SpectrumDescription);
179
180    /// Obtain a reference to the [`BinaryArrayMap`] if one is available for the peak
181    /// information. This may not be the most refined version of the peak signal if
182    /// it has been processed further.
183    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap>;
184
185    /// Check if this spectrum has an ion mobility dimension/array. This is distinct from
186    /// having a scan-level point measure of ion mobility.
187    fn has_ion_mobility_dimension(&self) -> bool {
188        self.raw_arrays()
189            .map(|a| a.has_ion_mobility())
190            .unwrap_or_default()
191    }
192
193    /// A single point of entry for checking if ion mobility is present.
194    fn has_ion_mobility_class(&self) -> HasIonMobility {
195        if self.has_ion_mobility_dimension() {
196            HasIonMobility::Dimension
197        } else if self.has_ion_mobility() {
198            HasIonMobility::Point
199        } else {
200            HasIonMobility::None
201        }
202    }
203
204    /// Find the type of spectrum described.
205    ///
206    /// A spectrum in `mzdata` is *usually* a mass spectrum of some sort,
207    /// but that's not guaranteed to be the case. `mzdata` can handle non-MS spectra,
208    /// but little of the signal processing machinery it provides currently supports
209    /// those other kinds of data.
210    fn spectrum_type(&self) -> Option<mzdata_meta::SpectrumType> {
211        self.description().spectrum_type()
212    }
213
214    /// Set the kind of spectrum represented.
215    fn set_spectrum_type(&mut self, spectrum_type: mzdata_meta::SpectrumType) {
216        self.description_mut().set_spectrum_type(spectrum_type);
217    }
218
219    /// Compute and update the the total ion current, base peak, and m/z range for
220    /// the spectrum based upon its current peak data.
221    ///
222    /// Uses [`RefPeakDataLevel::fetch_summaries`]
223    fn update_summaries(&mut self) {
224        let SpectrumSummary {
225            tic,
226            base_peak: bp,
227            mz_range,
228            count: _,
229        } = self.peaks().fetch_summaries();
230
231        let desc = self.description_mut();
232        let params = desc.params_mut();
233        let tic_curie = curie!(MS:1000285);
234
235        if let Some(p) = params.iter_mut().find(|p| **p == tic_curie) {
236            p.value = Value::Float(tic as f64);
237        } else {
238            let mut p = tic_curie.as_param();
239            p.name = "total ion current".to_string();
240            p.value = Value::Float(tic as f64);
241            p.unit = Unit::DetectorCounts;
242            params.push(p)
243        }
244
245        let bpmz_curie = curie!(MS:1000504);
246        if let Some(p) = params.iter_mut().find(|p| **p == bpmz_curie) {
247            p.value = Value::Float(bp.mz());
248        } else {
249            let mut p = bpmz_curie.as_param();
250            p.name = "base peak m/z".to_string();
251            p.value = Value::Float(bp.mz());
252            p.unit = Unit::MZ;
253            params.push(p);
254        }
255
256        let lowest_mz_curie = curie!(MS:1000528);
257        if let Some(p) = params.iter_mut().find(|p| **p == lowest_mz_curie) {
258            p.value = Value::Float(mz_range.0);
259        } else {
260            let mut p = lowest_mz_curie.as_param();
261            p.name = "lowest observed m/z".to_string();
262            p.value = Value::Float(mz_range.0);
263            p.unit = Unit::MZ;
264            params.push(p);
265        }
266
267        let highest_mz_curie = curie!(MS:1000527);
268        if let Some(p) = params.iter_mut().find(|p| **p == highest_mz_curie) {
269            p.value = Value::Float(mz_range.0);
270        } else {
271            let mut p = highest_mz_curie.as_param();
272            p.name = "highest observed m/z".to_string();
273            p.value = Value::Float(mz_range.1);
274            p.unit = Unit::MZ;
275            params.push(p);
276        }
277
278        let bpint_curie = curie!(MS:1000505);
279        if let Some(p) = params.iter_mut().find(|p| **p == bpint_curie) {
280            p.value = Value::Float(bp.intensity() as f64);
281        } else {
282            let mut p = bpint_curie.as_param();
283            p.name = "base peak intensity".to_string();
284            p.value = Value::Float(bp.intensity() as f64);
285            p.unit = Unit::DetectorCounts;
286            params.push(p);
287        }
288    }
289}
290
291#[derive(Default, Debug, Clone)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
293/// Represents a spectrum that hasn't been processed yet, with only
294/// data arrays, potentially no discrete peaks. A raw spectrum may still
295/// be centroided, but the peaks still need to be decoded.
296pub struct RawSpectrum {
297    /// The spectrum metadata describing acquisition conditions and details.
298    pub description: SpectrumDescription,
299    /// The data arrays describing the m/z, intensity, and potentially other
300    /// measured properties
301    pub arrays: BinaryArrayMap,
302}
303
304impl ParamDescribed for RawSpectrum {
305    fn params(&self) -> &[crate::params::Param] {
306        <SpectrumDescription as ParamDescribed>::params(&self.description)
307    }
308
309    fn params_mut(&mut self) -> &mut crate::params::ParamList {
310        <SpectrumDescription as ParamDescribed>::params_mut(&mut self.description)
311    }
312}
313
314/// Errors that may arise when converting between different [`SpectrumLike`] types
315#[derive(Debug, Clone, PartialEq, Error)]
316pub enum SpectrumConversionError {
317    #[error("m/z array does not match size of intensity array")]
318    MZIntensityArraySizeMismatch,
319    #[error("Operation expected charge-deconvolved data but did not find it")]
320    NotDeconvoluted,
321    #[error("Operation expected centroided data but did not find it")]
322    NotCentroided,
323    #[error("No peak data of any kind was found")]
324    NoPeakData,
325    #[error("An error occurred while accessing raw data arrays: {0}")]
326    ArrayRetrievalError(
327        #[from]
328        #[source]
329        ArrayRetrievalError,
330    ),
331}
332
333/// Errors that may arise when performing signal processing or other data transformation
334#[derive(Debug, Clone, thiserror::Error)]
335pub enum SpectrumProcessingError {
336    #[cfg(feature = "mzsignal")]
337    #[error("An error occurred while denoising: {0:?}")]
338    DenoisingError(
339        #[from]
340        #[source]
341        DenoisingError,
342    ),
343
344    #[cfg(feature = "mzsignal")]
345    #[error("An error occurred while peak picking: {0:?}")]
346    PeakPickerError(
347        #[from]
348        #[source]
349        PeakPickerError,
350    ),
351
352    #[error("An error occurred while trying to convert spectrum types: {0}")]
353    SpectrumConversionError(
354        #[from]
355        #[source]
356        SpectrumConversionError,
357    ),
358
359    #[error("An error occurred while accessing raw data arrays: {0}")]
360    ArrayRetrievalError(
361        #[from]
362        #[source]
363        ArrayRetrievalError,
364    ),
365}
366
367impl<'transient, 'lifespan: 'transient> RawSpectrum {
368    pub fn new(description: SpectrumDescription, arrays: BinaryArrayMap) -> Self {
369        Self {
370            description,
371            arrays,
372        }
373    }
374
375    /// Convert a spectrum into a [`CentroidSpectrumType`].
376    ///
377    /// # Errors
378    /// This operation returns [`SpectrumConversionError::NotCentroided`] if the
379    /// [`SpectrumLike::signal_continuity`] != [`SignalContinuity::Centroid`].
380    ///
381    /// # See also
382    /// To pick peaks from profile data, see [`RawSpectrum::pick_peaks_into`]
383    pub fn into_centroid<C>(self) -> Result<CentroidSpectrumType<C>, SpectrumConversionError>
384    where
385        C: BuildFromArrayMap + CentroidLike,
386    {
387        if !matches!(
388            self.description.signal_continuity,
389            SignalContinuity::Centroid
390        ) {
391            return Err(SpectrumConversionError::NotCentroided);
392        }
393
394        let peaks = match C::try_from_arrays(&self.arrays) {
395            Ok(peaks) => peaks.into(),
396            Err(e) => return Err(e.into()),
397        };
398        let mut centroid = CentroidSpectrumType::<C> {
399            description: self.description,
400            peaks,
401        };
402        centroid.description.signal_continuity = SignalContinuity::Centroid;
403
404        Ok(centroid)
405    }
406
407    /// Access the m/z array.
408    ///
409    /// # Panics
410    /// This function will panic if [`ArrayType::MZArray`] is not
411    /// present in [`RawSpectrum::arrays`]
412    pub fn mzs(&'lifespan self) -> Cow<'transient, [f64]> {
413        self.arrays.mzs().unwrap()
414    }
415
416    /// Access the intensity array.
417    ///
418    /// # Panics
419    /// This function will panic if [`ArrayType::IntensityArray`] is not
420    /// present in [`RawSpectrum::arrays`]
421    pub fn intensities(&'lifespan self) -> Cow<'transient, [f32]> {
422        self.arrays.intensities().unwrap()
423    }
424
425    pub fn mzs_mut(&mut self) -> Result<&mut [f64], ArrayRetrievalError> {
426        self.arrays.mzs_mut()
427    }
428
429    pub fn intensities_mut(&mut self) -> Result<&mut [f32], ArrayRetrievalError> {
430        self.arrays.intensities_mut()
431    }
432
433    /// Explicitly decode any [`DataArray`] that is encoded or compressed still
434    /// so that they are ready for use.
435    ///
436    /// # See also
437    /// [`BinaryArrayMap::decode_all_arrays`]
438    pub fn decode_all_arrays(&mut self) -> Result<(), ArrayRetrievalError> {
439        self.arrays.decode_all_arrays()
440    }
441
442    /// Convert a spectrum into a [`MultiLayerSpectrum`].
443    pub fn into_spectrum<C, D>(self) -> Result<MultiLayerSpectrum<C, D>, SpectrumConversionError>
444    where
445        C: BuildFromArrayMap,
446        C: CentroidLike,
447        D: DeconvolutedCentroidLike,
448    {
449        Ok(MultiLayerSpectrum::<C, D> {
450            arrays: Some(self.arrays),
451            description: self.description,
452            ..Default::default()
453        })
454    }
455
456    /// Apply a local denoising algorithm to the signal that is suitable for profile mode
457    /// data using [`mzsignal::denoise`].
458    #[cfg(feature = "mzsignal")]
459    pub fn denoise(&mut self, scale: f32) -> Result<(), SpectrumProcessingError> {
460        let mut intensities_copy = self.arrays.intensities()?.into_owned();
461        let mz_array = self.arrays.mzs()?;
462        match denoise(&mz_array, &mut intensities_copy, scale) {
463            Ok(_) => {
464                let view = self.arrays.get_mut(&ArrayType::IntensityArray).unwrap();
465                view.store_as(BinaryDataArrayType::Float32)
466                    .expect("Failed to reformat intensity array");
467                view.update_buffer(&intensities_copy)
468                    .expect("Failed to update intensity array buffer");
469                Ok(())
470            }
471            Err(err) => Err(SpectrumProcessingError::DenoisingError(err)),
472        }
473    }
474
475    /// pick peaks with `peak_picker` and convert this spectrum into a [`MultiLayerSpectrum`] with a centroid peak list
476    /// as well as raw data arrays.
477    ///
478    /// # See also
479    /// [`MultiLayerSpectrum::pick_peaks_with`]
480    #[cfg(feature = "mzsignal")]
481    pub fn pick_peaks_with_into<
482        C: CentroidPeakAdapting + BuildFromArrayMap + From<FittedPeak>,
483        D: DeconvolutedPeakAdapting + BuildFromArrayMap,
484    >(
485        self,
486        peak_picker: &PeakPicker,
487    ) -> Result<MultiLayerSpectrum<C, D>, SpectrumProcessingError> {
488        let mut result = self.into_spectrum()?;
489        match result.pick_peaks_with(peak_picker) {
490            Ok(_) => Ok(result),
491            Err(err) => Err(err),
492        }
493    }
494
495    /// Pick peaks with a minimum signal-to-noise threshold and convert this spectrum into a [`MultiLayerSpectrum`] with
496    /// a centroid peak list as well as raw data arrays.
497    ///
498    /// # See also
499    /// [`RawSpectrum::pick_peaks_with_into`]
500    #[cfg(feature = "mzsignal")]
501    pub fn pick_peaks_into<
502        C: CentroidPeakAdapting + BuildFromArrayMap + From<FittedPeak>,
503        D: DeconvolutedPeakAdapting + BuildFromArrayMap,
504    >(
505        self,
506        signal_to_noise_threshold: f32,
507    ) -> Result<MultiLayerSpectrum<C, D>, SpectrumProcessingError> {
508        let peak_picker = PeakPicker {
509            fit_type: PeakFitType::Quadratic,
510            signal_to_noise_threshold,
511            ..Default::default()
512        };
513        self.pick_peaks_with_into(&peak_picker)
514    }
515
516    /// Pick peaks in the specified m/z ranges with a minimum signal-to-noise threshold and convert
517    /// this spectrum into a [`MultiLayerSpectrum`] with a centroid peak list as well as raw data arrays.
518    ///
519    /// # See also
520    /// [`MultiLayerSpectrum::pick_peaks_in_intervals`]
521    #[cfg(feature = "mzsignal")]
522    pub fn pick_peaks_in_intervals_into<
523        C: CentroidPeakAdapting + BuildFromArrayMap + From<FittedPeak>,
524        D: DeconvolutedPeakAdapting + BuildFromArrayMap,
525    >(
526        self,
527        signal_to_noise_threshold: f32,
528        intervals: &[(f64, f64)],
529    ) -> Result<MultiLayerSpectrum<C, D>, SpectrumProcessingError> {
530        let mut result = self.into_spectrum()?;
531        match result.pick_peaks_in_intervals(signal_to_noise_threshold, intervals) {
532            Ok(_) => Ok(result),
533            Err(err) => Err(err),
534        }
535    }
536}
537
538impl RawSpectrum {
539    /// The method to access the spectrum description itself, which supplies
540    /// the data for most other methods on this trait.
541    pub fn description(&self) -> &SpectrumDescription {
542        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::description(self)
543    }
544
545    /// The method to access the spectrum descript itself, mutably.
546    pub fn description_mut(&mut self) -> &mut SpectrumDescription {
547        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::description_mut(self)
548    }
549
550    /// Access the acquisition information for this spectrum.
551    #[inline]
552    pub fn acquisition(&self) -> &Acquisition {
553        &self.description().acquisition
554    }
555
556    /// Access the precursor information, if it exists.
557    #[inline]
558    pub fn precursor(&self) -> Option<&Precursor> {
559        let desc = self.description();
560        desc.precursor.first()
561    }
562
563    /// Iterate over all precursors of the spectrum
564    pub fn precursor_iter(&self) -> impl Iterator<Item = &Precursor> {
565        let desc = self.description();
566        desc.precursor.iter()
567    }
568
569    /// Mutably access the (first) precursor information, if it exists
570    pub fn precursor_mut(&mut self) -> Option<&mut Precursor> {
571        let desc = self.description_mut();
572        desc.precursor.first_mut()
573    }
574
575    /// Iterate over all precursors of the spectrum mutably
576    pub fn precursor_iter_mut(&mut self) -> impl Iterator<Item = &mut Precursor> {
577        let desc = self.description_mut();
578        desc.precursor.iter_mut()
579    }
580
581    /// A shortcut method to retrieve the scan start time of a spectrum
582    #[inline]
583    pub fn start_time(&self) -> f64 {
584        let acq = self.acquisition();
585        match acq.scans.first() {
586            Some(evt) => evt.start_time,
587            None => 0.0,
588        }
589    }
590
591    /// Access the MS exponentiation level
592    #[inline]
593    pub fn ms_level(&self) -> u8 {
594        self.description().ms_level
595    }
596
597    /// Access the native ID string for the spectrum
598    #[inline]
599    pub fn id(&self) -> &str {
600        &self.description().id
601    }
602
603    /// Access the index of the spectrum in the source file
604    #[inline]
605    pub fn index(&self) -> usize {
606        self.description().index
607    }
608
609    /// Access a description of how raw the signal is, whether a
610    /// profile spectrum is available or only centroids are present.
611    #[inline]
612    pub fn signal_continuity(&self) -> SignalContinuity {
613        self.description().signal_continuity
614    }
615
616    /// Access a description of the spectrum polarity
617    #[inline]
618    pub fn polarity(&self) -> ScanPolarity {
619        self.description().polarity
620    }
621
622    /// Access the point measure of ion mobility associated with the scan if present. This is distinct from
623    /// having a frame-level scan across the ion mobility dimension.
624    pub fn ion_mobility(&self) -> Option<f64> {
625        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::ion_mobility(self)
626    }
627
628    /// Check if this spectrum has a point measure of ion mobility. This is distinct from
629    /// having a frame-level scan across the ion mobility dimension.
630    pub fn has_ion_mobility(&self) -> bool {
631        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::ion_mobility(self).is_some()
632    }
633
634    /// Retrieve the most processed representation of the mass spectrum's signal
635    pub fn peaks(&'_ self) -> RefPeakDataLevel<'_, CentroidPeak, DeconvolutedPeak> {
636        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::peaks(self)
637    }
638
639    pub fn into_peaks_and_description(
640        self,
641    ) -> (
642        PeakDataLevel<CentroidPeak, DeconvolutedPeak>,
643        SpectrumDescription,
644    ) {
645        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::into_peaks_and_description(
646            self,
647        )
648    }
649
650    /// Obtain a reference to the [`BinaryArrayMap`] if one is available for the peak
651    /// information. This may not be the most refined version of the peak signal if
652    /// it has been processed further.
653    pub fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap> {
654        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::raw_arrays(self)
655    }
656
657    /// Check if this spectrum has an ion mobility dimension/array. This is distinct from
658    /// having a scan-level point measure of ion mobility.
659    pub fn has_ion_mobility_dimension(&self) -> bool {
660        self.raw_arrays()
661            .map(|a| a.has_ion_mobility())
662            .unwrap_or_default()
663    }
664
665    /// Compute and update the the total ion current, base peak, and m/z range for
666    /// the spectrum based upon its current peak data.
667    ///
668    /// Uses [`RefPeakDataLevel::fetch_summaries`]
669    pub fn update_summaries(&mut self) {
670        <RawSpectrum as SpectrumLike<CentroidPeak, DeconvolutedPeak>>::update_summaries(self)
671    }
672}
673
674impl<C: CentroidLike, D: DeconvolutedCentroidLike> SpectrumLike<C, D> for RawSpectrum {
675    #[inline]
676    fn description(&self) -> &SpectrumDescription {
677        &self.description
678    }
679
680    fn description_mut(&mut self) -> &mut SpectrumDescription {
681        &mut self.description
682    }
683
684    fn peaks(&'_ self) -> RefPeakDataLevel<'_, C, D> {
685        RefPeakDataLevel::RawData(&self.arrays)
686    }
687
688    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap> {
689        Some(&self.arrays)
690    }
691
692    fn into_peaks_and_description(self) -> (PeakDataLevel<C, D>, SpectrumDescription) {
693        (PeakDataLevel::RawData(self.arrays), self.description)
694    }
695}
696
697#[derive(Debug, Clone)]
698#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
699/// Represents a spectrum that has been centroided into discrete m/z points, a
700/// process also called "peak picking".
701///
702/// This type of spectrum represents data in exactly one format.
703pub struct CentroidSpectrumType<C: CentroidLike> {
704    /// The spectrum metadata describing acquisition conditions and details.
705    pub description: SpectrumDescription,
706    /// The picked centroid peaks, sorted by m/z in a fast searchable structure.
707    pub peaks: MZPeakSetType<C>,
708}
709
710impl<C: CentroidLike> Default for CentroidSpectrumType<C> {
711    fn default() -> Self {
712        Self {
713            description: Default::default(),
714            peaks: PeakSetVec::empty(),
715        }
716    }
717}
718
719/// When the `mzsignal` feature is enabled, [`CentroidSpectrumType`] can be converted back into a profile-like
720/// state using a peak shape model.
721#[cfg(feature = "mzsignal")]
722impl<C: CentroidLike + BuildArrayMapFrom + BuildFromArrayMap> CentroidSpectrumType<C> {
723    /// Convert the centroid peaks in theoretical profile signal, producing a [`MultiLayerSpectrum`]
724    /// which contains both the `peaks` as well as a raw data array in profile mode.
725    ///
726    /// # Arguments
727    /// - `dx`: The m/z spacing to reprofile over. The smaller the value, the higher the resolution, but also
728    ///    the more memory required. For high resolution instruments, a value between `0.005` and `0.001` is suitable.
729    /// - `fwhm`: The full width at half max to assume when reprofiling each peak. This affects the peak shape width.
730    pub fn reprofile_with_shape_into(
731        self,
732        dx: f64,
733        fwhm: f32,
734    ) -> Result<MultiLayerSpectrum<C, DeconvolutedPeak>, SpectrumProcessingError> {
735        let mut spectrum = self.into_spectrum()?;
736        spectrum.reprofile_with_shape(dx, fwhm)?;
737        Ok(spectrum)
738    }
739}
740
741impl<C: CentroidLike> ParamDescribed for CentroidSpectrumType<C> {
742    fn params(&self) -> &[crate::params::Param] {
743        <SpectrumDescription as ParamDescribed>::params(&self.description)
744    }
745
746    fn params_mut(&mut self) -> &mut crate::params::ParamList {
747        <SpectrumDescription as ParamDescribed>::params_mut(&mut self.description)
748    }
749}
750
751/// [`CentroidSpectrumType`] implements [`SpectrumLike`] for `C`.
752impl<C: CentroidLike> SpectrumLike<C> for CentroidSpectrumType<C> {
753    #[inline]
754    fn description(&self) -> &SpectrumDescription {
755        &self.description
756    }
757
758    fn description_mut(&mut self) -> &mut SpectrumDescription {
759        &mut self.description
760    }
761
762    fn peaks(&'_ self) -> RefPeakDataLevel<'_, C, DeconvolutedPeak> {
763        RefPeakDataLevel::Centroid(&self.peaks)
764    }
765
766    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap> {
767        None
768    }
769
770    fn into_peaks_and_description(
771        self,
772    ) -> (PeakDataLevel<C, DeconvolutedPeak>, SpectrumDescription) {
773        (PeakDataLevel::Centroid(self.peaks), self.description)
774    }
775}
776
777impl<C: CentroidLike> CentroidSpectrumType<C> {
778    pub fn new(description: SpectrumDescription, peaks: MZPeakSetType<C>) -> Self {
779        Self { description, peaks }
780    }
781
782    /// Convert a spectrum into a [`MultiLayerSpectrum`] on `C`, but preserve all information
783    /// associated with this spectrum as-is.
784    pub fn into_spectrum<D>(self) -> Result<MultiLayerSpectrum<C, D>, SpectrumConversionError>
785    where
786        D: DeconvolutedCentroidLike,
787    {
788        let val = MultiLayerSpectrum::<C, D> {
789            peaks: Some(self.peaks),
790            description: self.description,
791            ..Default::default()
792        };
793        Ok(val)
794    }
795
796    /// Convert a [`CentroidSpectrumType`] into a [`MultiLayerSpectrum`] over any peak type, but
797    /// recode the peak list as [`DataArray`], potentially losing information if the new peak types
798    /// do not re-use the same [`ArrayType`] during peak reconstruction.
799    ///
800    /// For example, reinterpreting [`mzsignal::FittedPeak`] to [`mzpeaks::CentroidPeak`] will convert
801    pub fn reinterpret<C1, D1>(self) -> Result<MultiLayerSpectrum<C1, D1>, SpectrumConversionError>
802    where
803        C1: CentroidLike + BuildArrayMapFrom + BuildFromArrayMap,
804        D1: DeconvolutedCentroidLike + BuildArrayMapFrom + BuildFromArrayMap,
805        C: BuildArrayMapFrom,
806    {
807        let arrays = C::as_arrays(self.peaks.as_slice());
808        Ok(MultiLayerSpectrum::new(
809            self.description,
810            Some(arrays),
811            None,
812            None,
813        ))
814    }
815}
816
817pub type CentroidSpectrum = CentroidSpectrumType<CentroidPeak>;
818
819impl<C: CentroidPeakAdapting> Index<usize> for CentroidSpectrumType<C> {
820    type Output = <MZPeakSetType<C> as Index<usize>>::Output;
821
822    fn index(&self, index: usize) -> &Self::Output {
823        self.peaks.index(index)
824    }
825}
826
827impl<C: CentroidPeakAdapting> PeakCollection<C, MZ> for CentroidSpectrumType<C>
828where
829    MZPeakSetType<C>: PeakCollection<C, MZ>,
830    <MZPeakSetType<C> as Index<usize>>::Output: CoordinateLike<MZ>,
831{
832    fn len(&self) -> usize {
833        self.peaks.len()
834    }
835
836    fn get_item(&self, i: usize) -> &C {
837        self.peaks.get_item(i)
838    }
839
840    fn get_slice(&self, i: std::ops::Range<usize>) -> &[C] {
841        self.peaks.get_slice(i)
842    }
843
844    fn search_by(&self, query: f64) -> Result<usize, usize> {
845        self.peaks.search_by(query)
846    }
847
848    fn iter<'a>(&'a self) -> impl Iterator<Item = &'a C>
849    where
850        C: 'a,
851    {
852        self.peaks.iter()
853    }
854}
855
856/// Represents a spectrum that has been centroided, deisotoped, and charge state deconvolved.
857///
858/// This type of spectrum represents data in exactly one format.
859#[derive(Debug, Clone)]
860#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
861pub struct DeconvolutedSpectrumType<D: DeconvolutedCentroidLike> {
862    /// The spectrum metadata describing acquisition conditions and details.
863    pub description: SpectrumDescription,
864    /// The deisotoped and charge state deconvolved peaks, sorted by neutral mass
865    /// in a fast searchable structure.
866    pub deconvoluted_peaks: MassPeakSetType<D>,
867}
868
869impl<D: DeconvolutedCentroidLike> Default for DeconvolutedSpectrumType<D> {
870    fn default() -> Self {
871        Self {
872            description: Default::default(),
873            deconvoluted_peaks: PeakSetVec::empty(),
874        }
875    }
876}
877
878impl<D: DeconvolutedCentroidLike> DeconvolutedSpectrumType<D> {
879    pub fn new(description: SpectrumDescription, deconvoluted_peaks: MassPeakSetType<D>) -> Self {
880        Self {
881            description,
882            deconvoluted_peaks,
883        }
884    }
885
886    /// Convert a spectrum into a [`MultiLayerSpectrum`] on `D`, but preserve all information
887    /// associated with this spectrum as-is.
888    pub fn into_spectrum<C>(self) -> Result<MultiLayerSpectrum<C, D>, SpectrumConversionError>
889    where
890        C: CentroidLike,
891    {
892        let val = MultiLayerSpectrum::<C, D> {
893            deconvoluted_peaks: Some(self.deconvoluted_peaks),
894            description: self.description,
895            ..Default::default()
896        };
897        Ok(val)
898    }
899
900    /// Convert a [`CentroidSpectrumType`] into a [`MultiLayerSpectrum`] over any peak type, but
901    /// recode the peak list as [`DataArray`], potentially losing information if the new peak types
902    /// do not re-use the same [`ArrayType`] during peak reconstruction.
903    pub fn reinterpret<C1, D1>(self) -> MultiLayerSpectrum<C1, D1>
904    where
905        C1: CentroidLike + BuildArrayMapFrom + BuildFromArrayMap,
906        D1: DeconvolutedCentroidLike + BuildArrayMapFrom + BuildFromArrayMap,
907        D: BuildArrayMapFrom,
908    {
909        let arrays = D::as_arrays(self.deconvoluted_peaks.as_slice());
910        MultiLayerSpectrum::new(self.description, Some(arrays), None, None)
911    }
912}
913
914impl<D: DeconvolutedCentroidLike> ParamDescribed for DeconvolutedSpectrumType<D> {
915    fn params(&self) -> &[crate::params::Param] {
916        <SpectrumDescription as ParamDescribed>::params(&self.description)
917    }
918
919    fn params_mut(&mut self) -> &mut crate::params::ParamList {
920        <SpectrumDescription as ParamDescribed>::params_mut(&mut self.description)
921    }
922}
923
924/// [`DeconvolutedSpectrumType`] implements [`SpectrumLike`] for `D`.
925impl<D: DeconvolutedCentroidLike> SpectrumLike<CentroidPeak, D> for DeconvolutedSpectrumType<D> {
926    #[inline]
927    fn description(&self) -> &SpectrumDescription {
928        &self.description
929    }
930
931    fn description_mut(&mut self) -> &mut SpectrumDescription {
932        &mut self.description
933    }
934
935    fn peaks(&'_ self) -> RefPeakDataLevel<'_, CentroidPeak, D> {
936        RefPeakDataLevel::Deconvoluted(&self.deconvoluted_peaks)
937    }
938
939    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap> {
940        None
941    }
942
943    fn into_peaks_and_description(self) -> (PeakDataLevel<CentroidPeak, D>, SpectrumDescription) {
944        (
945            PeakDataLevel::Deconvoluted(self.deconvoluted_peaks),
946            self.description,
947        )
948    }
949}
950
951pub type DeconvolutedSpectrum = DeconvolutedSpectrumType<DeconvolutedPeak>;
952
953impl<D: DeconvolutedPeakAdapting> Index<usize> for DeconvolutedSpectrumType<D> {
954    type Output = <MassPeakSetType<D> as Index<usize>>::Output;
955
956    fn index(&self, index: usize) -> &Self::Output {
957        self.deconvoluted_peaks.index(index)
958    }
959}
960
961impl<D: DeconvolutedPeakAdapting> PeakCollection<D, Mass> for DeconvolutedSpectrumType<D>
962where
963    MassPeakSetType<D>: PeakCollection<D, Mass>,
964    <MassPeakSetType<D> as Index<usize>>::Output: CoordinateLike<Mass>,
965{
966    fn len(&self) -> usize {
967        self.deconvoluted_peaks.len()
968    }
969
970    fn get_item(&self, i: usize) -> &D {
971        self.deconvoluted_peaks.get_item(i)
972    }
973
974    fn get_slice(&self, i: std::ops::Range<usize>) -> &[D] {
975        self.deconvoluted_peaks.get_slice(i)
976    }
977
978    fn search_by(&self, query: f64) -> Result<usize, usize> {
979        self.deconvoluted_peaks.search_by(query)
980    }
981
982    fn iter<'a>(&'a self) -> impl Iterator<Item = &'a D>
983    where
984        D: 'a,
985    {
986        self.deconvoluted_peaks.iter()
987    }
988}
989
990#[derive(Debug, Clone)]
991#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
992/// Represent a spectrum with multiple layers of representation of the
993/// peak data.
994///
995/// This type is useful because it permits us to hold spectra in incrementally
996/// more processed representations without loss of information.
997pub struct MultiLayerSpectrum<
998    C: CentroidLike = CentroidPeak,
999    D: DeconvolutedCentroidLike = DeconvolutedPeak,
1000> {
1001    /// The spectrum metadata describing acquisition conditions and details.
1002    pub description: SpectrumDescription,
1003
1004    /// The (potentially absent) data arrays describing the m/z, intensity,
1005    /// and potentially other measured properties
1006    pub arrays: Option<BinaryArrayMap>,
1007
1008    // The (potentially absent) centroid peaks
1009    pub peaks: Option<MZPeakSetType<C>>,
1010
1011    // The (potentially absent) deconvoluted peaks
1012    pub deconvoluted_peaks: Option<MassPeakSetType<D>>,
1013}
1014
1015impl<C: CentroidLike, D: DeconvolutedCentroidLike> MultiLayerSpectrum<C, D> {
1016    /// Create a new [`MultiLayerSpectrum`] from a [`SpectrumDescription`] and
1017    /// any/all/none of the different levels of processing details for signal
1018    /// data.
1019    pub fn new(
1020        description: SpectrumDescription,
1021        arrays: Option<BinaryArrayMap>,
1022        peaks: Option<MZPeakSetType<C>>,
1023        deconvoluted_peaks: Option<MassPeakSetType<D>>,
1024    ) -> Self {
1025        Self {
1026            description,
1027            arrays,
1028            peaks,
1029            deconvoluted_peaks,
1030        }
1031    }
1032}
1033
1034impl<C: CentroidLike, D: DeconvolutedCentroidLike> Default for MultiLayerSpectrum<C, D> {
1035    fn default() -> Self {
1036        Self {
1037            description: Default::default(),
1038            arrays: None,
1039            peaks: None,
1040            deconvoluted_peaks: None,
1041        }
1042    }
1043}
1044
1045impl<C: CentroidLike, D: DeconvolutedCentroidLike> ParamDescribed for MultiLayerSpectrum<C, D> {
1046    fn params(&self) -> &[crate::params::Param] {
1047        <SpectrumDescription as ParamDescribed>::params(&self.description)
1048    }
1049
1050    fn params_mut(&mut self) -> &mut crate::params::ParamList {
1051        <SpectrumDescription as ParamDescribed>::params_mut(&mut self.description)
1052    }
1053}
1054
1055impl<C: CentroidLike, D: DeconvolutedCentroidLike> SpectrumLike<C, D> for MultiLayerSpectrum<C, D> {
1056    #[inline]
1057    fn description(&self) -> &SpectrumDescription {
1058        &self.description
1059    }
1060
1061    fn description_mut(&mut self) -> &mut SpectrumDescription {
1062        &mut self.description
1063    }
1064
1065    fn peaks(&'_ self) -> RefPeakDataLevel<'_, C, D> {
1066        if let Some(peaks) = &self.deconvoluted_peaks {
1067            RefPeakDataLevel::Deconvoluted(peaks)
1068        } else if let Some(peaks) = &self.peaks {
1069            RefPeakDataLevel::Centroid(peaks)
1070        } else if let Some(arrays) = &self.arrays {
1071            RefPeakDataLevel::RawData(arrays)
1072        } else {
1073            RefPeakDataLevel::Missing
1074        }
1075    }
1076
1077    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap> {
1078        self.arrays.as_ref()
1079    }
1080
1081    fn into_peaks_and_description(self) -> (PeakDataLevel<C, D>, SpectrumDescription) {
1082        if let Some(peaks) = self.deconvoluted_peaks {
1083            (PeakDataLevel::Deconvoluted(peaks), self.description)
1084        } else if let Some(peaks) = self.peaks {
1085            (PeakDataLevel::Centroid(peaks), self.description)
1086        } else if let Some(arrays) = self.arrays {
1087            (PeakDataLevel::RawData(arrays), self.description)
1088        } else {
1089            (PeakDataLevel::Missing, self.description)
1090        }
1091    }
1092}
1093
1094impl<C: CentroidLike, D: DeconvolutedCentroidLike> MultiLayerSpectrum<C, D>
1095where
1096    C: BuildFromArrayMap + BuildArrayMapFrom,
1097    D: BuildFromArrayMap + BuildArrayMapFrom,
1098{
1099    /// Convert a spectrum into a [`CentroidSpectrumType`].
1100    ///
1101    /// This will try the following steps to get the peak list to use in the new instance:
1102    ///   1. If the `peaks` field is populated, use it directly .
1103    ///   2. If the [`Self::signal_continuity`] is [`SignalContinuity::Centroid`] and
1104    ///      [`Self::arrays`] is populated, attempt [`C::try_from_arrays`] to build
1105    ///      the peak list from the raw arrays. Otherwise assume the peak list is empty.
1106    ///   3. Fail with [`SpectrumConversionError::NotCentroided`]
1107    pub fn into_centroid(self) -> Result<CentroidSpectrumType<C>, SpectrumConversionError> {
1108        if let Some(peaks) = self.peaks {
1109            let mut result = CentroidSpectrumType::<C> {
1110                peaks,
1111                description: self.description,
1112            };
1113            result.description.signal_continuity = SignalContinuity::Centroid;
1114            return Ok(result);
1115        } else if self.signal_continuity() == SignalContinuity::Centroid {
1116            if let Some(arrays) = &self.arrays {
1117                let peaks = C::try_from_arrays(arrays)?.into();
1118                let mut centroid = CentroidSpectrumType::<C> {
1119                    description: self.description,
1120                    peaks,
1121                };
1122                centroid.description.signal_continuity = SignalContinuity::Centroid;
1123                return Ok(centroid);
1124            } else {
1125                let mut result = CentroidSpectrumType::<C> {
1126                    peaks: MZPeakSetType::<C>::empty(),
1127                    description: self.description,
1128                };
1129                result.description.signal_continuity = SignalContinuity::Centroid;
1130                return Ok(result);
1131            }
1132        }
1133        Err(SpectrumConversionError::NotCentroided)
1134    }
1135
1136    /// Convert a spectrum into a [`RawSpectrum`]
1137    pub fn into_raw(self) -> Result<RawSpectrum, SpectrumConversionError> {
1138        if let Some(arrays) = self.arrays {
1139            Ok(RawSpectrum {
1140                arrays,
1141                description: self.description,
1142            })
1143        } else if let Some(peaks) = self.deconvoluted_peaks {
1144            let arrays = D::as_arrays(&peaks[0..]);
1145            let mut result = RawSpectrum {
1146                arrays,
1147                description: self.description,
1148            };
1149            result.description.signal_continuity = SignalContinuity::Centroid;
1150            Ok(result)
1151        } else if let Some(peaks) = self.peaks {
1152            let arrays = C::as_arrays(&peaks[0..]);
1153
1154            let mut result = RawSpectrum {
1155                arrays,
1156                description: self.description,
1157            };
1158            result.description.signal_continuity = SignalContinuity::Centroid;
1159            Ok(result)
1160        } else {
1161            let arrays = BinaryArrayMap::from(&PeakSet::empty());
1162            Ok(RawSpectrum {
1163                arrays,
1164                description: self.description,
1165            })
1166        }
1167    }
1168
1169    /// Construct a [`MultiLayerSpectrum`] with `description` and the provided raw data arrays
1170    pub fn from_arrays_and_description(
1171        arrays: BinaryArrayMap,
1172        description: SpectrumDescription,
1173    ) -> Self {
1174        Self {
1175            description,
1176            arrays: Some(arrays),
1177            ..Default::default()
1178        }
1179    }
1180
1181    /// Construct a [`MultiLayerSpectrum`] with `description` and *no* peak data of any kind
1182    pub fn from_description(description: SpectrumDescription) -> Self {
1183        Self {
1184            description,
1185            ..Default::default()
1186        }
1187    }
1188
1189    /// Construct a [`MultiLayerSpectrum`] with `description` and a [`PeakDataLevel`] containing
1190    /// at most *one* of the data modalities. This is convenient for reconstructing a spectrum
1191    /// from its parts.
1192    pub fn from_peaks_data_levels_and_description(
1193        peaks: PeakDataLevel<C, D>,
1194        description: SpectrumDescription,
1195    ) -> Self {
1196        match peaks {
1197            PeakDataLevel::Missing => Self::new(description, None, None, None),
1198            PeakDataLevel::RawData(arrays) => Self::new(description, Some(arrays), None, None),
1199            PeakDataLevel::Centroid(peaks) => Self::new(description, None, Some(peaks), None),
1200            PeakDataLevel::Deconvoluted(peaks) => Self::new(description, None, None, Some(peaks)),
1201        }
1202    }
1203
1204    /// Converts an arbitrary [`SpectrumLike`] type into a [`MultiLayerSpectrum`] with the same
1205    /// peak types.
1206    ///
1207    /// This is a thin wrapper around [`SpectrumLike::into_peaks_and_description`] and [`MultiLayerSpectrum::from_peaks_data_levels_and_description`]
1208    pub fn from_spectrum_like<S: SpectrumLike<C, D>>(spectrum: S) -> Self {
1209        let (peaks, description) = spectrum.into_peaks_and_description();
1210        Self::from_peaks_data_levels_and_description(peaks, description)
1211    }
1212
1213    /// Convert a [`MultiLayerSpectrum`] with one set of peak types to another with any peak type, but
1214    /// recode the peak list as [`DataArray`] as an intermediary, potentially losing information if
1215    /// the new peak types do not re-use the same [`ArrayType`] during peak reconstruction.
1216    ///
1217    /// ## Note
1218    /// Peak data is not actually reconstructed from the intermediate data arrays. If that is desired,
1219    /// call [`MultiLayerSpectrum::try_build_peaks`]
1220    pub fn reinterpret<C1, D1>(self) -> MultiLayerSpectrum<C1, D1>
1221    where
1222        C1: CentroidLike + BuildArrayMapFrom + BuildFromArrayMap,
1223        D1: DeconvolutedCentroidLike + BuildArrayMapFrom + BuildFromArrayMap,
1224        C: BuildArrayMapFrom,
1225        D: BuildArrayMapFrom,
1226    {
1227        let arrays = if let Some(peaks) = self.deconvoluted_peaks {
1228            D::as_arrays(peaks.as_slice())
1229        } else if let Some(peaks) = self.peaks {
1230            C::as_arrays(peaks.as_slice())
1231        } else if let Some(arrays) = self.arrays {
1232            arrays
1233        } else {
1234            BinaryArrayMap::new()
1235        };
1236
1237        MultiLayerSpectrum::new(self.description, Some(arrays), None, None)
1238    }
1239
1240    #[cfg(feature = "mzsignal")]
1241    /// Apply a local denoising algorithm to the signal that is suitable for profile mode
1242    /// data using [`mzsignal::denoise`].
1243    pub fn denoise(&mut self, scale: f32) -> Result<(), SpectrumProcessingError> {
1244        match &mut self.arrays {
1245            Some(arrays) => {
1246                let mut intensities_copy = arrays.intensities()?.into_owned();
1247                let mz_array = arrays.mzs()?;
1248                match denoise(&mz_array, &mut intensities_copy, scale) {
1249                    Ok(_) => {
1250                        let view = arrays.get_mut(&ArrayType::IntensityArray).unwrap();
1251                        view.store_as(BinaryDataArrayType::Float32)?;
1252                        view.update_buffer(&intensities_copy)?;
1253                        Ok(())
1254                    }
1255                    Err(err) => Err(SpectrumProcessingError::DenoisingError(err)),
1256                }
1257            }
1258            None => Err(SpectrumProcessingError::SpectrumConversionError(
1259                SpectrumConversionError::NoPeakData,
1260            )),
1261        }
1262    }
1263
1264    #[cfg(feature = "mzsignal")]
1265    /// When `mzsignal` is available, use [`mzsignal::reprofile::PeakSetReprofiler`] to create
1266    /// new profile m/z and intensity [`DataArray`]s from [`MultiLayerSpectrum::peaks`], updating
1267    /// [`MultiLayerSpectrum::arrays`].
1268    ///
1269    /// **NOTE**: This does not modify [`SpectrumLike::signal_continuity`], and if it is desired
1270    /// you should update this manually to [`SignalContinuity::Profile`].
1271    ///
1272    /// # Arguments
1273    /// - `dx`: The m/z grid spacing that the profile signal will be projected onto. The
1274    ///   best value to use here depends upon the spectrum's resolution, but when resources
1275    ///   are not an issue between `0.005` and `0.001` are is a choice. This may produce a lot
1276    ///   of wasted space however.
1277    /// - `fwhm`: The uniform peak full-width-at-half-max to assume for all peaks. It controls
1278    ///   the broadness of the theoretical gaussian profile created around each centroid peak.
1279    ///   If non-uniform shapes are needed, directly use [`PeakSetReprofiler`](mzsignal::reprofile::PeakSetReprofiler).
1280    pub fn reprofile_with_shape(
1281        &mut self,
1282        dx: f64,
1283        fwhm: f32,
1284    ) -> Result<(), SpectrumProcessingError> {
1285        if let Some(peaks) = self.peaks.as_ref() {
1286            let reprofiler = reprofile::PeakSetReprofiler::new(
1287                peaks.first().map(|p| p.mz() - 1.0).unwrap_or_default(),
1288                peaks.last().map(|p| p.mz() + 1.0).unwrap_or_default(),
1289                dx,
1290            );
1291
1292            let models: Vec<_> = peaks
1293                .iter()
1294                .map(|p| {
1295                    PeakShapeModel::from_centroid(p.mz(), p.intensity(), fwhm, PeakShape::Gaussian)
1296                })
1297                .collect();
1298
1299            let pair = reprofiler.reprofile_from_models(&models);
1300
1301            let arrays = match self.arrays.as_mut() {
1302                Some(arrays) => arrays,
1303                None => {
1304                    self.arrays = Some(BinaryArrayMap::new());
1305                    self.arrays.as_mut().unwrap()
1306                }
1307            };
1308            arrays.add(DataArray::wrap(
1309                &ArrayType::MZArray,
1310                BinaryDataArrayType::Float64,
1311                pair.mz_array.iter().flat_map(|i| i.to_le_bytes()).collect(),
1312            ));
1313
1314            arrays.add(DataArray::wrap(
1315                &ArrayType::IntensityArray,
1316                BinaryDataArrayType::Float32,
1317                pair.intensity_array
1318                    .iter()
1319                    .flat_map(|i| i.to_le_bytes())
1320                    .collect(),
1321            ));
1322            Ok(())
1323        } else {
1324            Err(SpectrumConversionError::NoPeakData.into())
1325        }
1326    }
1327}
1328
1329impl<C: CentroidLike, D: DeconvolutedCentroidLike> MultiLayerSpectrum<C, D>
1330where
1331    C: BuildFromArrayMap,
1332    D: BuildFromArrayMap,
1333{
1334    /// Attempt to reconstruct one of the peak layers based upon the available data arrays
1335    /// if this spectrum is in centroid mode.
1336    ///
1337    /// # Errors
1338    /// If constructing the peak list encounters an error, or if data array access/decoding fails,
1339    /// a [`SpectrumConversionError`] is returned.
1340    pub fn try_build_peaks(
1341        &mut self,
1342    ) -> Result<RefPeakDataLevel<'_, C, D>, SpectrumConversionError> {
1343        if self.peaks.is_some() || self.deconvoluted_peaks.is_some() {
1344            return Ok(self.peaks());
1345        }
1346        if matches!(self.signal_continuity(), SignalContinuity::Centroid) {
1347            if let Some(arrays) = self.arrays.as_ref() {
1348                let peak_data: PeakDataLevel<C, D> = PeakDataLevel::try_from(arrays)?;
1349                match peak_data {
1350                    PeakDataLevel::Missing => Ok(RefPeakDataLevel::Missing),
1351                    PeakDataLevel::RawData(_) => panic!("not possible"),
1352                    PeakDataLevel::Centroid(peak_set_vec) => {
1353                        self.peaks = Some(peak_set_vec);
1354                        Ok(self.peaks())
1355                    }
1356                    PeakDataLevel::Deconvoluted(peak_set_vec) => {
1357                        self.deconvoluted_peaks = Some(peak_set_vec);
1358                        Ok(self.peaks())
1359                    }
1360                }
1361            } else {
1362                Ok(self.peaks())
1363            }
1364        } else {
1365            Ok(self.peaks())
1366        }
1367    }
1368
1369    pub fn try_build_centroids(&mut self) -> Result<&MZPeakSetType<C>, SpectrumConversionError> {
1370        if self.peaks.is_some() {
1371            Ok(self.peaks.as_ref().unwrap())
1372        } else if let Some(arrays) = self.arrays.as_ref() {
1373            match self.signal_continuity() {
1374                SignalContinuity::Centroid => {
1375                    let peaks = C::try_from_arrays(arrays)?.into();
1376                    self.peaks = Some(peaks);
1377                    Ok(self.peaks.as_ref().unwrap())
1378                }
1379                _ => Err(SpectrumConversionError::NotCentroided),
1380            }
1381        } else {
1382            Err(SpectrumConversionError::NoPeakData)
1383        }
1384    }
1385
1386    pub fn try_build_deconvoluted_centroids(
1387        &mut self,
1388    ) -> Result<&MassPeakSetType<D>, SpectrumConversionError> {
1389        if let Some(ref peaks) = self.deconvoluted_peaks {
1390            Ok(peaks)
1391        } else if let Some(ref arrays) = self.arrays {
1392            match self.signal_continuity() {
1393                SignalContinuity::Centroid => {
1394                    let peaks = D::try_from_arrays(arrays)?.into();
1395                    self.deconvoluted_peaks = Some(peaks);
1396                    Ok(self.deconvoluted_peaks.as_ref().unwrap())
1397                }
1398                _ => Err(SpectrumConversionError::NotCentroided),
1399            }
1400        } else {
1401            Err(SpectrumConversionError::NoPeakData)
1402        }
1403    }
1404}
1405
1406/// When [`mzsignal`] is available, [`MultiLayerSpectrum`] supports in-place signal processing operations.
1407///
1408/// The peak picking steps need to convert an [`mzsignal::FittedPeak`] into `C`. This is trivial for [`CentroidPeak`]
1409/// and [`mzsignal::FittedPeak`] itself.
1410#[cfg(feature = "mzsignal")]
1411impl<C: CentroidLike + From<FittedPeak>, D: DeconvolutedCentroidLike> MultiLayerSpectrum<C, D> {
1412    /// Using a pre-configured [`mzsignal::PeakPicker`](mzsignal::peak_picker::PeakPicker) to pick peaks
1413    /// from `arrays`'s signal, populating the `peaks` field.
1414    ///
1415    /// If [`SpectrumLike::signal_continuity`] returns [`SignalContinuity::Centroid`], then no filtering is
1416    /// performed and all points are directly converted into picked peaks.
1417    ///
1418    /// **NOTE**: This does not modify [`SpectrumLike::signal_continuity`], and if it is desired
1419    /// you should update this manually to [`SignalContinuity::Centroid`].
1420    pub fn pick_peaks_with(
1421        &mut self,
1422        peak_picker: &PeakPicker,
1423    ) -> Result<(), SpectrumProcessingError> {
1424        if let Some(arrays) = &self.arrays {
1425            let mz_array = arrays.mzs()?;
1426            let intensity_array = arrays.intensities()?;
1427
1428            if matches!(self.signal_continuity(), SignalContinuity::Centroid) {
1429                let mut peaks: MZPeakSetType<C> = mz_array
1430                    .iter()
1431                    .zip(intensity_array.iter())
1432                    .map(|(mz, inten)| FittedPeak::new(*mz, *inten, 0, 0.0, 0.0).into())
1433                    .collect();
1434                peaks.sort();
1435                self.peaks = Some(peaks);
1436                Ok(())
1437            } else {
1438                let mut acc = Vec::new();
1439                match peak_picker.discover_peaks(&mz_array, &intensity_array, &mut acc) {
1440                    Ok(_) => {
1441                        let peaks: MZPeakSetType<C> = acc.into_iter().map(|p| C::from(p)).collect();
1442                        self.peaks = Some(peaks);
1443                        Ok(())
1444                    }
1445                    Err(err) => Err(SpectrumProcessingError::PeakPickerError(err)),
1446                }
1447            }
1448        } else {
1449            Err(SpectrumProcessingError::SpectrumConversionError(
1450                SpectrumConversionError::NoPeakData,
1451            ))
1452        }
1453    }
1454
1455    /// Pick peaks with a minimum signal-to-noise threshold, populating [`MultiLayerSpectrum::peaks`] with
1456    /// an [`mzpeaks::MZPeakSetType`] over `C`.
1457    ///
1458    /// # See also
1459    /// [`MultiLayerSpectrum::pick_peaks_with`]
1460    pub fn pick_peaks(
1461        &mut self,
1462        signal_to_noise_threshold: f32,
1463    ) -> Result<(), SpectrumProcessingError> {
1464        let peak_picker = PeakPicker {
1465            fit_type: PeakFitType::Quadratic,
1466            signal_to_noise_threshold,
1467            ..Default::default()
1468        };
1469        self.pick_peaks_with(&peak_picker)
1470    }
1471
1472    /// Pick peaks in the specified m/z intervals with a minimum signal-to-noise threshold,
1473    /// populating [`MultiLayerSpectrum::peaks`] with an [`mzpeaks::MZPeakSetType`] over `C`
1474    /// from those intervals.
1475    ///
1476    /// If [`SpectrumLike::signal_continuity`] returns [`SignalContinuity::Centroid`], then
1477    /// array data is converted to peaks as in [`MultiLayerSpectrum::pick_peaks`] and then
1478    /// only those peaks in the requested intervals are kept.
1479    ///
1480    /// **NOTE**: This does not modify [`SpectrumLike::signal_continuity`], and if it is desired
1481    /// you should update this manually to [`SignalContinuity::Centroid`].
1482    pub fn pick_peaks_in_intervals(
1483        &mut self,
1484        signal_to_noise_threshold: f32,
1485        intervals: &[(f64, f64)],
1486    ) -> Result<(), SpectrumProcessingError> {
1487        let peak_picker = PeakPicker {
1488            fit_type: PeakFitType::Quadratic,
1489            signal_to_noise_threshold,
1490            ..Default::default()
1491        };
1492
1493        if matches!(self.signal_continuity(), SignalContinuity::Centroid) {
1494            self.pick_peaks_with(&peak_picker)?;
1495            let peaks = self
1496                .peaks
1497                .take()
1498                .unwrap()
1499                .into_iter()
1500                .filter(|p| {
1501                    let mz = p.mz();
1502                    intervals.iter().any(|(lo, hi)| *lo <= mz && mz < *hi)
1503                })
1504                .collect();
1505            self.peaks = Some(peaks);
1506            Ok(())
1507        } else if let Some(arrays) = &self.arrays {
1508            let mz_array = arrays.mzs()?;
1509            let intensity_array = arrays.intensities()?;
1510            let mut acc = Vec::new();
1511            for (mz_start, mz_end) in intervals {
1512                match peak_picker.discover_peaks_in_interval(
1513                    &mz_array,
1514                    &intensity_array,
1515                    &mut acc,
1516                    *mz_start,
1517                    *mz_end,
1518                ) {
1519                    Ok(_) => {}
1520                    Err(err) => return Err(SpectrumProcessingError::PeakPickerError(err)),
1521                }
1522            }
1523            let peaks: MZPeakSetType<C> = acc.into_iter().map(|p| C::from(p)).collect();
1524            self.peaks = Some(peaks);
1525            return Ok(());
1526        } else {
1527            return Err(SpectrumProcessingError::SpectrumConversionError(
1528                SpectrumConversionError::NoPeakData,
1529            ));
1530        }
1531    }
1532}
1533
1534impl<C: CentroidLike, D: DeconvolutedCentroidLike> TryFrom<MultiLayerSpectrum<C, D>>
1535    for CentroidSpectrumType<C>
1536where
1537    C: BuildFromArrayMap + BuildArrayMapFrom,
1538    D: BuildFromArrayMap + BuildArrayMapFrom,
1539{
1540    type Error = SpectrumConversionError;
1541
1542    fn try_from(
1543        spectrum: MultiLayerSpectrum<C, D>,
1544    ) -> Result<CentroidSpectrumType<C>, Self::Error> {
1545        spectrum.into_centroid()
1546    }
1547}
1548
1549/// A ready-to-use parameterized type for representing a spectrum
1550/// in multiple overlapping layers.
1551pub type Spectrum = MultiLayerSpectrum<CentroidPeak, DeconvolutedPeak>;
1552
1553impl<C: CentroidLike, D: DeconvolutedCentroidLike> From<CentroidSpectrumType<C>>
1554    for MultiLayerSpectrum<C, D>
1555{
1556    fn from(spectrum: CentroidSpectrumType<C>) -> MultiLayerSpectrum<C, D> {
1557        spectrum.into_spectrum().unwrap()
1558    }
1559}
1560
1561impl<C: CentroidLike, D: DeconvolutedCentroidLike> From<RawSpectrum> for MultiLayerSpectrum<C, D>
1562where
1563    C: BuildFromArrayMap,
1564    D: BuildFromArrayMap,
1565{
1566    fn from(spectrum: RawSpectrum) -> MultiLayerSpectrum<C, D> {
1567        spectrum.into_spectrum().unwrap()
1568    }
1569}
1570
1571impl<C: CentroidLike, D: DeconvolutedCentroidLike> From<MultiLayerSpectrum<C, D>> for RawSpectrum
1572where
1573    C: BuildFromArrayMap + BuildArrayMapFrom,
1574    D: BuildFromArrayMap + BuildArrayMapFrom,
1575{
1576    fn from(spectrum: MultiLayerSpectrum<C, D>) -> RawSpectrum {
1577        spectrum.into_raw().unwrap()
1578    }
1579}
1580
1581impl<C: CentroidLike> TryFrom<RawSpectrum> for CentroidSpectrumType<C>
1582where
1583    C: BuildFromArrayMap,
1584{
1585    type Error = SpectrumConversionError;
1586
1587    fn try_from(spectrum: RawSpectrum) -> Result<CentroidSpectrumType<C>, Self::Error> {
1588        spectrum.into_centroid()
1589    }
1590}
1591
1592impl<C: CentroidLike, D: DeconvolutedCentroidLike> TryFrom<MultiLayerSpectrum<C, D>>
1593    for DeconvolutedSpectrumType<D>
1594where
1595    C: BuildFromArrayMap + BuildArrayMapFrom,
1596    D: BuildFromArrayMap + BuildArrayMapFrom,
1597{
1598    type Error = SpectrumConversionError;
1599
1600    fn try_from(value: MultiLayerSpectrum<C, D>) -> Result<Self, Self::Error> {
1601        match value.peaks() {
1602            RefPeakDataLevel::Deconvoluted(_) => {
1603                let peaks = value.deconvoluted_peaks.unwrap();
1604                Ok(DeconvolutedSpectrumType {
1605                    description: value.description,
1606                    deconvoluted_peaks: peaks,
1607                })
1608            }
1609            RefPeakDataLevel::RawData(arrays) => {
1610                let peaks = match D::try_from_arrays(arrays) {
1611                    Ok(peaks) => peaks,
1612                    Err(e) => return Err(SpectrumConversionError::ArrayRetrievalError(e)),
1613                };
1614                Ok(DeconvolutedSpectrumType {
1615                    description: value.description,
1616                    deconvoluted_peaks: PeakSetVec::new(peaks),
1617                })
1618            }
1619            _ => Err(SpectrumConversionError::NotDeconvoluted),
1620        }
1621    }
1622}