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