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