use alloc::vec::Vec;
use p3_field::{ExtensionField, Field};
use p3_matrix::dense::RowMajorMatrix;
use thiserror::Error;
use crate::PolynomialSpace;
#[derive(Clone, Debug, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum PeriodicColumnShapeError {
#[error("periodic column {index} has length {length}, which is not a power of two")]
LengthNotPowerOfTwo {
index: usize,
length: usize,
},
#[error(
"periodic column {index} has length {length}, which does not divide the trace height {height}"
)]
LengthNotDividingHeight {
index: usize,
length: usize,
height: usize,
},
}
#[derive(Debug)]
pub struct PeriodicColumns<'a, F> {
columns: &'a [Vec<F>],
height: usize,
}
impl<F> Clone for PeriodicColumns<'_, F> {
fn clone(&self) -> Self {
*self
}
}
impl<F> Copy for PeriodicColumns<'_, F> {}
impl<'a, F> PeriodicColumns<'a, F> {
pub fn new(columns: &'a [Vec<F>], height: usize) -> Result<Self, PeriodicColumnShapeError> {
for (index, column) in columns.iter().enumerate() {
let length = column.len();
if !length.is_power_of_two() {
return Err(PeriodicColumnShapeError::LengthNotPowerOfTwo { index, length });
}
if !height.is_multiple_of(length) {
return Err(PeriodicColumnShapeError::LengthNotDividingHeight {
index,
length,
height,
});
}
}
Ok(Self { columns, height })
}
pub const fn as_slice(&self) -> &'a [Vec<F>] {
self.columns
}
pub const fn height(&self) -> usize {
self.height
}
pub const fn len(&self) -> usize {
self.columns.len()
}
pub const fn is_empty(&self) -> bool {
self.columns.is_empty()
}
pub fn max_period(&self) -> Option<usize> {
self.columns.iter().map(Vec::len).max()
}
}
#[derive(Clone, Debug)]
pub struct PeriodicLdeTable<F> {
values: RowMajorMatrix<F>,
height: usize,
}
impl<F: Clone + Send + Sync> PeriodicLdeTable<F> {
pub const fn new(values: RowMajorMatrix<F>) -> Self {
let height = match values.values.len().checked_div(values.width) {
Some(h) => h,
None => 0,
};
debug_assert!(
height == 0 || height.is_power_of_two(),
"PeriodicLdeTable height must be a power of two for bitmask indexing"
);
Self { values, height }
}
pub fn empty() -> Self {
Self {
values: RowMajorMatrix::new(Vec::new(), 0),
height: 0,
}
}
pub const fn is_empty(&self) -> bool {
self.values.values.is_empty()
}
pub const fn width(&self) -> usize {
self.values.width
}
pub const fn height(&self) -> usize {
self.height
}
pub const fn packed_group_period(&self, pack_width: usize) -> usize {
debug_assert!(pack_width > 0, "pack_width must be nonzero");
let log_gcd = if self.height.trailing_zeros() < pack_width.trailing_zeros() {
self.height.trailing_zeros()
} else {
pack_width.trailing_zeros()
};
self.height >> log_gcd
}
#[inline]
pub fn get(&self, lde_idx: usize, col_idx: usize) -> &F {
let height = self.height;
debug_assert!(height > 0, "cannot index into empty periodic table");
let row_idx = lde_idx & (height - 1);
&self.values.values[row_idx * self.values.width + col_idx]
}
}
pub trait PeriodicEvaluator<F: Field, D: PolynomialSpace<Val = F>> {
fn eval_on_lde(
periodic_table: &[Vec<F>],
trace_domain: &D,
lde_domain: &D,
) -> PeriodicLdeTable<F>;
fn eval_at_point<EF: ExtensionField<F>>(
periodic_table: &[Vec<F>],
trace_domain: &D,
point: EF,
) -> Vec<EF>;
}
impl<F: Field, D: PolynomialSpace<Val = F>> PeriodicEvaluator<F, D> for () {
fn eval_on_lde(
periodic_table: &[Vec<F>],
_trace_domain: &D,
_lde_domain: &D,
) -> PeriodicLdeTable<F> {
assert!(
periodic_table.is_empty(),
"AIR has periodic columns but no PeriodicEvaluator was specified. \
Use prove_with_periodic or verify_with_periodic with TwoAdicPeriodicEvaluator \
or CirclePeriodicEvaluator."
);
PeriodicLdeTable::empty()
}
fn eval_at_point<EF: ExtensionField<F>>(
periodic_table: &[Vec<F>],
_trace_domain: &D,
_point: EF,
) -> Vec<EF> {
assert!(
periodic_table.is_empty(),
"AIR has periodic columns but no PeriodicEvaluator was specified. \
Use prove_with_periodic or verify_with_periodic with TwoAdicPeriodicEvaluator \
or CirclePeriodicEvaluator."
);
Vec::new()
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn no_columns_accepts_any_height() {
for height in [0, 1, 3, 7, 12] {
let screened = PeriodicColumns::<u8>::new(&[], height).unwrap();
assert!(screened.is_empty());
assert_eq!(screened.len(), 0);
assert_eq!(screened.height(), height);
assert_eq!(screened.max_period(), None);
}
}
#[test]
fn every_power_of_two_divisor_of_the_height_is_accepted() {
for length in [1, 2, 4, 8] {
let columns = vec![vec![0u8; length]];
let screened = PeriodicColumns::new(&columns, 8).unwrap();
assert_eq!(screened.max_period(), Some(length));
assert_eq!(screened.as_slice(), columns.as_slice());
}
}
#[test]
fn the_longest_period_is_reported() {
let columns = vec![vec![0u8; 2], vec![0u8; 8], vec![0u8; 4]];
let screened = PeriodicColumns::new(&columns, 8).unwrap();
assert_eq!(screened.len(), 3);
assert_eq!(screened.max_period(), Some(8));
}
#[test]
fn non_power_of_two_length_is_rejected() {
let columns = vec![vec![0u8; 3]];
assert_eq!(
PeriodicColumns::new(&columns, 8).unwrap_err(),
PeriodicColumnShapeError::LengthNotPowerOfTwo {
index: 0,
length: 3
}
);
}
#[test]
fn empty_column_is_rejected() {
let columns: Vec<Vec<u8>> = vec![vec![]];
assert_eq!(
PeriodicColumns::new(&columns, 8).unwrap_err(),
PeriodicColumnShapeError::LengthNotPowerOfTwo {
index: 0,
length: 0
}
);
}
#[test]
fn length_that_fits_but_does_not_divide_is_rejected() {
let columns = vec![vec![0u8; 8]];
assert_eq!(
PeriodicColumns::new(&columns, 12).unwrap_err(),
PeriodicColumnShapeError::LengthNotDividingHeight {
index: 0,
length: 8,
height: 12
}
);
}
#[test]
fn the_first_offending_column_is_the_one_reported() {
let columns = vec![vec![0u8; 4], vec![0u8; 6], vec![0u8; 5]];
assert_eq!(
PeriodicColumns::new(&columns, 8).unwrap_err(),
PeriodicColumnShapeError::LengthNotPowerOfTwo {
index: 1,
length: 6
}
);
}
#[test]
fn on_a_power_of_two_height_fitting_and_dividing_agree() {
for log_height in 0..16 {
let height = 1usize << log_height;
for log_length in 0..16 {
let length = 1usize << log_length;
let columns = vec![vec![0u8; length]];
let fits = length <= height;
let divides = PeriodicColumns::new(&columns, height).is_ok();
assert_eq!(fits, divides, "height {height}, length {length}");
}
}
}
#[test]
fn packed_group_period_matches_modular_indexing() {
let cases = [
(8, 3, 8),
(8, 6, 4),
(8, 1, 8),
(8, 4, 2),
(8, 8, 1),
(4, 8, 1),
(1, 3, 1),
];
for (height, pack_width, expected) in cases {
let values: Vec<u32> = (0..height).map(|i| i as u32).collect();
let table = PeriodicLdeTable::new(RowMajorMatrix::new(values, 1));
let period = table.packed_group_period(pack_width);
assert_eq!(period, expected, "height {height}, pack_width {pack_width}");
for group in 0..4 * height {
let cached = group % period;
for offset in 0..pack_width {
assert_eq!(
table.get(group * pack_width + offset, 0),
table.get(cached * pack_width + offset, 0),
"height {height}, pack_width {pack_width}, group {group}, offset {offset}"
);
}
}
}
assert_eq!(PeriodicLdeTable::<u32>::empty().packed_group_period(3), 0);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "PeriodicLdeTable height must be a power of two")]
fn new_panics_on_non_power_of_two_height() {
use alloc::vec;
use p3_baby_bear::BabyBear;
use p3_field::PrimeCharacteristicRing;
use super::*;
type F = BabyBear;
let (a, b, c) = (F::ONE, F::TWO, F::from_u8(3));
let _ = PeriodicLdeTable::new(RowMajorMatrix::new(vec![a, b, c], 1));
}
}