use crate::MltResult;
use crate::codecs::alp::{candidates, carries, encode_exact};
use crate::codecs::float::FloatValue;
use crate::decoder::{Alp, AlpScale, FastPForKind, PhysicalEncoding};
use crate::encoder::model::EncoderConfig;
use crate::encoder::stream::codecs::PhysicalCodecs;
use crate::encoder::stream::float_cost::{ENCODING_BYTE, data_bytes, raw_stored_bytes, varint_len};
pub(crate) struct AlpStream {
pub(crate) params: Alp,
pub(crate) offsets: Vec<u64>,
packed: Option<(FastPForKind, Vec<u8>)>,
payload_bytes: usize,
}
impl AlpStream {
pub(crate) fn smallest<T: FloatValue>(values: &[T]) -> Option<Self> {
if values.is_empty() {
return None;
}
let mut codes = Vec::new();
let mut suspect = 0;
let scale = candidates().find(|&scale| {
carries(values[suspect], scale)
&& match encode_exact(values, scale, &mut codes) {
Ok(()) => true,
Err(rejected) => {
suspect = rejected;
false
}
}
})?;
let params = frame(&codes, scale);
let offsets: Vec<u64> = codes.iter().map(|&code| params.offset_of(code)).collect();
Some(Self {
payload_bytes: data_bytes(offsets.iter().copied()),
packed: None,
params,
offsets,
})
}
pub(crate) fn worth_building<T: FloatValue>(
values: &[T],
codecs: &mut PhysicalCodecs,
cfg: EncoderConfig,
) -> MltResult<Option<Self>> {
let Some(mut alp) = Self::smallest(values) else {
return Ok(None);
};
alp.race_fastpfor(codecs, cfg)?;
Ok(Some(alp).filter(|alp| alp.stored_bytes() < raw_stored_bytes::<T>(values.len())))
}
pub(crate) fn race_fastpfor(
&mut self,
codecs: &mut PhysicalCodecs,
cfg: EncoderConfig,
) -> MltResult<()> {
if cfg.fastpfor().is_none() {
return Ok(());
}
let Some(words) = self.narrow_offsets() else {
return Ok(());
};
let kind = cfg.wire_version().fastpfor_kind();
let packed = codecs.fastpfor(kind, &words)?;
let packed_bytes = varint_len(packed.len()) + packed.len();
if packed_bytes < self.payload_bytes {
self.payload_bytes = packed_bytes;
self.packed = Some((kind, packed.to_vec()));
}
Ok(())
}
pub(crate) fn payload<'a>(
&'a self,
codecs: &'a mut PhysicalCodecs,
) -> (PhysicalEncoding, &'a [u8]) {
match &self.packed {
Some((kind, packed)) => (PhysicalEncoding::FastPFor(*kind), packed),
None => (PhysicalEncoding::VarInt, codecs.varint(&self.offsets)),
}
}
fn narrow_offsets(&self) -> Option<Vec<u32>> {
self.offsets
.iter()
.map(|&o| u32::try_from(o).ok())
.collect()
}
pub(crate) fn stored_bytes(&self) -> usize {
header_bytes(self.params) + self.payload_bytes
}
}
const SCALE_BYTE: usize = 1;
fn frame(codes: &[i64], scale: AlpScale) -> Alp {
Alp {
scale,
base: codes.iter().copied().min().unwrap_or(0),
}
}
fn header_bytes(params: Alp) -> usize {
ENCODING_BYTE + SCALE_BYTE + varint_len(params.base)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn cost(codes: &[i64], scale: AlpScale) -> usize {
let params = frame(codes, scale);
header_bytes(params) + data_bytes(codes.iter().map(|&code| params.offset_of(code)))
}
fn exhaustive<T: FloatValue>(values: &[T]) -> Option<usize> {
let mut scratch = Vec::new();
let mut best: Option<usize> = None;
for scale in candidates() {
if encode_exact(values, scale, &mut scratch).is_err() {
continue;
}
let bytes = cost(&scratch, scale);
if best.is_none_or(|best_bytes| bytes < best_bytes) {
best = Some(bytes);
}
}
best
}
proptest! {
#[test]
fn early_exit_finds_the_same_size_as_the_exhaustive_search(
values in prop::collection::vec(-1e6f64..1e6, 1..40),
) {
let found = AlpStream::smallest(&values).map(|alp| alp.stored_bytes());
prop_assert_eq!(found, exhaustive(&values));
}
#[test]
fn every_column_that_encodes_at_all_returns_bit_for_bit(
values in prop::collection::vec(-1e6f64..1e6, 1..40),
) {
let Some(alp) = AlpStream::smallest(&values) else { return Ok(()) };
let codes: Vec<i64> = alp.offsets.iter().map(|&o| alp.params.code_at(o)).collect();
prop_assert_eq!(crate::codecs::alp::decode::<f64>(&codes, alp.params.scale), values);
}
#[test]
fn offsets_are_true_distances_from_the_smallest_scaled_integer(
values in prop::collection::vec(-1e6f64..1e6, 1..40),
) {
let Some(alp) = AlpStream::smallest(&values) else { return Ok(()) };
let codes: Vec<i64> = alp.offsets.iter().map(|&o| alp.params.code_at(o)).collect();
let smallest = codes.iter().copied().min().expect("non-empty");
prop_assert_eq!(smallest, alp.params.base);
prop_assert_eq!(alp.offsets.iter().copied().min(), Some(0));
for (&offset, &code) in alp.offsets.iter().zip(&codes) {
prop_assert_eq!(offset, code.wrapping_sub(smallest).cast_unsigned());
}
}
}
#[test]
fn a_column_rejected_only_by_its_last_value_takes_no_parameters() {
let mut values: Vec<f64> = (0..500).map(|i| f64::from(i) * 0.5).collect();
values.push(f64::NAN);
assert!(AlpStream::smallest(&values).is_none());
}
#[test]
fn a_three_decimal_f64_column_takes_no_alp() {
let values: Vec<f64> = (0..256)
.map(|i| f64::from(i * 7919 % 2_000_000 - 1_000_000) / 1000.0)
.collect();
assert!(AlpStream::smallest(&values).is_none());
}
#[test]
fn two_decimal_values_pick_the_smallest_net_exponent() {
let alp = AlpStream::smallest(&[1.25f64, 100.75, -0.25]).expect("some parameters fit");
assert_eq!(alp.params.scale.net(), 2);
}
#[test]
fn shifting_a_column_away_from_zero_costs_only_a_wider_base() {
let here: Vec<f64> = (0..256).map(|i| f64::from(i) * 0.25).collect();
let far: Vec<f64> = here.iter().map(|v| v + 52_500_000.0).collect();
let a = AlpStream::smallest(&here).expect("fits").stored_bytes();
let b = AlpStream::smallest(&far).expect("fits").stored_bytes();
assert!(b <= a + 5, "{b} vs {a}");
}
}