use itertools::Itertools;
use vortex_array::ArrayId;
use vortex_array::ArrayRef;
use vortex_array::Canonical;
use vortex_array::ExecutionCtx;
use vortex_array::IntoArray;
use vortex_array::VTable;
use vortex_array::arrays::Patched;
use vortex_array::arrays::PrimitiveArray;
use vortex_array::arrays::patched::use_experimental_patches;
use vortex_array::arrays::primitive::PrimitiveArrayExt;
use vortex_compressor::scheme::CompressionEstimate;
use vortex_compressor::scheme::DeferredEstimate;
use vortex_compressor::scheme::EstimateVerdict;
use vortex_error::VortexResult;
use vortex_fastlanes::BitPacked;
use vortex_fastlanes::BitPackedArray;
use vortex_fastlanes::BitWidths;
use vortex_fastlanes::bitpack_compress::bit_width_histogram;
use vortex_fastlanes::bitpack_compress::bitpack_encode;
use vortex_fastlanes::bitpack_compress::bitpack_encode_blocked;
use vortex_fastlanes::bitpack_compress::find_best_bit_width;
use vortex_fastlanes::bitpack_compress::find_best_bit_widths;
use vortex_fastlanes::bitpacked_v1_id;
use vortex_fastlanes::bitpacked_v2_id;
use crate::ArrayAndStats;
use crate::CascadingCompressor;
use crate::CompressorContext;
use crate::Scheme;
use crate::SchemeExt;
use crate::compress_patches;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum BitPackingSchemeMode {
V1,
V2,
}
pub(crate) static BITPACKING_V1: BitPackingScheme = BitPackingScheme::v1();
pub(crate) static BITPACKING_V2: BitPackingScheme = BitPackingScheme::v2();
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct BitPackingScheme {
mode: BitPackingSchemeMode,
}
impl BitPackingScheme {
pub const fn v1() -> Self {
Self {
mode: BitPackingSchemeMode::V1,
}
}
pub const fn v2() -> Self {
Self {
mode: BitPackingSchemeMode::V2,
}
}
}
impl Default for BitPackingScheme {
fn default() -> Self {
Self::v1()
}
}
impl Scheme for BitPackingScheme {
fn scheme_name(&self) -> &'static str {
"vortex.int.bitpacking"
}
fn matches(&self, canonical: &Canonical) -> bool {
canonical.dtype().is_int()
}
fn produced_encodings(&self) -> Vec<ArrayId> {
let mut encodings = match self.mode {
BitPackingSchemeMode::V1 => vec![bitpacked_v1_id()],
BitPackingSchemeMode::V2 => vec![bitpacked_v1_id(), bitpacked_v2_id()],
};
if use_experimental_patches() {
encodings.push(Patched.id());
}
encodings
}
fn refine(&self, allowed: &dyn Fn(&ArrayId) -> bool) -> &dyn Scheme {
if allowed(&bitpacked_v2_id()) {
&BITPACKING_V2
} else {
&BITPACKING_V1
}
}
fn num_children(&self) -> usize {
match self.mode {
BitPackingSchemeMode::V1 => 0,
BitPackingSchemeMode::V2 => 1,
}
}
fn expected_compression_ratio(
&self,
data: &ArrayAndStats,
_compress_ctx: CompressorContext,
exec_ctx: &mut ExecutionCtx,
) -> CompressionEstimate {
let stats = data.integer_stats(exec_ctx);
if stats.erased().min_is_negative() {
return CompressionEstimate::Verdict(EstimateVerdict::Skip);
}
CompressionEstimate::Deferred(DeferredEstimate::Sample)
}
fn compress(
&self,
compressor: &CascadingCompressor,
data: &ArrayAndStats,
compress_ctx: CompressorContext,
exec_ctx: &mut ExecutionCtx,
) -> VortexResult<ArrayRef> {
let primitive_array = data.array_as_primitive().into_owned();
let packed = match self.mode {
BitPackingSchemeMode::V1 => compress_global(&primitive_array, exec_ctx)?,
BitPackingSchemeMode::V2 => compress_blocked(&primitive_array, exec_ctx)?,
};
let Some(packed) = packed else {
return Ok(primitive_array.into_array());
};
let packed_stats = packed.statistics().to_owned();
let ptype = packed.dtype().as_ptype();
let mut parts = BitPacked::into_parts(packed);
let patches = if use_experimental_patches() {
parts.patches.take()
} else {
parts.patches = parts
.patches
.take()
.map(|p| compress_patches(p, exec_ctx))
.transpose()?;
None
};
let array = match parts.bit_widths {
BitWidths::Global(bw) => BitPacked::try_new(
parts.packed,
ptype,
parts.validity,
parts.patches,
bw,
parts.len,
parts.offset,
)?,
BitWidths::Blocked(block_offsets) => {
let block_offsets = compressor.compress_child(
&block_offsets,
&compress_ctx,
self.id(),
0,
exec_ctx,
)?;
BitPacked::try_new_with_block_offsets(
parts.packed,
ptype,
parts.validity,
parts.patches,
block_offsets,
parts.len,
parts.offset,
)?
}
};
match patches {
None => Ok(array.with_stats_set(packed_stats).into_array()),
Some(p) => Ok(
Patched::from_array_and_patches(array.into_array(), &p, exec_ctx)?
.with_stats_set(packed_stats)
.into_array(),
),
}
}
}
fn compress_global(
primitive_array: &PrimitiveArray,
exec_ctx: &mut ExecutionCtx,
) -> VortexResult<Option<BitPackedArray>> {
let histogram = bit_width_histogram(primitive_array.as_view(), exec_ctx)?;
let bw = find_best_bit_width(primitive_array.ptype(), &histogram)?;
if usize::from(bw) == primitive_array.ptype().bit_width() {
return Ok(None);
}
bitpack_encode(primitive_array, bw, Some(&histogram), exec_ctx).map(Some)
}
fn compress_blocked(
primitive_array: &PrimitiveArray,
exec_ctx: &mut ExecutionCtx,
) -> VortexResult<Option<BitPackedArray>> {
let (bit_widths, num_exceptions) = find_best_bit_widths(primitive_array, exec_ctx)?;
if bit_widths.is_empty() {
return Ok(None);
}
match bit_widths.iter().copied().all_equal_value() {
Ok(bw) if usize::from(bw) == primitive_array.ptype().bit_width() => Ok(None),
Ok(bw) => bitpack_encode(primitive_array, bw, None, exec_ctx).map(Some),
Err(_) => {
bitpack_encode_blocked(primitive_array, &bit_widths, Some(num_exceptions), exec_ctx)
.map(Some)
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use vortex_array::ArrayId;
use vortex_fastlanes::bitpacked_v1_id;
use vortex_fastlanes::bitpacked_v2_id;
use super::BITPACKING_V1;
use super::BITPACKING_V2;
use crate::Scheme;
use crate::SchemeExt;
#[rstest]
#[case::neither(false, false, false)]
#[case::v1(true, false, false)]
#[case::v2_only(false, true, true)]
#[case::both(true, true, true)]
fn refine_picks_v2_when_v2_id_is_allowed(
#[case] allow_v1: bool,
#[case] allow_v2: bool,
#[case] expect_v2: bool,
#[values(&BITPACKING_V1, &BITPACKING_V2)] scheme: &'static dyn Scheme,
) {
let allowed = |id: &ArrayId| {
(allow_v1 && *id == bitpacked_v1_id()) || (allow_v2 && *id == bitpacked_v2_id())
};
let refined = scheme.refine(&allowed);
assert_eq!(refined.id(), scheme.id());
let expected: &dyn Scheme = if expect_v2 {
&BITPACKING_V2
} else {
&BITPACKING_V1
};
assert_eq!(refined.produced_encodings(), expected.produced_encodings());
}
}