Skip to main content

vortex_alp/alp/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::mem::size_of;
7use std::mem::transmute;
8use std::mem::transmute_copy;
9
10use itertools::Itertools;
11use num_traits::CheckedSub;
12use num_traits::Float;
13use num_traits::PrimInt;
14use num_traits::ToPrimitive;
15
16mod array;
17mod compress;
18pub(crate) mod compute;
19mod decompress;
20mod ops;
21mod rules;
22
23#[cfg(test)]
24mod tests {
25    use prost::Message;
26    use vortex_array::dtype::PType;
27    use vortex_array::patches::PatchesMetadata;
28    use vortex_array::test_harness::check_metadata;
29
30    use crate::alp::array::ALPMetadata;
31
32    #[cfg_attr(miri, ignore)]
33    #[test]
34    fn test_alp_metadata() {
35        check_metadata(
36            "alp.metadata",
37            &ALPMetadata {
38                patches: Some(PatchesMetadata::new(
39                    usize::MAX,
40                    usize::MAX,
41                    PType::U64,
42                    None,
43                    None,
44                    None,
45                )),
46                exp_e: u32::MAX,
47                exp_f: u32::MAX,
48            }
49            .encode_to_vec(),
50        );
51    }
52}
53
54pub use array::*;
55pub use compress::alp_encode;
56pub use decompress::decompress_into_array;
57use vortex_array::dtype::NativePType;
58use vortex_array::scalar::PValue;
59use vortex_buffer::Buffer;
60use vortex_buffer::BufferMut;
61
62const SAMPLE_SIZE: usize = 32;
63
64#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
65pub struct Exponents {
66    pub e: u8,
67    pub f: u8,
68}
69
70impl Display for Exponents {
71    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72        write!(f, "e: {}, f: {}", self.e, self.f)
73    }
74}
75
76mod private {
77    pub trait Sealed {}
78
79    impl Sealed for f32 {}
80    impl Sealed for f64 {}
81}
82
83pub trait ALPFloat: private::Sealed + Float + Display + NativePType {
84    type ALPInt: PrimInt + Display + ToPrimitive + Copy + NativePType + Into<PValue>;
85
86    const FRACTIONAL_BITS: u8;
87    const MAX_EXPONENT: u8;
88    const SWEET: Self;
89    const F10: &'static [Self];
90    const IF10: &'static [Self];
91
92    /// Round to the nearest floating integer by shifting in and out of the low precision range.
93    #[inline]
94    fn fast_round(self) -> Self {
95        (self + Self::SWEET) - Self::SWEET
96    }
97
98    /// Equivalent to calling `as` to cast the primitive float to the target integer type.
99    fn as_int(self) -> Self::ALPInt;
100
101    /// Convert from the integer type back to the float type using `as`.
102    fn from_int(n: Self::ALPInt) -> Self;
103
104    fn find_best_exponents(values: &[Self]) -> Exponents {
105        let mut best_exp = Exponents { e: 0, f: 0 };
106        let mut best_nbytes: usize = usize::MAX;
107
108        let sample = (values.len() > SAMPLE_SIZE).then(|| {
109            values
110                .iter()
111                .step_by(values.len() / SAMPLE_SIZE)
112                .cloned()
113                .collect_vec()
114        });
115
116        for e in (0..Self::MAX_EXPONENT).rev() {
117            for f in 0..e {
118                let (_, encoded, _, exc_patches, _) = Self::encode(
119                    sample.as_deref().unwrap_or(values),
120                    Some(Exponents { e, f }),
121                );
122
123                let size = Self::estimate_encoded_size(&encoded, &exc_patches);
124                if size < best_nbytes {
125                    best_nbytes = size;
126                    best_exp = Exponents { e, f };
127                } else if size == best_nbytes && e - f < best_exp.e - best_exp.f {
128                    best_exp = Exponents { e, f };
129                }
130            }
131        }
132
133        best_exp
134    }
135
136    #[inline]
137    fn estimate_encoded_size(encoded: &[Self::ALPInt], patches: &[Self]) -> usize {
138        let bits_per_encoded = encoded
139            .iter()
140            .minmax()
141            .into_option()
142            // estimating bits per encoded value assuming frame-of-reference + bitpacking-without-patches
143            .and_then(|(min, max)| max.checked_sub(min))
144            .and_then(|range_size: <Self as ALPFloat>::ALPInt| range_size.to_u64())
145            .and_then(|range_size| {
146                range_size
147                    .checked_ilog2()
148                    .map(|bits| (bits + 1) as usize)
149                    .or(Some(0))
150            })
151            .unwrap_or(size_of::<Self::ALPInt>() * 8);
152
153        let encoded_bytes = (encoded.len() * bits_per_encoded).div_ceil(8);
154        // each patch is a value + a position
155        // in practice, patch positions are in [0, u16::MAX] because of how we chunk
156        let patch_bytes = patches.len() * (size_of::<Self>() + size_of::<u16>());
157
158        encoded_bytes + patch_bytes
159    }
160
161    #[expect(
162        clippy::type_complexity,
163        reason = "tuple return type is appropriate for multiple encoding outputs"
164    )]
165    fn encode(
166        values: &[Self],
167        exponents: Option<Exponents>,
168    ) -> (
169        Exponents,
170        Buffer<Self::ALPInt>,
171        Buffer<u64>,
172        Buffer<Self>,
173        BufferMut<u64>,
174    ) {
175        let exp = exponents.unwrap_or_else(|| Self::find_best_exponents(values));
176
177        let mut encoded_output = BufferMut::<Self::ALPInt>::with_capacity(values.len());
178
179        // Estimate capacity to be one patch per 32 values.
180        let mut patch_indices = BufferMut::<u64>::with_capacity(values.len() / 32);
181        let mut patch_values = BufferMut::<Self>::with_capacity(values.len() / 32);
182
183        // There's exactly one offset per 1024 chunk.
184        let mut chunk_offsets = BufferMut::<u64>::with_capacity(values.len().div_ceil(1024));
185        let mut fill_value: Option<Self::ALPInt> = None;
186
187        for chunk in values.chunks(1024) {
188            chunk_offsets.push(patch_indices.len() as u64);
189            encode_chunk_unchecked(
190                chunk,
191                exp,
192                &mut encoded_output,
193                &mut patch_indices,
194                &mut patch_values,
195                &mut fill_value,
196            );
197        }
198
199        (
200            exp,
201            encoded_output.freeze(),
202            patch_indices.freeze(),
203            patch_values.freeze(),
204            chunk_offsets,
205        )
206    }
207
208    #[inline]
209    fn encode_single(value: Self, exponents: Exponents) -> Option<Self::ALPInt> {
210        let encoded = Self::encode_single_unchecked(value, exponents);
211        let decoded = Self::decode_single(encoded, exponents);
212        if decoded.is_eq(value) {
213            return Some(encoded);
214        }
215        None
216    }
217
218    fn encode_above(value: Self, exponents: Exponents) -> Self::ALPInt {
219        (value * Self::F10[exponents.e as usize] * Self::IF10[exponents.f as usize])
220            .ceil()
221            .as_int()
222    }
223
224    fn encode_below(value: Self, exponents: Exponents) -> Self::ALPInt {
225        (value * Self::F10[exponents.e as usize] * Self::IF10[exponents.f as usize])
226            .floor()
227            .as_int()
228    }
229
230    fn decode(encoded: &[Self::ALPInt], exponents: Exponents) -> Vec<Self> {
231        let mut values = Vec::with_capacity(encoded.len());
232        for encoded in encoded {
233            values.push(Self::decode_single(*encoded, exponents));
234        }
235        values
236    }
237
238    fn decode_buffer(encoded: BufferMut<Self::ALPInt>, exponents: Exponents) -> BufferMut<Self> {
239        encoded.map_each_in_place(move |encoded| Self::decode_single(encoded, exponents))
240    }
241
242    fn decode_into(encoded: &[Self::ALPInt], exponents: Exponents, output: &mut [Self]) {
243        assert_eq!(encoded.len(), output.len());
244
245        for i in 0..encoded.len() {
246            output[i] = Self::decode_single(encoded[i], exponents)
247        }
248    }
249
250    fn decode_slice_inplace(encoded: &mut [Self::ALPInt], exponents: Exponents) {
251        let decoded: &mut [Self] = unsafe { transmute(encoded) };
252        decoded.iter_mut().for_each(|v| {
253            *v = Self::decode_single(
254                unsafe { transmute_copy::<Self, Self::ALPInt>(v) },
255                exponents,
256            )
257        })
258    }
259
260    #[inline(always)]
261    fn decode_single(encoded: Self::ALPInt, exponents: Exponents) -> Self {
262        Self::from_int(encoded) * Self::F10[exponents.f as usize] * Self::IF10[exponents.e as usize]
263    }
264
265    /// Encode single float value. The returned value might decode to a different value than passed in.
266    /// Consider using [`Self::encode_single`] if you want the checked version of this function.
267    #[inline(always)]
268    fn encode_single_unchecked(value: Self, exponents: Exponents) -> Self::ALPInt {
269        (value * Self::F10[exponents.e as usize] * Self::IF10[exponents.f as usize])
270            .fast_round()
271            .as_int()
272    }
273}
274
275#[expect(
276    clippy::cast_possible_truncation,
277    reason = "intentional truncation for ALP encoding"
278)]
279fn encode_chunk_unchecked<T: ALPFloat>(
280    chunk: &[T],
281    exp: Exponents,
282    encoded_output: &mut BufferMut<T::ALPInt>,
283    patch_indices: &mut BufferMut<u64>,
284    patch_values: &mut BufferMut<T>,
285    fill_value: &mut Option<T::ALPInt>,
286) {
287    let num_prev_encoded = encoded_output.len();
288    let num_prev_patches = patch_indices.len();
289    assert_eq!(patch_indices.len(), patch_values.len());
290    let has_filled = fill_value.is_some();
291
292    // encode the chunk, counting the number of patches
293    let mut chunk_patch_count = 0;
294    encoded_output.extend_trusted(chunk.iter().map(|&v| {
295        let encoded = T::encode_single_unchecked(v, exp);
296        let decoded = T::decode_single(encoded, exp);
297        let neq = !decoded.is_eq(v) as usize;
298        chunk_patch_count += neq;
299        encoded
300    }));
301    let chunk_patch_count = chunk_patch_count; // immutable hereafter
302    assert_eq!(encoded_output.len(), num_prev_encoded + chunk.len());
303
304    if chunk_patch_count > 0 {
305        // we need to gather the patches for this chunk
306        // preallocate space for the patches (plus one because our loop may attempt to write one past the end)
307        patch_indices.reserve(chunk_patch_count + 1);
308        patch_values.reserve(chunk_patch_count + 1);
309
310        // record the patches in this chunk
311        let patch_indices_mut = patch_indices.spare_capacity_mut();
312        let patch_values_mut = patch_values.spare_capacity_mut();
313        let mut chunk_patch_index = 0;
314        for i in num_prev_encoded..encoded_output.len() {
315            let decoded = T::decode_single(encoded_output[i], exp);
316            // write() is only safe to call more than once because the values are primitive (i.e., Drop is a no-op)
317            patch_indices_mut[chunk_patch_index].write(i as u64);
318            patch_values_mut[chunk_patch_index].write(chunk[i - num_prev_encoded]);
319            chunk_patch_index += !decoded.is_eq(chunk[i - num_prev_encoded]) as usize;
320        }
321        assert_eq!(chunk_patch_index, chunk_patch_count);
322        unsafe {
323            patch_indices.set_len(num_prev_patches + chunk_patch_count);
324            patch_values.set_len(num_prev_patches + chunk_patch_count);
325        }
326    }
327
328    // find the first successfully encoded value (i.e., not patched)
329    // this is our fill value for missing values
330    if fill_value.is_none() && (num_prev_encoded + chunk_patch_count < encoded_output.len()) {
331        assert_eq!(num_prev_encoded, num_prev_patches);
332        for i in num_prev_encoded..encoded_output.len() {
333            if i >= patch_indices.len() || patch_indices[i] != i as u64 {
334                *fill_value = Some(encoded_output[i]);
335                break;
336            }
337        }
338    }
339
340    // replace the patched values in the encoded array with the fill value
341    // for better downstream compression
342    if let Some(fill_value) = fill_value {
343        // handle the edge case where the first N >= 1 chunks are all patches
344        let start_patch = if !has_filled { 0 } else { num_prev_patches };
345        for patch_idx in &patch_indices[start_patch..] {
346            encoded_output[*patch_idx as usize] = *fill_value;
347        }
348    }
349}
350
351impl ALPFloat for f32 {
352    type ALPInt = i32;
353    const FRACTIONAL_BITS: u8 = 23;
354    const MAX_EXPONENT: u8 = 10;
355    const SWEET: Self =
356        (1 << Self::FRACTIONAL_BITS) as Self + (1 << (Self::FRACTIONAL_BITS - 1)) as Self;
357
358    const F10: &'static [Self] = &[
359        1.0,
360        10.0,
361        100.0,
362        1000.0,
363        10000.0,
364        100000.0,
365        1000000.0,
366        10000000.0,
367        100000000.0,
368        1000000000.0,
369        10000000000.0, // 10^10
370    ];
371    const IF10: &'static [Self] = &[
372        1.0,
373        0.1,
374        0.01,
375        0.001,
376        0.0001,
377        0.00001,
378        0.000001,
379        0.0000001,
380        0.00000001,
381        0.000000001,
382        0.0000000001, // 10^-10
383    ];
384
385    #[inline(always)]
386    #[expect(
387        clippy::cast_possible_truncation,
388        reason = "intentional float to int truncation for ALP encoding"
389    )]
390    fn as_int(self) -> Self::ALPInt {
391        self as _
392    }
393
394    #[inline(always)]
395    fn from_int(n: Self::ALPInt) -> Self {
396        n as _
397    }
398}
399
400impl ALPFloat for f64 {
401    type ALPInt = i64;
402    const FRACTIONAL_BITS: u8 = 52;
403    const MAX_EXPONENT: u8 = 18; // 10^18 is the maximum i64
404    const SWEET: Self =
405        (1u64 << Self::FRACTIONAL_BITS) as Self + (1u64 << (Self::FRACTIONAL_BITS - 1)) as Self;
406    const F10: &'static [Self] = &[
407        1.0,
408        10.0,
409        100.0,
410        1000.0,
411        10000.0,
412        100000.0,
413        1000000.0,
414        10000000.0,
415        100000000.0,
416        1000000000.0,
417        10000000000.0,
418        100000000000.0,
419        1000000000000.0,
420        10000000000000.0,
421        100000000000000.0,
422        1000000000000000.0,
423        10000000000000000.0,
424        100000000000000000.0,
425        1000000000000000000.0,
426        10000000000000000000.0,
427        100000000000000000000.0,
428        1000000000000000000000.0,
429        10000000000000000000000.0,
430        100000000000000000000000.0, // 10^23
431    ];
432
433    const IF10: &'static [Self] = &[
434        1.0,
435        0.1,
436        0.01,
437        0.001,
438        0.0001,
439        0.00001,
440        0.000001,
441        0.0000001,
442        0.00000001,
443        0.000000001,
444        0.0000000001,
445        0.00000000001,
446        0.000000000001,
447        0.0000000000001,
448        0.00000000000001,
449        0.000000000000001,
450        0.0000000000000001,
451        0.00000000000000001,
452        0.000000000000000001,
453        0.0000000000000000001,
454        0.00000000000000000001,
455        0.000000000000000000001,
456        0.0000000000000000000001,
457        0.00000000000000000000001, // 10^-23
458    ];
459
460    #[inline(always)]
461    #[expect(
462        clippy::cast_possible_truncation,
463        reason = "intentional float to int truncation for ALP encoding"
464    )]
465    fn as_int(self) -> Self::ALPInt {
466        self as _
467    }
468
469    #[inline(always)]
470    fn from_int(n: Self::ALPInt) -> Self {
471        n as _
472    }
473}