use alloc::vec::Vec;
use p3_binary_field::TowerLevel;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SkipDomainError {
#[error("the extension dimension {log_extended} must exceed the subspace dimension {log_size}")]
EmptyExtension {
log_size: usize,
log_extended: usize,
},
#[error("a dimension-{log_extended} subspace is above the limit of {limit}")]
ExtensionTooWide {
log_extended: usize,
limit: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkipDomain<F> {
log_size: usize,
log_extended: usize,
points: Vec<F>,
vanishing_on_extension: Vec<F>,
derivative_on_subspace: F,
}
impl<F: TowerLevel> SkipDomain<F> {
pub fn new(log_size: usize, log_extended: usize) -> Result<Self, SkipDomainError> {
if log_extended <= log_size {
return Err(SkipDomainError::EmptyExtension {
log_size,
log_extended,
});
}
let limit = (1usize << F::LOG_BITS).min(usize::BITS as usize - 1);
if log_extended > limit {
return Err(SkipDomainError::ExtensionTooWide {
log_extended,
limit,
});
}
let points = (0..1usize << log_extended)
.map(Self::point)
.collect::<Vec<_>>();
let vanishing_on_extension = points[1 << log_size..]
.iter()
.map(|&x| Self::vanishing_poly(log_size, x))
.collect::<Vec<_>>();
let derivative_on_subspace = points[1..1 << log_size].iter().copied().product::<F>();
Ok(Self {
log_size,
log_extended,
points,
vanishing_on_extension,
derivative_on_subspace,
})
}
pub fn for_degree(log_size: usize, degree: usize) -> Result<Self, SkipDomainError> {
let Some(extra) = Self::extra_dimensions(degree) else {
return Err(SkipDomainError::EmptyExtension {
log_size,
log_extended: log_size,
});
};
Self::new(log_size, log_size + extra)
}
#[must_use]
pub const fn extra_dimensions(degree: usize) -> Option<usize> {
if degree == 0 {
return None;
}
let extra = usize::BITS as usize - (degree - 1).leading_zeros() as usize;
Some(if extra == 0 { 1 } else { extra })
}
#[must_use]
pub const fn admits_degree(&self, degree: usize) -> bool {
match Self::extra_dimensions(degree) {
Some(extra) => self.log_size + extra <= self.log_extended,
None => false,
}
}
fn point(index: usize) -> F {
let mut point = F::ZERO;
let mut remaining = index;
let mut position = 0;
while remaining != 0 {
if remaining & 1 == 1 {
point += F::cantor_basis(position);
}
remaining >>= 1;
position += 1;
}
point
}
fn vanishing_poly(log_size: usize, x: F) -> F {
let mut value = x;
for _ in 0..log_size {
value = value.square() + value;
}
value
}
#[must_use]
pub const fn log_size(&self) -> usize {
self.log_size
}
#[must_use]
pub const fn size(&self) -> usize {
1 << self.log_size
}
#[must_use]
pub const fn log_extended(&self) -> usize {
self.log_extended
}
#[must_use]
pub const fn num_transmitted(&self) -> usize {
(1 << self.log_extended) - (1 << self.log_size)
}
#[must_use]
pub fn subspace(&self) -> &[F] {
&self.points[..self.size()]
}
#[must_use]
pub fn transmitted(&self) -> &[F] {
&self.points[self.size()..]
}
#[must_use]
pub fn vanishing(&self, x: F) -> F {
Self::vanishing_poly(self.log_size, x)
}
#[must_use]
pub fn vanishing_on_transmitted(&self) -> &[F] {
&self.vanishing_on_extension
}
#[must_use]
pub const fn derivative_on_subspace(&self) -> F {
self.derivative_on_subspace
}
#[must_use]
pub fn resampling_matrix(&self) -> Vec<F> {
self.resampling_prefix(self.size())
}
#[must_use]
pub fn resampling_prefix(&self, columns: usize) -> Vec<F> {
assert!(columns <= self.size(), "one column per subspace point");
let inverse_derivative = self.derivative_on_subspace.inverse();
let mut matrix = F::zero_vec(self.num_transmitted() * columns);
let rows = matrix.chunks_exact_mut(columns);
for ((row, &t), &vanishing) in rows
.zip(self.transmitted())
.zip(&self.vanishing_on_extension)
{
let scale = vanishing * inverse_derivative;
for (entry, &s) in row.iter_mut().zip(self.subspace()) {
*entry = scale * (t + s).inverse();
}
}
matrix
}
#[must_use]
pub fn interpolate(&self, values: &[F], at: F) -> F {
assert_eq!(values.len(), self.size(), "one value per subspace point");
if let Some(index) = self.subspace().iter().position(|&s| s == at) {
return values[index];
}
let scale = self.vanishing(at) * self.derivative_on_subspace.inverse();
scale
* values
.iter()
.zip(self.subspace())
.map(|(&value, &s)| value * (at + s).inverse())
.sum::<F>()
}
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryField8, BinaryField16, Ghash128};
use p3_field::PrimeCharacteristicRing;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
const LOG_SIZE: usize = 6;
#[test]
fn the_subspace_is_the_index_prefix_of_the_extension() {
let domain = SkipDomain::<BinaryField8>::new(LOG_SIZE, LOG_SIZE + 1).unwrap();
assert_eq!(domain.size(), 64);
assert_eq!(domain.num_transmitted(), 64);
for &t in domain.transmitted() {
assert!(!domain.subspace().contains(&t));
}
let mut seen = domain.subspace().to_vec();
seen.extend_from_slice(domain.transmitted());
for (index, &point) in seen.iter().enumerate() {
assert!(!seen[index + 1..].contains(&point), "index={index}");
}
}
#[test]
fn index_zero_is_the_origin_and_single_bits_are_basis_vectors() {
let domain = SkipDomain::<BinaryField8>::new(LOG_SIZE, LOG_SIZE + 1).unwrap();
assert_eq!(domain.subspace()[0], BinaryField8::ZERO);
for position in 0..LOG_SIZE {
assert_eq!(
domain.subspace()[1 << position],
BinaryField8::cantor_basis(position)
);
}
}
#[test]
fn indexing_is_additive_over_index_xor() {
let domain = SkipDomain::<BinaryField8>::new(3, 7).unwrap();
let points = domain
.subspace()
.iter()
.chain(domain.transmitted())
.copied()
.collect::<Vec<_>>();
for a in 0..points.len() {
for b in 0..points.len() {
assert_eq!(points[a ^ b], points[a] + points[b], "a={a} b={b}");
}
}
}
#[test]
fn vanishing_polynomial_matches_the_product_over_the_subspace() {
let domain = SkipDomain::<BinaryField8>::new(LOG_SIZE, LOG_SIZE + 1).unwrap();
for x in 0..=u8::MAX {
let x = BinaryField8::from_repr(x);
let product = domain
.subspace()
.iter()
.map(|&s| x + s)
.product::<BinaryField8>();
assert_eq!(domain.vanishing(x), product, "x={x:?}");
}
}
#[test]
fn vanishing_polynomial_is_zero_exactly_on_the_subspace() {
let domain = SkipDomain::<BinaryField8>::new(LOG_SIZE, LOG_SIZE + 1).unwrap();
for &s in domain.subspace() {
assert_eq!(domain.vanishing(s), BinaryField8::ZERO);
}
for (&t, &cached) in domain
.transmitted()
.iter()
.zip(domain.vanishing_on_transmitted())
{
assert_eq!(domain.vanishing(t), cached);
assert_ne!(cached, BinaryField8::ZERO);
}
}
#[test]
fn the_vanishing_polynomial_is_constant_on_each_coset() {
let domain = SkipDomain::<BinaryField8>::new(3, 6).unwrap();
for coset in domain
.vanishing_on_transmitted()
.chunks_exact(domain.size())
{
assert!(coset.iter().all(|&value| value == coset[0]));
}
}
#[test]
fn canonical_normalisation_makes_the_derivative_one() {
for log_size in 1..7 {
let domain = SkipDomain::<BinaryField8>::new(log_size, log_size + 1).unwrap();
assert_eq!(domain.derivative_on_subspace(), BinaryField8::ONE);
}
}
#[test]
fn resampling_matrix_agrees_with_lagrange_interpolation() {
let mut rng = SmallRng::seed_from_u64(7);
let domain = SkipDomain::<BinaryField8>::new(LOG_SIZE, LOG_SIZE + 1).unwrap();
let values = (0..domain.size())
.map(|_| rng.random::<BinaryField8>())
.collect::<Vec<_>>();
let matrix = domain.resampling_matrix();
for (index, row) in matrix.chunks_exact(domain.size()).enumerate() {
let by_matrix = row
.iter()
.zip(&values)
.map(|(&m, &v)| m * v)
.sum::<BinaryField8>();
assert_eq!(
by_matrix,
domain.interpolate(&values, domain.transmitted()[index]),
"index={index}"
);
}
}
#[test]
fn interpolation_reproduces_the_values_it_was_given() {
let mut rng = SmallRng::seed_from_u64(11);
let domain = SkipDomain::<BinaryField16>::new(5, 6).unwrap();
let values = (0..domain.size())
.map(|_| rng.random::<BinaryField16>())
.collect::<Vec<_>>();
for (index, &s) in domain.subspace().iter().enumerate() {
assert_eq!(domain.interpolate(&values, s), values[index]);
}
}
#[test]
fn degree_sizing_leaves_room_for_the_quotient() {
for degree in 1..=8usize {
let domain = SkipDomain::<BinaryField16>::for_degree(4, degree).unwrap();
let deg_p = degree * (domain.size() - 1);
let deg_q = deg_p.saturating_sub(domain.size());
let needed = deg_q + 1;
assert!(
domain.num_transmitted() >= needed,
"degree={degree} transmitted={} needs={needed}",
domain.num_transmitted(),
);
}
let domain = SkipDomain::<BinaryField8>::for_degree(6, 2).unwrap();
assert_eq!(domain.log_extended(), 7);
}
#[test]
fn rejects_an_extension_that_adds_nothing() {
assert_eq!(
SkipDomain::<BinaryField8>::new(4, 4),
Err(SkipDomainError::EmptyExtension {
log_size: 4,
log_extended: 4,
})
);
assert_eq!(
SkipDomain::<BinaryField8>::for_degree(4, 0),
Err(SkipDomainError::EmptyExtension {
log_size: 4,
log_extended: 4,
})
);
}
#[test]
fn rejects_an_extension_wider_than_the_field() {
assert_eq!(
SkipDomain::<BinaryField8>::new(8, 9),
Err(SkipDomainError::ExtensionTooWide {
log_extended: 9,
limit: 8,
})
);
assert!(SkipDomain::<BinaryField8>::new(7, 8).is_ok());
}
#[test]
fn rejects_an_extension_wider_than_a_machine_word() {
let limit = usize::BITS as usize - 1;
assert_eq!(
SkipDomain::<Ghash128>::new(99, 100),
Err(SkipDomainError::ExtensionTooWide {
log_extended: 100,
limit,
})
);
assert!(SkipDomain::<Ghash128>::new(3, 4).is_ok());
}
proptest! {
#[test]
fn interpolation_is_linear_in_the_values(seed: u64) {
let mut rng = SmallRng::seed_from_u64(seed);
let domain = SkipDomain::<Ghash128>::new(4, 5).unwrap();
let left = (0..domain.size()).map(|_| rng.random::<Ghash128>()).collect::<Vec<_>>();
let right = (0..domain.size()).map(|_| rng.random::<Ghash128>()).collect::<Vec<_>>();
let at = rng.random::<Ghash128>();
let summed = left.iter().zip(&right).map(|(&a, &b)| a + b).collect::<Vec<_>>();
prop_assert_eq!(
domain.interpolate(&summed, at),
domain.interpolate(&left, at) + domain.interpolate(&right, at)
);
}
}
}