1use core::marker::PhantomData;
4
5use p3_binary_field::TowerLevel;
6use p3_matrix::Matrix;
7use p3_matrix::dense::RowMajorMatrix;
8use p3_maybe_rayon::prelude::*;
9use p3_util::log2_strict_usize;
10
11use crate::domain::{domain_point, subspace_polynomial};
12use crate::traits::AdditiveNtt;
13
14#[derive(Clone, Debug, Default)]
25pub struct LchNtt<F> {
26 _marker: PhantomData<F>,
27}
28
29pub(crate) const BUTTERFLY_GRAIN: usize = 1 << 10;
41
42impl<F: TowerLevel> AdditiveNtt<F> for LchNtt<F> {
43 fn shifted_ntt_batch(&self, mut mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
44 let width = mat.width();
45 let log_n = log2_strict_usize(mat.height());
46
47 for j in (0..log_n).rev() {
48 let half = (1 << j) * width;
49 let base = subspace_polynomial::<F>(j, shift);
52 mat.values
53 .par_chunks_mut(half << 1)
54 .enumerate()
55 .for_each(|(blk, block)| {
56 let t = base + domain_point::<F>(blk << 1);
57 let zero = t.is_zero();
58 let (lo, hi) = block.split_at_mut(half);
59 let butterfly = |lo: &mut [F], hi: &mut [F]| {
60 if zero {
61 for (u, v) in lo.iter_mut().zip(hi) {
63 *v += *u;
64 }
65 } else {
66 for (u, v) in lo.iter_mut().zip(hi) {
67 *u += t * *v;
69 *v += *u;
70 }
71 }
72 };
73 if half <= BUTTERFLY_GRAIN {
76 butterfly(lo, hi);
77 } else {
78 lo.par_chunks_mut(BUTTERFLY_GRAIN)
79 .zip(hi.par_chunks_mut(BUTTERFLY_GRAIN))
80 .for_each(|(lo, hi)| butterfly(lo, hi));
81 }
82 });
83 }
84 mat
85 }
86
87 fn shifted_intt_batch(&self, mut mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
88 let width = mat.width();
89 let log_n = log2_strict_usize(mat.height());
90
91 for j in 0..log_n {
92 let half = (1 << j) * width;
93 let base = subspace_polynomial::<F>(j, shift);
95 mat.values
96 .par_chunks_mut(half << 1)
97 .enumerate()
98 .for_each(|(blk, block)| {
99 let t = base + domain_point::<F>(blk << 1);
100 let zero = t.is_zero();
101 let (lo, hi) = block.split_at_mut(half);
102 let butterfly = |lo: &mut [F], hi: &mut [F]| {
103 if zero {
104 for (u, v) in lo.iter_mut().zip(hi) {
106 *v += *u;
107 }
108 } else {
109 for (u, v) in lo.iter_mut().zip(hi) {
110 *v += *u;
112 *u += t * *v;
113 }
114 }
115 };
116 if half <= BUTTERFLY_GRAIN {
119 butterfly(lo, hi);
120 } else {
121 lo.par_chunks_mut(BUTTERFLY_GRAIN)
122 .zip(hi.par_chunks_mut(BUTTERFLY_GRAIN))
123 .for_each(|(lo, hi)| butterfly(lo, hi));
124 }
125 });
126 }
127 mat
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use alloc::vec;
134
135 use p3_binary_field::{
136 BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, TowerLevel,
137 };
138 use p3_matrix::dense::RowMajorMatrix;
139 use proptest::prelude::*;
140
141 use super::LchNtt;
142 use crate::naive::NaiveAdditiveNtt;
143 use crate::traits::AdditiveNtt;
144
145 fn sample<F: TowerLevel>(bits: u64) -> F {
147 F::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
148 }
149
150 fn matrix<F: TowerLevel>(log_n: usize, width: usize, seed: u64) -> RowMajorMatrix<F> {
152 RowMajorMatrix::new(
153 (0..(width << log_n))
154 .map(|i| {
155 sample::<F>(
156 seed.wrapping_mul(0x9e37_79b9_7f4a_7c15)
157 .wrapping_add(i as u64),
158 )
159 })
160 .collect(),
161 width,
162 )
163 }
164
165 fn check_matches_naive<F: TowerLevel>(log_n: usize, width: usize, seed: u64, shift: u64) {
167 let coeffs = matrix::<F>(log_n, width, seed);
168 let shift = sample::<F>(shift);
169
170 let fast = LchNtt::<F>::default().shifted_ntt_batch(coeffs.clone(), shift);
171 let slow = NaiveAdditiveNtt::<F>::default().shifted_ntt_batch(coeffs, shift);
172 assert_eq!(fast, slow);
173 }
174
175 proptest! {
176 #![proptest_config(ProptestConfig::with_cases(32))]
177
178 #[test]
179 fn lch_matches_naive_at_8_bits(
180 log_n in 0usize..=8,
181 width in 1usize..=5,
182 seed in any::<u64>(),
183 shift in any::<u64>(),
184 ) {
185 check_matches_naive::<BinaryField8>(log_n, width, seed, shift);
186 }
187
188 #[test]
189 fn lch_matches_naive_at_16_bits(
190 log_n in 0usize..=8,
191 width in 1usize..=5,
192 seed in any::<u64>(),
193 shift in any::<u64>(),
194 ) {
195 check_matches_naive::<BinaryField16>(log_n, width, seed, shift);
196 }
197
198 #[test]
199 fn lch_matches_naive_at_32_bits(
200 log_n in 0usize..=8,
201 width in 1usize..=5,
202 seed in any::<u64>(),
203 shift in any::<u64>(),
204 ) {
205 check_matches_naive::<BinaryField32>(log_n, width, seed, shift);
206 }
207
208 #[test]
209 fn lch_matches_naive_at_64_bits(
210 log_n in 0usize..=8,
211 width in 1usize..=5,
212 seed in any::<u64>(),
213 shift in any::<u64>(),
214 ) {
215 check_matches_naive::<BinaryField64>(log_n, width, seed, shift);
216 }
217
218 #[test]
219 fn lch_matches_naive_at_128_bits(
220 log_n in 0usize..=8,
221 width in 1usize..=5,
222 seed in any::<u64>(),
223 shift in any::<u64>(),
224 ) {
225 check_matches_naive::<BinaryField128>(log_n, width, seed, shift);
226 }
227
228 #[test]
229 fn lch_round_trips(
230 log_n in 0usize..=10,
231 width in 1usize..=3,
232 seed in any::<u64>(),
233 shift in any::<u64>(),
234 ) {
235 let coeffs = matrix::<BinaryField16>(log_n, width, seed);
236 let shift = sample::<BinaryField16>(shift);
237 let ntt = LchNtt::<BinaryField16>::default();
238 let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift);
239 prop_assert_eq!(ntt.shifted_intt_batch(evals, shift), coeffs);
240 }
241
242 #[test]
245 fn lch_lde_matches_naive(
246 log_n in 0usize..=6,
247 added in 0usize..=3,
248 width in 1usize..=3,
249 seed in any::<u64>(),
250 shift in any::<u64>(),
251 ) {
252 let coeffs = matrix::<BinaryField16>(log_n, width, seed);
253 let shift = sample::<BinaryField16>(shift);
254
255 let lde = LchNtt::<BinaryField16>::default()
256 .shifted_lde_batch(coeffs.clone(), added, shift);
257 let naive = NaiveAdditiveNtt::<BinaryField16>::default()
258 .shifted_lde_batch(coeffs.clone(), added, shift);
259 prop_assert_eq!(&lde, &naive);
260 prop_assert_eq!(&lde.values[..coeffs.values.len()], &coeffs.values[..]);
261 }
262 }
263
264 #[test]
267 #[should_panic]
268 fn shifted_ntt_batch_rejects_l_past_the_bit_width() {
269 let coeffs = matrix::<BinaryField8>((1 << BinaryField8::LOG_BITS) + 1, 1, 0);
271 let _ = LchNtt::<BinaryField8>::default().ntt_batch(coeffs);
272 }
273
274 #[test]
276 #[should_panic]
277 fn shifted_ntt_batch_rejects_a_non_power_of_two_height() {
278 let coeffs = RowMajorMatrix::new(vec![sample::<BinaryField8>(0); 3], 1);
279 let _ = LchNtt::<BinaryField8>::default().ntt_batch(coeffs);
280 }
281
282 #[test]
284 fn lch_is_linear() {
285 let ntt = LchNtt::<BinaryField16>::default();
286 let a = matrix::<BinaryField16>(5, 2, 1);
287 let b = matrix::<BinaryField16>(5, 2, 2);
288 let sum = RowMajorMatrix::new(
289 a.values
290 .iter()
291 .zip(&b.values)
292 .map(|(x, y)| *x + *y)
293 .collect(),
294 2,
295 );
296
297 let lhs = ntt.ntt_batch(sum);
298 let rhs_a = ntt.ntt_batch(a);
299 let rhs_b = ntt.ntt_batch(b);
300 for (i, v) in lhs.values.iter().enumerate() {
301 assert_eq!(*v, rhs_a.values[i] + rhs_b.values[i]);
302 }
303 }
304
305 #[test]
308 fn levels_agree_after_embedding() {
309 const LOG_N: usize = 6;
310 let coeffs32 = matrix::<BinaryField32>(LOG_N, 1, 7);
311 let coeffs128 = RowMajorMatrix::new(
312 coeffs32
313 .values
314 .iter()
315 .map(|v| BinaryField128::from_repr(u128::from(v.to_repr())))
316 .collect(),
317 1,
318 );
319
320 let small = LchNtt::<BinaryField32>::default().ntt_batch(coeffs32);
321 let large = LchNtt::<BinaryField128>::default().ntt_batch(coeffs128);
322 for (s, l) in small.values.iter().zip(&large.values) {
323 assert_eq!(u128::from(s.to_repr()), l.to_repr());
324 }
325 }
326}