use alloc::vec::Vec;
use core::marker::PhantomData;
use p3_binary_field::{Ghash128, TowerLevel};
use p3_field::{ExtensionField, Field, PrimeCharacteristicRing};
use p3_multilinear_util::poly::Poly;
use thiserror::Error;
use super::lde::{CHUNK_BITS, TABLE_ROWS};
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PinnedEqError {
#[error("{count} pinned coordinates need 2^{count} weights, above the {field_bits} available")]
TooManyPinned {
count: usize,
field_bits: usize,
},
#[error("the generator's 2^{index} power is one, leaving coordinate {index} undefined")]
DegenerateGenerator {
index: usize,
},
#[error("the equality weights are linearly dependent over the prime field")]
DependentWeights,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PinnedEqWeights<F> {
challenges: Vec<F>,
weights: Vec<F>,
}
impl<F: TowerLevel> PinnedEqWeights<F>
where
F::Repr: Into<u128>,
{
pub fn geometric(generator: F, count: usize) -> Result<Self, PinnedEqError> {
let field_bits = 1usize << F::LOG_BITS;
if count >= usize::BITS as usize || (1usize << count) > field_bits {
return Err(PinnedEqError::TooManyPinned { count, field_bits });
}
let mut challenges = Vec::with_capacity(count);
let mut power = generator;
for index in 0..count {
let denominator = F::ONE + power;
if denominator.is_zero() {
return Err(PinnedEqError::DegenerateGenerator { index });
}
challenges.push(power * denominator.inverse());
power = power.square();
}
challenges.reverse();
Self::from_challenges(challenges)
}
pub fn from_challenges(challenges: Vec<F>) -> Result<Self, PinnedEqError> {
let count = challenges.len();
let field_bits = 1usize << F::LOG_BITS;
if count >= usize::BITS as usize || (1usize << count) > field_bits {
return Err(PinnedEqError::TooManyPinned { count, field_bits });
}
let weights = Poly::new_from_point(&challenges, F::ONE).into_evals();
if !prime_field_independent(&weights) {
return Err(PinnedEqError::DependentWeights);
}
Ok(Self {
challenges,
weights,
})
}
}
impl<F: Field> PinnedEqWeights<F> {
#[must_use]
pub const fn num_pinned(&self) -> usize {
self.challenges.len()
}
#[must_use]
pub const fn block_len(&self) -> usize {
self.weights.len()
}
#[must_use]
pub fn challenges(&self) -> &[F] {
&self.challenges
}
#[must_use]
pub fn weights(&self) -> &[F] {
&self.weights
}
#[must_use]
pub fn fold<A>(&self, block: &[A]) -> A
where
A: p3_field::Algebra<F> + Copy,
{
assert_eq!(block.len(), self.weights.len(), "one value per weight");
block
.iter()
.zip(&self.weights)
.map(|(&value, &weight)| value * weight)
.sum()
}
pub fn fold_blocks<A>(&self, values: &[A], out: &mut [A])
where
A: p3_field::Algebra<F> + Copy,
{
assert_eq!(values.len(), out.len() * self.weights.len(), "whole blocks");
for (block, entry) in values.chunks_exact(self.weights.len()).zip(out) {
*entry = self.fold(block);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GeometricEqWeights {
num_pinned: usize,
scale: Ghash128,
}
impl GeometricEqWeights {
pub fn new(num_pinned: usize) -> Result<Self, PinnedEqError> {
let field_bits = 1usize << Ghash128::LOG_BITS;
if num_pinned >= usize::BITS as usize || (1usize << num_pinned) > field_bits {
return Err(PinnedEqError::TooManyPinned {
count: num_pinned,
field_bits,
});
}
let block_len = 1usize << num_pinned;
let all_powers = Ghash128::from_repr(u128::MAX >> (field_bits - block_len));
let scale = all_powers.inverse();
Ok(Self { num_pinned, scale })
}
#[must_use]
pub const fn num_pinned(&self) -> usize {
self.num_pinned
}
#[must_use]
pub const fn block_len(&self) -> usize {
1 << self.num_pinned
}
pub const fn scale(&self) -> Ghash128 {
self.scale
}
#[must_use]
pub fn challenges(&self) -> Vec<Ghash128> {
(0..self.num_pinned)
.rev()
.map(|i| {
let u = Ghash128::from_repr(1u128 << (1usize << i));
u * (Ghash128::ONE + u).inverse()
})
.collect()
}
pub fn fold_bits_unscaled(&self, bits: u128) -> Ghash128 {
let mask = u128::MAX >> ((1usize << Ghash128::LOG_BITS) - self.block_len());
assert_eq!(bits & !mask, 0, "one bit per weight");
Ghash128::from_repr(bits)
}
pub fn fold_bits(&self, bits: u128) -> Ghash128 {
self.scale * self.fold_bits_unscaled(bits)
}
pub fn fold(&self, block: &[Ghash128]) -> Ghash128 {
self.scale * self.fold_unscaled(block)
}
pub fn fold_unscaled(&self, block: &[Ghash128]) -> Ghash128 {
assert_eq!(block.len(), self.block_len(), "one value per weight");
Ghash128::dot_powers_of_x(block)
}
pub fn fold_blocks_unscaled(&self, values: &[Ghash128], out: &mut [Ghash128]) {
assert_eq!(values.len(), out.len() * self.block_len(), "whole blocks");
for (block, entry) in values.chunks_exact(self.block_len()).zip(out) {
*entry = Ghash128::dot_powers_of_x(block);
}
}
}
#[derive(Debug, Clone)]
pub struct SubfieldFoldTable<F, Sub> {
table: Vec<F>,
block_len: usize,
_sub: PhantomData<Sub>,
}
impl<F, Sub> SubfieldFoldTable<F, Sub>
where
F: Field + ExtensionField<Sub>,
Sub: TowerLevel,
{
#[must_use]
pub fn new(weights: &PinnedEqWeights<F>) -> Self
where
Sub::Repr: From<u8>,
{
assert_eq!(
1usize << Sub::LOG_BITS,
CHUNK_BITS,
"the fold table is indexed by one byte, so the subfield must be byte-wide"
);
let block_len = weights.block_len();
let mut table = F::zero_vec(block_len * TABLE_ROWS);
for (row, &weight) in table
.as_chunks_mut::<TABLE_ROWS>()
.0
.iter_mut()
.zip(weights.weights())
{
for (value, entry) in row.iter_mut().enumerate() {
let embedded = F::from(Sub::from_repr(Sub::Repr::from(value as u8)));
*entry = weight * embedded;
}
}
Self {
table,
block_len,
_sub: PhantomData,
}
}
#[must_use]
pub const fn block_len(&self) -> usize {
self.block_len
}
#[must_use]
pub fn fold(&self, block: &[Sub]) -> F
where
Sub::Repr: Into<u128>,
{
assert_eq!(block.len(), self.block_len, "one value per weight");
block
.iter()
.enumerate()
.map(|(index, &value)| {
let byte = value.to_repr().into() as usize & 0xff;
self.table[index * TABLE_ROWS + byte]
})
.sum()
}
}
fn prime_field_independent<F: TowerLevel>(elements: &[F]) -> bool
where
F::Repr: Into<u128>,
{
let mut pivots = Vec::<u128>::with_capacity(elements.len());
for &element in elements {
let mut pattern: u128 = element.to_repr().into();
for &pivot in &pivots {
let leading = 1u128 << (127 - pivot.leading_zeros());
if pattern & leading != 0 {
pattern ^= pivot;
}
}
if pattern == 0 {
return false;
}
pivots.push(pattern);
}
true
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryField8, BinaryField16, BinaryField128, Ghash128};
use p3_field::PrimeCharacteristicRing;
use p3_multilinear_util::poly::Poly;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
fn indeterminate() -> Ghash128 {
Ghash128::from_repr(2)
}
#[test]
fn geometric_weights_are_the_equality_table_of_the_pinned_point() {
let pinned = PinnedEqWeights::geometric(indeterminate(), 4).unwrap();
let expected = Poly::new_from_point(pinned.challenges(), Ghash128::ONE);
assert_eq!(pinned.weights(), expected.as_slice());
}
#[test]
fn geometric_weights_are_a_progression_in_the_generator() {
let pinned = PinnedEqWeights::geometric(indeterminate(), 4).unwrap();
for index in 1..pinned.block_len() {
assert_eq!(
pinned.weights()[index],
pinned.weights()[index - 1] * indeterminate(),
"index={index}"
);
}
}
#[test]
fn folding_a_block_matches_weighing_it_by_hand() {
let mut rng = SmallRng::seed_from_u64(19);
let pinned = PinnedEqWeights::geometric(indeterminate(), 3).unwrap();
let block = (0..pinned.block_len())
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
let expected = block
.iter()
.zip(pinned.weights())
.map(|(&value, &weight)| value * weight)
.sum::<Ghash128>();
assert_eq!(pinned.fold(&block), expected);
}
#[test]
fn folding_blocks_collapses_each_run_independently() {
let mut rng = SmallRng::seed_from_u64(23);
let pinned = PinnedEqWeights::geometric(indeterminate(), 2).unwrap();
let num_blocks = 5;
let values = (0..num_blocks * pinned.block_len())
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
let mut out = Ghash128::zero_vec(num_blocks);
pinned.fold_blocks(&values, &mut out);
for (index, block) in values.chunks_exact(pinned.block_len()).enumerate() {
assert_eq!(out[index], pinned.fold(block), "block={index}");
}
}
#[test]
fn pinning_reproduces_the_sum_the_random_point_would_have_given() {
let mut rng = SmallRng::seed_from_u64(29);
let pinned = PinnedEqWeights::geometric(indeterminate(), 3).unwrap();
let free = (0..3).map(|_| rng.random::<Ghash128>()).collect::<Vec<_>>();
let values = (0..1 << 6)
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
let whole = free
.iter()
.chain(pinned.challenges())
.copied()
.collect::<Vec<_>>();
let direct = Poly::new_from_point(&whole, Ghash128::ONE)
.as_slice()
.iter()
.zip(&values)
.map(|(&weight, &value)| weight * value)
.sum::<Ghash128>();
let mut folded = Ghash128::zero_vec(1 << 3);
pinned.fold_blocks(&values, &mut folded);
let split = Poly::new_from_point(&free, Ghash128::ONE)
.as_slice()
.iter()
.zip(&folded)
.map(|(&weight, &value)| weight * value)
.sum::<Ghash128>();
assert_eq!(direct, split);
}
#[test]
fn a_polynomial_basis_supports_seven_pinned_coordinates() {
assert!(PinnedEqWeights::geometric(indeterminate(), 7).is_ok());
assert_eq!(
PinnedEqWeights::geometric(indeterminate(), 8).unwrap_err(),
PinnedEqError::TooManyPinned {
count: 8,
field_bits: 128,
}
);
}
#[test]
fn the_independence_check_rejects_a_generator_whose_powers_repeat() {
assert_eq!(
PinnedEqWeights::geometric(Ghash128::ONE, 3).unwrap_err(),
PinnedEqError::DegenerateGenerator { index: 0 }
);
assert_eq!(
PinnedEqWeights::<BinaryField8>::geometric(BinaryField8::from_repr(2), 4).unwrap_err(),
PinnedEqError::TooManyPinned {
count: 4,
field_bits: 8,
}
);
}
#[test]
fn the_independence_check_rejects_a_dependent_explicit_point() {
assert_eq!(
PinnedEqWeights::from_challenges(alloc::vec![Ghash128::ZERO; 2]).unwrap_err(),
PinnedEqError::DependentWeights
);
}
#[test]
fn the_independence_check_accepts_a_basis_and_rejects_a_spanned_element() {
let independent = [1u128, 2, 4].map(Ghash128::from_repr);
assert!(prime_field_independent(&independent));
let dependent = [1u128, 2, 4, 3].map(Ghash128::from_repr);
assert!(!prime_field_independent(&dependent));
}
#[test]
fn a_generator_from_a_small_subfield_is_refused() {
assert_eq!(
PinnedEqWeights::geometric(BinaryField128::from_repr(2), 2).unwrap_err(),
PinnedEqError::DependentWeights
);
assert_eq!(
PinnedEqWeights::geometric(BinaryField16::from_repr(2), 2).unwrap_err(),
PinnedEqError::DependentWeights
);
assert!(PinnedEqWeights::geometric(indeterminate(), 7).is_ok());
assert!(
PinnedEqWeights::geometric(BinaryField128::from(indeterminate()), 7).is_ok(),
"the same element in the tower basis is still full degree"
);
}
fn tower_pinned(count: usize) -> PinnedEqWeights<BinaryField128> {
let mut rng = SmallRng::seed_from_u64(0xF01D);
loop {
let challenges = (0..count)
.map(|_| rng.random::<BinaryField128>())
.collect::<Vec<_>>();
if let Ok(pinned) = PinnedEqWeights::from_challenges(challenges) {
return pinned;
}
}
}
#[test]
fn the_subfield_table_folds_the_same_way_the_weights_do() {
let mut rng = SmallRng::seed_from_u64(31);
let pinned = tower_pinned(4);
let table = SubfieldFoldTable::<_, BinaryField8>::new(&pinned);
let block = (0..pinned.block_len())
.map(|_| rng.random::<BinaryField8>())
.collect::<Vec<_>>();
let embedded = block
.iter()
.map(|&value| BinaryField128::from(value))
.collect::<Vec<_>>();
assert_eq!(table.fold(&block), pinned.fold(&embedded));
}
#[test]
fn the_subfield_table_is_sixty_four_kibibytes_at_four_pinned_coordinates() {
let table = SubfieldFoldTable::<_, BinaryField8>::new(&tower_pinned(4));
assert_eq!(table.block_len(), 16);
assert_eq!(table.table.len() * size_of::<BinaryField128>(), 64 * 1024);
}
#[test]
fn the_geometric_form_pins_the_same_coordinates_as_the_general_one() {
for count in 0..=7 {
let general = PinnedEqWeights::geometric(indeterminate(), count).unwrap();
let structured = GeometricEqWeights::new(count).unwrap();
assert_eq!(
structured.challenges(),
general.challenges(),
"count={count}"
);
assert_eq!(structured.block_len(), general.block_len(), "count={count}");
}
}
#[test]
fn the_constant_is_the_weight_of_the_all_zero_assignment() {
for count in 0..=7 {
let general = PinnedEqWeights::geometric(indeterminate(), count).unwrap();
let structured = GeometricEqWeights::new(count).unwrap();
assert_eq!(structured.scale(), general.weights()[0], "count={count}");
}
}
#[test]
fn the_geometric_fold_agrees_with_the_general_weighted_sum() {
let mut rng = SmallRng::seed_from_u64(37);
for count in 0..=7 {
let general = PinnedEqWeights::geometric(indeterminate(), count).unwrap();
let structured = GeometricEqWeights::new(count).unwrap();
let block = (0..structured.block_len())
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
assert_eq!(
structured.fold(&block),
general.fold(&block),
"count={count}"
);
}
}
#[test]
fn the_unscaled_fold_is_short_of_exactly_the_constant() {
let mut rng = SmallRng::seed_from_u64(41);
let structured = GeometricEqWeights::new(4).unwrap();
let block = (0..structured.block_len())
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
assert_eq!(
structured.fold(&block),
structured.scale() * structured.fold_unscaled(&block)
);
}
#[test]
fn a_bit_valued_block_folds_to_its_own_bit_pattern() {
let mut rng = SmallRng::seed_from_u64(47);
let structured = GeometricEqWeights::new(7).unwrap();
let general = PinnedEqWeights::geometric(indeterminate(), 7).unwrap();
let bits = rng.random::<u128>();
assert_eq!(
structured.fold_bits_unscaled(bits),
Ghash128::from_repr(bits)
);
let block = (0..structured.block_len())
.map(|k| Ghash128::from_bool((bits >> k) & 1 == 1))
.collect::<Vec<_>>();
assert_eq!(structured.fold_bits(bits), general.fold(&block));
}
#[test]
#[should_panic(expected = "one bit per weight")]
fn the_bit_just_above_the_block_is_refused() {
let _ = GeometricEqWeights::new(3)
.unwrap()
.fold_bits_unscaled(1 << 8);
}
#[test]
#[should_panic(expected = "one bit per weight")]
fn the_top_bit_of_the_word_is_refused() {
let _ = GeometricEqWeights::new(3)
.unwrap()
.fold_bits_unscaled(1 << 127);
}
#[test]
#[should_panic(expected = "one bit per weight")]
fn the_first_bit_above_a_half_word_block_is_refused() {
let _ = GeometricEqWeights::new(6)
.unwrap()
.fold_bits_unscaled(1 << 64);
}
#[test]
fn the_geometric_form_stops_where_independence_does() {
assert!(GeometricEqWeights::new(7).is_ok());
assert_eq!(
GeometricEqWeights::new(8).unwrap_err(),
PinnedEqError::TooManyPinned {
count: 8,
field_bits: 128,
}
);
}
#[test]
fn the_geometric_form_folds_each_block_on_its_own() {
let mut rng = SmallRng::seed_from_u64(43);
let structured = GeometricEqWeights::new(3).unwrap();
let num_blocks = 5;
let values = (0..num_blocks * structured.block_len())
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
let mut out = Ghash128::zero_vec(num_blocks);
structured.fold_blocks_unscaled(&values, &mut out);
for (index, block) in values.chunks_exact(structured.block_len()).enumerate() {
assert_eq!(out[index], structured.fold_unscaled(block), "block={index}");
}
}
proptest! {
#[test]
fn pinned_coordinates_agree_with_the_equality_polynomial_at_every_width(
count in 1usize..=7,
) {
let pinned = PinnedEqWeights::geometric(indeterminate(), count).unwrap();
let expected = Poly::new_from_point(pinned.challenges(), Ghash128::ONE);
prop_assert_eq!(pinned.weights(), expected.as_slice());
}
#[test]
fn the_subfield_table_matches_the_plain_fold_at_every_width(
count in 1usize..=4,
seed: u64,
) {
let mut rng = SmallRng::seed_from_u64(seed);
let pinned = tower_pinned(count);
let table = SubfieldFoldTable::<_, BinaryField8>::new(&pinned);
let block = (0..pinned.block_len())
.map(|_| rng.random::<BinaryField8>())
.collect::<Vec<_>>();
let embedded = block.iter().map(|&v| BinaryField128::from(v)).collect::<Vec<_>>();
prop_assert_eq!(table.fold(&block), pinned.fold(&embedded));
}
#[test]
fn the_geometric_fold_matches_the_general_one_at_every_width(
count in 0usize..=7,
seed: u64,
) {
let mut rng = SmallRng::seed_from_u64(seed);
let general = PinnedEqWeights::geometric(indeterminate(), count).unwrap();
let structured = GeometricEqWeights::new(count).unwrap();
let block = (0..structured.block_len())
.map(|_| rng.random::<Ghash128>())
.collect::<Vec<_>>();
prop_assert_eq!(structured.fold(&block), general.fold(&block));
}
#[test]
fn the_bit_fold_matches_the_spelled_out_block_at_every_width(
count in 0usize..=7,
bits: u128,
) {
let structured = GeometricEqWeights::new(count).unwrap();
let general = PinnedEqWeights::geometric(indeterminate(), count).unwrap();
let bits = bits & (u128::MAX >> (128 - structured.block_len()));
let block = (0..structured.block_len())
.map(|k| Ghash128::from_bool((bits >> k) & 1 == 1))
.collect::<Vec<_>>();
prop_assert_eq!(structured.fold_bits(bits), general.fold(&block));
}
}
}