Skip to main content

alp/alp_rd/
mod.rs

1use crate::Exceptions;
2use fastlanes::BitPacking;
3use num_traits::{Float, One, PrimInt, Unsigned, Zero};
4use std::marker::PhantomData;
5use std::ops::{Range, Shl, Shr};
6
7/// Returns the number of bits required to represent `value`, with a minimum of one.
8#[inline]
9pub const fn bit_width(value: u64) -> u8 {
10    if value == 0 {
11        1
12    } else {
13        value.ilog2().wrapping_add(1) as u8
14    }
15}
16
17/// Maximum number of bits to cut from the MSB section of each float.
18pub const CUT_LIMIT: usize = 16;
19
20/// Maximum number of entries in the left-parts dictionary.
21pub const MAX_DICT_SIZE: u8 = 8;
22
23/// Target number of values examined when searching for the best cut point.
24///
25/// Dictionary search costs a pass over the sample, so capping it makes [`RDEncoder::new`] cost
26/// roughly the same for a million values as for a few thousand: the dominant left-bit patterns of a
27/// column follow from its exponent distribution, which a few thousand values already characterise.
28///
29/// Patterns the search never sees are not lost — they simply become exceptions in
30/// [`RDEncoder::split`], at a cost the size estimate already accounts for.
31const MAX_SAMPLE: usize = 4096;
32
33/// Length of each contiguous run of values taken by [`SamplePlan::subsample`].
34///
35/// Runs, rather than a fixed stride, are what make subsampling safe. A stride of `len / MAX_SAMPLE`
36/// aliases with any periodicity in the input — interleaved coordinate or embedding columns,
37/// round-robin sensor readings — and a strided sample then observes only one phase of the data.
38/// Measured on interleaved values of two very different magnitudes, striding chose cut points 2.9
39/// to 10.7 bits/value worse than a full scan, with 15-40% of the input left un-encodable. Runs come
40/// within 0.03 bits/value of a full scan on the same input, and touch far fewer cache lines than a
41/// wide stride. See `test_subsample_matches_full_scan_on_periodic_data`.
42const SAMPLE_BLOCK: usize = 64;
43
44mod private {
45    pub trait Sealed {}
46
47    impl Sealed for f32 {}
48    impl Sealed for f64 {}
49}
50
51/// Main trait for ALP-RD encodable floating-point numbers.
52///
53/// Like the paper, we limit this to the IEEE 754 single-precision (`f32`) and double-precision
54/// (`f64`) floating-point types.
55pub trait ALPRDFloat: private::Sealed + Float {
56    /// The unsigned integer type with the same bit-width as the floating-point type.
57    type UINT: PrimInt + BitPacking + Unsigned + One;
58
59    /// Number of bits the value occupies in registers.
60    const BITS: usize = size_of::<Self>() * 8;
61
62    /// Transmutes bit-wise from the unsigned integer type to the floating-point type.
63    fn from_bits(bits: Self::UINT) -> Self;
64
65    /// Transmutes bit-wise into the unsigned integer type.
66    fn to_bits(value: Self) -> Self::UINT;
67
68    /// Converts the unsigned integer type to `u16`, truncating.
69    fn to_u16(bits: Self::UINT) -> u16;
70
71    /// Converts a `u16` to the unsigned integer type, widening.
72    fn from_u16(value: u16) -> Self::UINT;
73}
74
75impl ALPRDFloat for f64 {
76    type UINT = u64;
77
78    #[inline]
79    fn from_bits(bits: Self::UINT) -> Self {
80        f64::from_bits(bits)
81    }
82
83    #[inline]
84    fn to_bits(value: Self) -> Self::UINT {
85        value.to_bits()
86    }
87
88    #[inline]
89    fn to_u16(bits: Self::UINT) -> u16 {
90        bits as u16
91    }
92
93    #[inline]
94    fn from_u16(value: u16) -> Self::UINT {
95        value as u64
96    }
97}
98
99impl ALPRDFloat for f32 {
100    type UINT = u32;
101
102    #[inline]
103    fn from_bits(bits: Self::UINT) -> Self {
104        f32::from_bits(bits)
105    }
106
107    #[inline]
108    fn to_bits(value: Self) -> Self::UINT {
109        value.to_bits()
110    }
111
112    #[inline]
113    fn to_u16(bits: Self::UINT) -> u16 {
114        bits as u16
115    }
116
117    #[inline]
118    fn from_u16(value: u16) -> Self::UINT {
119        value as u32
120    }
121}
122
123/// Encoder for ALP-RD ("real doubles") values.
124///
125/// The encoder calculates its parameters from a single sample of floating-point values,
126/// and then can be applied to many vectors.
127///
128/// ALP-RD uses the algorithm outlined in Section 3.4 of the paper. The crux of it is that the
129/// front (most significant) bits of many double vectors tend to be the same, i.e. most doubles in
130/// a vector often use the same exponent and front bits. Compression proceeds by finding the best
131/// prefix of up to 16 bits that can be collapsed into a dictionary of up to 8 elements. Each
132/// double can then be broken into the front/left `L` bits, which neatly bit-packs down to 1-3 bits
133/// per element (depending on the actual dictionary size). The remaining `R` bits naturally
134/// bit-pack.
135///
136/// In the ideal case, this scheme allows us to store a sequence of doubles in 49 bits-per-value.
137///
138/// Our implementation draws on the MIT-licensed [C++ implementation] provided by the original
139/// authors.
140///
141/// [C++ implementation]: https://github.com/cwida/ALP/blob/main/include/alp/rd.hpp
142pub struct RDEncoder {
143    right_bit_width: u8,
144    codes: Vec<u16>,
145    /// Reverse of `codes`, mapping a left-part bit pattern back to its code.
146    reverse: ReverseDict,
147}
148
149/// Number of slots in a [`ReverseDict::Window`] table.
150///
151/// A power of two, so the index is a mask. Wide enough that a window separating the dictionary is
152/// almost always found, and narrow enough that the table stays a couple of cache lines.
153const REVERSE_SLOTS: usize = 32;
154
155/// One bit per 16-bit lane of a [`ReverseDict::Lanes`] word, in the lane's low bit.
156const LANE_ONES: u128 = 0x0001_0001_0001_0001_0001_0001_0001_0001;
157
158/// One bit per 16-bit lane of a [`ReverseDict::Lanes`] word, in the lane's high bit.
159const LANE_HIGH_BITS: u128 = 0x8000_8000_8000_8000_8000_8000_8000_8000;
160
161/// Reverse of an ALP-RD dictionary: left-part bit pattern to dictionary code.
162///
163/// A dictionary holds at most [`MAX_DICT_SIZE`] patterns, but they are drawn from the whole 16-bit
164/// space, so a map indexed by the pattern itself would need a 64 KiB table — more expensive to
165/// allocate and zero than the entire cut-point search costs for a typical sample. Both variants here
166/// fit in a couple of cache lines and cost a handful of instructions to build.
167///
168/// Neither is approximate: a pattern the dictionary does not hold always reports a miss.
169enum ReverseDict {
170    /// `slots[(pattern >> shift) & (REVERSE_SLOTS - 1)]` holds a `(pattern, code)` pair. A slot
171    /// whose pattern equals the one being looked up is a hit; anything else is a miss.
172    ///
173    /// Indexing on a window of the pattern rather than the whole of it is what keeps the table
174    /// small, and storing each pattern next to its code is what keeps it exact — a pattern that
175    /// shares a window with a dictionary entry still fails the comparison. The window is chosen so
176    /// that no two dictionary entries claim the same slot; unused slots hold the first entry, which
177    /// cannot be mistaken for a hit, since the only pattern equal to it indexes its own slot.
178    Window {
179        slots: [(u16, u16); REVERSE_SLOTS],
180        shift: u8,
181    },
182    /// The dictionary patterns as eight 16-bit lanes of a single word, compared all at once.
183    ///
184    /// The fallback for the dictionaries no window separates, which needs no search to build and
185    /// costs a few more instructions per value than [`ReverseDict::Window`]. Lanes past the
186    /// dictionary's length repeat its first entry, which is harmless: a lane can only match the
187    /// pattern it holds, and the lowest matching lane wins.
188    Lanes(u128),
189}
190
191impl ReverseDict {
192    /// Builds the reverse of `codes`, preferring a window that separates its patterns.
193    fn new(codes: &[u16]) -> Self {
194        let Some(&first) = codes.first() else {
195            return Self::Lanes(0);
196        };
197
198        // Shifts past `16 - log2(REVERSE_SLOTS)` keep fewer bits than the table has slots, so they
199        // can only separate patterns a narrower shift already did.
200        for shift in 0..=(16 - REVERSE_SLOTS.ilog2()) as u8 {
201            let mut slots = [(first, 0u16); REVERSE_SLOTS];
202            let mut occupied = [false; REVERSE_SLOTS];
203            let separated = codes.iter().enumerate().all(|(code, &pattern)| {
204                let slot = Self::index(pattern, shift);
205                // A repeated pattern keeps the lowest code, matching a first-match-wins scan.
206                let free = !occupied[slot] || slots[slot].0 == pattern;
207                if !occupied[slot] {
208                    occupied[slot] = true;
209                    slots[slot] = (pattern, code as u16);
210                }
211                free
212            });
213            if separated {
214                return Self::Window { slots, shift };
215            }
216        }
217
218        let mut lanes = [first; MAX_DICT_SIZE as usize];
219        lanes[..codes.len()].copy_from_slice(codes);
220        Self::Lanes(
221            lanes
222                .iter()
223                .rev()
224                .fold(0u128, |word, &pattern| (word << 16) | u128::from(pattern)),
225        )
226    }
227
228    /// The slot a pattern occupies under a given shift.
229    #[inline]
230    fn index(pattern: u16, shift: u8) -> usize {
231        ((pattern >> shift) as usize) & (REVERSE_SLOTS - 1)
232    }
233}
234
235/// The code a [`ReverseDict::Window`] gives `pattern`, or `None` when it holds no such pattern.
236#[inline]
237fn window_code(slots: &[(u16, u16); REVERSE_SLOTS], shift: u8, pattern: u16) -> Option<u16> {
238    let (stored, code) = slots[ReverseDict::index(pattern, shift)];
239    (stored == pattern).then_some(code)
240}
241
242/// The code a [`ReverseDict::Lanes`] word gives `pattern`, or `None` when no lane holds it.
243#[inline]
244fn lanes_code(lanes: u128, pattern: u16) -> Option<u16> {
245    // Multiplying by a one-per-lane word copies the pattern into every lane, so one xor compares it
246    // against the whole dictionary: a lane is zero exactly where it matches.
247    let diff = u128::from(pattern).wrapping_mul(LANE_ONES) ^ lanes;
248
249    // Borrow-propagating subtraction flags every zero lane, and can additionally flag a lane holding
250    // one directly above a zero lane. Both sit above a genuine match, so the lowest flagged lane is
251    // always a real one.
252    let matches = diff.wrapping_sub(LANE_ONES) & !diff & LANE_HIGH_BITS;
253    (matches != 0).then(|| (matches.trailing_zeros() / 16) as u16)
254}
255
256/// The "cut" ALP-RD vector.
257///
258/// ALP-RD splits a vector of input floating-point numbers into left parts and right parts,
259/// divided at a cut point. The left and right values are held separately.
260pub struct Split<F, U> {
261    /// Dictionary codes for the left parts.
262    left_parts: Vec<u16>,
263
264    /// Exceptions for the `left_parts` that could not be dictionary encoded.
265    left_exceptions: Exceptions<u16>,
266
267    /// Dictionary for encoding the `left_parts`, holding `left_dict_len` live entries.
268    ///
269    /// Stored inline because it never exceeds [`MAX_DICT_SIZE`] entries, so a split does not
270    /// allocate for it. Reach for it through [`Split::left_dict`], which trims it to length.
271    left_dict: [u16; MAX_DICT_SIZE as usize],
272
273    /// Number of live entries in `left_dict`.
274    left_dict_len: u8,
275
276    /// Bit-width for the `left_parts` codes.
277    left_parts_bit_width: u8,
278
279    /// The right parts.
280    right_parts: Vec<U>,
281
282    /// Bit-width for the `right_parts` component.
283    right_parts_bit_width: u8,
284
285    phantom_data: PhantomData<F>,
286}
287
288impl<T, U> Split<T, U> {
289    /// Consumes the parts of the result.
290    pub fn into_parts(self) -> (Vec<u16>, Vec<u16>, Exceptions<u16>, Vec<U>, u8) {
291        // The inline dictionary is materialised only here, for callers that want to own it.
292        let left_dict = self.left_dict[..self.left_dict_len as usize].to_vec();
293        (
294            self.left_parts,
295            left_dict,
296            self.left_exceptions,
297            self.right_parts,
298            self.right_parts_bit_width,
299        )
300    }
301
302    /// Returns the dictionary codes of the left parts.
303    pub fn left_parts(&self) -> &[u16] {
304        &self.left_parts
305    }
306
307    /// Returns the dictionary used to encode the left parts.
308    pub fn left_dict(&self) -> &[u16] {
309        &self.left_dict[..self.left_dict_len as usize]
310    }
311
312    /// Returns the exceptions of the left parts.
313    pub fn left_exceptions(&self) -> &Exceptions<u16> {
314        &self.left_exceptions
315    }
316
317    /// Returns the right parts.
318    pub fn right_parts(&self) -> &[U] {
319        &self.right_parts
320    }
321
322    /// Returns the bit-width of just the left parts, i.e. the width of the dictionary codes.
323    pub fn left_parts_bit_width(&self) -> u8 {
324        self.left_parts_bit_width
325    }
326
327    /// Returns the bit-width of just the right parts.
328    pub fn right_parts_bit_width(&self) -> u8 {
329        self.right_parts_bit_width
330    }
331}
332
333impl<F, U> Split<F, U>
334where
335    F: ALPRDFloat<UINT = U>,
336{
337    /// Decodes back into a vector of the floating-point type.
338    pub fn decode(&self) -> Vec<F> {
339        alp_rd_decode(
340            &self.left_parts,
341            self.left_dict(),
342            self.right_parts_bit_width,
343            &self.right_parts,
344            &self.left_exceptions.positions,
345            &self.left_exceptions.values,
346        )
347    }
348}
349
350impl RDEncoder {
351    /// Builds a new encoder from a sample of doubles.
352    ///
353    /// Long samples are not read in full: past a few thousand values the search examines evenly
354    /// spread contiguous runs instead, so this call costs about the same for a million values as for
355    /// a few thousand. Values in the unexamined gaps still encode correctly — a left-part pattern
356    /// the search never saw becomes an exception in [`Self::split`].
357    ///
358    /// # Panics
359    ///
360    /// Panics if `sample` is empty.
361    pub fn new<T>(sample: &[T]) -> Self
362    where
363        T: ALPRDFloat,
364    {
365        assert!(
366            !sample.is_empty(),
367            "ALP-RD requires a non-empty sample to build a dictionary"
368        );
369
370        let plan = SamplePlan::subsample(sample.len(), MAX_SAMPLE, SAMPLE_BLOCK);
371        let dictionary = find_best_dictionary::<T>(sample, &plan);
372
373        Self::from_parts(dictionary.right_bit_width, dictionary.patterns().to_vec())
374    }
375
376    /// Builds a new encoder from known parameters.
377    ///
378    /// # Panics
379    ///
380    /// Panics if `codes` holds more than [`MAX_DICT_SIZE`] entries.
381    pub fn from_parts(right_bit_width: u8, codes: Vec<u16>) -> Self {
382        // Beyond MAX_DICT_SIZE the codes no longer fit the bit-width the decoder assumes, and
383        // `alp_rd_combine_codes_inplace` would mask them into the wrong dictionary slot.
384        assert!(
385            codes.len() <= MAX_DICT_SIZE as usize,
386            "ALP-RD dictionary must hold at most MAX_DICT_SIZE entries"
387        );
388
389        let reverse = ReverseDict::new(&codes);
390
391        Self {
392            right_bit_width,
393            codes,
394            reverse,
395        }
396    }
397
398    /// Returns the bit-width of the right (least significant) part of each value.
399    #[inline]
400    pub fn right_bit_width(&self) -> u8 {
401        self.right_bit_width
402    }
403
404    /// Returns the bit-width of the dictionary codes of the left (most significant) parts.
405    #[inline]
406    pub fn left_bit_width(&self) -> u8 {
407        bit_width(self.codes.len().saturating_sub(1) as u64)
408    }
409
410    /// Returns the dictionary of left parts, indexed by code.
411    #[inline]
412    pub fn codes(&self) -> &[u16] {
413        &self.codes
414    }
415
416    /// Encodes the floating-point values into a [`Split`].
417    ///
418    /// # Panics
419    ///
420    /// Panics if the encoder holds no dictionary entries, which only an empty `codes` passed to
421    /// [`Self::from_parts`] can produce.
422    pub fn split<T>(&self, doubles: &[T]) -> Split<T, T::UINT>
423    where
424        T: ALPRDFloat,
425    {
426        let (left_parts, right_parts, exception_pos, exception_values) = self.split_parts(doubles);
427
428        // TODO(aduffy): pack the exception positions.
429        let left_exceptions = Exceptions::new(exception_values, exception_pos);
430
431        // `from_parts` bounds the dictionary at MAX_DICT_SIZE, so this always fits.
432        let mut left_dict = [0u16; MAX_DICT_SIZE as usize];
433        left_dict[..self.codes.len()].copy_from_slice(&self.codes);
434
435        Split {
436            left_parts,
437            left_exceptions,
438            left_dict,
439            left_dict_len: self.codes.len() as u8,
440            left_parts_bit_width: self.left_bit_width(),
441            right_parts,
442            right_parts_bit_width: self.right_bit_width,
443            phantom_data: PhantomData,
444        }
445    }
446
447    /// Splits the floating-point values into their dictionary-encoded left parts, their right
448    /// parts, and the positions and values of the left parts that are not in the dictionary.
449    ///
450    /// The left parts are returned as dictionary codes, packable into
451    /// [`Self::left_bit_width`] bits; the right parts are packable into
452    /// [`Self::right_bit_width`] bits. Positions of exceptions hold a code of zero.
453    ///
454    /// # Panics
455    ///
456    /// Panics if the encoder holds no dictionary entries, which only an empty `codes` passed to
457    /// [`Self::from_parts`] can produce.
458    pub fn split_parts<T>(&self, doubles: &[T]) -> (Vec<u16>, Vec<T::UINT>, Vec<u64>, Vec<u16>)
459    where
460        T: ALPRDFloat,
461    {
462        assert!(
463            !self.codes.is_empty(),
464            "codes lookup table must be populated before RD encoding"
465        );
466
467        // Resolving the reverse dictionary out here specialises the hot loop for the one this
468        // encoder holds, rather than re-testing that for every value.
469        match &self.reverse {
470            ReverseDict::Window { slots, shift } => {
471                self.split_parts_with(doubles, |pattern| window_code(slots, *shift, pattern))
472            }
473            ReverseDict::Lanes(lanes) => {
474                self.split_parts_with(doubles, |pattern| lanes_code(*lanes, pattern))
475            }
476        }
477    }
478
479    /// [`Self::split_parts`], with the reverse dictionary already resolved to a single lookup.
480    #[inline(always)]
481    fn split_parts_with<T, L>(
482        &self,
483        doubles: &[T],
484        lookup: L,
485    ) -> (Vec<u16>, Vec<T::UINT>, Vec<u64>, Vec<u16>)
486    where
487        T: ALPRDFloat,
488        L: Fn(u16) -> Option<u16>,
489    {
490        let mut left_parts: Vec<u16> = Vec::with_capacity(doubles.len());
491        let mut right_parts: Vec<T::UINT> = Vec::with_capacity(doubles.len());
492        let mut exception_pos: Vec<u64> = Vec::with_capacity(doubles.len() / 4);
493        let mut exception_values: Vec<u16> = Vec::with_capacity(doubles.len() / 4);
494
495        // Mask for the right parts.
496        let right_mask = T::UINT::one().shl(self.right_bit_width as _) - T::UINT::one();
497
498        // Cut each value in two and dict-encode its left part in the same pass, keeping track of
499        // exceptions.
500        for (idx, v) in doubles.iter().copied().enumerate() {
501            let bits = T::to_bits(v);
502            right_parts.push(bits & right_mask);
503
504            let pattern = <T as ALPRDFloat>::to_u16(bits.shr(self.right_bit_width as _));
505            match lookup(pattern) {
506                Some(code) => left_parts.push(code),
507                None => {
508                    // Exceptions hold a code of zero and carry their true pattern out-of-band.
509                    exception_values.push(pattern);
510                    exception_pos.push(idx as u64);
511                    left_parts.push(0);
512                }
513            }
514        }
515
516        (left_parts, right_parts, exception_pos, exception_values)
517    }
518}
519
520/// Decodes a vector of ALP-RD encoded values back into their original floating-point format.
521///
522/// # Panics
523///
524/// Panics if `left_parts` and `right_parts` differ in length, or if `exc_pos` and `exceptions`
525/// differ in length.
526pub fn alp_rd_decode<T: ALPRDFloat>(
527    left_parts: &[u16],
528    left_parts_dict: &[u16],
529    right_bit_width: u8,
530    right_parts: &[T::UINT],
531    exc_pos: &[u64],
532    exceptions: &[u16],
533) -> Vec<T> {
534    assert_eq!(
535        left_parts.len(),
536        right_parts.len(),
537        "alp_rd_decode: left_parts.len != right_parts.len"
538    );
539
540    assert_eq!(
541        exc_pos.len(),
542        exceptions.len(),
543        "alp_rd_decode: exc_pos.len != exceptions.len"
544    );
545
546    let mut decoded: Vec<T::UINT> = right_parts.to_vec();
547
548    if exc_pos.is_empty() {
549        // Non-patched fast path: every code maps through the dictionary, so we can
550        // pre-shift the entire dictionary once and reduce the per-element hot loop to
551        // a single table lookup + OR.
552        alp_rd_combine_codes_inplace::<T>(
553            &mut decoded,
554            left_parts,
555            left_parts_dict,
556            right_bit_width,
557        );
558    } else {
559        // Patched path: some left-part codes map to exception values that live outside
560        // the dictionary. We must dictionary-decode first, then overwrite the exceptions,
561        // before we can combine with right-parts.
562        let mut left_parts = left_parts.to_vec();
563        alp_rd_dict_decode_inplace(&mut left_parts, left_parts_dict);
564        alp_rd_apply_patches(&mut left_parts, exc_pos, exceptions, 0);
565        alp_rd_combine_inplace::<T>(&mut decoded, &left_parts, right_bit_width);
566    }
567
568    decoded.into_iter().map(T::from_bits).collect()
569}
570
571/// Replaces each dictionary code in `left_parts` with the left bit-pattern it encodes.
572///
573/// # Panics
574///
575/// Panics if `left_parts` contains a code that is not in `left_parts_dict`.
576#[inline]
577pub fn alp_rd_dict_decode_inplace(left_parts: &mut [u16], left_parts_dict: &[u16]) {
578    for code in left_parts.iter_mut() {
579        *code = left_parts_dict[*code as usize];
580    }
581}
582
583/// Overwrites the exception positions of already dictionary-decoded `left_parts` with their true
584/// left bit-patterns.
585///
586/// `offset` is subtracted from every index, to support patches that are stored relative to the
587/// start of an unsliced array.
588///
589/// # Panics
590///
591/// Panics if `indices` and `patch_values` differ in length, or if an index is out of bounds.
592#[inline]
593pub fn alp_rd_apply_patches<I: PrimInt>(
594    left_parts: &mut [u16],
595    indices: &[I],
596    patch_values: &[u16],
597    offset: usize,
598) {
599    assert_eq!(
600        indices.len(),
601        patch_values.len(),
602        "alp_rd_apply_patches: indices.len != patch_values.len"
603    );
604
605    indices
606        .iter()
607        .copied()
608        .zip(patch_values.iter().copied())
609        .for_each(|(idx, value)| {
610            let idx = idx
611                .to_usize()
612                .expect("alp_rd_apply_patches: index out of range")
613                - offset;
614            left_parts[idx] = value;
615        });
616}
617
618/// Combines dictionary-decoded `left_parts` into `right_parts` in-place, so that each element of
619/// `right_parts` holds the bit-pattern of the original float.
620///
621/// # Panics
622///
623/// Panics if `left_parts` and `right_parts` differ in length.
624#[inline]
625pub fn alp_rd_combine_inplace<T: ALPRDFloat>(
626    right_parts: &mut [T::UINT],
627    left_parts: &[u16],
628    right_bit_width: u8,
629) {
630    assert_eq!(
631        left_parts.len(),
632        right_parts.len(),
633        "alp_rd_combine_inplace: left_parts.len != right_parts.len"
634    );
635
636    let shift = right_bit_width as usize;
637    for (right, left) in right_parts.iter_mut().zip(left_parts.iter().copied()) {
638        *right = (<T as ALPRDFloat>::from_u16(left) << shift) | *right;
639    }
640}
641
642/// Combines dictionary-encoded left parts into `right_parts` in-place, so that each element of
643/// `right_parts` holds the bit-pattern of the original float.
644///
645/// This is the unpatched fast path: the dictionary is pre-shifted once, reducing the hot loop to
646/// a table lookup and an OR. Codes are masked into the dictionary, so codes beyond the dictionary
647/// size decode to garbage rather than panicking.
648///
649/// # Panics
650///
651/// Panics if `left_parts` and `right_parts` differ in length, or if the dictionary holds more
652/// than [`MAX_DICT_SIZE`] entries.
653#[inline]
654pub fn alp_rd_combine_codes_inplace<T: ALPRDFloat>(
655    right_parts: &mut [T::UINT],
656    left_parts: &[u16],
657    left_parts_dict: &[u16],
658    right_bit_width: u8,
659) {
660    assert_eq!(
661        left_parts.len(),
662        right_parts.len(),
663        "alp_rd_combine_codes_inplace: left_parts.len != right_parts.len"
664    );
665    assert!(
666        left_parts_dict.len() <= MAX_DICT_SIZE as usize,
667        "alp_rd_combine_codes_inplace: dictionary larger than MAX_DICT_SIZE"
668    );
669
670    let shift = right_bit_width as usize;
671    let mut shifted_dict = [T::UINT::zero(); MAX_DICT_SIZE as usize];
672    for (i, &entry) in left_parts_dict.iter().enumerate() {
673        shifted_dict[i] = <T as ALPRDFloat>::from_u16(entry) << shift;
674    }
675
676    // Masking keeps the lookup in-bounds without a branch; codes are < dictionary size by
677    // construction.
678    const CODE_MASK: usize = MAX_DICT_SIZE as usize - 1;
679    for (right, code) in right_parts.iter_mut().zip(left_parts.iter().copied()) {
680        *right = shifted_dict[(code as usize) & CODE_MASK] | *right;
681    }
682}
683
684/// Which elements of the input to examine when searching for the best cut point.
685///
686/// A plan is a set of disjoint, ascending, contiguous ranges. It is built once and shared by every
687/// candidate cut point, so the trials all score the same values and their estimates stay
688/// comparable.
689#[derive(Debug)]
690struct SamplePlan {
691    ranges: Vec<Range<usize>>,
692    count: usize,
693}
694
695impl SamplePlan {
696    /// A plan covering every element of a `len`-element input.
697    fn full(len: usize) -> Self {
698        let mut ranges = Vec::new();
699        if len > 0 {
700            ranges.push(0..len);
701        }
702        Self { ranges, count: len }
703    }
704
705    /// A plan covering at most `max_sample` elements of a `len`-element input, as evenly spread
706    /// contiguous runs of `block` values.
707    ///
708    /// Falls back to [`Self::full`] for inputs already at or below `max_sample`.
709    fn subsample(len: usize, max_sample: usize, block: usize) -> Self {
710        let block = block.clamp(1, max_sample.max(1));
711        if len <= max_sample {
712            return Self::full(len);
713        }
714
715        let n_blocks = (max_sample / block).max(1);
716        // `len > max_sample >= n_blocks * block` puts the spacing at `block` or more, so the runs
717        // stay disjoint, and the last one starts at `len - block` or earlier, so all are in bounds.
718        // Multiplying the floored spacing (rather than dividing a product) keeps this from
719        // overflowing on absurd lengths.
720        let spacing = if n_blocks > 1 {
721            (len - block) / (n_blocks - 1)
722        } else {
723            0
724        };
725        let ranges: Vec<Range<usize>> = (0..n_blocks)
726            .map(|i| {
727                let start = i * spacing;
728                start..start + block
729            })
730            .collect();
731
732        let count = ranges.iter().map(Range::len).sum();
733        Self { ranges, count }
734    }
735
736    /// The ranges to sample, ascending and disjoint.
737    fn ranges(&self) -> &[Range<usize>] {
738        &self.ranges
739    }
740
741    /// Total number of elements the plan visits.
742    fn count(&self) -> usize {
743        self.count
744    }
745}
746
747/// Finds the best "cut point" for a set of floating-point values, i.e. the one with the lowest
748/// estimated compressed size, considering only the elements `plan` selects.
749///
750/// The [`CUT_LIMIT`] candidate cut points are all nested: cutting after `p` bits groups values by
751/// their leading `p` bits, and each of those groups is the union of two groups of the cut one bit
752/// deeper. So the search reads the sample once, at the deepest cut, and then walks outwards by
753/// merging pairs of adjacent groups — after which every remaining candidate costs a pass over the
754/// distinct patterns rather than over the sample.
755fn find_best_dictionary<T: ALPRDFloat>(samples: &[T], plan: &SamplePlan) -> ALPRDDictionary {
756    // Group counts at the deepest cut point, ascending by pattern, which is what lets the merge
757    // below combine each pair of sibling groups by looking only at its neighbour.
758    let mut patterns = gather_patterns::<T>(samples, plan);
759    radix_sort_u16(&mut patterns);
760    let mut groups = run_length_encode(&patterns);
761
762    let mut best_est_size = f64::MAX;
763    let mut best_dict = ALPRDDictionary::default();
764    for p in (1..=CUT_LIMIT).rev() {
765        let dictionary = select_dictionary((T::BITS - p) as u8, &groups);
766        let estimated_size = estimate_compression_size(
767            dictionary.right_bit_width,
768            dictionary.left_bit_width,
769            // Values whose pattern missed the dictionary. Per sampled element, not per input
770            // element, so that the term stays on the same scale as the counts it came from.
771            plan.count() - dictionary.encodable,
772            plan.count(),
773        );
774        // Cut points are visited deepest-first, so accepting ties leaves the shallowest of the
775        // equally-good cuts holding the title, as a search running the other way would.
776        if estimated_size <= best_est_size {
777            best_est_size = estimated_size;
778            best_dict = dictionary;
779        }
780
781        // Step out one bit, dropping the low bit of every pattern.
782        merge_sibling_groups(&mut groups);
783    }
784
785    best_dict
786}
787
788/// Collects the leading [`CUT_LIMIT`] bits of every value the plan selects.
789///
790/// Patterns at shallower cut points are prefixes of these, so one pass over the sample serves every
791/// candidate cut point.
792fn gather_patterns<T: ALPRDFloat>(samples: &[T], plan: &SamplePlan) -> Vec<u16> {
793    let shift = (T::BITS - CUT_LIMIT) as u32;
794    let mut patterns = Vec::with_capacity(plan.count());
795    for range in plan.ranges() {
796        patterns.extend(
797            samples[range.start..range.end]
798                .iter()
799                .map(|value| <T as ALPRDFloat>::to_u16(T::to_bits(*value).shr(shift as _))),
800        );
801    }
802    patterns
803}
804
805/// Sorts 16-bit keys with a two-digit least-significant-digit radix sort.
806///
807/// Counting the keys of a sample directly into a table indexed by the key would need 256 KiB, more
808/// than a typical sample is long; a comparison sort of the same keys costs several times this. Both
809/// digit passes are sequential reads and writes over the sample and a 1 KiB histogram.
810fn radix_sort_u16(values: &mut Vec<u16>) {
811    const DIGIT_BITS: u32 = 8;
812    const DIGITS: usize = 1 << DIGIT_BITS;
813
814    let mut scratch: Vec<u16> = Vec::with_capacity(values.len());
815    for shift in [0, DIGIT_BITS] {
816        let mut offsets = [0u32; DIGITS];
817        for &value in values.iter() {
818            offsets[((value >> shift) as usize) & (DIGITS - 1)] += 1;
819        }
820
821        // A digit the whole sample agrees on cannot reorder anything, and skipping its scatter is
822        // most of the sort for the low-cardinality data ALP-RD is aimed at.
823        if offsets.iter().any(|&count| count as usize == values.len()) {
824            continue;
825        }
826
827        let mut start = 0;
828        for offset in offsets.iter_mut() {
829            let count = *offset;
830            *offset = start;
831            start += count;
832        }
833
834        scratch.clear();
835        scratch.resize(values.len(), 0);
836        for &value in values.iter() {
837            let digit = ((value >> shift) as usize) & (DIGITS - 1);
838            scratch[offsets[digit] as usize] = value;
839            offsets[digit] += 1;
840        }
841        std::mem::swap(values, &mut scratch);
842    }
843}
844
845/// Counts the runs of equal patterns in a sorted slice, as `(pattern, count)` ascending.
846fn run_length_encode(sorted: &[u16]) -> Vec<(u16, u32)> {
847    let mut groups: Vec<(u16, u32)> = Vec::new();
848    for &pattern in sorted {
849        match groups.last_mut() {
850            Some((last, count)) if *last == pattern => *count += 1,
851            _ => groups.push((pattern, 1)),
852        }
853    }
854    groups
855}
856
857/// Rewrites groups of patterns as groups of patterns one bit shorter, summing the pairs of groups
858/// that share the shorter pattern.
859///
860/// Input and output are both ascending by pattern, so the pair of groups that merge are always
861/// adjacent and the rewrite is a single pass that never outruns itself.
862fn merge_sibling_groups(groups: &mut Vec<(u16, u32)>) {
863    let mut merged = 0;
864    let mut read = 0;
865    while read < groups.len() {
866        let (pattern, mut count) = groups[read];
867        let pattern = pattern >> 1;
868        read += 1;
869        if let Some(&(sibling, sibling_count)) = groups.get(read)
870            && sibling >> 1 == pattern
871        {
872            count += sibling_count;
873            read += 1;
874        }
875        groups[merged] = (pattern, count);
876        merged += 1;
877    }
878    groups.truncate(merged);
879}
880
881/// Picks the [`MAX_DICT_SIZE`] most common patterns as the dictionary for a cut point.
882///
883/// `groups` must be the pattern counts at that cut point, ascending by pattern.
884fn select_dictionary(right_bw: u8, groups: &[(u16, u32)]) -> ALPRDDictionary {
885    let mut patterns = [0u16; MAX_DICT_SIZE as usize];
886    let mut counts = [0u32; MAX_DICT_SIZE as usize];
887    let mut len = 0usize;
888
889    for &(pattern, count) in groups {
890        // Groups arrive ascending by pattern, so a group tying with one already held belongs after
891        // it: inserting only ahead of strictly smaller counts keeps the dictionary — and so the
892        // encoded output — decided by the data rather than by chance.
893        let Some(at) = counts[..len].iter().position(|&held| count > held) else {
894            if len < patterns.len() {
895                patterns[len] = pattern;
896                counts[len] = count;
897                len += 1;
898            }
899            continue;
900        };
901
902        len = (len + 1).min(patterns.len());
903        patterns[at..len].rotate_right(1);
904        counts[at..len].rotate_right(1);
905        patterns[at] = pattern;
906        counts[at] = count;
907    }
908
909    ALPRDDictionary {
910        patterns,
911        len: len as u8,
912        // The left bit-width follows from the number of codes the dictionary actually holds.
913        left_bit_width: bit_width(len.saturating_sub(1) as u64),
914        right_bit_width: right_bw,
915        encodable: counts[..len].iter().map(|&count| count as usize).sum(),
916    }
917}
918
919/// Estimates the bits-per-value when using these compression settings.
920fn estimate_compression_size(
921    right_bw: u8,
922    left_bw: u8,
923    exception_count: usize,
924    sample_n: usize,
925) -> f64 {
926    const EXC_POSITION_SIZE: usize = 16; // two bytes for exception position.
927    const EXC_SIZE: usize = 16; // two bytes for each exception (up to 16 front bits).
928
929    let exceptions_size = exception_count * (EXC_POSITION_SIZE + EXC_SIZE);
930    (right_bw as f64) + (left_bw as f64) + ((exceptions_size as f64) / (sample_n as f64))
931}
932
933/// The ALP-RD dictionary, encoding the "left parts" and their dictionary encoding.
934#[derive(Debug, Default)]
935struct ALPRDDictionary {
936    /// The left-part bit patterns of the dictionary, indexed by the code that encodes them, of
937    /// which `len` are live.
938    patterns: [u16; MAX_DICT_SIZE as usize],
939    /// Number of live entries in `patterns`.
940    len: u8,
941    /// The (compressed) left bit-width. This is after bit-packing the dictionary codes.
942    left_bit_width: u8,
943    /// The right bit-width. This is the bit-packed width of each of the "real double" values.
944    right_bit_width: u8,
945    /// How many of the counted values hold a pattern this dictionary encodes. The rest are
946    /// exceptions.
947    encodable: usize,
948}
949
950impl ALPRDDictionary {
951    /// The live left-part patterns, indexed by their code.
952    fn patterns(&self) -> &[u16] {
953        &self.patterns[..self.len as usize]
954    }
955}
956
957#[cfg(test)]
958mod test {
959    use super::{
960        ALPRDFloat, CUT_LIMIT, MAX_SAMPLE, REVERSE_SLOTS, ReverseDict, SAMPLE_BLOCK, SamplePlan,
961        estimate_compression_size, find_best_dictionary, lanes_code, window_code,
962    };
963    use crate::{
964        MAX_DICT_SIZE, RDEncoder, alp_rd_apply_patches, alp_rd_combine_codes_inplace,
965        alp_rd_combine_inplace, alp_rd_decode, alp_rd_dict_decode_inplace, bit_width,
966    };
967    use std::cmp::Reverse;
968    use std::collections::HashMap;
969
970    /// A small linear congruential generator, so the tests need no extra dependencies.
971    struct Lcg(u64);
972
973    impl Lcg {
974        fn new() -> Self {
975            Self(0x517C_C1B7_2722_0A95)
976        }
977
978        fn next_bits(&mut self) -> u64 {
979            self.0 = self
980                .0
981                .wrapping_mul(6_364_136_223_846_793_005)
982                .wrapping_add(1_442_695_040_888_963_407);
983            self.0
984        }
985
986        fn next_unit(&mut self) -> f64 {
987            (self.next_bits() >> 11) as f64 / (1u64 << 53) as f64
988        }
989
990        /// A log-normal-ish draw, spanning enough exponents to look like real scientific data.
991        fn next_log_normal(&mut self) -> f64 {
992            (self.next_unit() * 6.0 - 1.0).exp() * 1000.0
993        }
994    }
995
996    /// What a cut point actually costs over a whole input.
997    struct CutPointCost {
998        /// Compressed bits per value, exceptions included at the same weight the encoder's own
999        /// estimate uses.
1000        bits_per_value: f64,
1001        /// Fraction of values whose left parts fall outside the best dictionary at this cut point.
1002        exception_rate: f64,
1003    }
1004
1005    /// Measures what choosing `right_bw` costs over all of `values`.
1006    ///
1007    /// Sampling strategies are compared on this rather than on the `right_bit_width` they name.
1008    /// The width is only a proxy: adjacent cut points routinely score within a rounding error of
1009    /// each other, so two samplers can name different widths and still compress identically well.
1010    /// What matters is the cost of the choice.
1011    fn cut_point_cost(values: &[f64], right_bw: u8) -> CutPointCost {
1012        // Position plus value, matching `estimate_compression_size`.
1013        const EXCEPTION_BITS: f64 = 32.0;
1014
1015        let mut counts = HashMap::new();
1016        for value in values {
1017            *counts
1018                .entry((value.to_bits() >> right_bw) as u16)
1019                .or_insert(0usize) += 1;
1020        }
1021        let mut sorted: Vec<usize> = counts.into_values().collect();
1022        sorted.sort_unstable_by(|a, b| b.cmp(a));
1023
1024        let dict_len = (MAX_DICT_SIZE as usize).min(sorted.len());
1025        let encodable: usize = sorted.iter().take(dict_len).sum();
1026        let exception_rate = 1.0 - (encodable as f64 / values.len() as f64);
1027        let left_bw = bit_width(dict_len.saturating_sub(1) as u64);
1028
1029        CutPointCost {
1030            bits_per_value: right_bw as f64 + left_bw as f64 + exception_rate * EXCEPTION_BITS,
1031            exception_rate,
1032        }
1033    }
1034
1035    /// Compares what the subsampling encoder costs against what a full scan would have cost.
1036    fn subsample_vs_full_scan(values: &[f64]) -> (CutPointCost, CutPointCost) {
1037        let subsampled = RDEncoder::new(values).right_bit_width();
1038        let full =
1039            find_best_dictionary::<f64>(values, &SamplePlan::full(values.len())).right_bit_width;
1040        (
1041            cut_point_cost(values, subsampled),
1042            cut_point_cost(values, full),
1043        )
1044    }
1045
1046    /// The cut-point search, written the obvious way: count the left parts at every candidate cut
1047    /// point, score each, keep the cheapest. [`find_best_dictionary`] rearranges this into a single
1048    /// pass over the sample and must land on exactly the same dictionary.
1049    fn reference_best_dictionary<T: ALPRDFloat>(
1050        samples: &[T],
1051        plan: &SamplePlan,
1052    ) -> (u8, Vec<u16>) {
1053        let mut best: Option<(f64, u8, Vec<u16>)> = None;
1054
1055        for p in 1..=CUT_LIMIT {
1056            let right_bw = (T::BITS - p) as u8;
1057            let mut counts: HashMap<u16, usize> = HashMap::new();
1058            for range in plan.ranges() {
1059                for value in &samples[range.start..range.end] {
1060                    let pattern =
1061                        <T as ALPRDFloat>::to_u16(T::to_bits(*value) >> right_bw as usize);
1062                    *counts.entry(pattern).or_default() += 1;
1063                }
1064            }
1065
1066            let mut sorted: Vec<(u16, usize)> = counts.into_iter().collect();
1067            sorted.sort_unstable_by_key(|&(pattern, count)| (Reverse(count), pattern));
1068            let dict_len = (MAX_DICT_SIZE as usize).min(sorted.len());
1069            let exceptions: usize = sorted[dict_len..].iter().map(|&(_, count)| count).sum();
1070            let estimate = estimate_compression_size(
1071                right_bw,
1072                bit_width(dict_len.saturating_sub(1) as u64),
1073                exceptions,
1074                plan.count(),
1075            );
1076
1077            if best
1078                .as_ref()
1079                .is_none_or(|&(previous, _, _)| estimate < previous)
1080            {
1081                let codes = sorted[..dict_len].iter().map(|&(bits, _)| bits).collect();
1082                best = Some((estimate, right_bw, codes));
1083            }
1084        }
1085
1086        let (_, right_bw, codes) = best.expect("the search always considers a cut point");
1087        (right_bw, codes)
1088    }
1089
1090    /// Values shaped the ways that pull the search in different directions.
1091    fn distributions(len: usize) -> Vec<(&'static str, Vec<f64>)> {
1092        let mut rng = Lcg::new();
1093        let log_normal = (0..len).map(|_| rng.next_log_normal()).collect();
1094        let mut rng = Lcg::new();
1095        let narrow = (0..len).map(|_| 1.0 + rng.next_unit()).collect();
1096        let mut rng = Lcg::new();
1097        // Two magnitudes and both signs, so the left parts split into distant clusters.
1098        let bimodal = (0..len)
1099            .map(|i| {
1100                let magnitude = if i % 3 == 0 { -1e-8 } else { 1e12 };
1101                magnitude * (1.0 + rng.next_unit())
1102            })
1103            .collect();
1104        // Every value identical: one pattern, so most cut points score the same and the tie-break
1105        // decides the winner.
1106        let constant = vec![7.5f64; len];
1107        // More distinct patterns than the dictionary can hold at any cut point.
1108        let mut rng = Lcg::new();
1109        let high_cardinality = (0..len)
1110            .map(|_| f64::from_bits(rng.next_bits() & 0x7FEF_FFFF_FFFF_FFFF))
1111            .collect();
1112
1113        vec![
1114            ("log_normal", log_normal),
1115            ("narrow", narrow),
1116            ("bimodal", bimodal),
1117            ("constant", constant),
1118            ("high_cardinality", high_cardinality),
1119        ]
1120    }
1121
1122    #[test]
1123    fn test_search_matches_reference_search() {
1124        for len in [1usize, 2, 63, 64, 1000, 4 * MAX_SAMPLE + 7] {
1125            for (name, values) in distributions(len) {
1126                for plan in [
1127                    SamplePlan::full(len),
1128                    SamplePlan::subsample(len, MAX_SAMPLE, SAMPLE_BLOCK),
1129                ] {
1130                    let (right_bw, codes) = reference_best_dictionary::<f64>(&values, &plan);
1131                    let actual = find_best_dictionary::<f64>(&values, &plan);
1132                    assert_eq!(
1133                        (actual.right_bit_width, actual.patterns()),
1134                        (right_bw, codes.as_slice()),
1135                        "{name} at len {len} with {} sampled",
1136                        plan.count()
1137                    );
1138
1139                    let floats: Vec<f32> = values.iter().map(|&v| v as f32).collect();
1140                    let (right_bw, codes) = reference_best_dictionary::<f32>(&floats, &plan);
1141                    let actual = find_best_dictionary::<f32>(&floats, &plan);
1142                    assert_eq!(
1143                        (actual.right_bit_width, actual.patterns()),
1144                        (right_bw, codes.as_slice()),
1145                        "{name} as f32 at len {len} with {} sampled",
1146                        plan.count()
1147                    );
1148                }
1149            }
1150        }
1151    }
1152
1153    /// Both reverse-dictionary variants must answer for every one of the 65536 possible patterns
1154    /// exactly as a scan of the dictionary would.
1155    #[test]
1156    fn test_reverse_dict_variants_match_a_scan() {
1157        let mut rng = Lcg::new();
1158        let dictionaries = [
1159            vec![0u16],
1160            vec![0x3FF0, 0x3FF1, 0x3FF2],
1161            // Repeated patterns: the lowest code must win, as in a first-match-wins scan.
1162            vec![0x1234, 0x1234, 0x0001],
1163            // Defeats every window, so this one exercises the lane fallback.
1164            vec![0x0000, 0x8000, 0x0001],
1165            (0..MAX_DICT_SIZE as u16).map(|i| i * 0x1111).collect(),
1166            (0..MAX_DICT_SIZE).map(|_| rng.next_bits() as u16).collect(),
1167        ];
1168
1169        for codes in dictionaries {
1170            let reverse = ReverseDict::new(&codes);
1171            for pattern in 0..=u16::MAX {
1172                let expected = codes
1173                    .iter()
1174                    .position(|&bits| bits == pattern)
1175                    .map(|code| code as u16);
1176                let actual = match &reverse {
1177                    ReverseDict::Window { slots, shift } => window_code(slots, *shift, pattern),
1178                    ReverseDict::Lanes(lanes) => lanes_code(*lanes, pattern),
1179                };
1180                assert_eq!(
1181                    actual, expected,
1182                    "dictionary {codes:x?}, pattern {pattern:#06x}"
1183                );
1184            }
1185        }
1186    }
1187
1188    /// The lane fallback is reachable — patterns differing only in the top bit share every window —
1189    /// so encoding must be correct when a dictionary lands on it.
1190    #[test]
1191    fn test_lane_fallback_round_trips() {
1192        let codes = vec![0x0000u16, 0x8000, 0x0001];
1193        assert!(
1194            matches!(ReverseDict::new(&codes), ReverseDict::Lanes(_)),
1195            "expected this dictionary to defeat every window"
1196        );
1197
1198        let right_bw = 48;
1199        let encoder = RDEncoder::from_parts(right_bw, codes.clone());
1200        // One value per dictionary entry, plus one whose pattern is absent.
1201        let values: Vec<f64> = codes
1202            .iter()
1203            .map(|&bits| f64::from_bits((u64::from(bits) << right_bw) | 0xABC))
1204            .chain([f64::from_bits((0x4321u64 << right_bw) | 0xABC)])
1205            .collect();
1206
1207        let split = encoder.split(&values);
1208        assert_eq!(split.left_parts(), &[0, 1, 2, 0]);
1209        assert_eq!(split.left_exceptions().positions(), &[3]);
1210        assert_eq!(split.decode(), values);
1211    }
1212
1213    /// A window table is only exact if no two dictionary entries claim the same slot.
1214    #[test]
1215    fn test_window_slots_are_unshared() {
1216        let mut rng = Lcg::new();
1217        for _ in 0..256 {
1218            let mut codes: Vec<u16> = (0..MAX_DICT_SIZE).map(|_| rng.next_bits() as u16).collect();
1219            // A repeated pattern shares its slot with itself, which is not what this is about.
1220            codes.sort_unstable();
1221            codes.dedup();
1222            let ReverseDict::Window { shift, .. } = ReverseDict::new(&codes) else {
1223                continue;
1224            };
1225
1226            let mut claimed = [false; REVERSE_SLOTS];
1227            for &pattern in &codes {
1228                let slot = ReverseDict::index(pattern, shift);
1229                assert!(
1230                    !claimed[slot],
1231                    "{codes:x?} shares slot {slot} at shift {shift}"
1232                );
1233                claimed[slot] = true;
1234            }
1235        }
1236    }
1237
1238    #[test]
1239    fn test_encode_decode() {
1240        let values = vec![1.12345f64, 2.34567f64, 3.45678f64];
1241
1242        let encoder = RDEncoder::new(&values);
1243
1244        let split = encoder.split(&values);
1245        let decoded = split.decode();
1246        assert_eq!(decoded, values);
1247    }
1248
1249    #[test]
1250    fn test_encode_decode_with_exceptions() {
1251        // The outlier has a different exponent, so its left parts are not in the dictionary.
1252        let values = vec![0.1f64, 0.2f64, 3e100f64];
1253        let encoder = RDEncoder::new(&values[0..2]);
1254
1255        let split = encoder.split(&values);
1256        assert_eq!(split.left_exceptions().positions(), &[2]);
1257        assert_eq!(split.decode(), values);
1258    }
1259
1260    #[test]
1261    fn test_encode_decode_f32() {
1262        let values = vec![0.1f32, 0.2f32, 3e25f32];
1263        let encoder = RDEncoder::new(&values[0..2]);
1264
1265        let split = encoder.split(&values);
1266        assert_eq!(split.left_exceptions().positions(), &[2]);
1267        assert_eq!(split.decode(), values);
1268    }
1269
1270    #[test]
1271    fn test_from_parts_round_trips() {
1272        let values = vec![1.12345f64, 2.34567f64, 3.45678f64];
1273        let encoder = RDEncoder::new(&values);
1274
1275        let rebuilt = RDEncoder::from_parts(encoder.right_bit_width(), encoder.codes().to_vec());
1276        assert_eq!(rebuilt.right_bit_width(), encoder.right_bit_width());
1277        assert_eq!(rebuilt.codes(), encoder.codes());
1278        assert_eq!(rebuilt.split(&values).decode(), values);
1279    }
1280
1281    #[test]
1282    fn test_bit_widths() {
1283        let values = vec![1.12345f64, 2.34567f64, 3.45678f64];
1284        let encoder = RDEncoder::new(&values);
1285        let split = encoder.split(&values);
1286
1287        assert!(encoder.codes().len() <= MAX_DICT_SIZE as usize);
1288        assert_eq!(split.left_parts_bit_width(), encoder.left_bit_width());
1289        assert_eq!(
1290            split.left_parts_bit_width() as usize,
1291            bit_width((encoder.codes().len() - 1) as u64) as usize
1292        );
1293        assert_eq!(split.right_parts_bit_width(), encoder.right_bit_width());
1294        assert!(
1295            split
1296                .right_parts()
1297                .iter()
1298                .all(|v| *v < (1u64 << split.right_parts_bit_width()))
1299        );
1300    }
1301
1302    #[test]
1303    fn test_decode_primitives_match() {
1304        let values = vec![0.1f64, 0.2f64, 3e100f64];
1305        let encoder = RDEncoder::new(&values[0..2]);
1306        let (left_parts, right_parts, exc_pos, exc_values) = encoder.split_parts(&values);
1307        let dict = encoder.codes();
1308        let right_bit_width = encoder.right_bit_width();
1309
1310        // Piecewise decode, as a consumer holding its own buffers would do it.
1311        let mut left = left_parts.clone();
1312        alp_rd_dict_decode_inplace(&mut left, dict);
1313        alp_rd_apply_patches(&mut left, &exc_pos, &exc_values, 0);
1314        let mut combined = right_parts.clone();
1315        alp_rd_combine_inplace::<f64>(&mut combined, &left, right_bit_width);
1316        let decoded: Vec<f64> = combined.into_iter().map(f64::from_bits).collect();
1317
1318        assert_eq!(
1319            decoded,
1320            alp_rd_decode::<f64>(
1321                &left_parts,
1322                dict,
1323                right_bit_width,
1324                &right_parts,
1325                &exc_pos,
1326                &exc_values
1327            )
1328        );
1329        assert_eq!(decoded, values);
1330    }
1331
1332    #[test]
1333    fn test_combine_codes_matches_combine() {
1334        let values = vec![1.12345f64, 2.34567f64, 3.45678f64];
1335        let encoder = RDEncoder::new(&values);
1336        let (left_parts, right_parts, exc_pos, _) = encoder.split_parts(&values);
1337        assert!(exc_pos.is_empty());
1338
1339        let mut fast = right_parts.clone();
1340        alp_rd_combine_codes_inplace::<f64>(
1341            &mut fast,
1342            &left_parts,
1343            encoder.codes(),
1344            encoder.right_bit_width(),
1345        );
1346
1347        let mut left = left_parts;
1348        alp_rd_dict_decode_inplace(&mut left, encoder.codes());
1349        let mut slow = right_parts;
1350        alp_rd_combine_inplace::<f64>(&mut slow, &left, encoder.right_bit_width());
1351
1352        assert_eq!(fast, slow);
1353        assert_eq!(
1354            fast.into_iter().map(f64::from_bits).collect::<Vec<_>>(),
1355            values
1356        );
1357    }
1358
1359    #[test]
1360    fn test_apply_patches_with_offset() {
1361        // Patch indices are relative to the start of the unsliced array.
1362        let mut left = vec![0u16; 3];
1363        alp_rd_apply_patches(&mut left, &[10u64, 12], &[7u16, 9], 10);
1364        assert_eq!(left, vec![7, 0, 9]);
1365    }
1366
1367    #[test]
1368    fn test_bit_width_fn() {
1369        assert_eq!(bit_width(0), 1);
1370        assert_eq!(bit_width(1), 1);
1371        assert_eq!(bit_width(2), 2);
1372        assert_eq!(bit_width(7), 3);
1373        assert_eq!(bit_width(u64::MAX), 64);
1374    }
1375
1376    #[test]
1377    #[should_panic(expected = "at most MAX_DICT_SIZE entries")]
1378    fn test_from_parts_rejects_oversized_dictionary() {
1379        // A larger dictionary needs codes wider than the decoder's fast path masks for.
1380        RDEncoder::from_parts(52, vec![0u16; MAX_DICT_SIZE as usize + 1]);
1381    }
1382
1383    #[test]
1384    fn test_sample_plan_covers_short_inputs_fully() {
1385        for len in [0usize, 1, 63, 64, 4095, MAX_SAMPLE] {
1386            let plan = SamplePlan::subsample(len, MAX_SAMPLE, SAMPLE_BLOCK);
1387            assert_eq!(plan.count(), len, "short inputs must be scanned in full");
1388            let covered: usize = plan.ranges().iter().map(|r| r.len()).sum();
1389            assert_eq!(covered, len);
1390        }
1391    }
1392
1393    #[test]
1394    fn test_sample_plan_ranges_are_in_bounds_and_disjoint() {
1395        // Includes lengths that are and are not multiples of the block and sample sizes.
1396        for len in [
1397            MAX_SAMPLE + 1,
1398            2 * MAX_SAMPLE,
1399            3 * MAX_SAMPLE + 1,
1400            100_003,
1401            1 << 20,
1402        ] {
1403            let plan = SamplePlan::subsample(len, MAX_SAMPLE, SAMPLE_BLOCK);
1404            assert!(
1405                plan.count() <= MAX_SAMPLE,
1406                "subsampling must honour the MAX_SAMPLE budget for len {len}"
1407            );
1408            assert_eq!(
1409                plan.count(),
1410                plan.ranges().iter().map(|r| r.len()).sum::<usize>()
1411            );
1412
1413            let mut prev_end = 0;
1414            for range in plan.ranges() {
1415                assert!(
1416                    range.start >= prev_end,
1417                    "ranges must be ascending, disjoint"
1418                );
1419                assert!(
1420                    range.end <= len,
1421                    "range {}..{} exceeds {len}",
1422                    range.start,
1423                    range.end
1424                );
1425                prev_end = range.end;
1426            }
1427            assert!(!plan.ranges().is_empty());
1428        }
1429    }
1430
1431    /// Subsampling must not cost compression on data whose distribution is uniform throughout,
1432    /// which is the easy case.
1433    #[test]
1434    fn test_subsample_matches_full_scan_on_random_data() {
1435        let mut rng = Lcg::new();
1436        let values: Vec<f64> = (0..8 * MAX_SAMPLE).map(|_| rng.next_log_normal()).collect();
1437
1438        let (subsampled, full) = subsample_vs_full_scan(&values);
1439        assert!(
1440            subsampled.bits_per_value <= full.bits_per_value + 0.1,
1441            "subsampling cost {:.3} bits/value against a full scan's {:.3}",
1442            subsampled.bits_per_value,
1443            full.bits_per_value
1444        );
1445    }
1446
1447    /// Regression test for sampling that aliases with periodic input.
1448    ///
1449    /// Interleaving values of two very different magnitudes — flattened coordinate or embedding
1450    /// columns, round-robin sensor readings — gives the input a period. A sampler striding by
1451    /// `len / MAX_SAMPLE` lands on one phase whenever that stride is a multiple of the period, so
1452    /// the search sees only one magnitude and picks a cut point that is badly wrong for the whole
1453    /// input: measured at 2.9 to 10.7 bits/value worse than a full scan, with exception rates of
1454    /// 15% to 40%. Contiguous runs see every phase and land within 0.03 bits/value of a full scan.
1455    #[test]
1456    fn test_subsample_matches_full_scan_on_periodic_data() {
1457        for period in [2usize, 3, 4, 8] {
1458            // Size the input so that a striding sampler would use a stride of exactly `period`.
1459            let len = period * MAX_SAMPLE;
1460            let mut rng = Lcg::new();
1461            let values: Vec<f64> = (0..len)
1462                .map(|i| {
1463                    let magnitude = if i % period == 0 { 1e-8 } else { 1e12 };
1464                    magnitude * (1.0 + rng.next_unit())
1465                })
1466                .collect();
1467
1468            let (subsampled, full) = subsample_vs_full_scan(&values);
1469            assert!(
1470                subsampled.bits_per_value <= full.bits_per_value + 0.5,
1471                "period {period}: subsampling cost {:.3} bits/value against a full scan's {:.3}",
1472                subsampled.bits_per_value,
1473                full.bits_per_value
1474            );
1475            assert!(
1476                subsampled.exception_rate < 0.10,
1477                "period {period}: subsampling chose a cut point leaving {:.2}% of the input \
1478                 un-encodable",
1479                subsampled.exception_rate * 100.0
1480            );
1481        }
1482    }
1483
1484    /// Every value must round-trip, including the ones the dictionary search never looked at.
1485    #[test]
1486    fn test_large_input_round_trips_in_chunks() {
1487        let mut rng = Lcg::new();
1488        let values: Vec<f64> = (0..4 * MAX_SAMPLE + 7)
1489            .map(|_| rng.next_log_normal())
1490            .collect();
1491        let encoder = RDEncoder::new(&values);
1492
1493        for chunk in values.chunks(1024) {
1494            assert_eq!(&encoder.split(chunk).decode(), chunk);
1495        }
1496    }
1497
1498    /// The dictionary must not depend on hash iteration order or on how the sort breaks ties.
1499    #[test]
1500    fn test_dictionary_is_reproducible() {
1501        // Every pattern appears the same number of times, so selection is decided entirely by the
1502        // tie-break.
1503        let values: Vec<f64> = (0..1024)
1504            .map(|i| f64::from_bits(((i % 32) as u64) << 52 | 1))
1505            .collect();
1506
1507        let expected = RDEncoder::new(&values);
1508        for _ in 0..16 {
1509            let actual = RDEncoder::new(&values);
1510            assert_eq!(actual.codes(), expected.codes());
1511            assert_eq!(actual.right_bit_width(), expected.right_bit_width());
1512        }
1513    }
1514
1515    /// The reverse lookup table used by `split_parts` must agree with the forward `codes` table.
1516    #[test]
1517    fn test_lookup_agrees_with_codes() {
1518        let mut rng = Lcg::new();
1519        let values: Vec<f64> = (0..2048).map(|_| rng.next_log_normal()).collect();
1520        let encoder = RDEncoder::new(&values);
1521
1522        // Encoding the dictionary patterns themselves must yield their own codes, exception-free.
1523        let right_bw = encoder.right_bit_width();
1524        let patterns: Vec<f64> = encoder
1525            .codes()
1526            .iter()
1527            .map(|&bits| f64::from_bits((bits as u64) << right_bw))
1528            .collect();
1529        let (left_parts, _, exc_pos, _) = encoder.split_parts(&patterns);
1530
1531        assert!(exc_pos.is_empty(), "dictionary patterns must not except");
1532        assert_eq!(
1533            left_parts,
1534            (0..encoder.codes().len() as u16).collect::<Vec<_>>()
1535        );
1536    }
1537
1538    #[test]
1539    fn test_inline_dict_matches_into_parts() {
1540        let values = vec![1.12345f64, 2.34567f64, 3.45678f64];
1541        let encoder = RDEncoder::new(&values);
1542        let split = encoder.split(&values);
1543
1544        let inline_dict = split.left_dict().to_vec();
1545        let (_, owned_dict, _, _, _) = split.into_parts();
1546
1547        assert_eq!(inline_dict, owned_dict);
1548        assert_eq!(owned_dict, encoder.codes());
1549    }
1550}