Skip to main content

tensogram_sz3/
lib.rs

1// (C) Copyright 2026- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9//! High-level SZ3 compression API for Tensogram.
10//!
11//! This crate provides the same public API surface as the published `sz3`
12//! crate (v0.4.3), but is a clean-room implementation backed by
13//! `tensogram-sz3-sys` instead of `sz3-sys`.
14
15// Ensure the zstd native library is linked into the final binary.
16// tensogram-sz3-sys compiles C++ code that calls ZSTD_compress etc.;
17// the symbols come from zstd-sys's static archive.
18extern crate zstd_sys;
19
20use tensogram_sz3_sys::SZ3_Config;
21
22// ---------------------------------------------------------------------------
23// CompressionAlgorithm
24// ---------------------------------------------------------------------------
25
26/// Which prediction algorithm SZ3 should use during compression.
27#[derive(Clone, Debug, Copy)]
28#[non_exhaustive]
29pub enum CompressionAlgorithm {
30    Interpolation,
31    InterpolationLorenzo,
32    LorenzoRegression {
33        lorenzo: bool,
34        lorenzo_second_order: bool,
35        regression: bool,
36    },
37    BiologyMolecularData,
38    BiologyMolecularDataGromacsXtc,
39    NoPrediction,
40    Lossless,
41}
42
43impl CompressionAlgorithm {
44    fn decode(config: SZ3_Config) -> Result<Self> {
45        match config.cmprAlgo as u32 {
46            tensogram_sz3_sys::SZ3::ALGO_ALGO_INTERP => Ok(Self::Interpolation),
47            tensogram_sz3_sys::SZ3::ALGO_ALGO_INTERP_LORENZO => Ok(Self::InterpolationLorenzo),
48            tensogram_sz3_sys::SZ3::ALGO_ALGO_LORENZO_REG => Ok(Self::LorenzoRegression {
49                lorenzo: config.lorenzo,
50                lorenzo_second_order: config.lorenzo2,
51                regression: config.regression,
52            }),
53            tensogram_sz3_sys::SZ3::ALGO_ALGO_BIOMD => Ok(Self::BiologyMolecularData),
54            tensogram_sz3_sys::SZ3::ALGO_ALGO_BIOMDXTC => Ok(Self::BiologyMolecularDataGromacsXtc),
55            tensogram_sz3_sys::SZ3::ALGO_ALGO_NOPRED => Ok(Self::NoPrediction),
56            tensogram_sz3_sys::SZ3::ALGO_ALGO_LOSSLESS => Ok(Self::Lossless),
57            algo => Err(SZ3Error::UnsupportedAlgorithm(algo)),
58        }
59    }
60
61    fn code(&self) -> u8 {
62        (match self {
63            Self::Interpolation => tensogram_sz3_sys::SZ3::ALGO_ALGO_INTERP,
64            Self::InterpolationLorenzo => tensogram_sz3_sys::SZ3::ALGO_ALGO_INTERP_LORENZO,
65            Self::LorenzoRegression { .. } => tensogram_sz3_sys::SZ3::ALGO_ALGO_LORENZO_REG,
66            Self::BiologyMolecularData => tensogram_sz3_sys::SZ3::ALGO_ALGO_BIOMD,
67            Self::BiologyMolecularDataGromacsXtc => tensogram_sz3_sys::SZ3::ALGO_ALGO_BIOMDXTC,
68            Self::NoPrediction => tensogram_sz3_sys::SZ3::ALGO_ALGO_NOPRED,
69            Self::Lossless => tensogram_sz3_sys::SZ3::ALGO_ALGO_LOSSLESS,
70        }) as _
71    }
72
73    fn lorenzo(&self) -> bool {
74        match self {
75            Self::LorenzoRegression { lorenzo, .. } => *lorenzo,
76            _ => true,
77        }
78    }
79
80    fn lorenzo_second_order(&self) -> bool {
81        match self {
82            Self::LorenzoRegression {
83                lorenzo_second_order,
84                ..
85            } => *lorenzo_second_order,
86            _ => true,
87        }
88    }
89
90    fn regression(&self) -> bool {
91        match self {
92            Self::LorenzoRegression { regression, .. } => *regression,
93            _ => true,
94        }
95    }
96
97    /// Pure interpolation predictor.
98    pub fn interpolation() -> Self {
99        Self::Interpolation
100    }
101
102    /// Interpolation with Lorenzo fallback (the default).
103    pub fn interpolation_lorenzo() -> Self {
104        Self::InterpolationLorenzo
105    }
106
107    /// Lorenzo + regression predictor with default sub-flags.
108    pub fn lorenzo_regression() -> Self {
109        Self::LorenzoRegression {
110            lorenzo: true,
111            lorenzo_second_order: false,
112            regression: true,
113        }
114    }
115
116    /// Lorenzo + regression predictor with individually overridable sub-flags.
117    pub fn lorenzo_regression_custom(
118        lorenzo: Option<bool>,
119        lorenzo_second_order: Option<bool>,
120        regression: Option<bool>,
121    ) -> Self {
122        Self::LorenzoRegression {
123            lorenzo: lorenzo.unwrap_or(true),
124            lorenzo_second_order: lorenzo_second_order.unwrap_or(false),
125            regression: regression.unwrap_or(true),
126        }
127    }
128
129    /// Predictor optimised for molecular-dynamics data.
130    pub fn biology_molecular_data() -> Self {
131        Self::BiologyMolecularData
132    }
133
134    /// Predictor optimised for GROMACS XTC molecular-dynamics data.
135    pub fn biology_molecular_data_gromacs_xtc() -> Self {
136        Self::BiologyMolecularDataGromacsXtc
137    }
138
139    /// Skip prediction entirely; only quantise and encode.
140    pub fn no_prediction() -> Self {
141        Self::NoPrediction
142    }
143
144    /// Fully lossless compression (no quantisation).
145    pub fn lossless() -> Self {
146        Self::Lossless
147    }
148}
149
150impl Default for CompressionAlgorithm {
151    fn default() -> Self {
152        Self::interpolation_lorenzo()
153    }
154}
155
156// ---------------------------------------------------------------------------
157// ErrorBound
158// ---------------------------------------------------------------------------
159
160/// Error-bound mode that controls how much distortion SZ3 is allowed to
161/// introduce during lossy compression.
162#[derive(Clone, Debug, Copy)]
163#[non_exhaustive]
164pub enum ErrorBound {
165    Absolute(f64),
166    Relative(f64),
167    PSNR(f64),
168    L2Norm(f64),
169    AbsoluteAndRelative {
170        absolute_bound: f64,
171        relative_bound: f64,
172    },
173    AbsoluteOrRelative {
174        absolute_bound: f64,
175        relative_bound: f64,
176    },
177}
178
179impl ErrorBound {
180    fn decode(config: SZ3_Config) -> Result<Self> {
181        match config.errorBoundMode as u32 {
182            tensogram_sz3_sys::SZ3::EB_EB_ABS => Ok(Self::Absolute(config.absErrorBound)),
183            tensogram_sz3_sys::SZ3::EB_EB_REL => Ok(Self::Relative(config.relErrorBound)),
184            tensogram_sz3_sys::SZ3::EB_EB_PSNR => Ok(Self::PSNR(config.psnrErrorBound)),
185            tensogram_sz3_sys::SZ3::EB_EB_L2NORM => Ok(Self::L2Norm(config.l2normErrorBound)),
186            tensogram_sz3_sys::SZ3::EB_EB_ABS_OR_REL => Ok(Self::AbsoluteOrRelative {
187                absolute_bound: config.absErrorBound,
188                relative_bound: config.relErrorBound,
189            }),
190            tensogram_sz3_sys::SZ3::EB_EB_ABS_AND_REL => Ok(Self::AbsoluteAndRelative {
191                absolute_bound: config.absErrorBound,
192                relative_bound: config.relErrorBound,
193            }),
194            mode => Err(SZ3Error::UnsupportedErrorBound(mode)),
195        }
196    }
197
198    fn code(&self) -> u8 {
199        (match self {
200            Self::Absolute(_) => tensogram_sz3_sys::SZ3::EB_EB_ABS,
201            Self::Relative(_) => tensogram_sz3_sys::SZ3::EB_EB_REL,
202            Self::PSNR(_) => tensogram_sz3_sys::SZ3::EB_EB_PSNR,
203            Self::L2Norm(_) => tensogram_sz3_sys::SZ3::EB_EB_L2NORM,
204            Self::AbsoluteAndRelative { .. } => tensogram_sz3_sys::SZ3::EB_EB_ABS_AND_REL,
205            Self::AbsoluteOrRelative { .. } => tensogram_sz3_sys::SZ3::EB_EB_ABS_OR_REL,
206        }) as _
207    }
208
209    fn abs_bound(&self) -> f64 {
210        match self {
211            Self::Absolute(bound) => *bound,
212            Self::AbsoluteOrRelative { absolute_bound, .. }
213            | Self::AbsoluteAndRelative { absolute_bound, .. } => *absolute_bound,
214            _ => 0.0,
215        }
216    }
217
218    fn rel_bound(&self) -> f64 {
219        match self {
220            Self::Relative(bound) => *bound,
221            Self::AbsoluteOrRelative { relative_bound, .. }
222            | Self::AbsoluteAndRelative { relative_bound, .. } => *relative_bound,
223            _ => 0.0,
224        }
225    }
226
227    fn l2norm_bound(&self) -> f64 {
228        match self {
229            Self::L2Norm(bound) => *bound,
230            _ => 0.0,
231        }
232    }
233
234    fn psnr_bound(&self) -> f64 {
235        match self {
236            Self::PSNR(bound) => *bound,
237            _ => 0.0,
238        }
239    }
240}
241
242// ---------------------------------------------------------------------------
243// Config
244// ---------------------------------------------------------------------------
245
246/// Full configuration for an SZ3 compress/decompress round-trip.
247///
248/// Use the builder-style setters to customise individual parameters; the
249/// only required parameter is [`ErrorBound`], passed via [`Config::new`].
250#[derive(Clone, Debug)]
251pub struct Config {
252    compression_algorithm: CompressionAlgorithm,
253    error_bound: ErrorBound,
254    openmp: bool,
255    quantization_bincount: u32,
256    block_size: Option<u32>,
257}
258
259impl Config {
260    /// Create a new configuration with the given error bound and default
261    /// settings (interpolation-lorenzo algorithm, 65 536 quantization bins).
262    pub fn new(error_bound: ErrorBound) -> Self {
263        Self {
264            compression_algorithm: CompressionAlgorithm::default(),
265            error_bound,
266            openmp: false,
267            quantization_bincount: 65536,
268            block_size: None,
269        }
270    }
271
272    fn from_decompressed(config: SZ3_Config) -> Result<Self> {
273        Ok(Self {
274            compression_algorithm: CompressionAlgorithm::decode(config)?,
275            error_bound: ErrorBound::decode(config)?,
276            openmp: config.openmp,
277            quantization_bincount: config.quantbinCnt as _,
278            block_size: Some(config.blockSize as _),
279        })
280    }
281
282    /// Set the prediction algorithm.
283    pub fn compression_algorithm(mut self, compression_algorithm: CompressionAlgorithm) -> Self {
284        self.compression_algorithm = compression_algorithm;
285        self
286    }
287
288    /// Override the error bound.
289    pub fn error_bound(mut self, error_bound: ErrorBound) -> Self {
290        self.error_bound = error_bound;
291        self
292    }
293
294    /// Enable or disable OpenMP parallelism (requires the `openmp` feature).
295    #[cfg(feature = "openmp")]
296    pub fn openmp(mut self, openmp: bool) -> Self {
297        self.openmp = openmp;
298        self
299    }
300
301    /// Set the number of quantization bins (default: 65 536).
302    pub fn quantization_bincount(mut self, quantization_bincount: u32) -> Self {
303        self.quantization_bincount = quantization_bincount;
304        self
305    }
306
307    /// Set an explicit block size, overriding the automatic default.
308    pub fn block_size(mut self, block_size: u32) -> Self {
309        self.block_size = Some(block_size);
310        self
311    }
312
313    /// Revert to the automatic block size (chosen based on dimensionality).
314    pub fn automatic_block_size(mut self) -> Self {
315        self.block_size = None;
316        self
317    }
318}
319
320// ---------------------------------------------------------------------------
321// SZ3Compressible trait + sealed implementation
322// ---------------------------------------------------------------------------
323
324/// Marker trait for types that SZ3 can compress and decompress.
325///
326/// Implemented for `f32`, `f64`, `u8`, `i8`, `u16`, `i16`, `u32`, `i32`,
327/// `u64`, and `i64`.  This trait is sealed and cannot be implemented outside
328/// this crate.
329pub trait SZ3Compressible: private::Sealed + Sized {}
330impl SZ3Compressible for f32 {}
331impl SZ3Compressible for f64 {}
332impl SZ3Compressible for u8 {}
333impl SZ3Compressible for i8 {}
334impl SZ3Compressible for u16 {}
335impl SZ3Compressible for i16 {}
336impl SZ3Compressible for u32 {}
337impl SZ3Compressible for i32 {}
338impl SZ3Compressible for u64 {}
339impl SZ3Compressible for i64 {}
340
341mod private {
342    pub trait Sealed: Copy {
343        const SZ_DATA_TYPE: u8;
344
345        unsafe fn compress_size_bound(config: tensogram_sz3_sys::SZ3_Config) -> usize;
346
347        unsafe fn compress(
348            config: tensogram_sz3_sys::SZ3_Config,
349            data: *const Self,
350            compressed_data: *mut u8,
351            compressed_capacity: usize,
352        ) -> usize;
353
354        unsafe fn decompress(
355            compressed_data: *const u8,
356            compressed_len: usize,
357            decompressed_data: *mut Self,
358        );
359    }
360
361    macro_rules! impl_sealed {
362        ($($impl_mod:ident),*) => {
363            $(impl Sealed for tensogram_sz3_sys::$impl_mod::ty {
364                const SZ_DATA_TYPE: u8 = tensogram_sz3_sys::$impl_mod::DATA_TYPE_TYPE;
365
366                unsafe fn compress_size_bound(config: tensogram_sz3_sys::SZ3_Config) -> usize {
367                    unsafe { tensogram_sz3_sys::$impl_mod::compress_size_bound(config) }
368                }
369
370                unsafe fn compress(
371                    config: tensogram_sz3_sys::SZ3_Config,
372                    data: *const Self,
373                    compressed_data: *mut u8,
374                    compressed_capacity: usize,
375                ) -> usize {
376                    unsafe {
377                        tensogram_sz3_sys::$impl_mod::compress(
378                            config,
379                            data,
380                            compressed_data.cast(),
381                            compressed_capacity,
382                        )
383                    }
384                }
385
386                unsafe fn decompress(
387                    compressed_data: *const u8,
388                    compressed_len: usize,
389                    decompressed_data: *mut Self,
390                ) {
391                    unsafe {
392                        tensogram_sz3_sys::$impl_mod::decompress(
393                            compressed_data.cast(),
394                            compressed_len,
395                            decompressed_data,
396                        )
397                    }
398                }
399            })*
400        }
401    }
402
403    impl_sealed!(
404        impl_f32, impl_f64, impl_u8, impl_i8, impl_u16, impl_i16, impl_u32, impl_i32, impl_u64,
405        impl_i64
406    );
407}
408
409// ---------------------------------------------------------------------------
410// DimensionedData + builders
411// ---------------------------------------------------------------------------
412
413/// A data buffer together with its N-dimensional shape, ready for SZ3
414/// compression or decompression.
415///
416/// Construct via [`DimensionedData::build`] (immutable) or
417/// [`DimensionedData::build_mut`] (mutable).
418#[derive(Clone, Debug)]
419pub struct DimensionedData<V: SZ3Compressible, T: std::ops::Deref<Target = [V]>> {
420    data: T,
421    dims: Vec<usize>,
422}
423
424/// Builder for an immutable [`DimensionedData`] reference.
425#[derive(Clone, Debug)]
426pub struct DimensionedDataBuilder<'a, V> {
427    data: &'a [V],
428    dims: Vec<usize>,
429    remainder: usize,
430}
431
432/// Builder for a mutable [`DimensionedData`] reference.
433#[derive(Debug)]
434pub struct DimensionedDataBuilderMut<'a, V> {
435    data: &'a mut [V],
436    dims: Vec<usize>,
437    remainder: usize,
438}
439
440impl<V: SZ3Compressible, T: std::ops::Deref<Target = [V]>> DimensionedData<V, T> {
441    /// Start building an immutable dimensioned view over `data`.
442    pub fn build<'a>(data: &'a T) -> DimensionedDataBuilder<'a, V> {
443        DimensionedDataBuilder {
444            data,
445            dims: vec![],
446            remainder: data.len(),
447        }
448    }
449
450    /// Returns the underlying data as a flat slice.
451    pub fn data(&self) -> &[V] {
452        &self.data
453    }
454
455    /// Consume the wrapper and return the owned data container.
456    pub fn into_data(self) -> T {
457        self.data
458    }
459
460    /// Returns the dimension sizes.
461    pub fn dims(&self) -> &[usize] {
462        &self.dims
463    }
464
465    fn len(&self) -> usize {
466        self.data.len()
467    }
468
469    fn as_ptr(&self) -> *const V {
470        self.data.as_ptr()
471    }
472}
473
474impl<V: SZ3Compressible, T: std::ops::DerefMut<Target = [V]>> DimensionedData<V, T> {
475    /// Start building a mutable dimensioned view over `data`.
476    pub fn build_mut<'a>(data: &'a mut T) -> DimensionedDataBuilderMut<'a, V> {
477        DimensionedDataBuilderMut {
478            remainder: data.len(),
479            data,
480            dims: vec![],
481        }
482    }
483
484    /// Returns the underlying data as a mutable flat slice.
485    pub fn data_mut(&mut self) -> &mut [V] {
486        &mut self.data
487    }
488}
489
490// ---------------------------------------------------------------------------
491// SZ3Error
492// ---------------------------------------------------------------------------
493
494/// Errors returned by the SZ3 compression and dimension-building APIs.
495///
496/// `#[non_exhaustive]` (matching this crate's other public enums,
497/// `CompressionAlgorithm` and `ErrorBound`) so new error categories — such
498/// as the `MalformedCompressedStream` variant added for hostile-input
499/// hardening — can be introduced without breaking downstream exhaustive
500/// `match`es.  Callers should include a `_ =>` arm.
501#[derive(thiserror::Error, Debug)]
502#[non_exhaustive]
503pub enum SZ3Error {
504    #[error(
505        "invalid dimension specification for data of length {len}: already specified dimensions \
506         {dims:?}, and wanted to add dimension with length {wanted}, but this does not divide \
507         {remainder} cleanly"
508    )]
509    InvalidDimensionSize {
510        dims: Vec<usize>,
511        len: usize,
512        wanted: usize,
513        remainder: usize,
514    },
515    #[error("dimension with size one has no use")]
516    OneSizedDimension,
517    #[error(
518        "dimension specification {dims:?} for data of length {len} does not cover whole space, \
519         missing a dimension of {remainder}"
520    )]
521    UnderSpecifiedDimensions {
522        dims: Vec<usize>,
523        len: usize,
524        remainder: usize,
525    },
526    #[error("cannot decompress to array with a different data type")]
527    DecompressedDataTypeMismatch,
528    #[error("unsupported SZ3 compression algorithm code: {0}")]
529    UnsupportedAlgorithm(u32),
530    #[error("unsupported SZ3 error bound mode: {0}")]
531    UnsupportedErrorBound(u32),
532    #[error(
533        "cannot decompress array with dimensions {found:?} to array with different dimensions {expected:?}"
534    )]
535    DecompressedDimsMismatch {
536        found: Vec<usize>,
537        expected: Vec<usize>,
538    },
539    /// The compressed SZ3 stream is too short or its embedded sizes are
540    /// inconsistent with the buffer length (truncated / malformed /
541    /// hostile input).  The C++ header parser signals this by returning
542    /// a config with `N == 0`; see SEC-010 in `plans/SECURITY_ANALYSIS.md`.
543    #[error("malformed or truncated SZ3 compressed stream")]
544    MalformedCompressedStream,
545}
546
547type Result<T> = std::result::Result<T, SZ3Error>;
548
549// ---------------------------------------------------------------------------
550// Builder implementations (shared logic for immutable and mutable builders)
551// ---------------------------------------------------------------------------
552
553macro_rules! impl_dimensioned_data_builder {
554    ($($builder:ident => $data:ty),*) => {
555        $(impl<'a, V: SZ3Compressible> $builder<'a, V> {
556            /// Append a dimension with the given `length`.
557            ///
558            /// Returns an error if `length` does not evenly divide the
559            /// remaining element count.
560            pub fn dim(mut self, length: usize) -> Result<Self> {
561                if length == 1 {
562                    if self.dims.is_empty() && self.remainder == 1 {
563                        self.dims.push(1);
564                        Ok(self)
565                    } else {
566                        Err(SZ3Error::OneSizedDimension)
567                    }
568                } else if self.remainder % length != 0 {
569                    Err(SZ3Error::InvalidDimensionSize {
570                        dims: self.dims,
571                        len: self.data.len(),
572                        wanted: length,
573                        remainder: self.remainder,
574                    })
575                } else {
576                    self.dims.push(length);
577                    self.remainder /= length;
578                    Ok(self)
579                }
580            }
581
582            /// Append a final dimension that consumes all remaining elements.
583            pub fn remainder_dim(self) -> Result<$data> {
584                let remainder = self.remainder;
585                self.dim(remainder)?.finish()
586            }
587
588            /// Finalise the builder, returning the dimensioned data.
589            ///
590            /// Returns an error if the specified dimensions do not exactly
591            /// cover the data length.
592            pub fn finish(self) -> Result<$data> {
593                if self.remainder != 1 {
594                    Err(SZ3Error::UnderSpecifiedDimensions {
595                        dims: self.dims,
596                        len: self.data.len(),
597                        remainder: self.remainder,
598                    })
599                } else {
600                    Ok(DimensionedData {
601                        data: self.data,
602                        dims: self.dims,
603                    })
604                }
605            }
606        })*
607    };
608}
609
610impl_dimensioned_data_builder! {
611    DimensionedDataBuilder => DimensionedData<V, &'a [V]>,
612    DimensionedDataBuilderMut => DimensionedData<V, &'a mut [V]>
613}
614
615// ---------------------------------------------------------------------------
616// Internal: read config from compressed blob
617// ---------------------------------------------------------------------------
618
619struct ParsedConfig {
620    config: Config,
621    len: usize,
622    dims: Vec<usize>,
623    data_type: u8,
624}
625
626impl ParsedConfig {
627    fn from_compressed(compressed_data: &[u8]) -> Result<Self> {
628        let raw = unsafe {
629            tensogram_sz3_sys::sz3_decompress_config(
630                compressed_data.as_ptr().cast(),
631                compressed_data.len(),
632            )
633        };
634        // SECURITY (SEC-010): the C++ shim returns `N == 0` (with a null
635        // `dims`) when the input is too short or its embedded sizes are
636        // inconsistent with the buffer length — a malformed/truncated
637        // SZ3 stream from a hostile `.tgm`.  Reject it here rather than
638        // proceeding to read a null/garbage dims pointer.
639        if raw.N == 0 || raw.dims.is_null() {
640            // Free nothing: an invalid config carries a null dims.
641            return Err(SZ3Error::MalformedCompressedStream);
642        }
643        let dims: Vec<usize> = (0..raw.N)
644            .map(|i| unsafe { std::ptr::read(raw.dims.add(i as usize)) })
645            .collect();
646        unsafe {
647            tensogram_sz3_sys::sz3_dealloc_size_t(raw.dims);
648        }
649        let SZ3_Config {
650            num: len,
651            dataType: data_type,
652            ..
653        } = raw;
654        let config = Config::from_decompressed(raw)?;
655        Ok(Self {
656            config,
657            len,
658            dims,
659            data_type,
660        })
661    }
662}
663
664// ---------------------------------------------------------------------------
665// Public API: compress / decompress
666// ---------------------------------------------------------------------------
667
668/// Compress `data` with the given error bound, returning a new `Vec<u8>`.
669pub fn compress<V: SZ3Compressible, T: std::ops::Deref<Target = [V]>>(
670    data: &DimensionedData<V, T>,
671    error_bound: ErrorBound,
672) -> Result<Vec<u8>> {
673    let config = Config::new(error_bound);
674    compress_with_config(data, &config)
675}
676
677/// Compress `data` with a full [`Config`], returning a new `Vec<u8>`.
678pub fn compress_with_config<V: SZ3Compressible, T: std::ops::Deref<Target = [V]>>(
679    data: &DimensionedData<V, T>,
680    config: &Config,
681) -> Result<Vec<u8>> {
682    let mut compressed_data = Vec::new();
683    compress_into_with_config(data, config, &mut compressed_data)?;
684    Ok(compressed_data)
685}
686
687/// Compress `data` and **append** the result to `compressed_data`.
688pub fn compress_into<V: SZ3Compressible, T: std::ops::Deref<Target = [V]>>(
689    data: &DimensionedData<V, T>,
690    error_bound: ErrorBound,
691    compressed_data: &mut Vec<u8>,
692) -> Result<()> {
693    let config = Config::new(error_bound);
694    compress_into_with_config(data, &config, compressed_data)
695}
696
697/// Compress `data` with a full [`Config`] and **append** the result to
698/// `compressed_data`.
699pub fn compress_into_with_config<V: SZ3Compressible, T: std::ops::Deref<Target = [V]>>(
700    data: &DimensionedData<V, T>,
701    config: &Config,
702    compressed_data: &mut Vec<u8>,
703) -> Result<()> {
704    let block_size = config.block_size.unwrap_or(match data.dims().len() {
705        1 => 128,
706        2 => 16,
707        _ => 6,
708    });
709
710    let raw_config = SZ3_Config {
711        N: data.dims().len() as _,
712        dims: data.dims.as_ptr() as _,
713        num: data.len() as _,
714        errorBoundMode: config.error_bound.code(),
715        absErrorBound: config.error_bound.abs_bound(),
716        relErrorBound: config.error_bound.rel_bound(),
717        l2normErrorBound: config.error_bound.l2norm_bound(),
718        psnrErrorBound: config.error_bound.psnr_bound(),
719        cmprAlgo: config.compression_algorithm.code(),
720        lorenzo: config.compression_algorithm.lorenzo(),
721        lorenzo2: config.compression_algorithm.lorenzo_second_order(),
722        regression: config.compression_algorithm.regression(),
723        openmp: config.openmp,
724        dataType: V::SZ_DATA_TYPE as _,
725        blockSize: block_size as _,
726        quantbinCnt: config.quantization_bincount as _,
727    };
728
729    let capacity: usize = unsafe { V::compress_size_bound(raw_config) };
730    compressed_data.reserve(capacity);
731
732    let len = unsafe {
733        V::compress(
734            raw_config,
735            data.as_ptr(),
736            compressed_data
737                .spare_capacity_mut()
738                .as_mut_ptr()
739                .cast::<u8>(),
740            capacity,
741        )
742    };
743    unsafe { compressed_data.set_len(compressed_data.len() + len) };
744
745    Ok(())
746}
747
748/// Decompress an SZ3 blob into a new `Vec<V>`, returning the recovered
749/// [`Config`] and [`DimensionedData`].
750pub fn decompress<V: SZ3Compressible, T: std::ops::Deref<Target = [u8]>>(
751    compressed_data: T,
752) -> Result<(Config, DimensionedData<V, Vec<V>>)> {
753    let ParsedConfig {
754        config,
755        len,
756        dims,
757        data_type,
758    } = ParsedConfig::from_compressed(&compressed_data)?;
759
760    if data_type != V::SZ_DATA_TYPE {
761        return Err(SZ3Error::DecompressedDataTypeMismatch);
762    }
763
764    // SECURITY (SEC-010): a `len` of 0 only arises from a malformed config
765    // trailer — the dimension builder cannot construct a valid 0-element
766    // payload.  Reject it before calling the native decompressor: with
767    // `len == 0` the `Vec` has no spare capacity, so
768    // `spare_capacity_mut().as_mut_ptr()` is a dangling pointer, and we
769    // cannot rely on the C++ decoder leaving it untouched for a hostile
770    // zero-length stream.
771    if len == 0 {
772        return Err(SZ3Error::MalformedCompressedStream);
773    }
774
775    // SECURITY (SEC-011): `len` is the element count from the parsed
776    // SZ3 config trailer, which is attacker-controlled.  An infallible
777    // `Vec::with_capacity(len)` lets a hostile config claiming a huge
778    // `num` abort the process via OOM.  Reserve fallibly so a
779    // pathological count surfaces as a structured error instead.
780    let mut decompressed_data: Vec<V> = Vec::new();
781    decompressed_data
782        .try_reserve_exact(len)
783        .map_err(|_| SZ3Error::MalformedCompressedStream)?;
784
785    let decompressed_data = unsafe {
786        V::decompress(
787            compressed_data.as_ptr(),
788            compressed_data.len(),
789            decompressed_data
790                .spare_capacity_mut()
791                .as_mut_ptr()
792                .cast::<V>(),
793        );
794
795        decompressed_data.set_len(len);
796        decompressed_data
797    };
798
799    Ok((
800        config,
801        DimensionedData {
802            data: decompressed_data,
803            dims,
804        },
805    ))
806}
807
808/// Decompress an SZ3 blob into a pre-allocated [`DimensionedData`] buffer.
809///
810/// Returns an error if the data type or dimensions of the compressed blob do
811/// not match the destination buffer.
812pub fn decompress_into_dimensioned<
813    V: SZ3Compressible,
814    C: std::ops::Deref<Target = [u8]>,
815    D: std::ops::DerefMut<Target = [V]>,
816>(
817    compressed_data: C,
818    decompressed_data: &mut DimensionedData<V, D>,
819) -> Result<Config> {
820    let ParsedConfig {
821        config,
822        len,
823        dims,
824        data_type,
825    } = ParsedConfig::from_compressed(&compressed_data)?;
826
827    if data_type != V::SZ_DATA_TYPE {
828        return Err(SZ3Error::DecompressedDataTypeMismatch);
829    }
830
831    if decompressed_data.dims() != dims.as_slice() {
832        return Err(SZ3Error::DecompressedDimsMismatch {
833            found: dims,
834            expected: decompressed_data.dims.clone(),
835        });
836    }
837
838    assert_eq!(decompressed_data.len(), len);
839
840    unsafe {
841        V::decompress(
842            compressed_data.as_ptr(),
843            compressed_data.len(),
844            decompressed_data.data.as_mut_ptr(),
845        );
846    }
847
848    Ok(config)
849}
850
851// ---------------------------------------------------------------------------
852// Tests
853// ---------------------------------------------------------------------------
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    #[test]
860    fn round_trip_f64() {
861        let data: Vec<f64> = (0..256)
862            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin())
863            .collect();
864        let dimensioned = DimensionedData::<f64, _>::build(&data)
865            .dim(256)
866            .unwrap()
867            .finish()
868            .unwrap();
869        let compressed = compress(&dimensioned, ErrorBound::Absolute(1e-6)).unwrap();
870        let (_config, decompressed) = decompress::<f64, _>(&*compressed).unwrap();
871        assert_eq!(decompressed.data().len(), data.len());
872        for (orig, dec) in data.iter().zip(decompressed.data()) {
873            assert!(
874                (orig - dec).abs() <= 1e-6,
875                "orig={orig}, dec={dec}, diff={}",
876                (orig - dec).abs()
877            );
878        }
879    }
880
881    #[test]
882    fn round_trip_f32() {
883        let data: Vec<f32> = (0..256)
884            .map(|i| (i as f32 / 256.0 * std::f32::consts::PI).sin())
885            .collect();
886        let dimensioned = DimensionedData::<f32, _>::build(&data)
887            .dim(256)
888            .unwrap()
889            .finish()
890            .unwrap();
891        let compressed = compress(&dimensioned, ErrorBound::Absolute(1e-4)).unwrap();
892        let (_config, decompressed) = decompress::<f32, _>(&*compressed).unwrap();
893        assert_eq!(decompressed.data().len(), data.len());
894        for (orig, dec) in data.iter().zip(decompressed.data()) {
895            assert!(
896                (orig - dec).abs() <= 1e-4,
897                "orig={orig}, dec={dec}, diff={}",
898                (orig - dec).abs()
899            );
900        }
901    }
902
903    #[test]
904    fn dimension_errors() {
905        let data: Vec<f64> = vec![1.0; 100];
906        let err = DimensionedData::<f64, _>::build(&data).dim(1);
907        assert!(matches!(err.unwrap_err(), SZ3Error::OneSizedDimension));
908        let err = DimensionedData::<f64, _>::build(&data).dim(7);
909        assert!(matches!(
910            err.unwrap_err(),
911            SZ3Error::InvalidDimensionSize { .. }
912        ));
913        let err = DimensionedData::<f64, _>::build(&data)
914            .dim(10)
915            .unwrap()
916            .finish();
917        assert!(matches!(
918            err.unwrap_err(),
919            SZ3Error::UnderSpecifiedDimensions { .. }
920        ));
921    }
922
923    #[test]
924    fn round_trip_u8() {
925        let data: Vec<u8> = (0..256).map(|i| i as u8).collect();
926        let d = DimensionedData::<u8, _>::build(&data)
927            .dim(256)
928            .unwrap()
929            .finish()
930            .unwrap();
931        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
932        let (_, dec) = decompress::<u8, _>(&*compressed).unwrap();
933        assert_eq!(dec.data(), data.as_slice());
934    }
935
936    #[test]
937    fn round_trip_i8() {
938        let data: Vec<i8> = (-128..127).map(|i| i as i8).collect();
939        let d = DimensionedData::<i8, _>::build(&data)
940            .dim(data.len())
941            .unwrap()
942            .finish()
943            .unwrap();
944        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
945        let (_, dec) = decompress::<i8, _>(&*compressed).unwrap();
946        assert_eq!(dec.data(), data.as_slice());
947    }
948
949    #[test]
950    fn round_trip_u16() {
951        let data: Vec<u16> = (0..256).map(|i| i as u16 * 100).collect();
952        let d = DimensionedData::<u16, _>::build(&data)
953            .dim(256)
954            .unwrap()
955            .finish()
956            .unwrap();
957        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
958        let (_, dec) = decompress::<u16, _>(&*compressed).unwrap();
959        assert_eq!(dec.data(), data.as_slice());
960    }
961
962    #[test]
963    fn round_trip_i16() {
964        let data: Vec<i16> = (-128..128).map(|i| i as i16 * 50).collect();
965        let d = DimensionedData::<i16, _>::build(&data)
966            .dim(256)
967            .unwrap()
968            .finish()
969            .unwrap();
970        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
971        let (_, dec) = decompress::<i16, _>(&*compressed).unwrap();
972        assert_eq!(dec.data(), data.as_slice());
973    }
974
975    #[test]
976    fn round_trip_u32() {
977        let data: Vec<u32> = (0..256).map(|i| i as u32 * 1000).collect();
978        let d = DimensionedData::<u32, _>::build(&data)
979            .dim(256)
980            .unwrap()
981            .finish()
982            .unwrap();
983        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
984        let (_, dec) = decompress::<u32, _>(&*compressed).unwrap();
985        assert_eq!(dec.data(), data.as_slice());
986    }
987
988    #[test]
989    fn round_trip_i32() {
990        let data: Vec<i32> = (-128..128).map(|i: i32| i * 1000).collect();
991        let d = DimensionedData::<i32, _>::build(&data)
992            .dim(256)
993            .unwrap()
994            .finish()
995            .unwrap();
996        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
997        let (_, dec) = decompress::<i32, _>(&*compressed).unwrap();
998        assert_eq!(dec.data(), data.as_slice());
999    }
1000
1001    #[test]
1002    fn round_trip_u64() {
1003        let data: Vec<u64> = (0..256).map(|i| i as u64 * 10000).collect();
1004        let d = DimensionedData::<u64, _>::build(&data)
1005            .dim(256)
1006            .unwrap()
1007            .finish()
1008            .unwrap();
1009        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
1010        let (_, dec) = decompress::<u64, _>(&*compressed).unwrap();
1011        assert_eq!(dec.data(), data.as_slice());
1012    }
1013
1014    #[test]
1015    fn round_trip_i64() {
1016        let data: Vec<i64> = (-128..128).map(|i| i as i64 * 10000).collect();
1017        let d = DimensionedData::<i64, _>::build(&data)
1018            .dim(256)
1019            .unwrap()
1020            .finish()
1021            .unwrap();
1022        let compressed = compress(&d, ErrorBound::Absolute(0.0)).unwrap();
1023        let (_, dec) = decompress::<i64, _>(&*compressed).unwrap();
1024        assert_eq!(dec.data(), data.as_slice());
1025    }
1026
1027    #[test]
1028    fn error_bound_relative() {
1029        let data: Vec<f64> = (0..256)
1030            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin() + 2.0)
1031            .collect();
1032        let d = DimensionedData::<f64, _>::build(&data)
1033            .dim(256)
1034            .unwrap()
1035            .finish()
1036            .unwrap();
1037        let cfg = Config::new(ErrorBound::Relative(1e-4));
1038        let c = compress_with_config(&d, &cfg).unwrap();
1039        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1040        assert_eq!(dec.data().len(), data.len());
1041    }
1042
1043    #[test]
1044    fn error_bound_psnr() {
1045        let data: Vec<f64> = (0..256)
1046            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin())
1047            .collect();
1048        let d = DimensionedData::<f64, _>::build(&data)
1049            .dim(256)
1050            .unwrap()
1051            .finish()
1052            .unwrap();
1053        let cfg = Config::new(ErrorBound::PSNR(80.0));
1054        let c = compress_with_config(&d, &cfg).unwrap();
1055        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1056        assert_eq!(dec.data().len(), data.len());
1057    }
1058
1059    #[test]
1060    fn error_bound_l2norm() {
1061        let data: Vec<f64> = (0..256)
1062            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin())
1063            .collect();
1064        let d = DimensionedData::<f64, _>::build(&data)
1065            .dim(256)
1066            .unwrap()
1067            .finish()
1068            .unwrap();
1069        let cfg = Config::new(ErrorBound::L2Norm(1e-3));
1070        let c = compress_with_config(&d, &cfg).unwrap();
1071        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1072        assert_eq!(dec.data().len(), data.len());
1073    }
1074
1075    #[test]
1076    fn error_bound_abs_and_rel() {
1077        let data: Vec<f64> = (0..256)
1078            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin() + 2.0)
1079            .collect();
1080        let d = DimensionedData::<f64, _>::build(&data)
1081            .dim(256)
1082            .unwrap()
1083            .finish()
1084            .unwrap();
1085        let cfg = Config::new(ErrorBound::AbsoluteAndRelative {
1086            absolute_bound: 1e-4,
1087            relative_bound: 1e-3,
1088        });
1089        let c = compress_with_config(&d, &cfg).unwrap();
1090        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1091        assert_eq!(dec.data().len(), data.len());
1092    }
1093
1094    #[test]
1095    fn error_bound_abs_or_rel() {
1096        let data: Vec<f64> = (0..256)
1097            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin() + 2.0)
1098            .collect();
1099        let d = DimensionedData::<f64, _>::build(&data)
1100            .dim(256)
1101            .unwrap()
1102            .finish()
1103            .unwrap();
1104        let cfg = Config::new(ErrorBound::AbsoluteOrRelative {
1105            absolute_bound: 1e-4,
1106            relative_bound: 1e-3,
1107        });
1108        let c = compress_with_config(&d, &cfg).unwrap();
1109        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1110        assert_eq!(dec.data().len(), data.len());
1111    }
1112
1113    #[test]
1114    fn algo_interpolation() {
1115        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1116        let d = DimensionedData::<f64, _>::build(&data)
1117            .dim(256)
1118            .unwrap()
1119            .finish()
1120            .unwrap();
1121        let cfg = Config::new(ErrorBound::Absolute(1e-6))
1122            .compression_algorithm(CompressionAlgorithm::interpolation());
1123        let c = compress_with_config(&d, &cfg).unwrap();
1124        let (dc, dec) = decompress::<f64, _>(&*c).unwrap();
1125        assert_eq!(dec.data().len(), data.len());
1126        assert!(matches!(
1127            dc.compression_algorithm,
1128            CompressionAlgorithm::Interpolation
1129        ));
1130    }
1131
1132    #[test]
1133    fn algo_lorenzo_regression() {
1134        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1135        let d = DimensionedData::<f64, _>::build(&data)
1136            .dim(256)
1137            .unwrap()
1138            .finish()
1139            .unwrap();
1140        let cfg = Config::new(ErrorBound::Absolute(1e-6))
1141            .compression_algorithm(CompressionAlgorithm::lorenzo_regression());
1142        let c = compress_with_config(&d, &cfg).unwrap();
1143        let (dc, dec) = decompress::<f64, _>(&*c).unwrap();
1144        assert_eq!(dec.data().len(), data.len());
1145        assert!(matches!(
1146            dc.compression_algorithm,
1147            CompressionAlgorithm::LorenzoRegression { .. }
1148        ));
1149    }
1150
1151    #[test]
1152    fn algo_lossless() {
1153        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1154        let d = DimensionedData::<f64, _>::build(&data)
1155            .dim(256)
1156            .unwrap()
1157            .finish()
1158            .unwrap();
1159        let cfg = Config::new(ErrorBound::Absolute(0.0))
1160            .compression_algorithm(CompressionAlgorithm::lossless());
1161        let c = compress_with_config(&d, &cfg).unwrap();
1162        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1163        assert_eq!(dec.data(), data.as_slice());
1164    }
1165
1166    #[test]
1167    fn config_quantization_bincount() {
1168        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1169        let d = DimensionedData::<f64, _>::build(&data)
1170            .dim(256)
1171            .unwrap()
1172            .finish()
1173            .unwrap();
1174        let cfg = Config::new(ErrorBound::Absolute(1e-6)).quantization_bincount(1024);
1175        let c = compress_with_config(&d, &cfg).unwrap();
1176        let (dc, _) = decompress::<f64, _>(&*c).unwrap();
1177        assert_eq!(dc.quantization_bincount, 1024);
1178    }
1179
1180    #[test]
1181    fn config_block_size() {
1182        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1183        let d = DimensionedData::<f64, _>::build(&data)
1184            .dim(256)
1185            .unwrap()
1186            .finish()
1187            .unwrap();
1188        let cfg = Config::new(ErrorBound::Absolute(1e-6)).block_size(64);
1189        let c = compress_with_config(&d, &cfg).unwrap();
1190        let (dc, _) = decompress::<f64, _>(&*c).unwrap();
1191        assert_eq!(dc.block_size, Some(64));
1192    }
1193
1194    #[test]
1195    fn config_automatic_block_size() {
1196        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1197        let d = DimensionedData::<f64, _>::build(&data)
1198            .dim(256)
1199            .unwrap()
1200            .finish()
1201            .unwrap();
1202        let cfg = Config::new(ErrorBound::Absolute(1e-6))
1203            .block_size(64)
1204            .automatic_block_size();
1205        assert!(cfg.block_size.is_none());
1206        let c = compress_with_config(&d, &cfg).unwrap();
1207        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1208        assert_eq!(dec.data().len(), data.len());
1209    }
1210
1211    #[test]
1212    fn config_error_bound_setter() {
1213        let cfg = Config::new(ErrorBound::Absolute(1.0)).error_bound(ErrorBound::Relative(0.5));
1214        assert!(matches!(cfg.error_bound, ErrorBound::Relative(_)));
1215    }
1216
1217    #[test]
1218    fn dimensioned_data_accessors() {
1219        let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1220        let dd = DimensionedData::<f64, _>::build(&data)
1221            .dim(2)
1222            .unwrap()
1223            .dim(3)
1224            .unwrap()
1225            .finish()
1226            .unwrap();
1227        assert_eq!(dd.dims(), &[2, 3]);
1228        assert_eq!(dd.data(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1229        let owned = dd.into_data();
1230        assert_eq!(owned, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1231    }
1232
1233    #[test]
1234    fn dimensioned_data_build_mut() {
1235        let mut data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
1236        let mut dd = DimensionedData::<f64, _>::build_mut(&mut data)
1237            .dim(4)
1238            .unwrap()
1239            .finish()
1240            .unwrap();
1241        assert_eq!(dd.data(), &[1.0, 2.0, 3.0, 4.0]);
1242        dd.data_mut()[0] = 99.0;
1243        assert_eq!(dd.data()[0], 99.0);
1244    }
1245
1246    #[test]
1247    fn dimensioned_data_remainder_dim() {
1248        let data: Vec<f64> = vec![1.0; 120];
1249        let dd = DimensionedData::<f64, _>::build(&data)
1250            .dim(10)
1251            .unwrap()
1252            .remainder_dim()
1253            .unwrap();
1254        assert_eq!(dd.dims(), &[10, 12]);
1255        assert_eq!(dd.data().len(), 120);
1256    }
1257
1258    #[test]
1259    fn dimensioned_data_singleton() {
1260        let data: Vec<f64> = vec![42.0];
1261        let dd = DimensionedData::<f64, _>::build(&data)
1262            .dim(1)
1263            .unwrap()
1264            .finish()
1265            .unwrap();
1266        assert_eq!(dd.dims(), &[1]);
1267        assert_eq!(dd.data(), &[42.0]);
1268    }
1269
1270    #[test]
1271    fn decompress_into_dimensioned_round_trip() {
1272        let data: Vec<f64> = (0..256)
1273            .map(|i| (i as f64 / 256.0 * std::f64::consts::PI).sin())
1274            .collect();
1275        let d = DimensionedData::<f64, _>::build(&data)
1276            .dim(256)
1277            .unwrap()
1278            .finish()
1279            .unwrap();
1280        let compressed = compress(&d, ErrorBound::Absolute(1e-6)).unwrap();
1281        let mut output = vec![0.0f64; 256];
1282        let mut out_dim = DimensionedData::<f64, _>::build_mut(&mut output)
1283            .dim(256)
1284            .unwrap()
1285            .finish()
1286            .unwrap();
1287        let _cfg = decompress_into_dimensioned(&*compressed, &mut out_dim).unwrap();
1288        for (orig, dec) in data.iter().zip(out_dim.data()) {
1289            assert!((orig - dec).abs() <= 1e-6);
1290        }
1291    }
1292
1293    #[test]
1294    fn decompress_data_type_mismatch() {
1295        let data: Vec<f32> = (0..256).map(|i| i as f32).collect();
1296        let d = DimensionedData::<f32, _>::build(&data)
1297            .dim(256)
1298            .unwrap()
1299            .finish()
1300            .unwrap();
1301        let compressed = compress(&d, ErrorBound::Absolute(1e-4)).unwrap();
1302        let result = decompress::<f64, _>(&*compressed);
1303        assert!(matches!(
1304            result.unwrap_err(),
1305            SZ3Error::DecompressedDataTypeMismatch
1306        ));
1307    }
1308
1309    #[test]
1310    fn decompress_into_dims_mismatch() {
1311        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1312        let d = DimensionedData::<f64, _>::build(&data)
1313            .dim(256)
1314            .unwrap()
1315            .finish()
1316            .unwrap();
1317        let compressed = compress(&d, ErrorBound::Absolute(1e-6)).unwrap();
1318        let mut output = vec![0.0f64; 256];
1319        let mut out_dim = DimensionedData::<f64, _>::build_mut(&mut output)
1320            .dim(16)
1321            .unwrap()
1322            .dim(16)
1323            .unwrap()
1324            .finish()
1325            .unwrap();
1326        let result = decompress_into_dimensioned(&*compressed, &mut out_dim);
1327        assert!(matches!(
1328            result.unwrap_err(),
1329            SZ3Error::DecompressedDimsMismatch { .. }
1330        ));
1331    }
1332
1333    #[test]
1334    fn decompress_into_type_mismatch() {
1335        let data: Vec<f32> = (0..256).map(|i| i as f32).collect();
1336        let d = DimensionedData::<f32, _>::build(&data)
1337            .dim(256)
1338            .unwrap()
1339            .finish()
1340            .unwrap();
1341        let compressed = compress(&d, ErrorBound::Absolute(1e-4)).unwrap();
1342        let mut output = vec![0.0f64; 256];
1343        let mut out_dim = DimensionedData::<f64, _>::build_mut(&mut output)
1344            .dim(256)
1345            .unwrap()
1346            .finish()
1347            .unwrap();
1348        let result = decompress_into_dimensioned(&*compressed, &mut out_dim);
1349        assert!(matches!(
1350            result.unwrap_err(),
1351            SZ3Error::DecompressedDataTypeMismatch
1352        ));
1353    }
1354
1355    #[test]
1356    fn compress_into_appends() {
1357        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1358        let d = DimensionedData::<f64, _>::build(&data)
1359            .dim(256)
1360            .unwrap()
1361            .finish()
1362            .unwrap();
1363        let mut buf = vec![0xAA, 0xBB, 0xCC];
1364        compress_into(&d, ErrorBound::Absolute(1e-6), &mut buf).unwrap();
1365        assert_eq!(&buf[..3], &[0xAA, 0xBB, 0xCC]);
1366        assert!(buf.len() > 3);
1367        let (_, dec) = decompress::<f64, _>(&buf[3..]).unwrap();
1368        assert_eq!(dec.data().len(), data.len());
1369    }
1370
1371    #[test]
1372    fn compress_into_with_config_appends() {
1373        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1374        let d = DimensionedData::<f64, _>::build(&data)
1375            .dim(256)
1376            .unwrap()
1377            .finish()
1378            .unwrap();
1379        let cfg = Config::new(ErrorBound::Absolute(1e-6))
1380            .compression_algorithm(CompressionAlgorithm::lossless());
1381        let mut buf = vec![0xDD, 0xEE];
1382        compress_into_with_config(&d, &cfg, &mut buf).unwrap();
1383        assert_eq!(&buf[..2], &[0xDD, 0xEE]);
1384        let (_, dec) = decompress::<f64, _>(&buf[2..]).unwrap();
1385        assert_eq!(dec.data(), data.as_slice());
1386    }
1387
1388    #[test]
1389    fn round_trip_2d() {
1390        let data: Vec<f64> = (0..256).map(|i| i as f64).collect();
1391        let d = DimensionedData::<f64, _>::build(&data)
1392            .dim(16)
1393            .unwrap()
1394            .dim(16)
1395            .unwrap()
1396            .finish()
1397            .unwrap();
1398        let c = compress(&d, ErrorBound::Absolute(1e-6)).unwrap();
1399        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1400        assert_eq!(dec.dims(), &[16, 16]);
1401    }
1402
1403    #[test]
1404    fn round_trip_3d() {
1405        let data: Vec<f64> = (0..216).map(|i| i as f64).collect();
1406        let d = DimensionedData::<f64, _>::build(&data)
1407            .dim(6)
1408            .unwrap()
1409            .dim(6)
1410            .unwrap()
1411            .dim(6)
1412            .unwrap()
1413            .finish()
1414            .unwrap();
1415        let c = compress(&d, ErrorBound::Absolute(1e-6)).unwrap();
1416        let (_, dec) = decompress::<f64, _>(&*c).unwrap();
1417        assert_eq!(dec.dims(), &[6, 6, 6]);
1418    }
1419
1420    #[test]
1421    fn algo_constructors() {
1422        assert!(matches!(
1423            CompressionAlgorithm::interpolation(),
1424            CompressionAlgorithm::Interpolation
1425        ));
1426        assert!(matches!(
1427            CompressionAlgorithm::interpolation_lorenzo(),
1428            CompressionAlgorithm::InterpolationLorenzo
1429        ));
1430        assert!(matches!(
1431            CompressionAlgorithm::lorenzo_regression(),
1432            CompressionAlgorithm::LorenzoRegression {
1433                lorenzo: true,
1434                lorenzo_second_order: false,
1435                regression: true
1436            }
1437        ));
1438        let a =
1439            CompressionAlgorithm::lorenzo_regression_custom(Some(false), Some(true), Some(false));
1440        assert!(matches!(
1441            a,
1442            CompressionAlgorithm::LorenzoRegression {
1443                lorenzo: false,
1444                lorenzo_second_order: true,
1445                regression: false
1446            }
1447        ));
1448        let a = CompressionAlgorithm::lorenzo_regression_custom(None, None, None);
1449        assert!(matches!(
1450            a,
1451            CompressionAlgorithm::LorenzoRegression {
1452                lorenzo: true,
1453                lorenzo_second_order: false,
1454                regression: true
1455            }
1456        ));
1457        assert!(matches!(
1458            CompressionAlgorithm::biology_molecular_data(),
1459            CompressionAlgorithm::BiologyMolecularData
1460        ));
1461        assert!(matches!(
1462            CompressionAlgorithm::biology_molecular_data_gromacs_xtc(),
1463            CompressionAlgorithm::BiologyMolecularDataGromacsXtc
1464        ));
1465        assert!(matches!(
1466            CompressionAlgorithm::no_prediction(),
1467            CompressionAlgorithm::NoPrediction
1468        ));
1469        assert!(matches!(
1470            CompressionAlgorithm::lossless(),
1471            CompressionAlgorithm::Lossless
1472        ));
1473        assert!(matches!(
1474            CompressionAlgorithm::default(),
1475            CompressionAlgorithm::InterpolationLorenzo
1476        ));
1477    }
1478
1479    #[test]
1480    fn error_display_messages() {
1481        let e = SZ3Error::OneSizedDimension;
1482        assert!(format!("{e}").contains("size one"));
1483        let e = SZ3Error::DecompressedDataTypeMismatch;
1484        assert!(format!("{e}").contains("different data type"));
1485        let e = SZ3Error::DecompressedDimsMismatch {
1486            found: vec![256],
1487            expected: vec![16, 16],
1488        };
1489        assert!(format!("{e}").contains("[256]"));
1490        let e = SZ3Error::InvalidDimensionSize {
1491            dims: vec![10],
1492            len: 100,
1493            wanted: 7,
1494            remainder: 10,
1495        };
1496        assert!(format!("{e}").contains("7"));
1497        let e = SZ3Error::UnderSpecifiedDimensions {
1498            dims: vec![10],
1499            len: 100,
1500            remainder: 10,
1501        };
1502        assert!(format!("{e}").contains("10"));
1503    }
1504}