1#![expect(clippy::cast_possible_truncation)]
5
6pub use array::*;
7use vortex_array::ExecutionCtx;
8use vortex_array::IntoArray;
9use vortex_array::patches::Patches;
10use vortex_array::validity::Validity;
11use vortex_fastlanes::bitpack_compress::bitpack_encode_unchecked;
12
13mod array;
14mod compute;
15mod kernel;
16mod ops;
17mod rules;
18mod slice;
19
20use std::ops::Shl;
21use std::ops::Shr;
22
23use itertools::Itertools;
24use num_traits::Float;
25use num_traits::One;
26use num_traits::PrimInt;
27use rustc_hash::FxBuildHasher;
28use vortex_array::ArrayView;
29use vortex_array::arrays::Primitive;
30use vortex_array::arrays::PrimitiveArray;
31use vortex_array::dtype::DType;
32use vortex_array::dtype::NativePType;
33use vortex_array::match_each_integer_ptype;
34use vortex_buffer::Buffer;
35use vortex_buffer::BufferMut;
36use vortex_error::VortexExpect;
37use vortex_error::VortexResult;
38use vortex_error::vortex_panic;
39use vortex_utils::aliases::hash_map::HashMap;
40
41use crate::match_each_alp_float_ptype;
42
43macro_rules! bit_width {
44 ($value:expr) => {
45 if $value == 0 {
46 1
47 } else {
48 $value.ilog2().wrapping_add(1) as usize
49 }
50 };
51}
52
53const CUT_LIMIT: usize = 16;
55
56const MAX_DICT_SIZE: u8 = 8;
57
58mod private {
59 pub trait Sealed {}
60
61 impl Sealed for f32 {}
62 impl Sealed for f64 {}
63}
64
65pub trait ALPRDFloat: private::Sealed + Float + Copy + NativePType {
70 type UINT: NativePType + PrimInt + One + Copy;
72
73 const BITS: usize = size_of::<Self>() * 8;
75
76 fn from_bits(bits: Self::UINT) -> Self;
78
79 fn to_bits(value: Self) -> Self::UINT;
81
82 fn to_u16(bits: Self::UINT) -> u16;
84
85 fn from_u16(value: u16) -> Self::UINT;
87}
88
89impl ALPRDFloat for f64 {
90 type UINT = u64;
91
92 fn from_bits(bits: Self::UINT) -> Self {
93 f64::from_bits(bits)
94 }
95
96 fn to_bits(value: Self) -> Self::UINT {
97 value.to_bits()
98 }
99
100 fn to_u16(bits: Self::UINT) -> u16 {
101 bits as u16
102 }
103
104 fn from_u16(value: u16) -> Self::UINT {
105 value as u64
106 }
107}
108
109impl ALPRDFloat for f32 {
110 type UINT = u32;
111
112 fn from_bits(bits: Self::UINT) -> Self {
113 f32::from_bits(bits)
114 }
115
116 fn to_bits(value: Self) -> Self::UINT {
117 value.to_bits()
118 }
119
120 fn to_u16(bits: Self::UINT) -> u16 {
121 bits as u16
122 }
123
124 fn from_u16(value: u16) -> Self::UINT {
125 value as u32
126 }
127}
128
129pub struct RDEncoder {
148 right_bit_width: u8,
149 codes: Vec<u16>,
150}
151
152impl RDEncoder {
153 pub fn new<T>(sample: &[T]) -> Self
155 where
156 T: ALPRDFloat + NativePType,
157 T::UINT: NativePType,
158 {
159 let dictionary = find_best_dictionary::<T>(sample);
160
161 let mut codes = vec![0; dictionary.dictionary.len()];
162 dictionary.dictionary.into_iter().for_each(|(bits, code)| {
163 codes[code as usize] = bits
165 });
166
167 Self {
168 right_bit_width: dictionary.right_bit_width,
169 codes,
170 }
171 }
172
173 pub fn from_parts(right_bit_width: u8, codes: Vec<u16>) -> Self {
175 Self {
176 right_bit_width,
177 codes,
178 }
179 }
180
181 pub fn encode(&self, array: ArrayView<'_, Primitive>) -> ALPRDArray {
186 match_each_alp_float_ptype!(array.ptype(), |P| { self.encode_generic::<P>(array) })
187 }
188
189 fn encode_generic<T>(&self, array: ArrayView<'_, Primitive>) -> ALPRDArray
190 where
191 T: ALPRDFloat + NativePType,
192 T::UINT: NativePType,
193 {
194 assert!(
195 !self.codes.is_empty(),
196 "codes lookup table must be populated before RD encoding"
197 );
198
199 let doubles = array.as_slice::<T>();
200
201 let mut left_parts: BufferMut<u16> = BufferMut::with_capacity(doubles.len());
202 let mut right_parts: BufferMut<T::UINT> = BufferMut::with_capacity(doubles.len());
203 let mut exceptions_pos: BufferMut<u64> = BufferMut::with_capacity(doubles.len() / 4);
204 let mut exceptions: BufferMut<u16> = BufferMut::with_capacity(doubles.len() / 4);
205
206 let right_mask = T::UINT::one().shl(self.right_bit_width as _) - T::UINT::one();
208 let max_code = self.codes.len() - 1;
209 let left_bit_width = bit_width!(max_code);
210
211 for v in doubles.iter().copied() {
212 right_parts.push(T::to_bits(v) & right_mask);
213 left_parts.push(<T as ALPRDFloat>::to_u16(
214 T::to_bits(v).shr(self.right_bit_width as _),
215 ));
216 }
217
218 for (idx, left) in left_parts.iter_mut().enumerate() {
220 if let Some(code) = self.codes.iter().position(|v| *v == *left) {
222 *left = code as u16;
223 } else {
224 exceptions.push(*left);
225 exceptions_pos.push(idx as _);
226
227 *left = 0u16;
228 }
229 }
230
231 let primitive_left = PrimitiveArray::new(
233 left_parts,
234 array
235 .validity()
236 .vortex_expect("ALP RD validity should be derivable"),
237 );
238 let packed_left = unsafe {
240 bitpack_encode_unchecked(primitive_left, left_bit_width as _)
241 .vortex_expect("bitpack_encode_unchecked should succeed for left parts")
242 .into_array()
243 };
244
245 let primitive_right = PrimitiveArray::new(right_parts, Validity::NonNullable);
246 let packed_right = unsafe {
248 bitpack_encode_unchecked(primitive_right, self.right_bit_width as _)
249 .vortex_expect("bitpack_encode_unchecked should succeed for right parts")
250 .into_array()
251 };
252
253 let exceptions = (!exceptions_pos.is_empty()).then(|| {
257 let max_exc_pos = exceptions_pos.last().copied().unwrap_or_default();
258 let bw = bit_width!(max_exc_pos) as u8;
259
260 let exc_pos_array = PrimitiveArray::new(exceptions_pos, Validity::NonNullable);
261 let packed_pos = unsafe {
263 bitpack_encode_unchecked(exc_pos_array, bw)
264 .vortex_expect(
265 "bitpack_encode_unchecked should succeed for exception positions",
266 )
267 .into_array()
268 };
269
270 Patches::new(
271 doubles.len(),
272 0,
273 packed_pos,
274 exceptions.into_array(),
275 None,
277 )
278 .vortex_expect("Patches construction in encode")
279 });
280
281 ALPRD::try_new(
282 DType::Primitive(T::PTYPE, packed_left.dtype().nullability()),
283 packed_left,
284 Buffer::<u16>::copy_from(&self.codes),
285 packed_right,
286 self.right_bit_width,
287 exceptions,
288 )
289 .vortex_expect("ALPRDArray construction in encode")
290 }
291}
292
293pub fn alp_rd_decode<T: ALPRDFloat>(
299 mut left_parts: BufferMut<u16>,
300 left_parts_dict: &[u16],
301 right_bit_width: u8,
302 right_parts: BufferMut<T::UINT>,
303 left_parts_patches: Option<Patches>,
304 ctx: &mut ExecutionCtx,
305) -> VortexResult<Buffer<T>> {
306 if left_parts.len() != right_parts.len() {
307 vortex_panic!("alp_rd_decode: left_parts.len != right_parts.len");
308 }
309
310 let shift = right_bit_width as usize;
311
312 if let Some(patches) = left_parts_patches {
313 for code in left_parts.iter_mut() {
319 *code = left_parts_dict[*code as usize];
320 }
321
322 let indices = patches.indices().clone().execute::<PrimitiveArray>(ctx)?;
324 let patch_values = patches.values().clone().execute::<PrimitiveArray>(ctx)?;
325 alp_rd_apply_patches(&mut left_parts, &indices, &patch_values, patches.offset());
326
327 alp_rd_combine_inplace::<T>(
330 right_parts,
331 |right, &left| {
332 *right = (<T as ALPRDFloat>::from_u16(left) << shift) | *right;
333 },
334 left_parts.as_ref(),
335 )
336 } else {
337 let mut shifted_dict = [T::UINT::default(); MAX_DICT_SIZE as usize];
341 for (i, &entry) in left_parts_dict.iter().enumerate() {
342 shifted_dict[i] = <T as ALPRDFloat>::from_u16(entry) << shift;
343 }
344
345 alp_rd_combine_inplace::<T>(
347 right_parts,
348 |right, &code| {
349 *right = unsafe { *shifted_dict.get_unchecked(code as usize) } | *right;
351 },
352 left_parts.as_ref(),
353 )
354 }
355}
356
357fn alp_rd_apply_patches(
359 values: &mut BufferMut<u16>,
360 indices: &PrimitiveArray,
361 patch_values: &PrimitiveArray,
362 offset: usize,
363) {
364 match_each_integer_ptype!(indices.ptype(), |T| {
365 indices
366 .as_slice::<T>()
367 .iter()
368 .copied()
369 .map(|idx| idx - offset as T)
370 .zip(patch_values.as_slice::<u16>().iter())
371 .for_each(|(idx, v)| values[idx as usize] = *v);
372 })
373}
374
375fn alp_rd_combine_inplace<T: ALPRDFloat>(
378 mut right_parts: BufferMut<T::UINT>,
379 combine_fn: impl Fn(&mut T::UINT, &u16),
380 left_data: &[u16],
381) -> VortexResult<Buffer<T>> {
382 for (right, left) in right_parts.as_mut_slice().iter_mut().zip(left_data.iter()) {
383 combine_fn(right, left);
384 }
385 Ok(unsafe { right_parts.transmute::<T>() }.freeze())
387}
388fn find_best_dictionary<T: ALPRDFloat>(samples: &[T]) -> ALPRDDictionary {
391 let mut best_est_size = f64::MAX;
392 let mut best_dict = ALPRDDictionary::default();
393
394 for p in 1..=16 {
395 let candidate_right_bw = (T::BITS - p) as u8;
396 let (dictionary, exception_count) =
397 build_left_parts_dictionary::<T>(samples, candidate_right_bw, MAX_DICT_SIZE);
398 let estimated_size = estimate_compression_size(
399 dictionary.right_bit_width,
400 dictionary.left_bit_width,
401 exception_count,
402 samples.len(),
403 );
404 if estimated_size < best_est_size {
405 best_est_size = estimated_size;
406 best_dict = dictionary;
407 }
408 }
409
410 best_dict
411}
412
413fn build_left_parts_dictionary<T: ALPRDFloat>(
415 samples: &[T],
416 right_bw: u8,
417 max_dict_size: u8,
418) -> (ALPRDDictionary, usize) {
419 assert!(
420 right_bw >= (T::BITS - CUT_LIMIT) as _,
421 "left-parts must be <= 16 bits"
422 );
423
424 let mut counts = HashMap::new();
426 samples
427 .iter()
428 .copied()
429 .map(|v| <T as ALPRDFloat>::to_u16(T::to_bits(v).shr(right_bw as _)))
430 .for_each(|item| *counts.entry(item).or_default() += 1);
431
432 let mut sorted_bit_counts: Vec<(u16, usize)> = counts.into_iter().collect_vec();
434 sorted_bit_counts.sort_by_key(|(_, count)| count.wrapping_neg());
435
436 let mut dictionary = HashMap::with_capacity_and_hasher(max_dict_size as _, FxBuildHasher);
438 let mut code = 0u16;
439 while code < (max_dict_size as _) && (code as usize) < sorted_bit_counts.len() {
440 let (bits, _) = sorted_bit_counts[code as usize];
441 dictionary.insert(bits, code);
442 code += 1;
443 }
444
445 let exception_count: usize = sorted_bit_counts
447 .iter()
448 .skip(code as _)
449 .map(|(_, count)| *count)
450 .sum();
451
452 let max_code = dictionary.len() - 1;
454 let left_bw = bit_width!(max_code) as u8;
455
456 (
457 ALPRDDictionary {
458 dictionary,
459 right_bit_width: right_bw,
460 left_bit_width: left_bw,
461 },
462 exception_count,
463 )
464}
465
466fn estimate_compression_size(
468 right_bw: u8,
469 left_bw: u8,
470 exception_count: usize,
471 sample_n: usize,
472) -> f64 {
473 const EXC_POSITION_SIZE: usize = 16; const EXC_SIZE: usize = 16; let exceptions_size = exception_count * (EXC_POSITION_SIZE + EXC_SIZE);
477 (right_bw as f64) + (left_bw as f64) + ((exceptions_size as f64) / (sample_n as f64))
478}
479
480#[derive(Debug, Default)]
482struct ALPRDDictionary {
483 dictionary: HashMap<u16, u16, FxBuildHasher>,
485 left_bit_width: u8,
487 right_bit_width: u8,
489}