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