use alloc::vec::Vec;
use p3_binary_field::TowerLevel;
use p3_maybe_rayon::prelude::*;
use thiserror::Error;
use super::domain::SkipDomain;
pub const CHUNK_BITS: usize = 8;
pub(crate) const TABLE_ROWS: usize = 1 << CHUNK_BITS;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CompressedLdeError {
#[error("a dimension-{log_size} subspace is narrower than one {CHUNK_BITS}-bit chunk")]
SubspaceTooNarrow {
log_size: usize,
},
}
#[derive(Debug, Clone)]
pub struct CompressedLde<F> {
table: Vec<F>,
stride: usize,
num_chunks: usize,
}
impl<F: TowerLevel> CompressedLde<F> {
pub fn new(domain: &SkipDomain<F>) -> Result<Self, CompressedLdeError> {
if domain.size() < CHUNK_BITS {
return Err(CompressedLdeError::SubspaceTooNarrow {
log_size: domain.log_size(),
});
}
let stride = domain.num_transmitted();
let num_chunks = domain.size() / CHUNK_BITS;
let columns = domain.resampling_prefix(CHUNK_BITS);
let mut table = F::zero_vec(TABLE_ROWS * stride);
for value in 1..TABLE_ROWS {
let bit = value.trailing_zeros() as usize;
let (built, building) = table.split_at_mut(value * stride);
let previous = &built[(value & (value - 1)) * stride..][..stride];
for (index, entry) in building[..stride].iter_mut().enumerate() {
*entry = previous[index] + columns[index * CHUNK_BITS + bit];
}
}
Ok(Self {
table,
stride,
num_chunks,
})
}
#[must_use]
pub const fn num_transmitted(&self) -> usize {
self.stride
}
#[must_use]
pub const fn num_chunks(&self) -> usize {
self.num_chunks
}
#[must_use]
pub const fn table_len(&self) -> usize {
self.table.len()
}
fn chunk_row(&self, value: u8) -> &[F] {
&self.table[usize::from(value) * self.stride..][..self.stride]
}
pub fn extend(&self, row: &[u8], out: &mut [F]) {
assert_eq!(row.len(), self.num_chunks, "one byte per chunk");
assert_eq!(out.len(), self.stride, "one output per transmitted point");
let (blocks, tail) = out.as_chunks_mut::<CHUNK_BITS>();
debug_assert!(
tail.is_empty(),
"the transmitted count is a multiple of the chunk"
);
blocks.fill([F::ZERO; CHUNK_BITS]);
for (chunk, &value) in row.iter().enumerate() {
let (source, _) = self.chunk_row(value).as_chunks::<CHUNK_BITS>();
for (block, destination) in blocks.iter_mut().enumerate() {
let contribution = &source[block ^ chunk];
for index in 0..CHUNK_BITS {
destination[index] += contribution[index];
}
}
}
}
pub fn extend_batch(&self, rows: &[u8], out: &mut [F])
where
F: Send + Sync,
{
assert_eq!(rows.len() % self.num_chunks, 0, "whole input rows");
assert_eq!(out.len() % self.stride, 0, "whole output rows");
assert_eq!(
rows.len() / self.num_chunks,
out.len() / self.stride,
"one output row per input row"
);
out.par_chunks_exact_mut(self.stride)
.zip(rows.par_chunks_exact(self.num_chunks))
.for_each(|(destination, row)| self.extend(row, destination));
}
}
#[cfg(any(test, feature = "test-util"))]
pub fn extend_reference<F: TowerLevel>(domain: &SkipDomain<F>, row: &[u8], out: &mut [F]) {
assert_eq!(row.len() * CHUNK_BITS, domain.size(), "one byte per chunk");
assert_eq!(
out.len(),
domain.num_transmitted(),
"one output per transmitted point"
);
let values = (0..domain.size())
.map(|index| {
let bit = (row[index / CHUNK_BITS] >> (index % CHUNK_BITS)) & 1;
if bit == 1 { F::ONE } else { F::ZERO }
})
.collect::<Vec<_>>();
for (entry, &point) in out.iter_mut().zip(domain.transmitted()) {
*entry = domain.interpolate(&values, point);
}
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryField8, BinaryField16};
use p3_field::PrimeCharacteristicRing;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
const LOG_SIZE: usize = 6;
fn product_domain() -> SkipDomain<BinaryField8> {
SkipDomain::for_degree(LOG_SIZE, 2).unwrap()
}
#[test]
fn the_product_form_table_is_sixteen_kibibytes() {
let lde = CompressedLde::new(&product_domain()).unwrap();
assert_eq!(lde.num_chunks(), 8);
assert_eq!(lde.num_transmitted(), 64);
assert_eq!(lde.table_len() * size_of::<BinaryField8>(), 16 * 1024);
}
#[test]
fn the_empty_row_extends_to_zero() {
let domain = product_domain();
let lde = CompressedLde::new(&domain).unwrap();
let mut out = BinaryField8::zero_vec(lde.num_transmitted());
lde.extend(&[0u8; 8], &mut out);
assert!(out.iter().all(|&value| value == BinaryField8::ZERO));
}
#[test]
fn a_single_bit_reproduces_its_lagrange_basis_polynomial() {
let domain = product_domain();
let lde = CompressedLde::new(&domain).unwrap();
let matrix = domain.resampling_matrix();
let mut out = BinaryField8::zero_vec(lde.num_transmitted());
for column in 0..domain.size() {
let mut row = [0u8; 8];
row[column / CHUNK_BITS] = 1 << (column % CHUNK_BITS);
lde.extend(&row, &mut out);
for (index, &value) in out.iter().enumerate() {
assert_eq!(value, matrix[index * domain.size() + column], "c={column}");
}
}
}
#[test]
fn extension_is_additive_over_the_row_bits() {
let mut rng = SmallRng::seed_from_u64(5);
let lde = CompressedLde::new(&product_domain()).unwrap();
let left: [u8; 8] = rng.random();
let right: [u8; 8] = rng.random();
let mixed: [u8; 8] = core::array::from_fn(|index| left[index] ^ right[index]);
let mut a = BinaryField8::zero_vec(lde.num_transmitted());
let mut b = BinaryField8::zero_vec(lde.num_transmitted());
let mut c = BinaryField8::zero_vec(lde.num_transmitted());
lde.extend(&left, &mut a);
lde.extend(&right, &mut b);
lde.extend(&mixed, &mut c);
for index in 0..lde.num_transmitted() {
assert_eq!(c[index], a[index] + b[index], "index={index}");
}
}
#[test]
fn rejects_a_subspace_narrower_than_one_chunk() {
let domain = SkipDomain::<BinaryField8>::new(2, 3).unwrap();
assert_eq!(
CompressedLde::new(&domain).unwrap_err(),
CompressedLdeError::SubspaceTooNarrow { log_size: 2 }
);
let domain = SkipDomain::<BinaryField8>::new(3, 4).unwrap();
assert!(CompressedLde::new(&domain).is_ok());
}
#[test]
fn batching_agrees_with_extending_one_row_at_a_time() {
let mut rng = SmallRng::seed_from_u64(13);
let lde = CompressedLde::new(&product_domain()).unwrap();
let num_rows = 37;
let rows = (0..num_rows * lde.num_chunks())
.map(|_| rng.random::<u8>())
.collect::<Vec<_>>();
let mut batched = BinaryField8::zero_vec(num_rows * lde.num_transmitted());
lde.extend_batch(&rows, &mut batched);
let mut single = BinaryField8::zero_vec(lde.num_transmitted());
for (index, row) in rows.chunks_exact(lde.num_chunks()).enumerate() {
lde.extend(row, &mut single);
assert_eq!(
&batched[index * lde.num_transmitted()..][..lde.num_transmitted()],
&single[..],
"row={index}"
);
}
}
proptest! {
#[test]
fn tabulated_extension_matches_direct_interpolation(seed: u64) {
let mut rng = SmallRng::seed_from_u64(seed);
let domain = product_domain();
let lde = CompressedLde::new(&domain).unwrap();
let row = (0..lde.num_chunks()).map(|_| rng.random::<u8>()).collect::<Vec<_>>();
let mut fast = BinaryField8::zero_vec(lde.num_transmitted());
let mut slow = BinaryField8::zero_vec(lde.num_transmitted());
lde.extend(&row, &mut fast);
extend_reference(&domain, &row, &mut slow);
prop_assert_eq!(fast, slow);
}
#[test]
fn tabulated_extension_matches_across_domain_shapes(
seed: u64,
log_size in 3usize..=8,
extra in 1usize..=4,
) {
let mut rng = SmallRng::seed_from_u64(seed);
let domain = SkipDomain::<BinaryField16>::new(log_size, log_size + extra).unwrap();
let lde = CompressedLde::new(&domain).unwrap();
let row = (0..lde.num_chunks()).map(|_| rng.random::<u8>()).collect::<Vec<_>>();
let mut fast = BinaryField16::zero_vec(lde.num_transmitted());
let mut slow = BinaryField16::zero_vec(lde.num_transmitted());
lde.extend(&row, &mut fast);
extend_reference(&domain, &row, &mut slow);
prop_assert_eq!(fast, slow);
}
}
}