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