Skip to main content

vortex_alp/alp_rd/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use std::hash::Hash;
8use std::hash::Hasher;
9
10use itertools::Itertools;
11use prost::Message;
12use vortex_array::Array;
13use vortex_array::ArrayEq;
14use vortex_array::ArrayHash;
15use vortex_array::ArrayId;
16use vortex_array::ArrayParts;
17use vortex_array::ArrayRef;
18use vortex_array::ArraySlots;
19use vortex_array::ArrayView;
20use vortex_array::EqMode;
21use vortex_array::ExecutionCtx;
22use vortex_array::ExecutionResult;
23use vortex_array::IntoArray;
24use vortex_array::TypedArrayRef;
25use vortex_array::VortexSessionExecute;
26use vortex_array::array_slots;
27use vortex_array::arrays::Primitive;
28use vortex_array::arrays::PrimitiveArray;
29use vortex_array::buffer::BufferHandle;
30use vortex_array::dtype::DType;
31use vortex_array::dtype::Nullability;
32use vortex_array::dtype::PType;
33use vortex_array::patches::PatchSlotIndices;
34use vortex_array::patches::Patches;
35use vortex_array::patches::PatchesData;
36use vortex_array::patches::PatchesMetadata;
37use vortex_array::require_child;
38use vortex_array::require_patches;
39use vortex_array::serde::ArrayChildren;
40use vortex_array::smallvec::smallvec;
41use vortex_array::validity::Validity;
42use vortex_array::vtable::VTable;
43use vortex_array::vtable::ValidityChild;
44use vortex_array::vtable::ValidityVTableFromChild;
45use vortex_buffer::Buffer;
46use vortex_error::VortexExpect;
47use vortex_error::VortexResult;
48use vortex_error::vortex_bail;
49use vortex_error::vortex_ensure_eq;
50use vortex_error::vortex_err;
51use vortex_error::vortex_panic;
52use vortex_session::VortexSession;
53use vortex_session::registry::CachedId;
54
55use crate::alp_rd::rules::RULES;
56use crate::alp_rd_decode;
57
58/// A [`ALPRD`]-encoded Vortex array.
59pub type ALPRDArray = Array<ALPRD>;
60
61#[derive(Clone, prost::Message)]
62pub struct ALPRDMetadata {
63    #[prost(uint32, tag = "1")]
64    right_bit_width: u32,
65    #[prost(uint32, tag = "2")]
66    dict_len: u32,
67    #[prost(uint32, repeated, tag = "3")]
68    dict: Vec<u32>,
69    #[prost(enumeration = "PType", tag = "4")]
70    left_parts_ptype: i32,
71    #[prost(message, tag = "5")]
72    patches: Option<PatchesMetadata>,
73}
74
75impl ArrayHash for ALPRDData {
76    fn array_hash<H: Hasher>(&self, state: &mut H, accuracy: EqMode) {
77        self.left_parts_dictionary.array_hash(state, accuracy);
78        self.right_bit_width.hash(state);
79        self.patches_data.hash(state);
80    }
81}
82
83impl ArrayEq for ALPRDData {
84    fn array_eq(&self, other: &Self, accuracy: EqMode) -> bool {
85        self.left_parts_dictionary
86            .array_eq(&other.left_parts_dictionary, accuracy)
87            && self.right_bit_width == other.right_bit_width
88            && self.patches_data == other.patches_data
89    }
90}
91
92impl VTable for ALPRD {
93    type TypedArrayData = ALPRDData;
94
95    type OperationsVTable = Self;
96    type ValidityVTable = ValidityVTableFromChild;
97
98    fn id(&self) -> ArrayId {
99        static ID: CachedId = CachedId::new("vortex.alprd");
100        *ID
101    }
102
103    fn validate(
104        &self,
105        data: &ALPRDData,
106        dtype: &DType,
107        len: usize,
108        slots: &[Option<ArrayRef>],
109    ) -> VortexResult<()> {
110        let alprd_slots = ALPRDSlotsView::from_slots(slots);
111        validate_parts(
112            dtype,
113            len,
114            alprd_slots.left_parts,
115            alprd_slots.right_parts,
116            patches_from_slots(slots, data.patches_data.as_ref(), len).as_ref(),
117        )
118    }
119
120    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
121        0
122    }
123
124    fn buffer(_array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
125        vortex_panic!("ALPRDArray buffer index {idx} out of bounds")
126    }
127
128    fn buffer_name(_array: ArrayView<'_, Self>, _idx: usize) -> Option<String> {
129        None
130    }
131
132    fn with_buffers(
133        &self,
134        array: ArrayView<'_, Self>,
135        buffers: &[BufferHandle],
136    ) -> VortexResult<ArrayParts<Self>> {
137        vortex_array::vtable::with_empty_buffers(self, array, buffers)
138    }
139
140    fn serialize(
141        array: ArrayView<'_, Self>,
142        _session: &VortexSession,
143    ) -> VortexResult<Option<Vec<u8>>> {
144        let dict = array
145            .left_parts_dictionary()
146            .iter()
147            .map(|&i| i as u32)
148            .collect::<Vec<_>>();
149
150        Ok(Some(
151            ALPRDMetadata {
152                right_bit_width: array.right_bit_width() as u32,
153                dict_len: array.left_parts_dictionary().len() as u32,
154                dict,
155                left_parts_ptype: array.left_parts().dtype().as_ptype() as i32,
156                patches: array
157                    .left_parts_patches()
158                    .map(|p| p.to_metadata(array.len(), p.dtype()))
159                    .transpose()?,
160            }
161            .encode_to_vec(),
162        ))
163    }
164
165    fn deserialize(
166        &self,
167        dtype: &DType,
168        len: usize,
169        metadata: &[u8],
170        _buffers: &[BufferHandle],
171        children: &dyn ArrayChildren,
172        session: &VortexSession,
173    ) -> VortexResult<ArrayParts<Self>> {
174        let metadata = ALPRDMetadata::decode(metadata)?;
175        if children.len() < 2 {
176            vortex_bail!(
177                "Expected at least 2 children for ALPRD encoding, found {}",
178                children.len()
179            );
180        }
181
182        let left_parts_dtype = DType::Primitive(metadata.left_parts_ptype(), dtype.nullability());
183        let left_parts = children.get(0, &left_parts_dtype, len)?;
184        let left_parts_dictionary: Buffer<u16> = metadata.dict.as_slice()
185            [0..metadata.dict_len as usize]
186            .iter()
187            .map(|&i| {
188                u16::try_from(i)
189                    .map_err(|_| vortex_err!("left_parts_dictionary code {i} does not fit in u16"))
190            })
191            .try_collect()?;
192
193        let right_parts_dtype = match &dtype {
194            DType::Primitive(PType::F32, _) => {
195                DType::Primitive(PType::U32, Nullability::NonNullable)
196            }
197            DType::Primitive(PType::F64, _) => {
198                DType::Primitive(PType::U64, Nullability::NonNullable)
199            }
200            _ => vortex_bail!("Expected f32 or f64 dtype, got {:?}", dtype),
201        };
202        let right_parts = children.get(1, &right_parts_dtype, len)?;
203
204        let left_parts_patches = metadata
205            .patches
206            .map(|p| {
207                let indices = children.get(2, &p.indices_dtype()?, p.len()?)?;
208                let values = children.get(3, &left_parts_dtype.as_nonnullable(), p.len()?)?;
209
210                Patches::new(
211                    len,
212                    p.offset()?,
213                    indices,
214                    values,
215                    // TODO(0ax1): handle chunk offsets
216                    None,
217                    &mut session.create_execution_ctx(),
218                )
219            })
220            .transpose()?;
221        let slots = ALPRDData::make_slots(&left_parts, &right_parts, left_parts_patches.as_ref());
222        let data = ALPRDData::new(
223            left_parts_dictionary,
224            u8::try_from(metadata.right_bit_width).map_err(|_| {
225                vortex_err!(
226                    "right_bit_width {} out of u8 range",
227                    metadata.right_bit_width
228                )
229            })?,
230            left_parts_patches,
231        );
232        Ok(ArrayParts::new(
233            self.clone(),
234            dtype.clone(),
235            len,
236            data,
237            slots,
238        ))
239    }
240
241    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
242        ALPRDSlots::NAMES[idx].to_string()
243    }
244
245    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
246        let array = require_child!(array, array.left_parts(), ALPRDSlots::LEFT_PARTS => Primitive);
247        let array =
248            require_child!(array, array.right_parts(), ALPRDSlots::RIGHT_PARTS => Primitive);
249        require_patches!(
250            array,
251            ALPRDSlots::PATCH_INDICES,
252            ALPRDSlots::PATCH_VALUES,
253            ALPRDSlots::PATCH_CHUNK_OFFSETS
254        );
255
256        let dtype = array.dtype().clone();
257        let right_bit_width = array.right_bit_width();
258        let ALPRDDataParts {
259            left_parts,
260            right_parts,
261            left_parts_dictionary,
262            left_parts_patches,
263        } = ALPRDArrayOwnedExt::into_data_parts(array);
264        let ptype = dtype.as_ptype();
265
266        let left_parts = left_parts
267            .try_downcast::<Primitive>()
268            .ok()
269            .vortex_expect("ALPRD execute: left_parts is primitive");
270        let right_parts = right_parts
271            .try_downcast::<Primitive>()
272            .ok()
273            .vortex_expect("ALPRD execute: right_parts is primitive");
274
275        // Decode the left_parts using our builtin dictionary.
276        let left_parts_dict = left_parts_dictionary;
277        let validity = left_parts
278            .as_ref()
279            .validity()?
280            .execute_mask(left_parts.as_ref().len(), ctx)?;
281
282        let decoded_array = if ptype == PType::F32 {
283            PrimitiveArray::new(
284                alp_rd_decode::<f32>(
285                    left_parts.into_buffer_mut::<u16>(),
286                    &left_parts_dict,
287                    right_bit_width,
288                    right_parts.into_buffer_mut::<u32>(),
289                    left_parts_patches,
290                    ctx,
291                )?,
292                Validity::from_mask(validity, dtype.nullability()),
293            )
294        } else {
295            PrimitiveArray::new(
296                alp_rd_decode::<f64>(
297                    left_parts.into_buffer_mut::<u16>(),
298                    &left_parts_dict,
299                    right_bit_width,
300                    right_parts.into_buffer_mut::<u64>(),
301                    left_parts_patches,
302                    ctx,
303                )?,
304                Validity::from_mask(validity, dtype.nullability()),
305            )
306        };
307
308        Ok(ExecutionResult::done(decoded_array.into_array()))
309    }
310
311    fn reduce_parent(
312        array: ArrayView<'_, Self>,
313        parent: &ArrayRef,
314        child_idx: usize,
315    ) -> VortexResult<Option<ArrayRef>> {
316        RULES.evaluate(array, parent, child_idx)
317    }
318}
319
320#[array_slots(ALPRD)]
321pub struct ALPRDSlots {
322    /// The left (most significant) parts of the real-double encoded values.
323    #[slot(0)]
324    pub left_parts: ArrayRef,
325    /// The right (least significant) parts of the real-double encoded values.
326    #[slot(1)]
327    pub right_parts: ArrayRef,
328    /// The indices of left-parts exception values that could not be dictionary-encoded.
329    #[slot(2)]
330    pub patch_indices: Option<ArrayRef>,
331    /// The exception values for left-parts that could not be dictionary-encoded.
332    #[slot(3)]
333    pub patch_values: Option<ArrayRef>,
334    /// Chunk offsets for the left-parts patch indices/values.
335    #[slot(4)]
336    pub patch_chunk_offsets: Option<ArrayRef>,
337}
338
339const LP_PATCH_SLOTS: PatchSlotIndices = PatchSlotIndices {
340    indices: ALPRDSlots::PATCH_INDICES,
341    values: ALPRDSlots::PATCH_VALUES,
342    chunk_offsets: ALPRDSlots::PATCH_CHUNK_OFFSETS,
343};
344
345#[derive(Clone, Debug)]
346pub struct ALPRDData {
347    patches_data: Option<PatchesData>,
348    left_parts_dictionary: Buffer<u16>,
349    right_bit_width: u8,
350}
351
352impl Display for ALPRDData {
353    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
354        write!(f, "right_bit_width: {}", self.right_bit_width)?;
355        if let Some(pd) = &self.patches_data {
356            write!(f, ", patch_offset: {}", pd.offset())?;
357        }
358        Ok(())
359    }
360}
361
362#[derive(Clone, Debug)]
363pub struct ALPRDDataParts {
364    pub left_parts: ArrayRef,
365    pub left_parts_patches: Option<Patches>,
366    pub left_parts_dictionary: Buffer<u16>,
367    pub right_parts: ArrayRef,
368}
369
370#[derive(Clone, Debug)]
371pub struct ALPRD;
372
373impl ALPRD {
374    pub fn try_new(
375        dtype: DType,
376        left_parts: ArrayRef,
377        left_parts_dictionary: Buffer<u16>,
378        right_parts: ArrayRef,
379        right_bit_width: u8,
380        left_parts_patches: Option<Patches>,
381    ) -> VortexResult<ALPRDArray> {
382        let len = left_parts.len();
383        let slots = ALPRDData::make_slots(&left_parts, &right_parts, left_parts_patches.as_ref());
384        let data = ALPRDData::new(left_parts_dictionary, right_bit_width, left_parts_patches);
385        Array::try_from_parts(ArrayParts::new(ALPRD, dtype, len, data, slots))
386    }
387
388    /// # Safety
389    /// See [`ALPRD::try_new`] for preconditions.
390    pub unsafe fn new_unchecked(
391        dtype: DType,
392        left_parts: ArrayRef,
393        left_parts_dictionary: Buffer<u16>,
394        right_parts: ArrayRef,
395        right_bit_width: u8,
396        left_parts_patches: Option<Patches>,
397    ) -> ALPRDArray {
398        let len = left_parts.len();
399        let slots = ALPRDData::make_slots(&left_parts, &right_parts, left_parts_patches.as_ref());
400        let data = unsafe {
401            ALPRDData::new_unchecked(left_parts_dictionary, right_bit_width, left_parts_patches)
402        };
403        unsafe { Array::from_parts_unchecked(ArrayParts::new(ALPRD, dtype, len, data, slots)) }
404    }
405}
406
407impl ALPRDData {
408    /// Build a new `ALPRDArray` from components.
409    pub fn new(
410        left_parts_dictionary: Buffer<u16>,
411        right_bit_width: u8,
412        left_parts_patches: Option<Patches>,
413    ) -> Self {
414        Self {
415            patches_data: left_parts_patches.as_ref().map(PatchesData::from_patches),
416            left_parts_dictionary,
417            right_bit_width,
418        }
419    }
420
421    /// Build a new `ALPRDArray` from components. This does not perform any validation, and instead
422    /// it constructs it from parts.
423    pub(crate) unsafe fn new_unchecked(
424        left_parts_dictionary: Buffer<u16>,
425        right_bit_width: u8,
426        left_parts_patches: Option<Patches>,
427    ) -> Self {
428        Self::new(left_parts_dictionary, right_bit_width, left_parts_patches)
429    }
430
431    fn make_slots(
432        left_parts: &ArrayRef,
433        right_parts: &ArrayRef,
434        patches: Option<&Patches>,
435    ) -> ArraySlots {
436        let mut slots: ArraySlots = smallvec![Some(left_parts.clone()), Some(right_parts.clone())];
437        PatchesData::push_slots(&mut slots, patches);
438        slots
439    }
440
441    /// Return all the owned parts of the array
442    pub fn into_parts(self, left_parts: ArrayRef, right_parts: ArrayRef) -> ALPRDDataParts {
443        ALPRDDataParts {
444            left_parts,
445            left_parts_patches: None,
446            left_parts_dictionary: self.left_parts_dictionary,
447            right_parts,
448        }
449    }
450
451    #[inline]
452    pub fn right_bit_width(&self) -> u8 {
453        self.right_bit_width
454    }
455
456    /// The dictionary that maps the codes in `left_parts` into bit patterns.
457    #[inline]
458    pub fn left_parts_dictionary(&self) -> &Buffer<u16> {
459        &self.left_parts_dictionary
460    }
461}
462
463fn patches_from_slots(
464    slots: &[Option<ArrayRef>],
465    patches_data: Option<&PatchesData>,
466    len: usize,
467) -> Option<Patches> {
468    PatchesData::patches_from_slots(patches_data, len, slots, LP_PATCH_SLOTS)
469}
470
471#[allow(clippy::disallowed_methods)]
472fn validate_parts(
473    dtype: &DType,
474    len: usize,
475    left_parts: &ArrayRef,
476    right_parts: &ArrayRef,
477    left_parts_patches: Option<&Patches>,
478) -> VortexResult<()> {
479    if !dtype.is_float() {
480        vortex_bail!("ALPRDArray given invalid DType ({dtype})");
481    }
482
483    vortex_ensure_eq!(left_parts.len(), len);
484    vortex_ensure_eq!(right_parts.len(), len);
485
486    if !left_parts.dtype().is_unsigned_int() {
487        vortex_bail!("left_parts dtype must be uint");
488    }
489    if dtype.is_nullable() != left_parts.dtype().is_nullable() {
490        vortex_bail!(
491            "ALPRDArray dtype nullability ({}) must match left_parts dtype nullability ({})",
492            dtype,
493            left_parts.dtype()
494        );
495    }
496
497    let expected_right_parts_dtype = match dtype {
498        DType::Primitive(PType::F32, _) => DType::Primitive(PType::U32, Nullability::NonNullable),
499        DType::Primitive(PType::F64, _) => DType::Primitive(PType::U64, Nullability::NonNullable),
500        _ => vortex_bail!("Expected f32 or f64 dtype, got {:?}", dtype),
501    };
502    vortex_ensure_eq!(right_parts.dtype(), &expected_right_parts_dtype);
503
504    if let Some(patches) = left_parts_patches {
505        vortex_ensure_eq!(patches.array_len(), len);
506        // Left-parts exceptions are always all-valid and are stored as the non-nullable left-parts
507        // dtype. Requiring that exact dtype (rather than ignoring nullability) means each
508        // construction path must produce correct patches, removing the need to normalize them.
509        // Non-nullable also implies all-valid, so no separate validity check is required.
510        let expected = left_parts.dtype().as_nonnullable();
511        vortex_ensure_eq!(
512            patches.dtype(),
513            &expected,
514            "patches dtype must be the non-nullable left_parts dtype"
515        );
516    }
517
518    Ok(())
519}
520
521pub trait ALPRDArrayExt: ALPRDArraySlotsExt {
522    fn right_bit_width(&self) -> u8 {
523        ALPRDData::right_bit_width(self)
524    }
525
526    fn left_parts_patches(&self) -> Option<Patches> {
527        patches_from_slots(self.slots(), self.patches_data.as_ref(), self.len())
528    }
529
530    fn left_parts_dictionary(&self) -> &Buffer<u16> {
531        ALPRDData::left_parts_dictionary(self)
532    }
533}
534impl<T: TypedArrayRef<ALPRD>> ALPRDArrayExt for T {}
535
536pub trait ALPRDArrayOwnedExt {
537    fn into_data_parts(self) -> ALPRDDataParts;
538}
539
540impl ALPRDArrayOwnedExt for Array<ALPRD> {
541    fn into_data_parts(self) -> ALPRDDataParts {
542        let left_parts_patches = self.left_parts_patches();
543        let left_parts = self.left_parts().clone();
544        let right_parts = self.right_parts().clone();
545        let mut parts = ALPRDDataParts {
546            left_parts,
547            left_parts_patches: None,
548            left_parts_dictionary: self.left_parts_dictionary().clone(),
549            right_parts,
550        };
551        parts.left_parts_patches = left_parts_patches;
552        parts
553    }
554}
555
556impl ValidityChild<ALPRD> for ALPRD {
557    fn validity_child(array: ArrayView<'_, ALPRD>) -> ArrayRef {
558        array.left_parts().clone()
559    }
560}
561
562#[cfg(test)]
563mod test {
564    use std::sync::LazyLock;
565
566    use prost::Message;
567    use rstest::rstest;
568    use vortex_array::VortexSessionExecute;
569    use vortex_array::arrays::PrimitiveArray;
570    use vortex_array::assert_arrays_eq;
571    use vortex_array::dtype::NativePType;
572    use vortex_array::dtype::PType;
573    use vortex_array::patches::PatchesMetadata;
574    use vortex_array::test_harness::check_metadata;
575    use vortex_session::VortexSession;
576
577    use super::ALPRDMetadata;
578    use crate::ALPRDFloat;
579    use crate::RDEncoderExt;
580    use crate::alp_rd;
581
582    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
583        let session = vortex_array::array_session();
584        crate::initialize(&session);
585        session
586    });
587
588    #[rstest]
589    #[case(vec![0.1f32.next_up(); 1024], 1.123_848_f32)]
590    #[case(vec![0.1f64.next_up(); 1024], 1.123_848_591_110_992_f64)]
591    fn test_array_encode_with_nulls_and_patches<T: ALPRDFloat + NativePType>(
592        #[case] reals: Vec<T>,
593        #[case] seed: T,
594    ) {
595        let mut ctx = SESSION.create_execution_ctx();
596        assert_eq!(reals.len(), 1024, "test expects 1024-length fixture");
597        // Null out some of the values.
598        let mut reals: Vec<Option<T>> = reals.into_iter().map(Some).collect();
599        reals[1] = None;
600        reals[5] = None;
601        reals[900] = None;
602
603        // Create a new array from this.
604        let real_array = PrimitiveArray::from_option_iter(reals.iter().cloned());
605
606        // Pick a seed that we know will trigger lots of patches.
607        let encoder: alp_rd::RDEncoder = alp_rd::RDEncoder::new(&[seed.powi(-2)]);
608
609        let rd_array = encoder.encode(real_array.as_view());
610
611        assert_arrays_eq!(rd_array, PrimitiveArray::from_option_iter(reals), &mut ctx);
612    }
613
614    #[cfg_attr(miri, ignore)]
615    #[test]
616    fn test_alprd_metadata() {
617        check_metadata(
618            "alprd.metadata",
619            &ALPRDMetadata {
620                right_bit_width: u32::MAX,
621                patches: Some(PatchesMetadata::new(
622                    usize::MAX,
623                    usize::MAX,
624                    PType::U64,
625                    None,
626                    None,
627                    None,
628                )),
629                dict: Vec::new(),
630                left_parts_ptype: PType::U64 as i32,
631                dict_len: 8,
632            }
633            .encode_to_vec(),
634        );
635    }
636}