use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::hash_map::{Iter, IterMut};
use std::collections::HashMap;
use std::convert::TryFrom;
#[cfg(feature = "parallelism")]
use rayon::prelude::*;
use mzpeaks::Tolerance;
use crate::params::Unit;
use super::array::DataArray;
use super::encodings::{ArrayRetrievalError, ArrayType, BinaryCompressionType};
use super::traits::{ByteArrayView, ByteArrayViewMut};
use super::BinaryDataArrayType;
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BinaryArrayMap {
pub byte_buffer_map: HashMap<ArrayType, DataArray>,
}
impl BinaryArrayMap {
pub fn new() -> BinaryArrayMap {
BinaryArrayMap {
..Default::default()
}
}
pub fn len(&self) -> usize {
self.byte_buffer_map.len()
}
pub fn is_empty(&self) -> bool {
self.byte_buffer_map.is_empty()
}
pub fn has_ion_mobility(&self) -> bool {
self.byte_buffer_map.keys().any(|a| a.is_ion_mobility())
}
pub fn iter(&self) -> Iter<'_, ArrayType, DataArray> {
self.byte_buffer_map.iter()
}
#[cfg(feature = "parallelism")]
pub fn par_iter(&self) -> rayon::collections::hash_map::Iter<'_, ArrayType, DataArray> {
self.byte_buffer_map.par_iter()
}
pub fn iter_mut(&mut self) -> IterMut<'_, ArrayType, DataArray> {
self.byte_buffer_map.iter_mut()
}
#[cfg(feature = "parallelism")]
pub fn par_iter_mut(
&mut self,
) -> rayon::collections::hash_map::IterMut<'_, ArrayType, DataArray> {
self.byte_buffer_map.par_iter_mut()
}
pub fn encode_array(
&mut self,
array_type: &ArrayType,
compression: BinaryCompressionType,
) -> Result<(), ArrayRetrievalError> {
if let Some(arr) = self.get_mut(array_type) {
arr.store_compressed(compression)?;
Ok(())
} else {
Err(ArrayRetrievalError::NotFound(array_type.clone()))
}
}
pub fn decode_all_arrays(&mut self) -> Result<(), ArrayRetrievalError> {
#[cfg(not(feature = "parallelism"))]
{
self._decode_all_arrays()
}
#[cfg(feature = "parallelism")]
{
if self.len() > 2 {
self._decode_all_arrays_parallel()
} else {
self._decode_all_arrays()
}
}
}
fn _decode_all_arrays(&mut self) -> Result<(), ArrayRetrievalError> {
for (_key, value) in self.iter_mut() {
match value.compression {
BinaryCompressionType::Decoded => {}
_ => {
value.decode_and_store()?;
}
}
}
Ok(())
}
#[cfg(feature = "parallelism")]
fn _decode_all_arrays_parallel(&mut self) -> Result<(), ArrayRetrievalError> {
let res: Result<(), ArrayRetrievalError> = self
.iter_mut()
.par_bridge()
.map(|(_key, value)| {
match value.compression {
BinaryCompressionType::Decoded => {}
_ => {
value.decode_and_store()?;
}
}
Ok(())
})
.collect::<Result<(), ArrayRetrievalError>>();
res
}
pub fn decode_array(&mut self, array_type: &ArrayType) -> Result<(), ArrayRetrievalError> {
if let Some(array) = self.get_mut(array_type) {
array.decode_and_store()?;
Ok(())
} else {
Err(ArrayRetrievalError::NotFound(array_type.clone()))
}
}
pub fn add(&mut self, array: DataArray) {
self.byte_buffer_map.insert(array.name.clone(), array);
}
pub fn get(&self, array_type: &ArrayType) -> Option<&DataArray> {
self.byte_buffer_map.get(array_type)
}
pub fn get_mut(&mut self, array_type: &ArrayType) -> Option<&mut DataArray> {
self.byte_buffer_map.get_mut(array_type)
}
pub fn has_array(&self, array_type: &ArrayType) -> bool {
self.byte_buffer_map.contains_key(array_type)
}
pub fn clear(&mut self) {
self.byte_buffer_map.clear();
}
pub fn search(&self, query: f64, error_tolerance: Tolerance) -> Option<usize> {
if let Ok(mzs) = self.mzs() {
let (lower, _upper) = error_tolerance.bounds(query);
match mzs[..].binary_search_by(|m| m.partial_cmp(&lower).unwrap()) {
Ok(i) => {
let mut best_error = error_tolerance.call(query, mzs[i]).abs();
let mut best_index = i;
let mut index = i + 1;
while index < mzs.len() {
let error = error_tolerance.call(query, mzs[index]).abs();
if error < best_error {
best_index = index;
best_error = error;
}
index += 1;
}
if best_error < error_tolerance.tol() {
return Some(best_index);
}
None
}
Err(_err) => None,
}
} else {
None
}
}
pub fn mzs(&'_ self) -> Result<Cow<'_, [f64]>, ArrayRetrievalError> {
let mz_array = self
.get(&ArrayType::MZArray)
.ok_or(ArrayRetrievalError::NotFound(ArrayType::MZArray))?
.to_f64()
.inspect_err(|e| log::error!("Failed to decode m/z array: {e}"))?;
Ok(mz_array)
}
pub fn sort_from_indices(&mut self, mut mask: Vec<usize>) -> Result<(), ArrayRetrievalError> {
let n = mask.len();
const TOMBSTONE: usize = usize::MAX;
for idx in 0..n {
if mask[idx] != TOMBSTONE {
let mut current_idx = idx;
loop {
let next_idx = mask[current_idx];
mask[current_idx] = TOMBSTONE;
if mask[next_idx] == TOMBSTONE {
break;
}
for (_, v) in self.iter_mut() {
if v.data_len()? != n {
continue;
}
match v.dtype {
BinaryDataArrayType::Float64 => {
let view = v.coerce_mut::<f64>()?;
view.swap(current_idx, next_idx);
}
BinaryDataArrayType::Float32 => {
let view = v.coerce_mut::<f32>()?;
view.swap(current_idx, next_idx);
}
BinaryDataArrayType::Int64 => {
let view = v.coerce_mut::<i64>()?;
view.swap(current_idx, next_idx);
}
BinaryDataArrayType::Int32 => {
let view = v.coerce_mut::<i32>()?;
view.swap(current_idx, next_idx);
}
BinaryDataArrayType::ASCII => todo!(),
BinaryDataArrayType::Unknown => todo!(),
}
}
current_idx = next_idx;
}
}
}
Ok(())
}
pub fn sort_by_array(&mut self, name: &ArrayType) -> Result<(), ArrayRetrievalError> {
let query_axis = self
.get(name)
.ok_or_else(|| ArrayRetrievalError::NotFound(name.clone()))?;
macro_rules! sort_mask {
($conv:expr, $cmp:expr) => {{
let vals = $conv?;
if vals.is_sorted() {
return Ok(());
}
let n = vals.len();
let mut mask: Vec<usize> = (0..n).into_iter().collect();
mask.sort_by(|i, j| {
let a = vals[*i];
let b = vals[*j];
$cmp(&a, &b)
});
(mask, n)
}};
}
let (mask, _) = match query_axis.dtype() {
BinaryDataArrayType::Float64 => {
sort_mask!(query_axis.to_f64(), f64::total_cmp)
}
BinaryDataArrayType::Float32 => {
sort_mask!(query_axis.to_f32(), f32::total_cmp)
}
BinaryDataArrayType::Int64 => {
sort_mask!(query_axis.to_i64(), i64::cmp)
}
BinaryDataArrayType::Int32 => {
sort_mask!(query_axis.to_i32(), i32::cmp)
}
BinaryDataArrayType::ASCII => todo!(),
BinaryDataArrayType::Unknown => todo!(),
};
self.sort_from_indices(mask)
}
pub fn mzs_mut(&mut self) -> Result<&mut [f64], ArrayRetrievalError> {
if let Some(mz_array) = self.get_mut(&ArrayType::MZArray) {
mz_array
.decode_and_store()
.inspect_err(|e| log::error!("Failed to decode m/z array: {e}"))?;
mz_array.store_as(BinaryDataArrayType::Float64)?;
mz_array.coerce_mut()
} else {
Err(ArrayRetrievalError::NotFound(ArrayType::MZArray))
}
}
pub fn intensities(&'_ self) -> Result<Cow<'_, [f32]>, ArrayRetrievalError> {
let intensities = self
.get(&ArrayType::IntensityArray)
.ok_or(ArrayRetrievalError::NotFound(ArrayType::IntensityArray))?
.to_f32()
.inspect_err(|e| log::error!("Failed to decode intensity array: {e:?}"))?;
Ok(intensities)
}
pub fn intensities_mut(&mut self) -> Result<&mut [f32], ArrayRetrievalError> {
if let Some(mz_array) = self.get_mut(&ArrayType::IntensityArray) {
mz_array
.decode_and_store()
.inspect_err(|e| log::error!("Failed to decode intensity array: {e}"))?;
mz_array
.store_as(BinaryDataArrayType::Float32)
.inspect_err(|e| log::error!("Failed to decode intensity array: {e}"))?;
mz_array.coerce_mut()
} else {
Err(ArrayRetrievalError::NotFound(ArrayType::IntensityArray))
}
}
pub fn charges(&'_ self) -> Result<Cow<'_, [i32]>, ArrayRetrievalError> {
match self.get(&ArrayType::ChargeArray) {
Some(data_array) => data_array.to_i32(),
None => Err(ArrayRetrievalError::NotFound(ArrayType::ChargeArray)),
}
}
pub fn charge_mut(&mut self) -> Result<&mut [i32], ArrayRetrievalError> {
if let Some(mz_array) = self.get_mut(&ArrayType::ChargeArray) {
mz_array.decode_and_store()?;
mz_array
.store_as(BinaryDataArrayType::Int32)
.inspect_err(|e| log::error!("Failed to decode charge array: {e}"))?;
mz_array.coerce_mut()
} else {
Err(ArrayRetrievalError::NotFound(ArrayType::ChargeArray))
}
}
pub fn ion_mobility(&self) -> Result<(Cow<'_, [f64]>, ArrayType), ArrayRetrievalError> {
if let Some((array_type, data_array)) = self
.byte_buffer_map
.iter()
.find(|(a, _)| a.is_ion_mobility())
{
Ok((
data_array
.to_f64()
.inspect_err(|e| log::error!("Failed to decode ion mobility array: {e}"))?,
array_type.clone(),
))
} else {
Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray))
}
}
pub fn ion_mobility_mut(&mut self) -> Result<(&mut [f64], ArrayType), ArrayRetrievalError> {
if let Some((array_type, data_array)) = self
.byte_buffer_map
.iter_mut()
.find(|(a, _)| a.is_ion_mobility())
{
data_array
.decode_and_store()
.inspect_err(|e| log::error!("Failed to decode ion mobility array: {e}"))?;
data_array.store_as(BinaryDataArrayType::Float32)?;
Ok((data_array.coerce_mut()?, array_type.clone()))
} else {
Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray))
}
}
pub fn stack_ion_mobility(self) -> Result<BinaryArrayMap3D, ArrayRetrievalError> {
BinaryArrayMap3D::try_from(self)
}
}
impl IntoIterator for BinaryArrayMap {
type Item = (ArrayType, DataArray);
type IntoIter = <HashMap<ArrayType, DataArray> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.byte_buffer_map.into_iter()
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct NonNaNF64(f64);
impl std::hash::Hash for NonNaNF64 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
((self.0 * 10000.0) as i64).hash(state);
}
}
impl From<f64> for NonNaNF64 {
fn from(value: f64) -> Self {
Self::wrap(value)
.unwrap_or_else(|| panic!("Expected an order-able f64 value, but found {}", value))
}
}
impl NonNaNF64 {
fn wrap(value: f64) -> Option<Self> {
if value.is_nan() {
None
} else {
Some(Self(value))
}
}
}
impl PartialOrd for NonNaNF64 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Eq for NonNaNF64 {}
impl Ord for NonNaNF64 {
fn cmp(&self, other: &Self) -> Ordering {
self.0.total_cmp(&other.0)
}
}
macro_rules! _populate_stacked_array_from {
($im_dim:ident, $view:ident, $index_map:ident, $array_bins:ident, $array_type:ident, $array:ident) => {
for (i_im, im) in $im_dim.iter() {
let v = $view[*i_im];
let i_axis = $index_map[&NonNaNF64(*im)];
let bin = &mut $array_bins[i_axis];
if let Some(bin_array) = bin.get_mut($array_type) {
bin_array.push(v)?;
} else {
let mut bin_array = DataArray::from_name_and_type($array_type, $array.dtype());
*bin_array.unit_mut() = $array.unit();
bin_array.push(v)?;
bin.add(bin_array);
}
}
};
}
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", serde_with::serde_as)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BinaryArrayMap3D {
pub ion_mobility_dimension: Vec<f64>,
pub ion_mobility_type: ArrayType,
pub ion_mobility_unit: Unit,
pub arrays: Vec<BinaryArrayMap>,
pub additional_arrays: BinaryArrayMap,
#[cfg_attr(feature = "serde", serde_as(as = "Vec<(_, _)>"))]
ion_mobility_index: HashMap<NonNaNF64, usize>,
}
impl BinaryArrayMap3D {
pub fn from_ion_mobility_dimension(
ion_mobility_dimension: Vec<f64>,
ion_mobility_type: ArrayType,
ion_mobility_unit: Unit,
) -> BinaryArrayMap3D {
let ion_mobility_index: HashMap<_, _> = ion_mobility_dimension
.iter()
.copied()
.enumerate()
.map(|(i, f)| {
(
NonNaNF64::wrap(f)
.unwrap_or_else(|| panic!("Expected non-NaN value for ion mobility")),
i,
)
})
.collect();
let mut arrays = Vec::new();
arrays.resize_with(ion_mobility_dimension.len(), BinaryArrayMap::default);
Self {
ion_mobility_dimension,
ion_mobility_type,
ion_mobility_unit,
arrays,
ion_mobility_index,
additional_arrays: Default::default(),
}
}
pub fn from_ion_mobility_dimension_and_arrays(
ion_mobility_dimension: Vec<f64>,
ion_mobility_type: ArrayType,
ion_mobility_unit: Unit,
arrays: Vec<BinaryArrayMap>,
) -> BinaryArrayMap3D {
let ion_mobility_index: HashMap<_, _> = ion_mobility_dimension
.iter()
.copied()
.enumerate()
.map(|(i, f)| {
(
NonNaNF64::wrap(f)
.unwrap_or_else(|| panic!("Expected non-NaN value for ion mobility")),
i,
)
})
.collect();
Self {
ion_mobility_dimension,
ion_mobility_type,
ion_mobility_unit,
arrays,
ion_mobility_index,
additional_arrays: Default::default(),
}
}
pub fn get_ion_mobility(&self, ion_mobility: f64) -> Option<&BinaryArrayMap> {
if let Some(i) = NonNaNF64::wrap(ion_mobility) {
if let Some(i) = self.ion_mobility_index.get(&i) {
self.arrays.get(*i)
} else {
None
}
} else {
None
}
}
pub fn search_ion_mobility(
&self,
ion_mobility: f64,
error_tolerance: f64,
) -> Option<(&BinaryArrayMap, f64)> {
match self
.ion_mobility_dimension
.binary_search_by(|x: &f64| x.total_cmp(&ion_mobility))
{
Ok(i) => {
let delta = ion_mobility - self.ion_mobility_dimension[i];
if delta.abs() <= error_tolerance {
self.arrays.get(i).map(|a| (a, delta))
} else {
None
}
}
Err(i) => {
if self.arrays.is_empty() {
return None;
}
let delta = ion_mobility - self.ion_mobility_dimension[i];
if delta.abs() <= error_tolerance {
self.arrays.get(i).map(|a| (a, delta))
} else {
None
}
}
}
}
pub fn get_ion_mobility_mut(&mut self, ion_mobility: f64) -> Option<&mut BinaryArrayMap> {
if let Some(i) = NonNaNF64::wrap(ion_mobility) {
if let Some(i) = self.ion_mobility_index.get(&i) {
self.arrays.get_mut(*i)
} else {
None
}
} else {
None
}
}
pub fn iter(&self) -> impl Iterator<Item = (f64, &BinaryArrayMap)> {
self.ion_mobility_dimension
.iter()
.copied()
.zip(self.arrays.iter())
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (f64, &mut BinaryArrayMap)> {
self.ion_mobility_dimension
.iter()
.copied()
.zip(self.arrays.iter_mut())
}
pub fn unstack(&self) -> Result<BinaryArrayMap, ArrayRetrievalError> {
let mut destination = self.additional_arrays.clone();
let mut im_dim =
DataArray::from_name_and_type(&self.ion_mobility_type, BinaryDataArrayType::Float64);
im_dim.unit = self.ion_mobility_unit;
let mut sizes = Vec::new();
let mut mz_size = None;
for (layer, im) in self.arrays.iter().zip(self.ion_mobility_dimension.iter()) {
sizes.clear();
for (key, array) in layer.iter() {
match destination.get_mut(&key) {
Some(sink) => {
sink.extend_raw(&array.data).inspect_err(|e| {
log::error!("Failed to extend {key:?}: {e}");
})?;
},
None => {
destination.add(array.clone());
},
}
if matches!(key, ArrayType::MZArray) {
mz_size = Some(array.data_len()?);
} else {
sizes.push((key, array.data_len()?));
}
}
if let Some(mz_size) = mz_size {
im_dim.extend_iter(std::iter::repeat_n(*im, mz_size))?;
} else {
if let Some((_, size)) = sizes.first() {
im_dim.extend_iter(std::iter::repeat_n(*im, *size))?;
}
}
}
let sorter = if let Some(mz_array) = destination.get(&ArrayType::MZArray) {
let mzs = mz_array.to_f64()?;
let ims = im_dim.to_f64()?;
let mut indices: Vec<usize> = Vec::with_capacity(mzs.len());
indices.extend(0..mzs.len());
indices.sort_by(|i, j| {
mzs[*i].total_cmp(&mzs[*j]).then_with(|| ims[*i].total_cmp(&ims[*j]))
});
Some(indices)
} else {
None
};
let final_size = im_dim.data_len()?;
destination.add(im_dim);
if let Some(sorter) = sorter {
destination.sort_from_indices(sorter)?;
} else if final_size > 0 {
log::debug!("Unsorted unstack");
}
Ok(destination)
}
pub fn stack(source: &BinaryArrayMap) -> Result<Self, ArrayRetrievalError> {
let mut this = Self::default();
if !source.has_ion_mobility() {
return Err(ArrayRetrievalError::NotFound(ArrayType::IonMobilityArray));
}
let (im_dim, im_type) = source.ion_mobility()?;
this.ion_mobility_unit = source.get(&im_type).unwrap().unit;
this.ion_mobility_type = im_type;
if im_dim.is_empty() {
return Ok(this);
}
let mut im_dim: Vec<(usize, f64)> = im_dim.iter().copied().enumerate().collect();
im_dim.sort_by(|(_, va), (_, vb)| va.total_cmp(vb));
let mut im_axis = Vec::with_capacity(200);
let mut last_v = im_dim.first().unwrap().1 - 1.0;
let mut index_map = HashMap::new();
for (_, v) in im_dim.iter() {
if v.total_cmp(&last_v).is_gt() {
last_v = *v;
index_map.insert(NonNaNF64::from(*v), im_axis.len());
im_axis.push(*v);
}
}
let mut array_bins: Vec<BinaryArrayMap> = Vec::with_capacity(im_axis.len());
array_bins.resize(im_axis.len(), BinaryArrayMap::default());
for (array_type, array) in source.iter() {
if array_type.is_ion_mobility() {
continue;
}
if array.data_len()? != im_dim.len() {
this.additional_arrays.add(array.clone());
continue;
}
match array.dtype() {
BinaryDataArrayType::Unknown => {
panic!("Cannot re-sort opaque or unknown dimension data types")
}
BinaryDataArrayType::Float64 => {
let view = array.to_f64()?;
_populate_stacked_array_from!(
im_dim, view, index_map, array_bins, array_type, array
);
}
BinaryDataArrayType::Float32 => {
let view = array.to_f32()?;
_populate_stacked_array_from!(
im_dim, view, index_map, array_bins, array_type, array
);
}
BinaryDataArrayType::Int64 => {
let view = array.to_i64()?;
_populate_stacked_array_from!(
im_dim, view, index_map, array_bins, array_type, array
);
}
BinaryDataArrayType::Int32 => {
let view = array.to_i32()?;
_populate_stacked_array_from!(
im_dim, view, index_map, array_bins, array_type, array
);
}
BinaryDataArrayType::ASCII => {
let view = array.decode()?;
_populate_stacked_array_from!(
im_dim, view, index_map, array_bins, array_type, array
);
}
}
}
this.ion_mobility_dimension = im_axis;
this.ion_mobility_index = index_map;
this.arrays = array_bins;
Ok(this)
}
}
impl TryFrom<BinaryArrayMap> for BinaryArrayMap3D {
type Error = ArrayRetrievalError;
fn try_from(value: BinaryArrayMap) -> Result<Self, Self::Error> {
Self::stack(&value)
}
}
impl TryFrom<&BinaryArrayMap> for BinaryArrayMap3D {
type Error = ArrayRetrievalError;
fn try_from(value: &BinaryArrayMap) -> Result<Self, Self::Error> {
Self::stack(value)
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use crate::spectrum::BinaryDataArrayType;
use super::*;
use std::fs;
use std::io;
fn make_array_from_file() -> io::Result<DataArray> {
let mut fh = fs::File::open("./test/data/mz_f64_zlib_bas64.txt")?;
let mut buf = String::new();
fh.read_to_string(&mut buf)?;
let bytes: Vec<u8> = buf.into();
let mut da = DataArray::wrap(&ArrayType::MZArray, BinaryDataArrayType::Float64, bytes);
da.compression = BinaryCompressionType::Zlib;
Ok(da)
}
#[test]
fn test_construction() -> io::Result<()> {
let da = make_array_from_file()?;
let mut map = BinaryArrayMap::new();
assert!(!map.has_array(&ArrayType::MZArray));
map.add(da);
assert!(map.has_array(&ArrayType::MZArray));
Ok(())
}
#[test]
fn test_decode() -> io::Result<()> {
let da = make_array_from_file()?;
let mut map = BinaryArrayMap::new();
map.add(da);
assert_eq!(
map.get(&ArrayType::MZArray).unwrap().compression,
BinaryCompressionType::Zlib
);
map.decode_all_arrays()?;
assert_eq!(
map.get(&ArrayType::MZArray).unwrap().compression,
BinaryCompressionType::Decoded
);
Ok(())
}
#[test]
fn test_3d_stack_unstack() -> io::Result<()> {
let mut reader = crate::MZReader::open_gzipped_read(io::BufReader::new(fs::File::open(
"test/data/20200204_BU_8B8egg_1ug_uL_7charges_60_min_Slot2-11_1_244.mzML.gz",
)?))?;
let spec = reader.get_spectrum_by_id("merged=42869 frame=9717 scanStart=1 scanEnd=705").unwrap();
let mut arrays = spec.arrays.unwrap();
let units_map: HashMap<_, _> = arrays.iter().map(|(k, v)| {
(k.clone(), v.unit)
}).collect();
let mzs = arrays.mzs()?;
assert!(!mzs.is_sorted());
drop(mzs);
arrays.sort_by_array(&ArrayType::MZArray)?;
let mzs = arrays.mzs()?;
assert!(mzs.is_sorted());
let n = mzs.len();
let arrays_3d = BinaryArrayMap3D::stack(&arrays)?;
let stacked_n: usize = arrays_3d
.iter()
.map(|(_, va)| va.mzs().unwrap().len())
.sum();
assert_eq!(units_map[&arrays_3d.ion_mobility_type], arrays_3d.ion_mobility_unit);
assert_eq!(n, stacked_n);
let unstacked = arrays_3d.unstack()?;
let unstacked_n = unstacked.mzs()?.len();
assert_eq!(unstacked_n, n);
for (k, v) in unstacked.iter() {
assert_eq!(units_map[k], v.unit);
}
Ok(())
}
}