qpet_sperr/
lib.rs

1//! [![CI Status]][workflow] [![MSRV]][repo] [![Latest Version]][crates.io]
2//! [![Rust Doc Crate]][docs.rs] [![Rust Doc Main]][docs]
3//!
4//! [CI Status]: https://img.shields.io/github/actions/workflow/status/juntyr/sperr-rs/ci.yml?branch=main
5//! [workflow]: https://github.com/juntyr/sperr-rs/actions/workflows/ci.yml?query=branch%3Amain
6//!
7//! [MSRV]: https://img.shields.io/badge/MSRV-1.82.0-blue
8//! [repo]: https://github.com/juntyr/sperr-rs
9//!
10//! [Latest Version]: https://img.shields.io/crates/v/sperr
11//! [crates.io]: https://crates.io/crates/sperr
12//!
13//! [Rust Doc Crate]: https://img.shields.io/docsrs/sperr
14//! [docs.rs]: https://docs.rs/sperr/
15//!
16//! [Rust Doc Main]: https://img.shields.io/badge/docs-main-blue
17//! [docs]: https://juntyr.github.io/sperr-rs/sperr
18//!
19//! High-level bindigs to the [QPET-SPERR] compressor.
20//!
21//! [QPET-SPERR]: https://github.com/JLiu-1/QPET-Artifact/tree/sperr_qpet_revision
22
23use std::num::NonZeroUsize;
24
25use ndarray::{ArrayView3, ArrayViewMut3};
26
27#[derive(Copy, Clone, PartialEq, Debug)]
28#[non_exhaustive]
29/// QPET-SPERR compression mode / quality control
30pub enum CompressionMode<'a> {
31    /// Symbolic Quantity of Interest
32    SymbolicQuantityOfInterest {
33        /// quantity of interest expression
34        qoi: &'a str,
35        /// 3D block size (z,y,x) over which the quantity of interest errors
36        /// are averaged, 1x1x1 for pointwise
37        qoi_block_size: (NonZeroUsize, NonZeroUsize, NonZeroUsize),
38        /// positive (pointwise) absolute error bound over the quantity of
39        /// interest
40        qoi_pwe: f64,
41        /// optional positive pointwise absolute error bound over the data
42        data_pwe: Option<f64>,
43        /// positive quantity of interest k parameter (3.0 is a good default)
44        qoi_k: f64,
45        /// high precision mode for SPERR, useful for small error bounds
46        high_prec: bool,
47    },
48}
49
50#[derive(Debug, thiserror::Error)]
51/// Errors that can occur during compression and decompression with QPET-SPERR
52pub enum Error {
53    /// one or more parameters is invalid
54    #[error("one or more parameters is invalid")]
55    InvalidParameter,
56    /// cannot decompress to an array with a different shape
57    #[error("cannot decompress to an array with a different shape")]
58    DecompressShapeMismatch,
59    /// other error
60    #[error("other error")]
61    Other,
62}
63
64/// Compress a 3d `src` volume of data with the compression `mode` using the
65/// preferred `chunks`.
66///
67/// The compressed output can be decompressed with QPET-SPERR or SPERR.
68///
69/// # Errors
70///
71/// Errors with
72/// - [`Error::InvalidParameter`] if the compression `mode` is invalid
73/// - [`Error::Other`] if another error occurs inside QPET-SPERR
74#[allow(clippy::missing_panics_doc)]
75pub fn compress_3d<T: Element>(
76    src: ArrayView3<T>,
77    mode: CompressionMode,
78    chunks: (usize, usize, usize),
79) -> Result<Vec<u8>, Error> {
80    let src = src.as_standard_layout();
81
82    let mut dst = std::ptr::null_mut();
83    let mut dst_len = 0;
84
85    let CompressionMode::SymbolicQuantityOfInterest {
86        qoi,
87        qoi_block_size,
88        qoi_pwe,
89        data_pwe,
90        qoi_k,
91        high_prec,
92    } = mode;
93
94    let mut qoi = Vec::from(qoi.as_bytes());
95    qoi.push(b'\0');
96    let qoi = qoi;
97
98    #[allow(unsafe_code)] // Safety: FFI
99    let res = unsafe {
100        qpet_sperr_sys::qpet_sperr_comp_3d(
101            src.as_ptr().cast(),
102            T::IS_FLOAT.into(),
103            src.dim().2,
104            src.dim().1,
105            src.dim().0,
106            chunks.2,
107            chunks.1,
108            chunks.0,
109            data_pwe.unwrap_or(f64::MAX),
110            0,
111            std::ptr::addr_of_mut!(dst),
112            std::ptr::addr_of_mut!(dst_len),
113            qoi.as_ptr().cast(),
114            qoi_pwe,
115            qoi_block_size.2.get(),
116            qoi_block_size.1.get(),
117            qoi_block_size.0.get(),
118            qoi_k,
119            high_prec,
120        )
121    };
122
123    match res {
124        0 => (), // ok
125        #[allow(clippy::unreachable)]
126        1 => unreachable!("qpet_sperr_comp_3d: dst is not pointing to a NULL pointer"),
127        2 => return Err(Error::InvalidParameter),
128        -1 => return Err(Error::Other),
129        #[allow(clippy::panic)]
130        _ => panic!("qpet_sperr_comp_3d: unknown error kind {res}"),
131    }
132
133    #[allow(unsafe_code)] // Safety: dst is initialized by qpet_sperr_comp_3d
134    let compressed =
135        Vec::from(unsafe { std::slice::from_raw_parts(dst.cast_const().cast::<u8>(), dst_len) });
136
137    #[allow(unsafe_code)] // Safety: FFI, dst is allocated by qpet_sperr_comp_3d
138    unsafe {
139        qpet_sperr_sys::free_dst(dst);
140    }
141
142    Ok(compressed)
143}
144
145/// Decompress a 3d (QPET-)SPERR-compressed `compressed` buffer into the
146/// `decompressed` array.
147///
148/// # Errors
149///
150/// Errors with
151/// - [`Error::DecompressShapeMismatch`] if the `decompressed` array is of a
152///   different shape than the SPERR header indicates
153/// - [`Error::Other`] if another error occurs inside QPET-SPERR
154#[allow(clippy::missing_panics_doc)]
155pub fn decompress_into_3d<T: Element>(
156    compressed: &[u8],
157    mut decompressed: ArrayViewMut3<T>,
158) -> Result<(), Error> {
159    let mut dim_x = 0;
160    let mut dim_y = 0;
161    let mut dim_z = 0;
162    let mut is_float = 0;
163
164    #[allow(unsafe_code)] // Safety: FFI
165    unsafe {
166        qpet_sperr_sys::sperr_parse_header(
167            compressed.as_ptr().cast(),
168            std::ptr::addr_of_mut!(dim_x),
169            std::ptr::addr_of_mut!(dim_y),
170            std::ptr::addr_of_mut!(dim_z),
171            std::ptr::addr_of_mut!(is_float),
172        );
173    }
174
175    if (dim_z, dim_y, dim_x)
176        != (
177            decompressed.dim().0,
178            decompressed.dim().1,
179            decompressed.dim().2,
180        )
181    {
182        return Err(Error::DecompressShapeMismatch);
183    }
184
185    let mut dst = std::ptr::null_mut();
186
187    #[allow(unsafe_code)] // Safety: FFI
188    let res = unsafe {
189        qpet_sperr_sys::sperr_decomp_3d(
190            compressed.as_ptr().cast(),
191            compressed.len(),
192            T::IS_FLOAT.into(),
193            0,
194            std::ptr::addr_of_mut!(dim_x),
195            std::ptr::addr_of_mut!(dim_y),
196            std::ptr::addr_of_mut!(dim_z),
197            std::ptr::addr_of_mut!(dst),
198        )
199    };
200
201    match res {
202        0 => (), // ok
203        #[allow(clippy::unreachable)]
204        1 => unreachable!("sperr_decomp_3d: dst is not pointing to a NULL pointer"),
205        -1 => return Err(Error::Other),
206        #[allow(clippy::panic)]
207        _ => panic!("sperr_decomp_3d: unknown error kind {res}"),
208    }
209
210    #[allow(unsafe_code)] // Safety: dst is initialized by sperr_decomp_3d
211    let dec =
212        unsafe { ArrayView3::from_shape_ptr(decompressed.dim(), dst.cast_const().cast::<T>()) };
213    decompressed.assign(&dec);
214
215    #[allow(unsafe_code)] // Safety: FFI, dst is allocated by sperr_decomp_3d
216    unsafe {
217        qpet_sperr_sys::free_dst(dst);
218    }
219
220    Ok(())
221}
222
223/// Marker trait for element types that can be compressed with QPET-SPERR
224pub trait Element: sealed::Element {}
225
226impl Element for f32 {}
227impl sealed::Element for f32 {
228    const IS_FLOAT: bool = true;
229}
230
231impl Element for f64 {}
232impl sealed::Element for f64 {
233    const IS_FLOAT: bool = false;
234}
235
236mod sealed {
237    pub trait Element: Copy {
238        const IS_FLOAT: bool;
239    }
240}
241
242#[cfg(test)]
243#[allow(clippy::expect_used)]
244mod tests {
245    use ndarray::{linspace, logspace, Array1, Array3};
246
247    use super::*;
248
249    const ONE: NonZeroUsize = NonZeroUsize::MIN;
250    const THREE: NonZeroUsize = ONE.saturating_add(2);
251
252    fn compress_decompress(mode: CompressionMode) {
253        let data = linspace(1.0, 10.0, 128 * 128 * 128).collect::<Array1<f64>>()
254            + logspace(2.0, 0.0, 5.0, 128 * 128 * 128)
255                .rev()
256                .collect::<Array1<f64>>();
257        let data: Array3<f64> = data
258            .into_shape_clone((128, 128, 128))
259            .expect("create test data array");
260
261        let compressed =
262            compress_3d(data.view(), mode, (64, 64, 64)).expect("compression should not fail");
263
264        let mut decompressed = Array3::<f64>::zeros(data.dim());
265        decompress_into_3d(compressed.as_slice(), decompressed.view_mut())
266            .expect("decompression should not fail");
267
268        let data: Array3<f64> = Array3::zeros((64, 64, 1));
269
270        let compressed =
271            compress_3d(data.view(), mode, (256, 256, 256)).expect("compression should not fail");
272
273        let mut decompressed = Array3::<f64>::zeros(data.dim());
274        decompress_into_3d(compressed.as_slice(), decompressed.view_mut())
275            .expect("decompression should not fail");
276    }
277
278    #[test]
279    fn compress_decompress_square() {
280        compress_decompress(CompressionMode::SymbolicQuantityOfInterest {
281            qoi: "x^2",
282            qoi_block_size: (ONE, ONE, ONE),
283            qoi_pwe: 0.1,
284            data_pwe: None,
285            qoi_k: 3.0,
286            high_prec: false,
287        });
288
289        // compress_decompress(CompressionMode::SymbolicQuantityOfInterest {
290        //     qoi: "x^2",
291        //     qoi_block_size: (THREE, THREE, THREE),
292        //     qoi_pwe: 0.1,
293        //     data_pwe: None,
294        //     qoi_k: 3.0,
295        //     high_prec: false,
296        // });
297    }
298
299    #[test]
300    fn compress_decompress_log10() {
301        compress_decompress(CompressionMode::SymbolicQuantityOfInterest {
302            qoi: "log(x,10)",
303            qoi_block_size: (ONE, ONE, ONE),
304            qoi_pwe: 0.1,
305            data_pwe: None,
306            qoi_k: 3.0,
307            high_prec: true,
308        });
309
310        // compress_decompress(CompressionMode::SymbolicQuantityOfInterest {
311        //     qoi: "log(x,10)",
312        //     qoi_block_size: (THREE, THREE, THREE),
313        //     qoi_pwe: 0.1,
314        //     data_pwe: None,
315        //     qoi_k: 3.0,
316        //     high_prec: true,
317        // });
318    }
319}