Skip to main content

mzdata_spectrum/
frame.rs

1use std::{borrow::Cow, convert::TryFrom};
2
3use mzpeaks::{
4    coordinate::{IonMobility, MZ, Mass},
5    feature::{ChargedFeature, Feature, FeatureLike},
6    feature_map::FeatureMap,
7    prelude::*,
8};
9
10use crate::scan_properties::SCAN_TITLE;
11use crate::{
12    Acquisition, MultiLayerSpectrum, Precursor,
13    RawSpectrum, ScanPolarity, SignalContinuity, SpectrumConversionError, SpectrumDescription,
14};
15use mzdata_bindata::{
16    ArrayRetrievalError, ArrayType, ArraysAvailable, BinaryArrayMap, BinaryArrayMap3D,
17    BuildArrayMap3DFrom,
18    BuildFromArrayMap,
19    BuildFromArrayMap3D,
20    ByteArrayViewMut,
21};
22use mzdata_param::{
23    Param, ParamDescribed, ParamList, ParamValue, Unit, impl_param_described,
24};
25
26/// Represent an owned representation of one the kinds of feature data that a [`IonMobilityFrameLike`](crate::spectrum::IonMobilityFrameLike) instance
27/// might otherwise carry.
28///
29/// # See also
30/// [`RefFeatureDataLevel`] for borrowed version.
31#[derive(Debug)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub enum FeatureDataLevel<
34    C: FeatureLike<MZ, IonMobility> = Feature<MZ, IonMobility>,
35    D: FeatureLike<Mass, IonMobility> + KnownCharge = ChargedFeature<Mass, IonMobility>,
36> {
37    /// No feature-level data is available
38    Missing,
39    /// Raw [`BinaryArrayMap3D`] exists, but no features are extracted
40    RawData(BinaryArrayMap3D),
41    /// Extracted m/z [`FeatureMap`] exists
42    Centroid(FeatureMap<MZ, IonMobility, C>),
43    /// Extracted and neutral mass deconvoluted [`FeatureMap`] exists
44    Deconvoluted(FeatureMap<Mass, IonMobility, D>),
45}
46
47/// An variant for dispatching to different strategies of computing
48/// common statistics of different levels of feature data.
49#[derive(Debug)]
50pub enum RefFeatureDataLevel<
51    'a,
52    C: FeatureLike<MZ, IonMobility> = Feature<MZ, IonMobility>,
53    D: FeatureLike<Mass, IonMobility> + KnownCharge = ChargedFeature<Mass, IonMobility>,
54> {
55    /// No feature-level data is available
56    Missing,
57    /// Raw [`BinaryArrayMap3D`] exists, but no features are extracted
58    RawData(&'a BinaryArrayMap3D),
59    /// Extracted m/z [`FeatureMap`] exists
60    Centroid(&'a FeatureMap<MZ, IonMobility, C>),
61    /// Extracted and neutral mass deconvoluted [`FeatureMap`] exists
62    Deconvoluted(&'a FeatureMap<Mass, IonMobility, D>),
63}
64
65/// The set of descriptive metadata that give context for how an ion mobility frame was acquired
66/// within a particular run. This forms the basis for a large portion of the [`IonMobilityFrameLike`]
67/// trait.
68///
69/// This is the equivalent of the [`SpectrumDescription`] type.
70#[derive(Debug, Default, Clone)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct IonMobilityFrameDescription {
73    /// The spectrum's native identifier
74    pub id: String,
75
76    /// The ordinal sequence number for the spectrum
77    pub index: usize,
78
79    /// The degree of exponentiation of the spectrum, e.g MS1, MS2, MS3, etc
80    pub ms_level: u8,
81
82    /// The spectrum is in positive or negative mode
83    pub polarity: ScanPolarity,
84
85    /// The spectrum's main representation is as a peak list or a continuous
86    /// profile
87    pub signal_continuity: SignalContinuity,
88
89    /// A set of controlled or uncontrolled descriptors of the spectrum not already
90    /// covered by fields
91    pub params: ParamList,
92
93    /// A description of how the spectrum was acquired including time, scan windows, and more
94    pub acquisition: Acquisition,
95
96    /// The parent ion or ions and their isolation and activation description
97    pub precursor: Vec<Precursor>,
98
99    /// The unit of the ion mobility dimension
100    pub ion_mobility_unit: Unit,
101}
102
103impl_param_described!(IonMobilityFrameDescription);
104
105impl IonMobilityFrameDescription {
106    #[allow(clippy::too_many_arguments)]
107    pub fn new(
108        id: String,
109        index: usize,
110        ms_level: u8,
111        polarity: ScanPolarity,
112        signal_continuity: SignalContinuity,
113        params: ParamList,
114        acquisition: Acquisition,
115        precursor: impl Into<super::AsPrecursorCollection>,
116        ion_mobility_unit: Unit,
117    ) -> Self {
118        Self {
119            id,
120            index,
121            ms_level,
122            polarity,
123            signal_continuity,
124            params,
125            acquisition,
126            precursor: precursor.into().into(),
127            ion_mobility_unit,
128        }
129    }
130
131    pub fn title(&self) -> Option<Cow<'_, str>> {
132        self.get_param_by_curie(&SCAN_TITLE).map(|p| p.as_str())
133    }
134}
135
136impl From<SpectrumDescription> for IonMobilityFrameDescription {
137    fn from(value: SpectrumDescription) -> Self {
138        Self::new(
139            value.id,
140            value.index,
141            value.ms_level,
142            value.polarity,
143            value.signal_continuity,
144            value.params,
145            value.acquisition,
146            value.precursor,
147            Unit::Unknown,
148        )
149    }
150}
151
152impl From<IonMobilityFrameDescription> for SpectrumDescription {
153    fn from(value: IonMobilityFrameDescription) -> Self {
154        Self::new(
155            value.id,
156            value.index,
157            value.ms_level,
158            value.polarity,
159            value.signal_continuity,
160            value.params,
161            value.acquisition,
162            value.precursor,
163        )
164    }
165}
166
167/// A trait for providing a uniform delegated access to ion mobility frame metadata.
168///
169/// This is analogous to [`SpectrumLike`] but adapted for working with different underlying
170/// types.
171pub trait IonMobilityFrameLike<
172    C: FeatureLike<MZ, IonMobility>,
173    D: FeatureLike<Mass, IonMobility> + KnownCharge,
174>: ParamDescribed
175{
176    /// The method to access the spectrum description itself, which supplies
177    /// the data for most other methods on this trait.
178    fn description(&self) -> &IonMobilityFrameDescription;
179
180    /// The method to access the spectrum descript itself, mutably.
181    fn description_mut(&mut self) -> &mut IonMobilityFrameDescription;
182
183    /// The unit of measure of the ion mobility dimension
184    fn ion_mobility_unit(&self) -> Unit {
185        let unit = self.description().ion_mobility_unit;
186        if unit == Unit::Unknown {
187            self.raw_arrays()
188                .map(|a| a.ion_mobility_unit)
189                .unwrap_or_default()
190        } else {
191            unit
192        }
193    }
194
195    /// Get a mutable reference to the ion mobility dimension's unit
196    fn ion_mobility_unit_mut(&mut self) -> &mut Unit {
197        if self.description().ion_mobility_unit == Unit::Unknown {
198            if let Some(unit) = self.raw_arrays().map(|a| a.ion_mobility_unit) {
199                self.description_mut().ion_mobility_unit = unit;
200            }
201        }
202        &mut self.description_mut().ion_mobility_unit
203    }
204
205    /// Access the acquisition information for this frame.
206    #[inline]
207    fn acquisition(&self) -> &Acquisition {
208        &self.description().acquisition
209    }
210
211    /// Access the precursor information, if it exists.
212    #[inline]
213    fn precursor(&self) -> Option<&Precursor> {
214        let desc = self.description();
215        if let Some(precursor) = &desc.precursor.first() {
216            Some(precursor)
217        } else {
218            None
219        }
220    }
221
222    /// Iterate over all precursors of the frame
223    fn precursor_iter(&self) -> impl Iterator<Item = &Precursor> + ExactSizeIterator {
224        let desc = self.description();
225        desc.precursor.iter()
226    }
227
228    /// The number of precursor levels recorded
229    fn precursor_count(&self) -> usize {
230        self.description().precursor.len()
231    }
232
233    /// Mutably access the precursor information, if it exists
234    fn precursor_mut(&mut self) -> Option<&mut Precursor> {
235        let desc = self.description_mut();
236        if let Some(precursor) = desc.precursor.first_mut() {
237            Some(precursor)
238        } else {
239            None
240        }
241    }
242
243    /// Iterate over all precursors of the frame mutably
244    fn precursor_iter_mut(&mut self) -> impl Iterator<Item = &mut Precursor> + ExactSizeIterator {
245        let desc = self.description_mut();
246        desc.precursor.iter_mut()
247    }
248
249    /// Add a precursor to the list of precursors for this frame.
250    ///
251    /// Precursors beyond the first one correspond to lower exponentiated spectra, e.g. for an MS3 frame
252    /// the first precursor is the MS2 product ion that was selected, and the second precursor corresponds
253    /// to the original MS1 ion that was chosen for MS2.
254    ///
255    /// Higher order precursors may be accessed with [`IonMobilityFrameLike::precursor_iter`].
256    fn add_precursor(&mut self, precursor: Precursor) {
257        self.description_mut().precursor.push(precursor);
258    }
259
260    /// Remove the precursor entry at `index` in the precursor list.
261    ///
262    /// Care should be taken if you are attempting to re-order precursors.
263    /// It may be simpler to use [`IonMobilityFrameLike::precursor_iter_mut`] to
264    /// re-arrange entries.
265    fn remove_precursor(&mut self, index: usize) -> Precursor {
266        self.description_mut().precursor.remove(index)
267    }
268
269    /// A shortcut method to retrieve the scan start time of a spectrum
270    #[inline]
271    fn start_time(&self) -> f64 {
272        let acq = self.acquisition();
273        match acq.scans.first() {
274            Some(evt) => evt.start_time,
275            None => 0.0,
276        }
277    }
278
279    /// Access the MS exponentiation level
280    #[inline]
281    fn ms_level(&self) -> u8 {
282        self.description().ms_level
283    }
284
285    /// Access the native ID string for the spectrum
286    #[inline]
287    fn id(&self) -> &str {
288        &self.description().id
289    }
290
291    /// Access the index of the spectrum in the source file
292    #[inline]
293    fn index(&self) -> usize {
294        self.description().index
295    }
296
297    /// Access a description of how raw the signal is, whether a
298    /// profile spectrum is available or only centroids are present.
299    ///
300    /// This is the m/z continuity. Ion mobility continuity should always
301    /// profile for ion mobility frames.
302    #[inline]
303    fn signal_continuity(&self) -> SignalContinuity {
304        self.description().signal_continuity
305    }
306
307    /// Access a description of the spectrum polarity
308    #[inline]
309    fn polarity(&self) -> ScanPolarity {
310        self.description().polarity
311    }
312
313    /// Obtain a reference to the [`BinaryArrayMap3D`] if one is available for the feature
314    /// information. This may not be the most refined version of the feature signal if
315    /// it has been processed further.
316    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap3D>;
317
318    /// Retrieve the most processed representation of the ion mobility frame's signal
319    fn features(&self) -> RefFeatureDataLevel<'_, C, D>;
320
321    /// Consume the ion mobility frame, decomposing it into the [`IonMobilityFrameDescription`] and an owning
322    /// [`FeatureDataLevel`].
323    ///
324    /// # See also
325    /// [`IonMobilityFrameLike::features`]
326    /// [`IonMobilityFrameLike::description`]
327    fn into_features_and_parts(self) -> (FeatureDataLevel<C, D>, IonMobilityFrameDescription);
328}
329
330/// An ion mobility dimension-equivalent of [`MultiLayerSpectrum`].
331///
332/// An ion mobility frame is just like a mass spectrum, except the signal
333/// is spread across multiple ion mobility "points". This means that instead
334/// producing "points" in the mass coordinate system, this produces traces-over-time
335/// or "features".
336///
337/// An ion mobility frames can usually be inter-converted with spectra.
338#[derive(Debug, Default, Clone)]
339#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
340pub struct MultiLayerIonMobilityFrame<
341    C: FeatureLike<MZ, IonMobility> = Feature<MZ, IonMobility>,
342    D: FeatureLike<Mass, IonMobility> + KnownCharge = ChargedFeature<Mass, IonMobility>,
343> {
344    /// The raw data arrays, split by ion mobility point
345    pub arrays: Option<BinaryArrayMap3D>,
346    /// The (potentially absent) m/z coordinate feature map
347    pub features: Option<FeatureMap<MZ, IonMobility, C>>,
348    /// The (potentially absent) neutral mass coordinate feature map
349    pub deconvoluted_features: Option<FeatureMap<Mass, IonMobility, D>>,
350    pub description: IonMobilityFrameDescription,
351}
352
353impl<
354    C: FeatureLike<MZ, IonMobility> + BuildFromArrayMap3D,
355    D: FeatureLike<Mass, IonMobility> + KnownCharge + BuildFromArrayMap3D,
356> MultiLayerIonMobilityFrame<C, D>
357{
358    /// Try to build extracted feature maps from the raw [`BinaryArrayMap3D`]
359    ///
360    /// This fails if the [`BinaryArrayMap3D`] is not present, or if the required
361    /// arrays for `C` and `D` are absent.
362    pub fn try_build_features(
363        &mut self,
364    ) -> Result<RefFeatureDataLevel<'_, C, D>, SpectrumConversionError> {
365        if matches!(self.signal_continuity(), SignalContinuity::Centroid) {
366            if let Some(arrays) = self.arrays.as_ref() {
367                if let ArraysAvailable::Ok = D::has_arrays_3d_for(arrays) {
368                    let peaks = D::try_from_arrays_3d(arrays).map(|f| FeatureMap::new(f))?;
369                    self.deconvoluted_features = Some(peaks);
370                    return Ok(self.features());
371                }
372
373                if let ArraysAvailable::Ok = C::has_arrays_3d_for(arrays) {
374                    let peaks = C::try_from_arrays_3d(arrays).map(|f| FeatureMap::new(f))?;
375                    self.features = Some(peaks);
376                    return Ok(self.features());
377                }
378                Ok(RefFeatureDataLevel::Missing)
379            } else {
380                Ok(self.features())
381            }
382        } else {
383            Ok(self.features())
384        }
385    }
386}
387
388impl<C: FeatureLike<MZ, IonMobility>, D: FeatureLike<Mass, IonMobility> + KnownCharge>
389    ParamDescribed for MultiLayerIonMobilityFrame<C, D>
390{
391    fn params(&self) -> &[Param] {
392        &self.description().params
393    }
394
395    fn params_mut(&mut self) -> &mut ParamList {
396        &mut self.description_mut().params
397    }
398}
399
400impl<C: FeatureLike<MZ, IonMobility>, D: FeatureLike<Mass, IonMobility> + KnownCharge>
401    MultiLayerIonMobilityFrame<C, D>
402{
403    /// Create a new [`MultiLayerIonMobilityFrame`] from a [`IonMobilityFrameDescription`] and
404    /// any/all/none of the different levels of processing details for signal
405    /// data.
406    pub fn new(
407        arrays: Option<BinaryArrayMap3D>,
408        features: Option<FeatureMap<MZ, IonMobility, C>>,
409        deconvoluted_features: Option<FeatureMap<Mass, IonMobility, D>>,
410        description: IonMobilityFrameDescription,
411    ) -> Self {
412        let mut this = Self {
413            arrays,
414            features,
415            deconvoluted_features,
416            description,
417        };
418        if this.description.ion_mobility_unit.is_unknown() {
419            if let Some(arrays) = this.raw_arrays() {
420                this.description.ion_mobility_unit = arrays.ion_mobility_unit
421            }
422        }
423        this
424    }
425}
426
427impl<C: FeatureLike<MZ, IonMobility>, D: FeatureLike<Mass, IonMobility> + KnownCharge>
428    IonMobilityFrameLike<C, D> for MultiLayerIonMobilityFrame<C, D>
429{
430    fn description(&self) -> &IonMobilityFrameDescription {
431        &self.description
432    }
433
434    fn description_mut(&mut self) -> &mut IonMobilityFrameDescription {
435        &mut self.description
436    }
437
438    fn raw_arrays(&'_ self) -> Option<&'_ BinaryArrayMap3D> {
439        self.arrays.as_ref()
440    }
441
442    fn features(&self) -> RefFeatureDataLevel<'_, C, D> {
443        let state = if let Some(d) = self.deconvoluted_features.as_ref() {
444            RefFeatureDataLevel::Deconvoluted(d)
445        } else if let Some(c) = self.features.as_ref() {
446            RefFeatureDataLevel::Centroid(c)
447        } else if let Some(arrays) = self.arrays.as_ref() {
448            RefFeatureDataLevel::RawData(arrays)
449        } else {
450            RefFeatureDataLevel::Missing
451        };
452        state
453    }
454
455    fn into_features_and_parts(self) -> (FeatureDataLevel<C, D>, IonMobilityFrameDescription) {
456        let state = if let Some(d) = self.deconvoluted_features {
457            FeatureDataLevel::Deconvoluted(d)
458        } else if let Some(c) = self.features {
459            FeatureDataLevel::Centroid(c)
460        } else if let Some(arrays) = self.arrays {
461            FeatureDataLevel::RawData(arrays)
462        } else {
463            FeatureDataLevel::Missing
464        };
465
466        (state, self.description)
467    }
468}
469
470/// [`RawSpectrum`] instances can be converted to [`MultiLayerIonMobilityFrame`] by
471/// using [`BinaryArrayMap3D::stack`] on the flat [`RawSpectrum::arrays`].
472///
473/// # Errors
474/// This will fail if [`BinaryArrayMap3D::stack`] fails
475impl<CFeat: FeatureLike<MZ, IonMobility>, DFeat: FeatureLike<Mass, IonMobility> + KnownCharge>
476    TryFrom<RawSpectrum> for MultiLayerIonMobilityFrame<CFeat, DFeat>
477{
478    type Error = ArrayRetrievalError;
479
480    fn try_from(value: RawSpectrum) -> Result<Self, Self::Error> {
481        let arrays = BinaryArrayMap3D::try_from(value.arrays)?;
482        let descr = IonMobilityFrameDescription::from(value.description);
483
484        Ok(MultiLayerIonMobilityFrame::new(
485            Some(arrays),
486            None,
487            None,
488            descr,
489        ))
490    }
491}
492
493/// [`MultiLayerSpectrum`] instances can be converted to [`MultiLayerIonMobilityFrame`] by
494/// using [`BinaryArrayMap3D::stack`] on the flat [`MultiLayerSpectrum::arrays`].
495///
496/// # Errors
497/// This will fail if [`BinaryArrayMap3D::stack`] fails
498impl<
499    CFeat: FeatureLike<MZ, IonMobility>,
500    DFeat: FeatureLike<Mass, IonMobility> + KnownCharge,
501    CPeak: CentroidLike,
502    DPeak: DeconvolutedCentroidLike,
503> TryFrom<MultiLayerSpectrum<CPeak, DPeak>> for MultiLayerIonMobilityFrame<CFeat, DFeat>
504{
505    type Error = ArrayRetrievalError;
506
507    fn try_from(value: MultiLayerSpectrum<CPeak, DPeak>) -> Result<Self, Self::Error> {
508        let arrays = match value.arrays {
509            Some(arrays) => BinaryArrayMap3D::try_from(arrays)?,
510            None => return Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray)),
511        };
512
513        let descr = IonMobilityFrameDescription::from(value.description);
514
515        Ok(MultiLayerIonMobilityFrame::new(
516            Some(arrays),
517            None,
518            None,
519            descr,
520        ))
521    }
522}
523
524/// A [`MultiLayerIonMobilityFrame`] can be converted to [`RawSpectrum`] by flattening
525/// the feature data into a [`BinaryArrayMap`].
526impl<
527    C: FeatureLike<MZ, IonMobility> + BuildArrayMap3DFrom,
528    D: FeatureLike<Mass, IonMobility> + KnownCharge + BuildArrayMap3DFrom,
529> TryFrom<MultiLayerIonMobilityFrame<C, D>> for RawSpectrum
530{
531    type Error = ArrayRetrievalError;
532
533    fn try_from(value: MultiLayerIonMobilityFrame<C, D>) -> Result<Self, Self::Error> {
534        let im_unit = value.ion_mobility_unit();
535        let mut arrays = if let Some(d) = value.deconvoluted_features {
536            BuildArrayMap3DFrom::as_arrays_3d(&d[..]).unstack()?
537        } else if let Some(c) = value.features {
538            BuildArrayMap3DFrom::as_arrays_3d(&c[..]).unstack()?
539        } else if let Some(arrays) = value.arrays {
540            arrays.unstack()?
541        } else {
542            BinaryArrayMap::default()
543        };
544
545        arrays.iter_mut().for_each(|(at, da)| {
546            if at.is_ion_mobility() {
547                *da.unit_mut() = im_unit;
548            }
549        });
550
551        let descr = value.description.into();
552
553        Ok(RawSpectrum::new(descr, arrays))
554    }
555}
556
557
558/// A [`MultiLayerIonMobilityFrame`] can be converted to [`MultiLayerSpectrum`] by flattening
559/// the feature data into a [`BinaryArrayMap`].
560impl<
561    CFeat: FeatureLike<MZ, IonMobility> + BuildArrayMap3DFrom,
562    DFeat: FeatureLike<Mass, IonMobility> + KnownCharge + BuildArrayMap3DFrom,
563    CPeak: CentroidLike + BuildFromArrayMap,
564    DPeak: DeconvolutedCentroidLike + BuildFromArrayMap,
565> TryFrom<MultiLayerIonMobilityFrame<CFeat, DFeat>> for MultiLayerSpectrum<CPeak, DPeak>
566{
567    type Error = ArrayRetrievalError;
568
569    fn try_from(value: MultiLayerIonMobilityFrame<CFeat, DFeat>) -> Result<Self, Self::Error> {
570        let raw: RawSpectrum = value.try_into()?;
571        Ok(raw.into())
572    }
573}
574
575#[cfg(feature = "mzsignal")]
576mod mzsignal_impl {
577    use super::*;
578
579    use mzpeaks::{CentroidPeak, Tolerance, feature::Feature, peak_set::PeakSetVec};
580    use mzsignal::{
581        PeakFitType,
582        feature_mapping::{FeatureExtracterType, MapState, PeakMapState},
583        peak_picker::PeakPicker,
584    };
585
586    /// When [`mzsignal`] is available, [`MultiLayerIonMobilityFrame`] supports performing feature extraction
587    /// to construct ion mobility-distributed traces in the m/z dimension similar to [`MultiLayerSpectrum::pick_peaks`].
588    impl<
589        C: FeatureLike<MZ, IonMobility> + FeatureLikeMut<MZ, IonMobility>,
590        D: FeatureLike<Mass, IonMobility> + KnownCharge,
591    > MultiLayerIonMobilityFrame<C, D>
592    {
593        /// Extract features using the provided [`MapState`] type to build the `C`-type features
594        /// directly.
595        ///
596        /// If the m/z dimension is not centroided, this will pick peaks for each ion
597        /// mobility slice as well.
598        ///
599        /// # Arguments
600        /// - `error_tolerance`: The mass accuracy error accepted in the m/z dimension between
601        ///                      members of the same feature or trace.
602        /// - `min_length`: The minimum number of time points a feature must contain to be kept
603        ///                 in the solution.
604        /// - `maximum_gap_size`: The maximum time period between m/z observations to tolerate
605        ///                       within a feature.
606        /// - `peak_picker`: A configured [`PeakPicker`] instance to use to centroid profile
607        ///                  m/z spectra. If not provided, the default configuration will be
608        ///                  used.
609        ///
610        /// # Type Arguments
611        /// - `E`: a [`MapState`] implementing type that will control how [`FeatureExtracterType`]
612        ///       can construct features.
613        ///
614        /// If the m/z dimension is not centroided, this will pick peaks for each ion
615        /// mobility slice as well.
616        ///
617        /// # Note
618        /// This requires directly specifying the extraction type to use which opens up a wide
619        /// range of customization options. For the simple cases and for convenience, see
620        /// [`MultiLayerIonMobilityFrame::extract_features_simple`].
621        pub fn extract_features_with<
622            E: MapState<CentroidPeak, MZ, IonMobility, FeatureType = C> + Default,
623        >(
624            &mut self,
625            error_tolerance: Tolerance,
626            min_length: usize,
627            maximum_gap_size: f64,
628            peak_picker: Option<PeakPicker>,
629        ) -> Result<(), ArrayRetrievalError> {
630            if let Some(arrays) = self.arrays.as_ref() {
631                let features = match self.signal_continuity() {
632                    SignalContinuity::Unknown => {
633                        panic!("Cannot extract feature, unknown signal continuities")
634                    }
635                    SignalContinuity::Centroid => {
636                        let mut extractor: FeatureExtracterType<E, _, _, IonMobility> = arrays
637                            .iter()
638                            .flat_map(
639                                |(time, submap)| -> Result<
640                                    (f64, PeakSetVec<CentroidPeak, MZ>),
641                                    ArrayRetrievalError,
642                                > {
643                                    let mzs = submap.mzs()?;
644                                    let intens = submap.intensities()?;
645                                    let peaks = mzs
646                                        .iter()
647                                        .zip(intens.iter())
648                                        .map(|(mz, inten)| CentroidPeak::new(*mz, *inten, 0))
649                                        .collect();
650                                    Ok((time, peaks))
651                                },
652                            )
653                            .collect();
654                        extractor.extract_features(error_tolerance, min_length, maximum_gap_size)
655                    }
656                    SignalContinuity::Profile => {
657                        let peak_picker = peak_picker.unwrap_or_else(|| {
658                            PeakPicker::new(1.0, 1.0, 1.0, PeakFitType::Quadratic)
659                        });
660                        let mut extractor: FeatureExtracterType<E, _, _, IonMobility> = arrays
661                            .iter()
662                            .flat_map(
663                                |(time, submap)| -> Result<
664                                    (f64, PeakSetVec<CentroidPeak, MZ>),
665                                    ArrayRetrievalError,
666                                > {
667                                    let mzs = submap.mzs()?;
668                                    let intens = submap.intensities()?;
669                                    let mut peaks = Vec::new();
670                                    peak_picker
671                                        .discover_peaks(&mzs, &intens, &mut peaks)
672                                        .unwrap();
673                                    let peaks =
674                                        peaks.into_iter().map(|p| p.as_centroid()).collect();
675                                    Ok((time, peaks))
676                                },
677                            )
678                            .collect();
679                        extractor.extract_features(error_tolerance, min_length, maximum_gap_size)
680                    }
681                };
682                self.features = Some(features);
683            }
684            Ok(())
685        }
686    }
687
688    /// A simplified API for feature extraction is available for the basic [`Feature`] type
689    /// where the default [`MapState`] type is known.
690    impl<D: FeatureLike<Mass, IonMobility> + KnownCharge>
691        MultiLayerIonMobilityFrame<Feature<MZ, IonMobility>, D>
692    {
693        /// Extract features using the default algorithm.
694        ///
695        /// # Arguments
696        /// - `error_tolerance`: The mass accuracy error accepted in the m/z dimension between
697        ///                      members of the same feature or trace.
698        /// - `min_length`: The minimum number of time points a feature must contain to be kept
699        ///                 in the solution.
700        /// - `maximum_gap_size`: The maximum time period between m/z observations to tolerate
701        ///                       within a feature.
702        /// - `peak_picker`: A configured [`PeakPicker`] instance to use to centroid profile
703        ///                  m/z spectra. If not provided, the default configuration will be
704        ///                  used.
705        ///
706        /// If the m/z dimension is not centroided, this will pick peaks for each ion
707        /// mobility slice as well.
708        pub fn extract_features_simple(
709            &mut self,
710            error_tolerance: Tolerance,
711            min_length: usize,
712            maximum_gap_size: f64,
713            peak_picker: Option<PeakPicker>,
714        ) -> Result<(), ArrayRetrievalError> {
715            self.extract_features_with::<PeakMapState<CentroidPeak, MZ>>(
716                error_tolerance,
717                min_length,
718                maximum_gap_size,
719                peak_picker,
720            )
721        }
722    }
723}