qpet_sz/
lib.rs

1use std::num::NonZeroUsize;
2
3use ndarray::{Array, ArrayView, Dimension, IxDyn};
4
5#[derive(Clone, Debug)]
6pub enum CompressionMode<'a> {
7    /// Symbolic Quantity of Interest
8    SymbolicQuantityOfInterest {
9        /// quantity of interest expression
10        qoi: &'a str,
11        /// side length of the region of the quantity of interest,
12        /// 1 for pointwise
13        qoi_region_size: NonZeroUsize,
14        /// quantity of interest error bound
15        qoi_error_bound: QoIErrorBound,
16        /// data error bound
17        data_error_bound: DataErrorBound,
18        /// positive quantity of interest c parameter (2.0 is a good default)
19        qoi_c: f64,
20    },
21}
22
23#[derive(Clone, Debug, Copy)]
24pub enum DataErrorBound {
25    Absolute(f64),
26    Relative(f64),
27    PSNR(f64),
28    L2Norm(f64),
29    AbsoluteAndRelative {
30        absolute_bound: f64,
31        relative_bound: f64,
32    },
33    AbsoluteOrRelative {
34        absolute_bound: f64,
35        relative_bound: f64,
36    },
37}
38
39#[derive(Clone, Debug, Copy)]
40pub enum QoIErrorBound {
41    /// Absolute error bound
42    Absolute(f64),
43    /// Range-relative error bound
44    Relative(f64),
45}
46
47impl DataErrorBound {
48    const fn code(&self) -> u8 {
49        #[allow(clippy::cast_possible_truncation)]
50        match self {
51            Self::Absolute(_) => const { qpet_sz_sys::QoZ_EB_EB_ABS as u8 },
52            Self::Relative(_) => const { qpet_sz_sys::QoZ_EB_EB_REL as u8 },
53            Self::PSNR(_) => const { qpet_sz_sys::QoZ_EB_EB_PSNR as u8 },
54            Self::L2Norm(_) => const { qpet_sz_sys::QoZ_EB_EB_L2NORM as u8 },
55            Self::AbsoluteAndRelative { .. } => const { qpet_sz_sys::QoZ_EB_EB_ABS_AND_REL as u8 },
56            Self::AbsoluteOrRelative { .. } => const { qpet_sz_sys::QoZ_EB_EB_ABS_OR_REL as u8 },
57        }
58    }
59
60    const fn abs_bound(&self) -> f64 {
61        match self {
62            Self::Absolute(bound) => *bound,
63            Self::AbsoluteOrRelative { absolute_bound, .. }
64            | Self::AbsoluteAndRelative { absolute_bound, .. } => *absolute_bound,
65            _ => 0.0,
66        }
67    }
68
69    const fn rel_bound(&self) -> f64 {
70        match self {
71            Self::Relative(bound) => *bound,
72            Self::AbsoluteOrRelative { relative_bound, .. }
73            | Self::AbsoluteAndRelative { relative_bound, .. } => *relative_bound,
74            _ => 0.0,
75        }
76    }
77
78    const fn l2norm_bound(&self) -> f64 {
79        match self {
80            Self::L2Norm(bound) => *bound,
81            _ => 0.0,
82        }
83    }
84
85    const fn psnr_bound(&self) -> f64 {
86        match self {
87            Self::PSNR(bound) => *bound,
88            _ => 0.0,
89        }
90    }
91}
92
93impl QoIErrorBound {
94    const fn code(&self) -> u8 {
95        #[allow(clippy::cast_possible_truncation)]
96        match self {
97            Self::Absolute(_) => const { qpet_sz_sys::QoZ_EB_EB_ABS as u8 },
98            Self::Relative(_) => const { qpet_sz_sys::QoZ_EB_EB_REL as u8 },
99        }
100    }
101
102    const fn bound(&self) -> f64 {
103        match self {
104            Self::Absolute(bound) | Self::Relative(bound) => *bound,
105        }
106    }
107}
108
109#[derive(Clone, Debug)]
110pub struct Config<'a> {
111    mode: CompressionMode<'a>,
112    openmp: bool,
113}
114
115pub trait Element: private::Element {}
116impl Element for f32 {}
117impl Element for f64 {}
118
119mod private {
120    pub trait Element: Copy {
121        unsafe fn compress(
122            config: qpet_sz_sys::QoZ_Config,
123            data: *const Self,
124            len: *mut usize,
125        ) -> *mut u8;
126
127        unsafe fn decompress(
128            compressed_data: *const u8,
129            compressed_len: usize,
130            decompressed_data: *mut *mut Self,
131        ) -> qpet_sz_sys::QoZ_Config;
132
133        unsafe fn dealloc(data: *mut Self);
134    }
135
136    impl Element for f32 {
137        unsafe fn compress(
138            config: qpet_sz_sys::QoZ_Config,
139            data: *const Self,
140            len: *mut usize,
141        ) -> *mut u8 {
142            qpet_sz_sys::compress_float(config, data, len).cast()
143        }
144
145        unsafe fn decompress(
146            compressed_data: *const u8,
147            compressed_len: usize,
148            decompressed_data: *mut *mut Self,
149        ) -> qpet_sz_sys::QoZ_Config {
150            qpet_sz_sys::decompress_float(compressed_data.cast(), compressed_len, decompressed_data)
151        }
152
153        unsafe fn dealloc(data: *mut Self) {
154            qpet_sz_sys::dealloc_float(data);
155        }
156    }
157
158    impl Element for f64 {
159        unsafe fn compress(
160            config: qpet_sz_sys::QoZ_Config,
161            data: *const Self,
162            len: *mut usize,
163        ) -> *mut u8 {
164            qpet_sz_sys::compress_double(config, data, len).cast()
165        }
166
167        unsafe fn decompress(
168            compressed_data: *const u8,
169            compressed_len: usize,
170            decompressed_data: *mut *mut Self,
171        ) -> qpet_sz_sys::QoZ_Config {
172            qpet_sz_sys::decompress_double(
173                compressed_data.cast(),
174                compressed_len,
175                decompressed_data,
176            )
177        }
178
179        unsafe fn dealloc(data: *mut Self) {
180            qpet_sz_sys::dealloc_double(data);
181        }
182    }
183}
184
185impl<'a> Config<'a> {
186    #[must_use]
187    pub const fn new(mode: CompressionMode<'a>) -> Self {
188        Self {
189            mode,
190            openmp: false,
191        }
192    }
193
194    #[cfg(feature = "openmp")]
195    #[must_use]
196    pub const fn openmp(mut self, openmp: bool) -> Self {
197        self.openmp = openmp;
198        self
199    }
200}
201
202#[allow(clippy::needless_pass_by_value)]
203#[allow(clippy::missing_panics_doc)] // FIXME
204pub fn compress<T: Element, D: Dimension>(data: ArrayView<T, D>, config: &Config) -> Vec<u8> {
205    let Config {
206        mode:
207            CompressionMode::SymbolicQuantityOfInterest {
208                qoi,
209                qoi_region_size,
210                qoi_error_bound,
211                data_error_bound,
212                qoi_c,
213            },
214        openmp,
215    } = config;
216
217    let mut qoi_string = Vec::from(qoi.as_bytes());
218    qoi_string.push(b'\0');
219
220    #[allow(clippy::unwrap_used)] // FIXME
221    let raw_config = qpet_sz_sys::QoZ_Config {
222        N: data.ndim().try_into().unwrap(),
223        dims: data.shape().as_ptr().cast_mut(),
224        num: data.len(),
225        errorBoundMode: data_error_bound.code(),
226        absErrorBound: data_error_bound.abs_bound(),
227        relErrorBound: data_error_bound.rel_bound(),
228        l2normErrorBound: data_error_bound.l2norm_bound(),
229        psnrErrorBound: data_error_bound.psnr_bound(),
230        openmp: *openmp,
231        qoi: 14, // symbolic QoI
232        qoiEB: qoi_error_bound.bound(),
233        qoiEBMode: qoi_error_bound.code(),
234        qoi_string: qoi_string.as_mut_ptr().cast(),
235        qoiRegionSize: qoi_region_size.get().try_into().unwrap(),
236        qoiRegionMode: match *qoi_region_size {
237            NonZeroUsize::MIN => 0, // pointwise
238            _ => 1,                 // regional average
239        },
240        error_std_rate: *qoi_c,
241        analytical: true,
242    };
243
244    // FIXME: remove debug print
245    eprintln!(
246        "compress config N={} num={} shape={:?}",
247        raw_config.N,
248        raw_config.num,
249        data.shape()
250    );
251
252    let mut len: usize = 0;
253
254    let compressed_ptr = unsafe { T::compress(raw_config, data.as_ptr(), &raw mut len) };
255
256    let compressed_data = unsafe { std::slice::from_raw_parts(compressed_ptr, len) };
257    let compressed_data = Vec::from(compressed_data);
258
259    unsafe { qpet_sz_sys::dealloc_char(compressed_ptr.cast()) };
260
261    compressed_data
262}
263
264#[must_use]
265#[allow(clippy::missing_panics_doc)] // FIXME
266pub fn decompress<T: Element>(compressed_data: &[u8]) -> Array<T, IxDyn> {
267    let mut decompressed_ptr = std::ptr::null_mut();
268
269    let config = unsafe {
270        T::decompress(
271            compressed_data.as_ptr(),
272            compressed_data.len(),
273            &raw mut decompressed_ptr,
274        )
275    };
276
277    let decompressed_data = unsafe { std::slice::from_raw_parts(decompressed_ptr, config.num) };
278    let decompressed_data = Vec::from(decompressed_data);
279
280    unsafe { T::dealloc(decompressed_ptr) };
281
282    #[allow(clippy::unwrap_used)] // FIXME
283    let shape: Vec<usize> = (0..config.N)
284        .map(|i| unsafe { std::ptr::read(config.dims.add(i.try_into().unwrap())) })
285        .collect();
286
287    // FIXME: remove debug print
288    eprintln!(
289        "decompress config N={} num={} shape={:?}",
290        config.N, config.num, shape
291    );
292
293    unsafe {
294        qpet_sz_sys::dealloc_size_t(config.dims);
295    }
296
297    unsafe {
298        qpet_sz_sys::dealloc_char(config.qoi_string);
299    }
300
301    #[allow(clippy::unwrap_used)] // FIXME
302    let decompressed_data = Array::from_shape_vec(shape, decompressed_data).unwrap();
303
304    decompressed_data
305}
306
307#[cfg(test)]
308#[allow(clippy::unwrap_used)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn log10_decode() {
314        let encoded = compress(
315            Array::<f32, _>::logspace(2.0, 0.0, 100.0, 721 * 1440)
316                .into_shape_clone((721, 1440))
317                .unwrap()
318                .view(),
319            &Config::new(CompressionMode::SymbolicQuantityOfInterest {
320                qoi: "log(x, 10)",
321                qoi_region_size: NonZeroUsize::MIN,
322                qoi_error_bound: QoIErrorBound::Absolute(0.25),
323                data_error_bound: DataErrorBound::Absolute(f64::MAX),
324                qoi_c: 2.0,
325            }),
326        );
327        let decoded = decompress::<f32>(&encoded);
328
329        assert_eq!(decoded.len(), 721 * 1440);
330        assert_eq!(decoded.shape(), &[721, 1440]);
331    }
332}