use core::marker::PhantomData;
use p3_binary_field::TowerLevel;
use p3_matrix::Matrix;
use p3_matrix::dense::RowMajorMatrix;
use p3_maybe_rayon::prelude::*;
use p3_util::log2_strict_usize;
use crate::domain::{domain_point, subspace_polynomial};
use crate::traits::AdditiveNtt;
#[derive(Clone, Debug, Default)]
pub struct LchNtt<F> {
_marker: PhantomData<F>,
}
pub(crate) const BUTTERFLY_GRAIN: usize = 1 << 10;
impl<F: TowerLevel> AdditiveNtt<F> for LchNtt<F> {
fn shifted_ntt_batch(&self, mut mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
for j in (0..log_n).rev() {
let half = (1 << j) * width;
let base = subspace_polynomial::<F>(j, shift);
mat.values
.par_chunks_mut(half << 1)
.enumerate()
.for_each(|(blk, block)| {
let t = base + domain_point::<F>(blk << 1);
let zero = t.is_zero();
let (lo, hi) = block.split_at_mut(half);
let butterfly = |lo: &mut [F], hi: &mut [F]| {
if zero {
for (u, v) in lo.iter_mut().zip(hi) {
*v += *u;
}
} else {
for (u, v) in lo.iter_mut().zip(hi) {
*u += t * *v;
*v += *u;
}
}
};
if half <= BUTTERFLY_GRAIN {
butterfly(lo, hi);
} else {
lo.par_chunks_mut(BUTTERFLY_GRAIN)
.zip(hi.par_chunks_mut(BUTTERFLY_GRAIN))
.for_each(|(lo, hi)| butterfly(lo, hi));
}
});
}
mat
}
fn shifted_intt_batch(&self, mut mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
for j in 0..log_n {
let half = (1 << j) * width;
let base = subspace_polynomial::<F>(j, shift);
mat.values
.par_chunks_mut(half << 1)
.enumerate()
.for_each(|(blk, block)| {
let t = base + domain_point::<F>(blk << 1);
let zero = t.is_zero();
let (lo, hi) = block.split_at_mut(half);
let butterfly = |lo: &mut [F], hi: &mut [F]| {
if zero {
for (u, v) in lo.iter_mut().zip(hi) {
*v += *u;
}
} else {
for (u, v) in lo.iter_mut().zip(hi) {
*v += *u;
*u += t * *v;
}
}
};
if half <= BUTTERFLY_GRAIN {
butterfly(lo, hi);
} else {
lo.par_chunks_mut(BUTTERFLY_GRAIN)
.zip(hi.par_chunks_mut(BUTTERFLY_GRAIN))
.for_each(|(lo, hi)| butterfly(lo, hi));
}
});
}
mat
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use p3_binary_field::{
BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, TowerLevel,
};
use p3_matrix::dense::RowMajorMatrix;
use proptest::prelude::*;
use super::LchNtt;
use crate::naive::NaiveAdditiveNtt;
use crate::traits::AdditiveNtt;
fn sample<F: TowerLevel>(bits: u64) -> F {
F::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
}
fn matrix<F: TowerLevel>(log_n: usize, width: usize, seed: u64) -> RowMajorMatrix<F> {
RowMajorMatrix::new(
(0..(width << log_n))
.map(|i| {
sample::<F>(
seed.wrapping_mul(0x9e37_79b9_7f4a_7c15)
.wrapping_add(i as u64),
)
})
.collect(),
width,
)
}
fn check_matches_naive<F: TowerLevel>(log_n: usize, width: usize, seed: u64, shift: u64) {
let coeffs = matrix::<F>(log_n, width, seed);
let shift = sample::<F>(shift);
let fast = LchNtt::<F>::default().shifted_ntt_batch(coeffs.clone(), shift);
let slow = NaiveAdditiveNtt::<F>::default().shifted_ntt_batch(coeffs, shift);
assert_eq!(fast, slow);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn lch_matches_naive_at_8_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField8>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_16_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField16>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_32_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField32>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_64_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField64>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_128_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField128>(log_n, width, seed, shift);
}
#[test]
fn lch_round_trips(
log_n in 0usize..=10,
width in 1usize..=3,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix::<BinaryField16>(log_n, width, seed);
let shift = sample::<BinaryField16>(shift);
let ntt = LchNtt::<BinaryField16>::default();
let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift);
prop_assert_eq!(ntt.shifted_intt_batch(evals, shift), coeffs);
}
#[test]
fn lch_lde_matches_naive(
log_n in 0usize..=6,
added in 0usize..=3,
width in 1usize..=3,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix::<BinaryField16>(log_n, width, seed);
let shift = sample::<BinaryField16>(shift);
let lde = LchNtt::<BinaryField16>::default()
.shifted_lde_batch(coeffs.clone(), added, shift);
let naive = NaiveAdditiveNtt::<BinaryField16>::default()
.shifted_lde_batch(coeffs.clone(), added, shift);
prop_assert_eq!(&lde, &naive);
prop_assert_eq!(&lde.values[..coeffs.values.len()], &coeffs.values[..]);
}
}
#[test]
#[should_panic]
fn shifted_ntt_batch_rejects_l_past_the_bit_width() {
let coeffs = matrix::<BinaryField8>((1 << BinaryField8::LOG_BITS) + 1, 1, 0);
let _ = LchNtt::<BinaryField8>::default().ntt_batch(coeffs);
}
#[test]
#[should_panic]
fn shifted_ntt_batch_rejects_a_non_power_of_two_height() {
let coeffs = RowMajorMatrix::new(vec![sample::<BinaryField8>(0); 3], 1);
let _ = LchNtt::<BinaryField8>::default().ntt_batch(coeffs);
}
#[test]
fn lch_is_linear() {
let ntt = LchNtt::<BinaryField16>::default();
let a = matrix::<BinaryField16>(5, 2, 1);
let b = matrix::<BinaryField16>(5, 2, 2);
let sum = RowMajorMatrix::new(
a.values
.iter()
.zip(&b.values)
.map(|(x, y)| *x + *y)
.collect(),
2,
);
let lhs = ntt.ntt_batch(sum);
let rhs_a = ntt.ntt_batch(a);
let rhs_b = ntt.ntt_batch(b);
for (i, v) in lhs.values.iter().enumerate() {
assert_eq!(*v, rhs_a.values[i] + rhs_b.values[i]);
}
}
#[test]
fn levels_agree_after_embedding() {
const LOG_N: usize = 6;
let coeffs32 = matrix::<BinaryField32>(LOG_N, 1, 7);
let coeffs128 = RowMajorMatrix::new(
coeffs32
.values
.iter()
.map(|v| BinaryField128::from_repr(u128::from(v.to_repr())))
.collect(),
1,
);
let small = LchNtt::<BinaryField32>::default().ntt_batch(coeffs32);
let large = LchNtt::<BinaryField128>::default().ntt_batch(coeffs128);
for (s, l) in small.values.iter().zip(&large.values) {
assert_eq!(u128::from(s.to_repr()), l.to_repr());
}
}
}