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#[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
17pub const CUT_LIMIT: usize = 16;
19
20pub const MAX_DICT_SIZE: u8 = 8;
22
23const MAX_SAMPLE: usize = 4096;
32
33const SAMPLE_BLOCK: usize = 64;
43
44mod private {
45 pub trait Sealed {}
46
47 impl Sealed for f32 {}
48 impl Sealed for f64 {}
49}
50
51pub trait ALPRDFloat: private::Sealed + Float {
56 type UINT: PrimInt + BitPacking + Unsigned + One;
58
59 const BITS: usize = size_of::<Self>() * 8;
61
62 fn from_bits(bits: Self::UINT) -> Self;
64
65 fn to_bits(value: Self) -> Self::UINT;
67
68 fn to_u16(bits: Self::UINT) -> u16;
70
71 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
123pub struct RDEncoder {
143 right_bit_width: u8,
144 codes: Vec<u16>,
145 reverse: ReverseDict,
147}
148
149const REVERSE_SLOTS: usize = 32;
154
155const LANE_ONES: u128 = 0x0001_0001_0001_0001_0001_0001_0001_0001;
157
158const LANE_HIGH_BITS: u128 = 0x8000_8000_8000_8000_8000_8000_8000_8000;
160
161enum ReverseDict {
170 Window {
179 slots: [(u16, u16); REVERSE_SLOTS],
180 shift: u8,
181 },
182 Lanes(u128),
189}
190
191impl ReverseDict {
192 fn new(codes: &[u16]) -> Self {
194 let Some(&first) = codes.first() else {
195 return Self::Lanes(0);
196 };
197
198 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 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 #[inline]
230 fn index(pattern: u16, shift: u8) -> usize {
231 ((pattern >> shift) as usize) & (REVERSE_SLOTS - 1)
232 }
233}
234
235#[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#[inline]
244fn lanes_code(lanes: u128, pattern: u16) -> Option<u16> {
245 let diff = u128::from(pattern).wrapping_mul(LANE_ONES) ^ lanes;
248
249 let matches = diff.wrapping_sub(LANE_ONES) & !diff & LANE_HIGH_BITS;
253 (matches != 0).then(|| (matches.trailing_zeros() / 16) as u16)
254}
255
256pub struct Split<F, U> {
261 left_parts: Vec<u16>,
263
264 left_exceptions: Exceptions<u16>,
266
267 left_dict: [u16; MAX_DICT_SIZE as usize],
272
273 left_dict_len: u8,
275
276 left_parts_bit_width: u8,
278
279 right_parts: Vec<U>,
281
282 right_parts_bit_width: u8,
284
285 phantom_data: PhantomData<F>,
286}
287
288impl<T, U> Split<T, U> {
289 pub fn into_parts(self) -> (Vec<u16>, Vec<u16>, Exceptions<u16>, Vec<U>, u8) {
291 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 pub fn left_parts(&self) -> &[u16] {
304 &self.left_parts
305 }
306
307 pub fn left_dict(&self) -> &[u16] {
309 &self.left_dict[..self.left_dict_len as usize]
310 }
311
312 pub fn left_exceptions(&self) -> &Exceptions<u16> {
314 &self.left_exceptions
315 }
316
317 pub fn right_parts(&self) -> &[U] {
319 &self.right_parts
320 }
321
322 pub fn left_parts_bit_width(&self) -> u8 {
324 self.left_parts_bit_width
325 }
326
327 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 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 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 pub fn from_parts(right_bit_width: u8, codes: Vec<u16>) -> Self {
382 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 #[inline]
400 pub fn right_bit_width(&self) -> u8 {
401 self.right_bit_width
402 }
403
404 #[inline]
406 pub fn left_bit_width(&self) -> u8 {
407 bit_width(self.codes.len().saturating_sub(1) as u64)
408 }
409
410 #[inline]
412 pub fn codes(&self) -> &[u16] {
413 &self.codes
414 }
415
416 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 let left_exceptions = Exceptions::new(exception_values, exception_pos);
430
431 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 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 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 #[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 let right_mask = T::UINT::one().shl(self.right_bit_width as _) - T::UINT::one();
497
498 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 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
520pub 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 alp_rd_combine_codes_inplace::<T>(
553 &mut decoded,
554 left_parts,
555 left_parts_dict,
556 right_bit_width,
557 );
558 } else {
559 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#[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#[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#[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#[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 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#[derive(Debug)]
690struct SamplePlan {
691 ranges: Vec<Range<usize>>,
692 count: usize,
693}
694
695impl SamplePlan {
696 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 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 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 fn ranges(&self) -> &[Range<usize>] {
738 &self.ranges
739 }
740
741 fn count(&self) -> usize {
743 self.count
744 }
745}
746
747fn find_best_dictionary<T: ALPRDFloat>(samples: &[T], plan: &SamplePlan) -> ALPRDDictionary {
756 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 plan.count() - dictionary.encodable,
772 plan.count(),
773 );
774 if estimated_size <= best_est_size {
777 best_est_size = estimated_size;
778 best_dict = dictionary;
779 }
780
781 merge_sibling_groups(&mut groups);
783 }
784
785 best_dict
786}
787
788fn 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
805fn 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 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
845fn 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
857fn 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
881fn 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 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 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
919fn 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; const EXC_SIZE: usize = 16; 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#[derive(Debug, Default)]
935struct ALPRDDictionary {
936 patterns: [u16; MAX_DICT_SIZE as usize],
939 len: u8,
941 left_bit_width: u8,
943 right_bit_width: u8,
945 encodable: usize,
948}
949
950impl ALPRDDictionary {
951 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 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 fn next_log_normal(&mut self) -> f64 {
992 (self.next_unit() * 6.0 - 1.0).exp() * 1000.0
993 }
994 }
995
996 struct CutPointCost {
998 bits_per_value: f64,
1001 exception_rate: f64,
1003 }
1004
1005 fn cut_point_cost(values: &[f64], right_bw: u8) -> CutPointCost {
1012 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 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 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 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 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 let constant = vec![7.5f64; len];
1107 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 #[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 vec![0x1234, 0x1234, 0x0001],
1163 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 #[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 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 #[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 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 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 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 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 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 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 #[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 #[test]
1456 fn test_subsample_matches_full_scan_on_periodic_data() {
1457 for period in [2usize, 3, 4, 8] {
1458 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 #[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 #[test]
1500 fn test_dictionary_is_reproducible() {
1501 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 #[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 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}