Skip to main content

mzdata_bindata/
map.rs

1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::collections::HashMap;
4use std::collections::hash_map::{Iter, IterMut};
5use std::convert::TryFrom;
6use std::ffi::c_void;
7use std::hash::BuildHasherDefault;
8
9use identity_hash::IdentityHasher;
10#[cfg(feature = "parallelism")]
11use rayon::prelude::*;
12
13use mzpeaks::Tolerance;
14
15use mzdata_param::{Param, ParamDescribed, Unit};
16
17use super::BinaryDataArrayType;
18use super::array::DataArray;
19use super::encodings::{ArrayRetrievalError, ArrayType, BinaryCompressionType};
20use super::traits::{ByteArrayView, ByteArrayViewMut};
21
22/// A collection of [`DataArray`]s that are identified by name.
23///
24/// # Examples
25/// ```
26/// use mzdata_bindata::{ArrayType, BinaryArrayMap, BinaryDataArrayType, DataArray};
27///
28/// let mut map = BinaryArrayMap::new();
29/// assert!(!map.has_array(&ArrayType::MZArray));
30///
31/// let array = DataArray::from_name_and_type(&ArrayType::MZArray, BinaryDataArrayType::Float64);
32/// map.add(array);
33///
34/// assert!(map.has_array(&ArrayType::MZArray));
35/// assert_eq!(map.len(), 1);
36/// ```
37#[derive(Debug, Default, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct BinaryArrayMap {
40    pub byte_buffer_map: HashMap<ArrayType, DataArray>,
41}
42
43impl BinaryArrayMap {
44    pub fn new() -> BinaryArrayMap {
45        BinaryArrayMap {
46            ..Default::default()
47        }
48    }
49
50    /// Get the number of arrays in the map
51    pub fn len(&self) -> usize {
52        self.byte_buffer_map.len()
53    }
54
55    pub fn is_empty(&self) -> bool {
56        self.byte_buffer_map.is_empty()
57    }
58
59    /// Check if there is an ion mobility array present
60    pub fn has_ion_mobility(&self) -> bool {
61        self.byte_buffer_map.keys().any(|a| a.is_ion_mobility())
62    }
63
64    /// Iterate over references to the key-value pairs of this map
65    pub fn iter(&self) -> Iter<'_, ArrayType, DataArray> {
66        self.byte_buffer_map.iter()
67    }
68
69    #[cfg(feature = "parallelism")]
70    pub fn par_iter(&self) -> rayon::collections::hash_map::Iter<'_, ArrayType, DataArray> {
71        self.byte_buffer_map.par_iter()
72    }
73
74    /// Iterate over mutable references to the key-value pairs of this map
75    pub fn iter_mut(&mut self) -> IterMut<'_, ArrayType, DataArray> {
76        self.byte_buffer_map.iter_mut()
77    }
78
79    #[cfg(feature = "parallelism")]
80    pub fn par_iter_mut(
81        &mut self,
82    ) -> rayon::collections::hash_map::IterMut<'_, ArrayType, DataArray> {
83        self.byte_buffer_map.par_iter_mut()
84    }
85
86    /// Compress a specific [`DataArray`] with the `compression` scheme provided, if it is present.
87    ///
88    /// This method may fail if the compression fails or if the array type is missing.
89    ///
90    /// ## See Also
91    /// [`DataArray::store_compressed`]
92    pub fn encode_array(
93        &mut self,
94        array_type: &ArrayType,
95        compression: BinaryCompressionType,
96    ) -> Result<(), ArrayRetrievalError> {
97        if let Some(arr) = self.get_mut(array_type) {
98            arr.store_compressed(compression)?;
99            Ok(())
100        } else {
101            Err(ArrayRetrievalError::NotFound(array_type.clone()))
102        }
103    }
104
105    /// Decode all [`DataArray`] in this map if they have not been decoded already so
106    /// that they are ready for use. If there are many arrays and the `parallelism` feature
107    /// is enabled, arrays may be decoded in parallel.
108    ///
109    /// # Examples
110    /// ```
111    /// use mzdata_bindata::{ArrayType, BinaryArrayMap, BinaryCompressionType, BinaryDataArrayType, DataArray};
112    ///
113    /// let mut mz_array = DataArray::from_name_and_type(&ArrayType::MZArray, BinaryDataArrayType::Float64);
114    /// mz_array.extend(&[1.0f64, 2.0, 3.0]).unwrap();
115    /// mz_array.store_compressed(BinaryCompressionType::Zlib).unwrap();
116    ///
117    /// let mut map = BinaryArrayMap::new();
118    /// map.add(mz_array);
119    /// assert_eq!(map.get(&ArrayType::MZArray).unwrap().compression, BinaryCompressionType::Zlib);
120    ///
121    /// map.decode_all_arrays().unwrap();
122    /// assert_eq!(map.get(&ArrayType::MZArray).unwrap().compression, BinaryCompressionType::Decoded);
123    /// ```
124    pub fn decode_all_arrays(&mut self) -> Result<(), ArrayRetrievalError> {
125        #[cfg(not(feature = "parallelism"))]
126        {
127            self._decode_all_arrays()
128        }
129        #[cfg(feature = "parallelism")]
130        {
131            if self.len() > 2 {
132                self._decode_all_arrays_parallel()
133            } else {
134                self._decode_all_arrays()
135            }
136        }
137    }
138
139    fn _decode_all_arrays(&mut self) -> Result<(), ArrayRetrievalError> {
140        for (_key, value) in self.iter_mut() {
141            match value.compression {
142                BinaryCompressionType::Decoded => {}
143                _ => {
144                    value.decode_and_store()?;
145                }
146            }
147        }
148        Ok(())
149    }
150
151    #[cfg(feature = "parallelism")]
152    fn _decode_all_arrays_parallel(&mut self) -> Result<(), ArrayRetrievalError> {
153        let res: Result<(), ArrayRetrievalError> = self
154            .iter_mut()
155            .par_bridge()
156            .map(|(_key, value)| {
157                match value.compression {
158                    BinaryCompressionType::Decoded => {}
159                    _ => {
160                        value.decode_and_store()?;
161                    }
162                }
163                Ok(())
164            })
165            .collect::<Result<(), ArrayRetrievalError>>();
166        res
167    }
168
169    /// Decode a specific [`DataArray`] if it is present.
170    ///
171    /// This method may fail if decoding fails or if the array type is missing.
172    ///
173    /// See [`DataArray::decode_and_store`]
174    pub fn decode_array(&mut self, array_type: &ArrayType) -> Result<(), ArrayRetrievalError> {
175        if let Some(array) = self.get_mut(array_type) {
176            array.decode_and_store()?;
177            Ok(())
178        } else {
179            Err(ArrayRetrievalError::NotFound(array_type.clone()))
180        }
181    }
182
183    /// Add a [`DataArray`] to the map by its [`ArrayType`] name
184    pub fn add(&mut self, array: DataArray) {
185        self.byte_buffer_map.insert(array.name.clone(), array);
186    }
187
188    /// Get a reference to a specific [`DataArray`] if present
189    pub fn get(&self, array_type: &ArrayType) -> Option<&DataArray> {
190        self.byte_buffer_map.get(array_type)
191    }
192
193    /// Get a mutable reference to a specific [`DataArray`] if present
194    pub fn get_mut(&mut self, array_type: &ArrayType) -> Option<&mut DataArray> {
195        self.byte_buffer_map.get_mut(array_type)
196    }
197
198    /// Check whether a specific [`ArrayType`] is present
199    pub fn has_array(&self, array_type: &ArrayType) -> bool {
200        self.byte_buffer_map.contains_key(array_type)
201    }
202
203    /// Clear the map, discarding any array data
204    pub fn clear(&mut self) {
205        self.byte_buffer_map.clear();
206    }
207
208    /// Search for a specific m/z within a specified [`Tolerance`].
209    ///
210    /// This effectively calls [`BinaryArrayMap::mzs`] to get the m/z array and then
211    /// performs a binary search to find the best index within range, if one exists.
212    pub fn search(&self, query: f64, error_tolerance: Tolerance) -> Option<usize> {
213        if let Ok(mzs) = self.mzs() {
214            let (lower, _upper) = error_tolerance.bounds(query);
215            match mzs[..].binary_search_by(|m| m.partial_cmp(&lower).unwrap()) {
216                Ok(i) => {
217                    let mut best_error = error_tolerance.call(query, mzs[i]).abs();
218                    let mut best_index = i;
219                    let mut index = i + 1;
220                    while index < mzs.len() {
221                        let error = error_tolerance.call(query, mzs[index]).abs();
222                        if error < best_error {
223                            best_index = index;
224                            best_error = error;
225                        }
226                        index += 1;
227                    }
228                    if best_error < error_tolerance.tol() {
229                        return Some(best_index);
230                    }
231                    None
232                }
233                Err(_err) => None,
234            }
235        } else {
236            None
237        }
238    }
239
240    /// Get a reference to the m/z array if it is present
241    ///
242    /// # Examples
243    /// ```
244    /// use mzdata_bindata::{ArrayType, BinaryArrayMap, BinaryDataArrayType, DataArray};
245    ///
246    /// let mut mz_array = DataArray::from_name_and_type(&ArrayType::MZArray, BinaryDataArrayType::Float64);
247    /// mz_array.extend(&[100.0f64, 200.0, 300.0]).unwrap();
248    ///
249    /// let mut map = BinaryArrayMap::new();
250    /// map.add(mz_array);
251    ///
252    /// assert_eq!(&*map.mzs().unwrap(), &[100.0f64, 200.0, 300.0][..]);
253    /// ```
254    pub fn mzs(&'_ self) -> Result<Cow<'_, [f64]>, ArrayRetrievalError> {
255        let mz_array = self
256            .get(&ArrayType::MZArray)
257            .ok_or(ArrayRetrievalError::NotFound(ArrayType::MZArray))?
258            .to_f64()
259            .inspect_err(|e| log::error!("Failed to decode m/z array: {e}"))?;
260        Ok(mz_array)
261    }
262
263    /// Re-order all arrays of matching length with respect to the provided index `mask` operating
264    /// on the decoded native value states.
265    ///
266    /// The mask would be created using an indirect sort from another array:
267    /// ```notest
268    /// let vals = self.mzs().unwrap();
269    /// let n = vals.len();
270    /// let mut mask: Vec<usize> = (0..n).into_iter().collect();
271    /// mask.sort_by(|i, j| {
272    ///     let a = vals[*i];
273    ///     let b = vals[*j];
274    ///     a.total_cmp(&b)
275    /// });
276    /// drop(vals);
277    /// self.sort_from_indices(mask)
278    /// ```
279    pub fn sort_from_indices(&mut self, mut mask: Vec<usize>) -> Result<(), ArrayRetrievalError> {
280        let n = mask.len();
281        const TOMBSTONE: usize = usize::MAX;
282        let mut tracks: Vec<(*mut c_void, BinaryDataArrayType)> = Vec::new();
283        for (_, v) in self.iter_mut() {
284            if v.data_len()? != n {
285                continue;
286            }
287            match v.dtype {
288                BinaryDataArrayType::Float64 => {
289                    let view = v.coerce_mut::<f64>()?;
290                    tracks.push((view.as_mut_ptr() as *mut c_void, v.dtype))
291                }
292                BinaryDataArrayType::Float32 => {
293                    let view = v.coerce_mut::<f32>()?;
294                    tracks.push((view.as_mut_ptr() as *mut c_void, v.dtype))
295                }
296                BinaryDataArrayType::Int64 => {
297                    let view = v.coerce_mut::<i64>()?;
298                    tracks.push((view.as_mut_ptr() as *mut c_void, v.dtype))
299                }
300                BinaryDataArrayType::Int32 => {
301                    let view = v.coerce_mut::<i32>()?;
302                    tracks.push((view.as_mut_ptr() as *mut c_void, v.dtype))
303                }
304                BinaryDataArrayType::ASCII => todo!(),
305                BinaryDataArrayType::Unknown => todo!(),
306            }
307        }
308
309        for idx in 0..n {
310            if mask[idx] != TOMBSTONE {
311                let mut current_idx = idx;
312                loop {
313                    let next_idx = mask[current_idx];
314                    mask[current_idx] = TOMBSTONE;
315                    if mask[next_idx] == TOMBSTONE {
316                        break;
317                    }
318                    for (dat, dtype) in tracks.iter_mut () {
319                        match dtype {
320                            BinaryDataArrayType::Float64 => {
321                                unsafe {
322                                    let v = core::slice::from_raw_parts_mut(*dat as *mut f64, n);
323                                    v.swap(current_idx, next_idx);
324                                }
325                            },
326                            BinaryDataArrayType::Float32 => {
327                                unsafe {
328                                    let v = core::slice::from_raw_parts_mut(*dat as *mut f32, n);
329                                    v.swap(current_idx, next_idx);
330                                }
331                            },
332                            BinaryDataArrayType::Int64 => {
333                                unsafe {
334                                    let v = core::slice::from_raw_parts_mut(*dat as *mut i64, n);
335                                    v.swap(current_idx, next_idx);
336                                }
337                            },
338                            BinaryDataArrayType::Int32 => {
339                                unsafe {
340                                    let v = core::slice::from_raw_parts_mut(*dat as *mut i32, n);
341                                    v.swap(current_idx, next_idx);
342                                }
343                            },
344                            BinaryDataArrayType::Unknown => todo!(),
345                            BinaryDataArrayType::ASCII => todo!(),
346                        }
347                    }
348                    current_idx = next_idx;
349                }
350            }
351        }
352        /*
353        for idx in 0..n {
354            if mask[idx] != TOMBSTONE {
355                let mut current_idx = idx;
356                loop {
357                    let next_idx = mask[current_idx];
358                    mask[current_idx] = TOMBSTONE;
359                    if mask[next_idx] == TOMBSTONE {
360                        break;
361                    }
362                    for (_, v) in self.iter_mut() {
363                        if v.data_len()? != n {
364                            continue;
365                        }
366                        match v.dtype {
367                            BinaryDataArrayType::Float64 => {
368                                let view = v.coerce_mut::<f64>()?;
369                                view.swap(current_idx, next_idx);
370                            }
371                            BinaryDataArrayType::Float32 => {
372                                let view = v.coerce_mut::<f32>()?;
373                                view.swap(current_idx, next_idx);
374                            }
375                            BinaryDataArrayType::Int64 => {
376                                let view = v.coerce_mut::<i64>()?;
377                                view.swap(current_idx, next_idx);
378                            }
379                            BinaryDataArrayType::Int32 => {
380                                let view = v.coerce_mut::<i32>()?;
381                                view.swap(current_idx, next_idx);
382                            }
383                            BinaryDataArrayType::ASCII => todo!(),
384                            BinaryDataArrayType::Unknown => todo!(),
385                        }
386                    }
387                    current_idx = next_idx;
388                }
389            }
390        }
391        */
392        Ok(())
393    }
394
395    /// Sort all the arrays in the map by `name` if they are the same length. This operates
396    /// on the decoded state directly.
397    ///
398    /// This builds the sorting mask and then leaves [`Self::sort_from_indices`] to perform
399    /// the re-ordering.
400    ///
401    /// Not supported for [`BinaryDataArrayType::ASCII`] and [`BinaryDataArrayType::Unknown`].
402    pub fn sort_by_array(&mut self, name: &ArrayType) -> Result<(), ArrayRetrievalError> {
403        let query_axis = self
404            .get(name)
405            .ok_or_else(|| ArrayRetrievalError::NotFound(name.clone()))?;
406        macro_rules! sort_mask {
407            ($conv:expr, $cmp:expr) => {{
408                let vals = $conv?;
409                if vals.is_sorted() {
410                    return Ok(());
411                }
412                let n = vals.len();
413                let mut mask: Vec<usize> = (0..n).into_iter().collect();
414                mask.sort_by(|i, j| {
415                    let a = vals[*i];
416                    let b = vals[*j];
417                    $cmp(&a, &b)
418                });
419                (mask, n)
420            }};
421        }
422        let (mask, _) = match query_axis.dtype() {
423            BinaryDataArrayType::Float64 => {
424                sort_mask!(query_axis.to_f64(), f64::total_cmp)
425            }
426            BinaryDataArrayType::Float32 => {
427                sort_mask!(query_axis.to_f32(), f32::total_cmp)
428            }
429            BinaryDataArrayType::Int64 => {
430                sort_mask!(query_axis.to_i64(), i64::cmp)
431            }
432            BinaryDataArrayType::Int32 => {
433                sort_mask!(query_axis.to_i32(), i32::cmp)
434            }
435            BinaryDataArrayType::ASCII => todo!(),
436            BinaryDataArrayType::Unknown => todo!(),
437        };
438
439        self.sort_from_indices(mask)
440    }
441
442    /// Get a mutable reference to the m/z array if it is present.
443    ///
444    /// This method obtains the m/z array, calls [`DataArray::decode_and_store`] to force
445    /// the data to be stored natively in memory, and forces the storage to be
446    /// [`BinaryDataArrayType::Float64`] if not already. Finally it returns a mutable
447    /// view of that decoded dataas `&mut [f64]`
448    pub fn mzs_mut(&mut self) -> Result<&mut [f64], ArrayRetrievalError> {
449        if let Some(mz_array) = self.get_mut(&ArrayType::MZArray) {
450            mz_array
451                .decode_and_store()
452                .inspect_err(|e| log::error!("Failed to decode m/z array: {e}"))?;
453            mz_array.store_as(BinaryDataArrayType::Float64)?;
454            mz_array.coerce_mut()
455        } else {
456            Err(ArrayRetrievalError::NotFound(ArrayType::MZArray))
457        }
458    }
459
460    /// Get a reference to the intensity array if it is present
461    pub fn intensities(&'_ self) -> Result<Cow<'_, [f32]>, ArrayRetrievalError> {
462        let intensities = self
463            .get(&ArrayType::IntensityArray)
464            .ok_or(ArrayRetrievalError::NotFound(ArrayType::IntensityArray))?
465            .to_f32()
466            .inspect_err(|e| log::error!("Failed to decode intensity array: {e:?}"))?;
467        Ok(intensities)
468    }
469
470    /// Get a mutable reference to the intensity array if it is present.
471    ///
472    /// See [`BinaryArrayMap::mzs_mut`] for an explanation of the intermediate steps,
473    /// save that the forced storage type is [`BinaryArrayDataType::Float32`].
474    pub fn intensities_mut(&mut self) -> Result<&mut [f32], ArrayRetrievalError> {
475        if let Some(arr) = self.get_mut(&ArrayType::IntensityArray) {
476            arr
477                .decode_and_store()
478                .inspect_err(|e| log::error!("Failed to decode intensity array: {e}"))?;
479            arr
480                .store_as(BinaryDataArrayType::Float32)
481                .inspect_err(|e| log::error!("Failed to decode intensity array: {e}"))?;
482            arr.coerce_mut()
483        } else {
484            Err(ArrayRetrievalError::NotFound(ArrayType::IntensityArray))
485        }
486    }
487
488    /// Get a reference to the charge array if it is present
489    pub fn charges(&'_ self) -> Result<Cow<'_, [i32]>, ArrayRetrievalError> {
490        match self.get(&ArrayType::ChargeArray) {
491            Some(data_array) => data_array.to_i32(),
492            None => Err(ArrayRetrievalError::NotFound(ArrayType::ChargeArray)),
493        }
494    }
495
496    /// Get a mutable reference to the charge array if it is present.
497    ///
498    /// See [`BinaryArrayMap::mzs_mut`] for an explanation of the intermediate steps,
499    /// save that the forced storage type is [`BinaryArrayDataType::Int32`].
500    pub fn charge_mut(&mut self) -> Result<&mut [i32], ArrayRetrievalError> {
501        if let Some(arr) = self.get_mut(&ArrayType::ChargeArray) {
502            arr.decode_and_store()?;
503            arr
504                .store_as(BinaryDataArrayType::Int32)
505                .inspect_err(|e| log::error!("Failed to decode charge array: {e}"))?;
506            arr.coerce_mut()
507        } else {
508            Err(ArrayRetrievalError::NotFound(ArrayType::ChargeArray))
509        }
510    }
511
512    /// Get a reference to the ion mobility array if it is present
513    pub fn ion_mobility(&self) -> Result<(Cow<'_, [f64]>, ArrayType), ArrayRetrievalError> {
514        if let Some((array_type, data_array)) = self
515            .byte_buffer_map
516            .iter()
517            .find(|(a, _)| a.is_ion_mobility())
518        {
519            Ok((
520                data_array
521                    .to_f64()
522                    .inspect_err(|e| log::error!("Failed to decode ion mobility array: {e}"))?,
523                array_type.clone(),
524            ))
525        } else {
526            Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray))
527        }
528    }
529
530    /// Get a mutable reference to the ion mobility array if it is present.
531    ///
532    /// See [`BinaryArrayMap::mzs_mut`] for an explanation of the intermediate steps.
533    pub fn ion_mobility_mut(&mut self) -> Result<(&mut [f64], ArrayType), ArrayRetrievalError> {
534        if let Some((array_type, data_array)) = self
535            .byte_buffer_map
536            .iter_mut()
537            .find(|(a, _)| a.is_ion_mobility())
538        {
539            data_array
540                .decode_and_store()
541                .inspect_err(|e| log::error!("Failed to decode ion mobility array: {e}"))?;
542            data_array.store_as(BinaryDataArrayType::Float32)?;
543            Ok((data_array.coerce_mut()?, array_type.clone()))
544        } else {
545            Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray))
546        }
547    }
548
549    /// Split an array map along the ion mobility dimension if it is present.
550    ///
551    /// This function will fail if there is no ion mobility dimension.
552    pub fn stack_ion_mobility(self) -> Result<BinaryArrayMap3D, ArrayRetrievalError> {
553        BinaryArrayMap3D::try_from(self)
554    }
555}
556
557impl IntoIterator for BinaryArrayMap {
558    type Item = (ArrayType, DataArray);
559
560    type IntoIter = <HashMap<ArrayType, DataArray> as IntoIterator>::IntoIter;
561
562    fn into_iter(self) -> Self::IntoIter {
563        self.byte_buffer_map.into_iter()
564    }
565}
566
567#[cfg(feature = "mzsignal")]
568mod mzsignal_impl {
569    use super::*;
570
571    use crate::to_bytes;
572    use mzsignal::ArrayPair;
573
574    impl From<ArrayPair<'_>> for BinaryArrayMap {
575        fn from(value: ArrayPair<'_>) -> Self {
576            let mz_array = DataArray::wrap(
577                &ArrayType::MZArray,
578                BinaryDataArrayType::Float64,
579                to_bytes(&value.mz_array),
580            );
581
582            let intensity_array = DataArray::wrap(
583                &ArrayType::IntensityArray,
584                BinaryDataArrayType::Float32,
585                to_bytes(&value.intensity_array),
586            );
587
588            let mut array_map = BinaryArrayMap::new();
589            array_map.add(mz_array);
590            array_map.add(intensity_array);
591            array_map
592        }
593    }
594}
595
596#[derive(Debug, Default, Clone, Copy, PartialEq)]
597#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
598struct NonNaNF64(f64);
599
600impl std::hash::Hash for NonNaNF64 {
601    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
602        state.write_u64(self.0.to_bits());
603    }
604}
605
606impl identity_hash::IdentityHashable for NonNaNF64 {}
607
608impl From<f64> for NonNaNF64 {
609    fn from(value: f64) -> Self {
610        Self::wrap(value)
611            .unwrap_or_else(|| panic!("Expected an order-able f64 value, but found {}", value))
612    }
613}
614
615impl NonNaNF64 {
616    fn wrap(value: f64) -> Option<Self> {
617        if value.is_nan() {
618            None
619        } else {
620            Some(Self(value))
621        }
622    }
623}
624
625impl PartialOrd for NonNaNF64 {
626    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
627        Some(self.cmp(other))
628    }
629}
630
631impl Eq for NonNaNF64 {}
632
633impl Ord for NonNaNF64 {
634    fn cmp(&self, other: &Self) -> Ordering {
635        self.0.total_cmp(&other.0)
636    }
637}
638
639macro_rules! _populate_stacked_array_from {
640    ($im_dim:ident, $view:ident, $index_map:ident, $array_bins:ident, $array_type:ident, $array:ident) => {
641        for (i_im, im) in $im_dim.iter() {
642            let v = $view[*i_im];
643            let i_axis = $index_map[&NonNaNF64(*im)];
644            let bin = &mut $array_bins[i_axis];
645            if let Some(bin_array) = bin.get_mut($array_type) {
646                bin_array.push(v)?;
647            } else {
648                let mut bin_array = DataArray::from_name_and_type($array_type, $array.dtype());
649                *bin_array.unit_mut() = $array.unit();
650                bin_array.push(v)?;
651                bin.add(bin_array);
652            }
653        }
654    };
655}
656
657/// Represent a set of [`BinaryArrayMap`] that has been split across an
658/// ion mobility dimension.
659///
660/// This effectively removes an ion mobility [`DataArray`] and for each unique
661/// value in that array, creates a new [`BinaryArrayMap`] with subsets of each
662/// other [`DataArray`] that co-occur at the same ion mobility coordinate.
663///
664/// Because [`BinaryArrayMap3D`] spreads [`DataArray`] over multiple ion mobility
665/// bins, it cannot share a single [`ParamList`] for each of the dimensions. Instead,
666/// all split [`ArrayType`] have their [`ParamList`] moved to separate storage accessed
667/// using [`params_of`] and its siblins. These [`ParamList`] will be propagated by [`Self::stack`]
668/// and [`BinaryArrayMap3D::unstack`] as appropriate.
669#[derive(Debug, Default, Clone)]
670#[cfg_attr(feature = "serde", serde_with::serde_as)]
671#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
672pub struct BinaryArrayMap3D {
673    pub ion_mobility_dimension: Vec<f64>,
674    pub ion_mobility_type: ArrayType,
675    pub ion_mobility_unit: Unit,
676    pub arrays: Vec<BinaryArrayMap>,
677    pub additional_arrays: BinaryArrayMap,
678    #[cfg_attr(feature = "serde", serde_as(as = "Vec<(_, _)>"))]
679    ion_mobility_index: HashMap<NonNaNF64, usize, BuildHasherDefault<IdentityHasher<NonNaNF64>>>,
680    parameters_of: HashMap<ArrayType, mzdata_param::ParamList>
681}
682
683impl BinaryArrayMap3D {
684    /// Create an empty [`BinaryArrayMap3D`] with a defined ion mobility dimension
685    /// but no content at each ion mobility point.
686    ///
687    /// # Panics
688    /// If the ion mobility dimension contains a NaN value
689    pub fn from_ion_mobility_dimension(
690        ion_mobility_dimension: Vec<f64>,
691        ion_mobility_type: ArrayType,
692        ion_mobility_unit: Unit,
693    ) -> BinaryArrayMap3D {
694        let ion_mobility_index: HashMap<_, _, BuildHasherDefault<IdentityHasher<NonNaNF64>>> = ion_mobility_dimension
695            .iter()
696            .copied()
697            .enumerate()
698            .map(|(i, f)| {
699                (
700                    NonNaNF64::wrap(f)
701                        .unwrap_or_else(|| panic!("Expected non-NaN value for ion mobility")),
702                    i,
703                )
704            })
705            .collect();
706        let mut arrays = Vec::new();
707        arrays.resize_with(ion_mobility_dimension.len(), BinaryArrayMap::default);
708        Self {
709            ion_mobility_dimension,
710            ion_mobility_type,
711            ion_mobility_unit,
712            arrays,
713            ion_mobility_index,
714            additional_arrays: Default::default(),
715            parameters_of: Default::default(),
716        }
717    }
718
719    /// Construct a [`BinaryArrayMap3D`] from a defined ion mobility dimension and
720    /// a [`Vec`] of [`BinaryArrayMap`] at each point.
721    ///
722    /// # Panics
723    /// If the ion mobility dimension contains a NaN value or `ion_mobility_dimension.len() != arrays.len()`
724    pub fn from_ion_mobility_dimension_and_arrays(
725        ion_mobility_dimension: Vec<f64>,
726        ion_mobility_type: ArrayType,
727        ion_mobility_unit: Unit,
728        arrays: Vec<BinaryArrayMap>,
729    ) -> BinaryArrayMap3D {
730        let ion_mobility_index: HashMap<_, _, _> = ion_mobility_dimension
731            .iter()
732            .copied()
733            .enumerate()
734            .map(|(i, f)| {
735                (
736                    NonNaNF64::wrap(f)
737                        .unwrap_or_else(|| panic!("Expected non-NaN value for ion mobility")),
738                    i,
739                )
740            })
741            .collect();
742
743        assert_eq!(
744            ion_mobility_dimension.len(),
745            arrays.len(),
746            "The number of ion mobility points does not match the number of point-level maps!"
747        );
748
749        Self {
750            ion_mobility_dimension,
751            ion_mobility_type,
752            ion_mobility_unit,
753            arrays,
754            ion_mobility_index,
755            additional_arrays: Default::default(),
756            parameters_of: Default::default(),
757        }
758    }
759
760    /// Get a reference to the [`ParamList`] for the specified [`ArrayType`], if one exists.
761    ///
762    /// # See also
763    /// - [`Self::params_of_mut`] to mutate existing [`ParamList`] for extant [`ArrayType`]
764    /// - [`Self::insert_params_of`] to create or overwrite the [`ParamList`] for an [`ArrayType`]
765    pub fn params_of(&self, array_type: &ArrayType) -> Option<&Vec<Param>> {
766        self.parameters_of.get(array_type)
767    }
768
769    /// Get a mutable reference to the [`ParamList`] for the specified [`ArrayType`], if one exists.
770    ///
771    /// # See also
772    /// - [`Self::params_of`] to read existing [`ParamList`] for extant [`ArrayType`]
773    /// - [`Self::insert_params_of`] to create or overwrite the [`ParamList`] for an [`ArrayType`]
774    /// - [`Self::params_of_entry`] to create or modify the [`ParamList`] using a lower
775    /// level Entry API
776    pub fn params_of_mut(&mut self, array_type: &ArrayType) -> Option<&mut Vec<Param>> {
777        self.parameters_of.get_mut(array_type)
778    }
779
780    /// A lower level create or update version of [`Self::insert_params_of`] that uses the
781    /// underlying [`HashMap`]'s Entry API
782    pub fn params_of_entry(&mut self, array_type: ArrayType) -> std::collections::hash_map::Entry<'_, ArrayType, Vec<Param>> {
783        self.parameters_of.entry(array_type)
784    }
785
786    /// Set the [`ParamList`] for the specified [`ArrayType`].
787    ///
788    /// Returns any previous [`ParamList`] associated with that [`ArrayType`].
789    ///
790    /// # See also
791    /// - [`Self::params_of`] to read existing [`ParamList`] for extant [`ArrayType`]
792    /// - [`Self::params_of_mut`] to mutate existing [`ParamList`] for extant [`ArrayType`]
793    /// - [`Self::params_of_entry`] to create or modify the [`ParamList`] using a lower
794    /// level Entry API
795    pub fn insert_params_of(&mut self, array_type: ArrayType, params: Vec<Param>) -> Option<Vec<Param>> {
796        self.parameters_of.insert(array_type, params)
797    }
798
799    /// Get the associated arrays at the requested ion mobility, if they exist
800    pub fn get_ion_mobility(&self, ion_mobility: f64) -> Option<&BinaryArrayMap> {
801        if let Some(i) = NonNaNF64::wrap(ion_mobility) {
802            if let Some(i) = self.ion_mobility_index.get(&i) {
803                self.arrays.get(*i)
804            } else {
805                None
806            }
807        } else {
808            None
809        }
810    }
811
812    /// Perform a binary search on the ion mobility dimension and find the nearest
813    /// point to `ion_mobility` within `error_tolerance` absolute distance.
814    ///
815    /// Returns the [`BinaryArrayMap`] at that point and the distance from `ion_mobility`
816    /// matched.
817    pub fn search_ion_mobility(
818        &self,
819        ion_mobility: f64,
820        error_tolerance: f64,
821    ) -> Option<(&BinaryArrayMap, f64)> {
822        match self
823            .ion_mobility_dimension
824            .binary_search_by(|x: &f64| x.total_cmp(&ion_mobility))
825        {
826            Ok(i) => {
827                let delta = ion_mobility - self.ion_mobility_dimension[i];
828                if delta.abs() <= error_tolerance {
829                    self.arrays.get(i).map(|a| (a, delta))
830                } else {
831                    None
832                }
833            }
834            Err(i) => {
835                if self.arrays.is_empty() {
836                    return None;
837                }
838                let delta = ion_mobility - self.ion_mobility_dimension[i];
839                if delta.abs() <= error_tolerance {
840                    self.arrays.get(i).map(|a| (a, delta))
841                } else {
842                    None
843                }
844            }
845        }
846    }
847
848    /// Perform a binary search on the ion mobility dimension and find the nearest
849    /// point to `ion_mobility` within `error_tolerance` absolute distance.
850    ///
851    /// Unlike [`Self::search_ion_mobility`], this returns the index in the ion mobility
852    /// dimension that matched and the ion mobility value at that index.
853    pub fn search_ion_mobility_index(&self, ion_mobility: f64, error_tolerance: f64) -> Option<(usize, f64)> {
854        match self
855            .ion_mobility_dimension
856            .binary_search_by(|x: &f64| x.total_cmp(&ion_mobility))
857        {
858            Ok(i) => {
859                let delta = ion_mobility - self.ion_mobility_dimension[i];
860                if delta.abs() <= error_tolerance {
861                    Some((i, self.ion_mobility_dimension[i]))
862                } else {
863                    None
864                }
865            }
866            Err(i) => {
867                if self.arrays.is_empty() {
868                    return None;
869                }
870                let delta = ion_mobility - self.ion_mobility_dimension[i];
871                if delta.abs() <= error_tolerance {
872                    Some((i, self.ion_mobility_dimension[i]))
873                } else {
874                    None
875                }
876            }
877        }
878    }
879
880    /// Get the a mutable reference to the associated arrays at the requested ion mobility, if they exist.
881    ///
882    /// The ion mobility value must be an *exact* match
883    pub fn get_ion_mobility_mut(&mut self, ion_mobility: f64) -> Option<&mut BinaryArrayMap> {
884        if let Some(i) = NonNaNF64::wrap(ion_mobility) {
885            if let Some(i) = self.ion_mobility_index.get(&i) {
886                self.arrays.get_mut(*i)
887            } else {
888                None
889            }
890        } else {
891            None
892        }
893    }
894
895    /// Iterate over the ion mobility dimension and associated arrays
896    /// at each point.
897    pub fn iter(&self) -> impl Iterator<Item = (f64, &BinaryArrayMap)> {
898        self.ion_mobility_dimension
899            .iter()
900            .copied()
901            .zip(self.arrays.iter())
902    }
903
904    /// Iterate over the ion mobility dimension and a mutable reference to the associated arrays
905    /// at each point.
906    pub fn iter_mut(&mut self) -> impl Iterator<Item = (f64, &mut BinaryArrayMap)> {
907        self.ion_mobility_dimension
908            .iter()
909            .copied()
910            .zip(self.arrays.iter_mut())
911    }
912
913    /// Flatten this array into a single [`BinaryArrayMap`].
914    ///
915    /// The flattened map is sorted by m/z and ion mobility. If no m/z array is found,
916    /// *no* sorting is done, and the flattened order is left in roughly ion mobility sorted
917    /// order.
918    ///
919    /// # Errors
920    /// [`ArrayRetrievalError`] errors related to array decoding occur if
921    /// any [`DataArray`] cannot be decoded, or if an expected array is absent.
922    pub fn unstack(&self) -> Result<BinaryArrayMap, ArrayRetrievalError> {
923        let mut destination = self.additional_arrays.clone();
924
925        let mut im_dim =
926            DataArray::from_name_and_type(&self.ion_mobility_type, BinaryDataArrayType::Float64);
927        im_dim.unit = self.ion_mobility_unit;
928        let mut sizes = Vec::new();
929        let mut mz_size = None;
930        for (layer, im) in self.arrays.iter().zip(self.ion_mobility_dimension.iter()) {
931            sizes.clear();
932            for (key, array) in layer.iter() {
933                match destination.get_mut(key) {
934                    Some(sink) => {
935                        sink.extend_raw(&array.data).inspect_err(|e| {
936                            log::error!("Failed to extend {key:?}: {e}");
937                        })?;
938                    }
939                    None => {
940                        destination.add(array.clone());
941                    }
942                }
943                if matches!(key, ArrayType::MZArray) {
944                    mz_size = Some(array.data_len()?);
945                } else {
946                    sizes.push((key, array.data_len()?));
947                }
948            }
949            if let Some(mz_size) = mz_size {
950                im_dim.extend_iter(std::iter::repeat_n(*im, mz_size))?;
951            } else if let Some((_, size)) = sizes.first() {
952                im_dim.extend_iter(std::iter::repeat_n(*im, *size))?;
953            }
954        }
955
956        let sorter = if let Some(mz_array) = destination.get(&ArrayType::MZArray) {
957            let mzs = mz_array.to_f64()?;
958            let ims = im_dim.to_f64()?;
959
960            let mut indices: Vec<usize> = Vec::with_capacity(mzs.len());
961            indices.extend(0..mzs.len());
962            indices.sort_by(|i, j| {
963                mzs[*i]
964                    .total_cmp(&mzs[*j])
965                    .then_with(|| ims[*i].total_cmp(&ims[*j]))
966            });
967            Some(indices)
968        } else {
969            None
970        };
971
972        let final_size = im_dim.data_len()?;
973
974        destination.add(im_dim);
975
976        if let Some(sorter) = sorter {
977            destination.sort_from_indices(sorter)?;
978        } else if final_size > 0 {
979            log::debug!("Unsorted unstack");
980        }
981
982        for (k, v) in destination.iter_mut(){
983            if let Some(pars) = self.parameters_of.get(k) {
984                v.params_mut().extend(pars.iter().cloned());
985            }
986        }
987        Ok(destination)
988    }
989
990    /// Convert a [`BinaryArrayMap`] into a [`BinaryArrayMap3D`] if it has an ion mobility dimension.
991    ///
992    /// Any arrays that aren't the same length as the ion mobility dimension will be in
993    /// [`BinaryArrayMap3D::additional_arrays`].
994    ///
995    /// This uses *exact* ion mobility values as keys, bit-level equality. If ion mobility
996    /// values are computed, they may vary at trivial bits and be equivalent, but not **equal**.
997    ///
998    /// # Errors
999    /// [`ArrayRetrievalError`] errors related to array decoding occur if any [`DataArray`]
1000    /// cannot be decoded, or if an expected array is absent.
1001    pub fn stack(source: &BinaryArrayMap) -> Result<Self, ArrayRetrievalError> {
1002        let mut this = Self::default();
1003        if !source.has_ion_mobility() {
1004            return Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray));
1005        }
1006        let (im_dim, im_type) = source.ion_mobility()?;
1007        this.ion_mobility_unit = source.get(&im_type).unwrap().unit;
1008        this.ion_mobility_type = im_type;
1009        if im_dim.is_empty() {
1010            return Ok(this);
1011        }
1012        let mut im_dim: Vec<(usize, f64)> = im_dim.iter().copied().enumerate().collect();
1013        im_dim.sort_by(|(_, va), (_, vb)| va.total_cmp(vb));
1014
1015        let mut im_axis = Vec::with_capacity(200);
1016        let mut last_v = im_dim.first().unwrap().1 - 1.0;
1017        let mut index_map = HashMap::default();
1018        for (_, v) in im_dim.iter() {
1019            if v.total_cmp(&last_v).is_gt() {
1020                last_v = *v;
1021                index_map.insert(NonNaNF64::from(*v), im_axis.len());
1022                im_axis.push(*v);
1023            }
1024        }
1025
1026        let mut array_bins: Vec<BinaryArrayMap> = Vec::with_capacity(im_axis.len());
1027        array_bins.resize(im_axis.len(), BinaryArrayMap::default());
1028        for (array_type, array) in source.iter() {
1029            if array_type.is_ion_mobility() {
1030                continue;
1031            }
1032            if array.data_len()? != im_dim.len() {
1033                this.additional_arrays.add(array.clone());
1034                continue;
1035            }
1036            if !array.params().is_empty() {
1037                this.parameters_of.insert(array_type.clone(), array.params().to_vec());
1038            }
1039            match array.dtype() {
1040                BinaryDataArrayType::Unknown => {
1041                    panic!("Cannot re-sort opaque or unknown dimension data types")
1042                }
1043                BinaryDataArrayType::Float64 => {
1044                    let view = array.to_f64()?;
1045                    _populate_stacked_array_from!(
1046                        im_dim, view, index_map, array_bins, array_type, array
1047                    );
1048                }
1049                BinaryDataArrayType::Float32 => {
1050                    let view = array.to_f32()?;
1051                    _populate_stacked_array_from!(
1052                        im_dim, view, index_map, array_bins, array_type, array
1053                    );
1054                }
1055                BinaryDataArrayType::Int64 => {
1056                    let view = array.to_i64()?;
1057                    _populate_stacked_array_from!(
1058                        im_dim, view, index_map, array_bins, array_type, array
1059                    );
1060                }
1061                BinaryDataArrayType::Int32 => {
1062                    let view = array.to_i32()?;
1063                    _populate_stacked_array_from!(
1064                        im_dim, view, index_map, array_bins, array_type, array
1065                    );
1066                }
1067                BinaryDataArrayType::ASCII => {
1068                    let view = array.decode()?;
1069                    _populate_stacked_array_from!(
1070                        im_dim, view, index_map, array_bins, array_type, array
1071                    );
1072                }
1073            }
1074        }
1075        this.ion_mobility_dimension = im_axis;
1076        this.ion_mobility_index = index_map;
1077        this.arrays = array_bins;
1078        Ok(this)
1079    }
1080}
1081
1082impl TryFrom<BinaryArrayMap> for BinaryArrayMap3D {
1083    type Error = ArrayRetrievalError;
1084
1085    /// A wrapper around [`BinaryArrayMap3D::stack`]
1086    fn try_from(value: BinaryArrayMap) -> Result<Self, Self::Error> {
1087        Self::stack(&value)
1088    }
1089}
1090
1091/// A [`BinaryArrayMap3D`] aliases [`TryFrom`] on [`BinaryArrayMap`] with [`BinaryArrayMap3D::stack`]
1092impl TryFrom<&BinaryArrayMap> for BinaryArrayMap3D {
1093    type Error = ArrayRetrievalError;
1094
1095    fn try_from(value: &BinaryArrayMap) -> Result<Self, Self::Error> {
1096        Self::stack(value)
1097    }
1098}
1099
1100impl TryFrom<BinaryArrayMap3D> for BinaryArrayMap {
1101    type Error = ArrayRetrievalError;
1102
1103    fn try_from(value: BinaryArrayMap3D) -> Result<Self, Self::Error> {
1104        value.unstack()
1105    }
1106}
1107
1108/// A [`BinaryArrayMap`] aliases [`TryFrom`] on [`BinaryArrayMap3D`] with [`BinaryArrayMap3D::unstack`]
1109impl TryFrom<&BinaryArrayMap3D> for BinaryArrayMap {
1110    type Error = ArrayRetrievalError;
1111
1112    fn try_from(value: &BinaryArrayMap3D) -> Result<Self, Self::Error> {
1113        value.unstack()
1114    }
1115}
1116
1117
1118
1119#[cfg(test)]
1120mod test {
1121    use crate::BinaryDataArrayType;
1122
1123    use super::*;
1124    use std::fs;
1125    use std::io::{self, prelude::*};
1126
1127    fn make_array_from_file() -> io::Result<DataArray> {
1128        let mut fh = fs::File::open("../../test/data/mz_f64_zlib_bas64.txt")?;
1129        let mut buf = String::new();
1130        fh.read_to_string(&mut buf)?;
1131        let bytes: Vec<u8> = buf.into();
1132        let mut da = DataArray::wrap(&ArrayType::MZArray, BinaryDataArrayType::Float64, bytes);
1133        da.compression = BinaryCompressionType::Zlib;
1134        Ok(da)
1135    }
1136
1137    #[test]
1138    fn test_construction() -> io::Result<()> {
1139        let da = make_array_from_file()?;
1140        let mut map = BinaryArrayMap::new();
1141        assert!(!map.has_array(&ArrayType::MZArray));
1142        map.add(da);
1143        assert!(map.has_array(&ArrayType::MZArray));
1144        Ok(())
1145    }
1146
1147    #[test]
1148    fn test_decode() -> io::Result<()> {
1149        let da = make_array_from_file()?;
1150        let mut map = BinaryArrayMap::new();
1151        map.add(da);
1152        assert_eq!(
1153            map.get(&ArrayType::MZArray).unwrap().compression,
1154            BinaryCompressionType::Zlib
1155        );
1156        map.decode_all_arrays()?;
1157        assert_eq!(
1158            map.get(&ArrayType::MZArray).unwrap().compression,
1159            BinaryCompressionType::Decoded
1160        );
1161        Ok(())
1162    }
1163
1164    #[test]
1165    fn test_sort_from() -> io::Result<()> {
1166        let mut arr = make_array_from_file()?;
1167        let dat = arr.coerce_mut::<f64>()?;
1168        dat.reverse();
1169
1170        let mut arr2 = make_array_from_file()?;
1171        arr2.name = ArrayType::TimeArray;
1172        arr2.store_as(BinaryDataArrayType::Float32)?;
1173        let dat = arr2.coerce_mut::<f32>()?;
1174        dat.reverse();
1175
1176        let mut map = BinaryArrayMap::new();
1177        map.add(arr);
1178        map.add(arr2);
1179        map.sort_by_array(&ArrayType::MZArray)?;
1180        let arr = map.mzs()?;
1181        let (_, state) = arr.iter().copied().fold((-f64::INFINITY, true), |(last, state), next|{
1182            if !state {
1183                (last, state)
1184            } else {
1185                (next, last <= next)
1186            }
1187        });
1188        assert!(state, "Failed to sort m/z array {arr:?}");
1189
1190        let arr = map.get(&ArrayType::TimeArray).unwrap().to_f64()?;
1191        let (_, state) = arr.iter().copied().fold((-f64::INFINITY, true), |(last, state), next|{
1192            if !state {
1193                (last, state)
1194            } else {
1195                (next, last <= next)
1196            }
1197        });
1198        assert!(state, "Failed to sort parallel array {arr:?}");
1199        Ok(())
1200    }
1201}