Skip to main content

p3_mds/
karatsuba_convolution.rs

1//! Calculate the convolution of two vectors using a Karatsuba-style
2//! decomposition and the CRT.
3//!
4//! This is not a new idea, but we did have the pleasure of
5//! reinventing it independently. Some references:
6//! - `<https://cr.yp.to/lineartime/multapps-20080515.pdf>`
7//! - `<https://2π.com/23/convolution/>`
8//!
9//! Given a vector `v \in F^N`, let `v(x) \in F[x]` denote the polynomial
10//! v_0 + v_1 x + ... + v_{N - 1} x^{N - 1}.  Then w is equal to the
11//! convolution v * u if and only if w(x) = v(x)u(x) mod x^N - 1.
12//! Additionally, define the negacyclic convolution by w(x) = v(x)u(x)
13//! mod x^N + 1.  Using the Chinese remainder theorem we can compute
14//! w(x) as
15//!     w(x) = 1/2 (w_0(x) + w_1(x)) + x^{N/2}/2 (w_0(x) - w_1(x))
16//! where
17//!     w_0 = v(x)u(x) mod x^{N/2} - 1
18//!     w_1 = v(x)u(x) mod x^{N/2} + 1
19//!
20//! To compute w_0 and w_1 we first compute
21//!                  v_0(x) = v(x) mod x^{N/2} - 1
22//!                  v_1(x) = v(x) mod x^{N/2} + 1
23//!                  u_0(x) = u(x) mod x^{N/2} - 1
24//!                  u_1(x) = u(x) mod x^{N/2} + 1
25//!
26//! Now w_0 is the convolution of v_0 and u_0 which we can compute
27//! recursively.  For w_1 we compute the negacyclic convolution
28//! v_1(x)u_1(x) mod x^{N/2} + 1 using Karatsuba.
29//!
30//! There are 2 possible approaches to applying Karatsuba which mirror
31//! the DIT vs DIF approaches to FFT's, the left/right decomposition
32//! or the even/odd decomposition. The latter seems to have fewer
33//! operations and so it is the one implemented below, though it does
34//! require a bit more data manipulation. It works as follows:
35//!
36//! Define the even v_e and odd v_o parts so that v(x) = (v_e(x^2) + x v_o(x^2)).
37//! Then v(x)u(x)
38//!    = (v_e(x^2)u_e(x^2) + x^2 v_o(x^2)u_o(x^2))
39//!      + x ((v_e(x^2) + v_o(x^2))(u_e(x^2) + u_o(x^2))
40//!            - (v_e(x^2)u_e(x^2) + v_o(x^2)u_o(x^2)))
41//! This reduces the problem to 3 negacyclic convolutions of size N/2 which
42//! can be computed recursively.
43//!
44//! Of course, for small sizes we just explicitly write out the O(n^2)
45//! approach.
46
47use core::marker::PhantomData;
48use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
49
50use p3_field::{Algebra, PrimeCharacteristicRing};
51
52/// Bound alias for the wide operand type (used for both lhs and output).
53///
54/// Must support addition, subtraction, negation, and in-place variants.
55pub trait ConvolutionElt:
56    Add<Output = Self> + AddAssign + Clone + Neg<Output = Self> + Sub<Output = Self> + SubAssign
57{
58}
59
60impl<T> ConvolutionElt for T where
61    T: Add<Output = T> + AddAssign + Clone + Neg<Output = T> + Sub<Output = T> + SubAssign
62{
63}
64
65/// Bound alias for the narrow operand type (rhs only).
66///
67/// Requires addition, subtraction, negation, and clone.
68pub trait ConvolutionRhs:
69    Add<Output = Self> + Clone + Neg<Output = Self> + Sub<Output = Self>
70{
71}
72
73impl<T> ConvolutionRhs for T where T: Add<Output = T> + Clone + Neg<Output = T> + Sub<Output = T> {}
74
75/// Trait for computing cyclic and negacyclic convolutions.
76///
77/// Implementors choose how to lift field elements into a wider type,
78/// compute dot products, and reduce back.
79/// This allows integer-lifted arithmetic (e.g. i64) to avoid modular
80/// reductions inside the inner loops.
81///
82/// # Overflow contract
83///
84/// The recursive Karatsuba decomposition forms sums/differences of operands *before*
85/// multiplying, so by the time an operand reaches the base-case dot product it may already
86/// be scaled by a factor of about N (the convolution size): the resulting product can be as
87/// large as `N^2 * |T| * |U|`, not just `|T| * |U|`. Implementors must choose `T`/`U` wide
88/// enough (and pick a reduction point) to absorb this `N^2` growth, not just a single product.
89///
90/// # Performance notes
91///
92/// In practice one operand is a compile-time constant (the MDS matrix).
93/// The compiler folds the constant arithmetic at compile time.
94/// For large matrices (N >= 24), the compile-time-generated constants
95/// are about N times bigger than strictly necessary.
96pub trait Convolve<F, T: ConvolutionElt, U: ConvolutionRhs> {
97    /// Additive identity for the wide operand type `T`.
98    ///
99    /// Used to initialize output and scratch arrays before the convolution
100    /// fills them with computed values.
101    const T_ZERO: T;
102
103    /// Additive identity for the narrow operand type `U`.
104    ///
105    /// Used to initialize temporary arrays for the RHS decomposition
106    /// in the recursive CRT / Karatsuba steps.
107    const U_ZERO: U;
108
109    /// Divide an element of `T` by 2.
110    ///
111    /// - For integers (`i64`, `i128`): arithmetic right shift by 1.
112    /// - For field elements: multiplication by the multiplicative inverse of 2.
113    fn halve(val: T) -> T;
114
115    /// Given an input element, retrieve the corresponding internal
116    /// element that will be used in calculations.
117    fn read(input: F) -> T;
118
119    /// Given input vectors `lhs` and `rhs`, calculate their dot
120    /// product. The result can be reduced with respect to the modulus
121    /// (of `F`), but it must have the same lower 10 bits as the dot
122    /// product if all inputs are considered integers. See
123    /// `monty-31/src/mds.rs::barrett_red_monty31()` for an example
124    /// of how this can be implemented in practice.
125    fn parity_dot<const N: usize>(lhs: [T; N], rhs: [U; N]) -> T;
126
127    /// Convert an internal element of type `T` back into an external
128    /// element.
129    fn reduce(z: T) -> F;
130
131    /// Convolve `lhs` and `rhs`.
132    ///
133    /// The parameter `conv` should be the function in this trait that
134    /// corresponds to length `N`.
135    #[inline(always)]
136    fn apply<const N: usize, C: Fn([T; N], [U; N], &mut [T])>(
137        lhs: [F; N],
138        rhs: [U; N],
139        conv: C,
140    ) -> [F; N] {
141        let lhs = lhs.map(Self::read);
142        let mut output = [Self::T_ZERO; N];
143        conv(lhs, rhs, &mut output);
144        output.map(Self::reduce)
145    }
146
147    #[inline(always)]
148    fn conv3(lhs: [T; 3], rhs: [U; 3], output: &mut [T]) {
149        output[0] = Self::parity_dot(
150            lhs.clone(),
151            [rhs[0].clone(), rhs[2].clone(), rhs[1].clone()],
152        );
153        output[1] = Self::parity_dot(
154            lhs.clone(),
155            [rhs[1].clone(), rhs[0].clone(), rhs[2].clone()],
156        );
157        output[2] = Self::parity_dot(lhs, [rhs[2].clone(), rhs[1].clone(), rhs[0].clone()]);
158    }
159
160    #[inline(always)]
161    fn negacyclic_conv3(lhs: [T; 3], rhs: [U; 3], output: &mut [T]) {
162        output[0] = Self::parity_dot(
163            lhs.clone(),
164            [rhs[0].clone(), -rhs[2].clone(), -rhs[1].clone()],
165        );
166        output[1] = Self::parity_dot(
167            lhs.clone(),
168            [rhs[1].clone(), rhs[0].clone(), -rhs[2].clone()],
169        );
170        output[2] = Self::parity_dot(lhs, [rhs[2].clone(), rhs[1].clone(), rhs[0].clone()]);
171    }
172
173    #[inline(always)]
174    fn conv4(lhs: [T; 4], rhs: [U; 4], output: &mut [T]) {
175        // NB: This is just explicitly implementing
176        // conv_n_recursive::<4, 2, _, _>(lhs, rhs, output, Self::conv2, Self::negacyclic_conv2)
177        let u_p = [
178            lhs[0].clone() + lhs[2].clone(),
179            lhs[1].clone() + lhs[3].clone(),
180        ];
181        let u_m = [
182            lhs[0].clone() - lhs[2].clone(),
183            lhs[1].clone() - lhs[3].clone(),
184        ];
185        let v_p = [
186            rhs[0].clone() + rhs[2].clone(),
187            rhs[1].clone() + rhs[3].clone(),
188        ];
189        let v_m = [
190            rhs[0].clone() - rhs[2].clone(),
191            rhs[1].clone() - rhs[3].clone(),
192        ];
193
194        output[0] = Self::parity_dot(u_m.clone(), [v_m[0].clone(), -v_m[1].clone()]);
195        output[1] = Self::parity_dot(u_m, [v_m[1].clone(), v_m[0].clone()]);
196        output[2] = Self::parity_dot(u_p.clone(), v_p.clone());
197        output[3] = Self::parity_dot(u_p, [v_p[1].clone(), v_p[0].clone()]);
198
199        output[0] += output[2].clone();
200        output[1] += output[3].clone();
201
202        output[0] = Self::halve(output[0].clone());
203        output[1] = Self::halve(output[1].clone());
204
205        output[2] -= output[0].clone();
206        output[3] -= output[1].clone();
207    }
208
209    #[inline(always)]
210    fn negacyclic_conv4(lhs: [T; 4], rhs: [U; 4], output: &mut [T]) {
211        output[0] = Self::parity_dot(
212            lhs.clone(),
213            [
214                rhs[0].clone(),
215                -rhs[3].clone(),
216                -rhs[2].clone(),
217                -rhs[1].clone(),
218            ],
219        );
220        output[1] = Self::parity_dot(
221            lhs.clone(),
222            [
223                rhs[1].clone(),
224                rhs[0].clone(),
225                -rhs[3].clone(),
226                -rhs[2].clone(),
227            ],
228        );
229        output[2] = Self::parity_dot(
230            lhs.clone(),
231            [
232                rhs[2].clone(),
233                rhs[1].clone(),
234                rhs[0].clone(),
235                -rhs[3].clone(),
236            ],
237        );
238        output[3] = Self::parity_dot(
239            lhs,
240            [
241                rhs[3].clone(),
242                rhs[2].clone(),
243                rhs[1].clone(),
244                rhs[0].clone(),
245            ],
246        );
247    }
248
249    /// Compute output(x) = lhs(x)rhs(x) mod x^N - 1 recursively using
250    /// a convolution and negacyclic convolution of size HALF_N = N/2.
251    #[inline(always)]
252    fn conv_n_recursive<const N: usize, const HALF_N: usize, C, NC>(
253        lhs: [T; N],
254        rhs: [U; N],
255        output: &mut [T],
256        inner_conv: C,
257        inner_negacyclic_conv: NC,
258    ) where
259        C: Fn([T; HALF_N], [U; HALF_N], &mut [T]),
260        NC: Fn([T; HALF_N], [U; HALF_N], &mut [T]),
261    {
262        debug_assert_eq!(2 * HALF_N, N);
263        let mut lhs_pos = [Self::T_ZERO; HALF_N]; // lhs_pos = lhs(x) mod x^{N/2} - 1
264        let mut lhs_neg = [Self::T_ZERO; HALF_N]; // lhs_neg = lhs(x) mod x^{N/2} + 1
265        let mut rhs_pos = [Self::U_ZERO; HALF_N]; // rhs_pos = rhs(x) mod x^{N/2} - 1
266        let mut rhs_neg = [Self::U_ZERO; HALF_N]; // rhs_neg = rhs(x) mod x^{N/2} + 1
267
268        for i in 0..HALF_N {
269            let s = lhs[i].clone();
270            let t = lhs[i + HALF_N].clone();
271
272            lhs_pos[i] = s.clone() + t.clone();
273            lhs_neg[i] = s - t;
274
275            let s = rhs[i].clone();
276            let t = rhs[i + HALF_N].clone();
277
278            rhs_pos[i] = s.clone() + t.clone();
279            rhs_neg[i] = s - t;
280        }
281
282        let (left, right) = output.split_at_mut(HALF_N);
283
284        // left = w1 = lhs(x)rhs(x) mod x^{N/2} + 1
285        inner_negacyclic_conv(lhs_neg, rhs_neg, left);
286
287        // right = w0 = lhs(x)rhs(x) mod x^{N/2} - 1
288        inner_conv(lhs_pos, rhs_pos, right);
289
290        for i in 0..HALF_N {
291            left[i] += right[i].clone(); // w_0 + w_1
292            left[i] = Self::halve(left[i].clone()); // (w_0 + w_1)/2
293            right[i] -= left[i].clone(); // (w_0 - w_1)/2
294        }
295    }
296
297    /// Compute output(x) = lhs(x)rhs(x) mod x^N + 1 recursively using
298    /// three negacyclic convolutions of size HALF_N = N/2.
299    #[inline(always)]
300    fn negacyclic_conv_n_recursive<const N: usize, const HALF_N: usize, NC>(
301        lhs: [T; N],
302        rhs: [U; N],
303        output: &mut [T],
304        inner_negacyclic_conv: NC,
305    ) where
306        NC: Fn([T; HALF_N], [U; HALF_N], &mut [T]),
307    {
308        debug_assert_eq!(2 * HALF_N, N);
309        let mut lhs_even = [Self::T_ZERO; HALF_N];
310        let mut lhs_odd = [Self::T_ZERO; HALF_N];
311        let mut lhs_sum = [Self::T_ZERO; HALF_N];
312        let mut rhs_even = [Self::U_ZERO; HALF_N];
313        let mut rhs_odd = [Self::U_ZERO; HALF_N];
314        let mut rhs_sum = [Self::U_ZERO; HALF_N];
315
316        for i in 0..HALF_N {
317            let s = lhs[2 * i].clone();
318            let t = lhs[2 * i + 1].clone();
319            lhs_sum[i] = s.clone() + t.clone();
320            lhs_even[i] = s;
321            lhs_odd[i] = t;
322
323            let s = rhs[2 * i].clone();
324            let t = rhs[2 * i + 1].clone();
325            rhs_sum[i] = s.clone() + t.clone();
326            rhs_even[i] = s;
327            rhs_odd[i] = t;
328        }
329
330        let mut even_s_conv = [Self::T_ZERO; HALF_N];
331        let (left, right) = output.split_at_mut(HALF_N);
332
333        // Recursively compute the size N/2 negacyclic convolutions of
334        // the even parts, odd parts, and sums.
335        inner_negacyclic_conv(lhs_even, rhs_even, &mut even_s_conv);
336        inner_negacyclic_conv(lhs_odd, rhs_odd, left);
337        inner_negacyclic_conv(lhs_sum, rhs_sum, right);
338
339        // Adjust so that the correct values are in right and
340        // even_s_conv respectively:
341        right[0] -= even_s_conv[0].clone() + left[0].clone();
342        even_s_conv[0] -= left[HALF_N - 1].clone();
343
344        for i in 1..HALF_N {
345            right[i] -= even_s_conv[i].clone() + left[i].clone();
346            even_s_conv[i] += left[i - 1].clone();
347        }
348
349        // Interleave even_s_conv and right in the output:
350        for i in 0..HALF_N {
351            output[2 * i] = even_s_conv[i].clone();
352            output[2 * i + 1] = output[i + HALF_N].clone();
353        }
354    }
355
356    #[inline(always)]
357    fn conv6(lhs: [T; 6], rhs: [U; 6], output: &mut [T]) {
358        Self::conv_n_recursive(lhs, rhs, output, Self::conv3, Self::negacyclic_conv3);
359    }
360
361    #[inline(always)]
362    fn negacyclic_conv6(lhs: [T; 6], rhs: [U; 6], output: &mut [T]) {
363        Self::negacyclic_conv_n_recursive(lhs, rhs, output, Self::negacyclic_conv3);
364    }
365
366    #[inline(always)]
367    fn conv8(lhs: [T; 8], rhs: [U; 8], output: &mut [T]) {
368        Self::conv_n_recursive(lhs, rhs, output, Self::conv4, Self::negacyclic_conv4);
369    }
370
371    #[inline(always)]
372    fn negacyclic_conv8(lhs: [T; 8], rhs: [U; 8], output: &mut [T]) {
373        Self::negacyclic_conv_n_recursive(lhs, rhs, output, Self::negacyclic_conv4);
374    }
375
376    #[inline(always)]
377    fn conv12(lhs: [T; 12], rhs: [U; 12], output: &mut [T]) {
378        Self::conv_n_recursive(lhs, rhs, output, Self::conv6, Self::negacyclic_conv6);
379    }
380
381    #[inline(always)]
382    fn negacyclic_conv12(lhs: [T; 12], rhs: [U; 12], output: &mut [T]) {
383        Self::negacyclic_conv_n_recursive(lhs, rhs, output, Self::negacyclic_conv6);
384    }
385
386    #[inline(always)]
387    fn conv16(lhs: [T; 16], rhs: [U; 16], output: &mut [T]) {
388        Self::conv_n_recursive(lhs, rhs, output, Self::conv8, Self::negacyclic_conv8);
389    }
390
391    #[inline(always)]
392    fn negacyclic_conv16(lhs: [T; 16], rhs: [U; 16], output: &mut [T]) {
393        Self::negacyclic_conv_n_recursive(lhs, rhs, output, Self::negacyclic_conv8);
394    }
395
396    #[inline(always)]
397    fn conv24(lhs: [T; 24], rhs: [U; 24], output: &mut [T]) {
398        Self::conv_n_recursive(lhs, rhs, output, Self::conv12, Self::negacyclic_conv12);
399    }
400
401    #[inline(always)]
402    fn conv32(lhs: [T; 32], rhs: [U; 32], output: &mut [T]) {
403        Self::conv_n_recursive(lhs, rhs, output, Self::conv16, Self::negacyclic_conv16);
404    }
405
406    #[inline(always)]
407    fn negacyclic_conv32(lhs: [T; 32], rhs: [U; 32], output: &mut [T]) {
408        Self::negacyclic_conv_n_recursive(lhs, rhs, output, Self::negacyclic_conv16);
409    }
410
411    #[inline(always)]
412    fn conv64(lhs: [T; 64], rhs: [U; 64], output: &mut [T]) {
413        Self::conv_n_recursive(lhs, rhs, output, Self::conv32, Self::negacyclic_conv32);
414    }
415}
416
417/// Convolution implementor that stays entirely within the field.
418///
419/// No integer lifting — all operations are native field arithmetic.
420/// Used by the public Karatsuba entry points for generic field/algebra pairs.
421struct FieldConvolve<F, A>(PhantomData<(F, A)>);
422
423impl<F: PrimeCharacteristicRing, A: Algebra<F> + Clone> Convolve<A, A, F> for FieldConvolve<F, A> {
424    const T_ZERO: A = A::ZERO;
425    const U_ZERO: F = F::ZERO;
426
427    #[inline(always)]
428    fn halve(val: A) -> A {
429        val.halve()
430    }
431
432    #[inline(always)]
433    fn read(input: A) -> A {
434        input
435    }
436
437    #[inline(always)]
438    fn parity_dot<const N: usize>(lhs: [A; N], rhs: [F; N]) -> A {
439        A::mixed_dot_product(&lhs, &rhs)
440    }
441
442    #[inline(always)]
443    fn reduce(z: A) -> A {
444        z
445    }
446}
447
448/// Circulant matrix-vector multiply for width 8 via Karatsuba convolution.
449#[inline]
450pub fn mds_circulant_karatsuba_8<F: PrimeCharacteristicRing, A: Algebra<F> + Clone>(
451    state: &mut [A; 8],
452    col: &[F; 8],
453) {
454    let input = state.clone();
455    FieldConvolve::<F, A>::conv8(input, col.clone(), state.as_mut_slice());
456}
457
458/// Circulant matrix-vector multiply for width 12 via Karatsuba convolution.
459#[inline]
460pub fn mds_circulant_karatsuba_12<F: PrimeCharacteristicRing, A: Algebra<F> + Clone>(
461    state: &mut [A; 12],
462    col: &[F; 12],
463) {
464    let input = state.clone();
465    FieldConvolve::<F, A>::conv12(input, col.clone(), state.as_mut_slice());
466}
467
468/// Circulant matrix-vector multiply for width 16 via Karatsuba convolution.
469#[inline]
470pub fn mds_circulant_karatsuba_16<F: PrimeCharacteristicRing, A: Algebra<F> + Clone>(
471    state: &mut [A; 16],
472    col: &[F; 16],
473) {
474    let input = state.clone();
475    FieldConvolve::<F, A>::conv16(input, col.clone(), state.as_mut_slice());
476}
477
478/// Circulant matrix-vector multiply for width 24 via Karatsuba convolution.
479#[inline]
480pub fn mds_circulant_karatsuba_24<F: PrimeCharacteristicRing, A: Algebra<F> + Clone>(
481    state: &mut [A; 24],
482    col: &[F; 24],
483) {
484    let input = state.clone();
485    FieldConvolve::<F, A>::conv24(input, col.clone(), state.as_mut_slice());
486}
487
488#[cfg(test)]
489mod tests {
490    use p3_baby_bear::BabyBear;
491    use p3_field::PrimeCharacteristicRing;
492    use proptest::prelude::*;
493
494    use super::*;
495
496    type F = BabyBear;
497
498    fn arb_f() -> impl Strategy<Value = F> {
499        prop::num::u32::ANY.prop_map(F::from_u32)
500    }
501
502    fn naive_cyclic_conv<const N: usize>(lhs: [F; N], rhs: [F; N]) -> [F; N] {
503        // O(N^2) reference: w[i] = sum_j lhs[j] * rhs[(i - j) mod N].
504        core::array::from_fn(|i| {
505            let mut acc = F::ZERO;
506            for j in 0..N {
507                acc += lhs[j] * rhs[(N + i - j) % N];
508            }
509            acc
510        })
511    }
512
513    fn naive_negacyclic_conv<const N: usize>(lhs: [F; N], rhs: [F; N]) -> [F; N] {
514        // O(N^2) reference: w(x) = lhs(x) * rhs(x) mod (x^N + 1).
515        // Coefficients that wrap past degree N-1 are subtracted (negacyclic).
516        let mut out = [F::ZERO; N];
517        for (i, &l) in lhs.iter().enumerate() {
518            for (j, &r) in rhs.iter().enumerate() {
519                let k = i + j;
520                if k < N {
521                    out[k] += l * r;
522                } else {
523                    out[k - N] -= l * r;
524                }
525            }
526        }
527        out
528    }
529
530    fn check_conv<const N: usize>(
531        lhs: [F; N],
532        rhs: [F; N],
533        conv_fn: fn([F; N], [F; N], &mut [F]),
534        naive_fn: fn([F; N], [F; N]) -> [F; N],
535    ) {
536        let expected = naive_fn(lhs, rhs);
537        let mut output = [F::ZERO; N];
538        conv_fn(lhs, rhs, &mut output);
539        assert_eq!(output, expected, "convolution mismatch");
540    }
541
542    macro_rules! conv_test {
543        ($name:ident, $n:expr, $conv:expr, $naive:expr, $arr:ident) => {
544            proptest! {
545                #[test]
546                fn $name(
547                    lhs in prop::array::$arr(arb_f()),
548                    rhs in prop::array::$arr(arb_f()),
549                ) {
550                    check_conv::<$n>(lhs, rhs, $conv, $naive);
551                }
552            }
553        };
554    }
555
556    // Width 3
557    conv_test!(
558        conv3_matches_naive,
559        3,
560        FieldConvolve::<F, F>::conv3,
561        naive_cyclic_conv,
562        uniform3
563    );
564    conv_test!(
565        negacyclic_conv3_matches_naive,
566        3,
567        FieldConvolve::<F, F>::negacyclic_conv3,
568        naive_negacyclic_conv,
569        uniform3
570    );
571
572    // Width 4
573    conv_test!(
574        conv4_matches_naive,
575        4,
576        FieldConvolve::<F, F>::conv4,
577        naive_cyclic_conv,
578        uniform4
579    );
580    conv_test!(
581        negacyclic_conv4_matches_naive,
582        4,
583        FieldConvolve::<F, F>::negacyclic_conv4,
584        naive_negacyclic_conv,
585        uniform4
586    );
587
588    // Width 6
589    conv_test!(
590        conv6_matches_naive,
591        6,
592        FieldConvolve::<F, F>::conv6,
593        naive_cyclic_conv,
594        uniform6
595    );
596    conv_test!(
597        negacyclic_conv6_matches_naive,
598        6,
599        FieldConvolve::<F, F>::negacyclic_conv6,
600        naive_negacyclic_conv,
601        uniform6
602    );
603
604    // Width 8
605    conv_test!(
606        conv8_matches_naive,
607        8,
608        FieldConvolve::<F, F>::conv8,
609        naive_cyclic_conv,
610        uniform8
611    );
612    conv_test!(
613        negacyclic_conv8_matches_naive,
614        8,
615        FieldConvolve::<F, F>::negacyclic_conv8,
616        naive_negacyclic_conv,
617        uniform8
618    );
619
620    // Width 12
621    conv_test!(
622        conv12_matches_naive,
623        12,
624        FieldConvolve::<F, F>::conv12,
625        naive_cyclic_conv,
626        uniform12
627    );
628    conv_test!(
629        negacyclic_conv12_matches_naive,
630        12,
631        FieldConvolve::<F, F>::negacyclic_conv12,
632        naive_negacyclic_conv,
633        uniform12
634    );
635
636    // Width 16
637    conv_test!(
638        conv16_matches_naive,
639        16,
640        FieldConvolve::<F, F>::conv16,
641        naive_cyclic_conv,
642        uniform16
643    );
644    conv_test!(
645        negacyclic_conv16_matches_naive,
646        16,
647        FieldConvolve::<F, F>::negacyclic_conv16,
648        naive_negacyclic_conv,
649        uniform16
650    );
651
652    // Width 24
653    conv_test!(
654        conv24_matches_naive,
655        24,
656        FieldConvolve::<F, F>::conv24,
657        naive_cyclic_conv,
658        uniform24
659    );
660
661    // Width 32
662    conv_test!(
663        conv32_matches_naive,
664        32,
665        FieldConvolve::<F, F>::conv32,
666        naive_cyclic_conv,
667        uniform32
668    );
669    conv_test!(
670        negacyclic_conv32_matches_naive,
671        32,
672        FieldConvolve::<F, F>::negacyclic_conv32,
673        naive_negacyclic_conv,
674        uniform32
675    );
676
677    #[test]
678    fn conv64_matches_naive_fixed() {
679        let lhs: [F; 64] = core::array::from_fn(|i| F::from_u32(i as u32 + 1));
680        let rhs: [F; 64] = core::array::from_fn(|i| F::from_u32(64 - i as u32));
681        check_conv::<64>(lhs, rhs, FieldConvolve::<F, F>::conv64, naive_cyclic_conv);
682    }
683
684    #[test]
685    fn conv64_all_ones() {
686        let ones = [F::ONE; 64];
687        let expected = naive_cyclic_conv(ones, ones);
688        let mut output = [F::ZERO; 64];
689        FieldConvolve::<F, F>::conv64(ones, ones, &mut output);
690        assert_eq!(output, expected);
691    }
692
693    proptest! {
694        #[test]
695        fn karatsuba_16_matches_naive(
696            col in prop::array::uniform16(arb_f()),
697            state in prop::array::uniform16(arb_f()),
698        ) {
699            let expected = naive_cyclic_conv(state, col);
700            let mut actual = state;
701            mds_circulant_karatsuba_16(&mut actual, &col);
702            prop_assert_eq!(actual, expected);
703        }
704
705        #[test]
706        fn karatsuba_24_matches_naive(
707            col in prop::array::uniform24(arb_f()),
708            state in prop::array::uniform24(arb_f()),
709        ) {
710            let expected = naive_cyclic_conv(state, col);
711            let mut actual = state;
712            mds_circulant_karatsuba_24(&mut actual, &col);
713            prop_assert_eq!(actual, expected);
714        }
715    }
716
717    proptest! {
718        #[test]
719        fn conv8_commutative(
720            a in prop::array::uniform8(arb_f()),
721            b in prop::array::uniform8(arb_f()),
722        ) {
723            // Cyclic convolution is commutative: a * b = b * a.
724            let mut ab = [F::ZERO; 8];
725            let mut ba = [F::ZERO; 8];
726            FieldConvolve::<F, F>::conv8(a, b, &mut ab);
727            FieldConvolve::<F, F>::conv8(b, a, &mut ba);
728            prop_assert_eq!(ab, ba);
729        }
730
731        #[test]
732        fn conv8_identity(a in prop::array::uniform8(arb_f())) {
733            // The delta impulse [1, 0, 0, ...] is the convolution identity.
734            let mut id = [F::ZERO; 8];
735            id[0] = F::ONE;
736            let mut out = [F::ZERO; 8];
737            FieldConvolve::<F, F>::conv8(a, id, &mut out);
738            prop_assert_eq!(out, a);
739        }
740
741        #[test]
742        fn conv8_zero(a in prop::array::uniform8(arb_f())) {
743            // Convolving with the zero vector must produce all zeros.
744            let zeros = [F::ZERO; 8];
745            let mut out = [F::ZERO; 8];
746            FieldConvolve::<F, F>::conv8(a, zeros, &mut out);
747            prop_assert_eq!(out, zeros);
748        }
749    }
750}