Skip to main content

p3_mds/
util.rs

1use core::ops::{AddAssign, Mul};
2
3use p3_dft::TwoAdicSubgroupDft;
4use p3_field::{Algebra, PrimeCharacteristicRing, TwoAdicField};
5
6/// This will throw an error if N = 0 but it's hard to imagine this case coming up.
7#[inline(always)]
8pub fn dot_product<T, const N: usize>(u: [T; N], v: [T; N]) -> T
9where
10    T: Copy + AddAssign + Mul<Output = T>,
11{
12    debug_assert_ne!(N, 0);
13    let mut dp = u[0] * v[0];
14    for i in 1..N {
15        dp += u[i] * v[i];
16    }
17    dp
18}
19
20/// Given the first row `circ_matrix` of an NxN circulant matrix, say
21/// C, return the product `C*input`.
22///
23/// NB: This is a naive O(N^2) implementation. It serves as a fallback
24/// for cases where faster paths (Karatsuba convolution or FFT) do not
25/// apply — e.g. non-power-of-two widths, non-two-adic fields, or
26/// packed types without a specialised implementation.
27pub fn apply_circulant<R: PrimeCharacteristicRing, const N: usize>(
28    circ_matrix: &[u64; N],
29    input: &[R; N],
30) -> [R; N] {
31    let matrix = circ_matrix.map(R::from_u64);
32
33    core::array::from_fn(|row| {
34        // Build the circulant row: C[row][col] = first_row[(N + col - row) % N].
35        let rotated: [R; N] = core::array::from_fn(|col| matrix[(N + col - row) % N].clone());
36        R::dot_product(&rotated, input)
37    })
38}
39
40/// Given the first row of a circulant matrix, return the first column.
41///
42/// For example if, `v = [0, 1, 2, 3, 4, 5]` then `output = [0, 5, 4, 3, 2, 1]`,
43/// i.e. the first element is the same and the other elements are reversed.
44///
45/// This is useful to prepare a circulant matrix for input to an FFT
46/// algorithm, which expects the first column of the matrix rather
47/// than the first row (as we normally store them).
48///
49/// NB: The algorithm is inefficient but simple enough that this
50/// function can be declared `const`, and that is the intended context
51/// for use.
52pub const fn first_row_to_first_col<const N: usize, T: Copy>(v: &[T; N]) -> [T; N] {
53    // Start with a copy; the first element is shared between row and column.
54    let mut output = *v;
55    let mut i = 1;
56    while i < N {
57        // Reverse the remaining elements: col[i] = row[N - i].
58        output[i] = v[N - i];
59        i += 1;
60    }
61    output
62}
63
64/// Use the convolution theorem to calculate the product of the given
65/// circulant matrix and the given vector.
66///
67/// The circulant matrix must be specified by its first *column*, not its first row. If you have
68/// the row as an array, you can obtain the column with `first_row_to_first_col()`.
69#[inline]
70pub fn apply_circulant_fft<F: TwoAdicField, const N: usize, FFT: TwoAdicSubgroupDft<F>>(
71    fft: &FFT,
72    column: [u64; N],
73    input: &[F; N],
74) -> [F; N] {
75    // Transform the circulant column to the frequency domain.
76    let column = column.map(F::from_u64).to_vec();
77    let matrix = fft.dft(column);
78    let freq_column: [F; N] = matrix.try_into().unwrap();
79
80    apply_circulant_fft_precomputed(fft, &freq_column, input)
81}
82
83/// Use the convolution theorem to calculate the product of a circulant matrix
84/// and the given vector, where the matrix's first column has already been
85/// transformed to the frequency domain.
86///
87/// Useful when the circulant matrix is fixed across many calls: the caller
88/// can compute `freq_column` once (e.g. `fft.dft(column.map(F::from_u64).to_vec())`)
89/// instead of re-transforming a compile-time-constant column on every call.
90#[inline]
91pub fn apply_circulant_fft_precomputed<
92    F: TwoAdicField,
93    const N: usize,
94    FFT: TwoAdicSubgroupDft<F>,
95>(
96    fft: &FFT,
97    freq_column: &[F; N],
98    input: &[F; N],
99) -> [F; N] {
100    // Transform the input vector to the frequency domain.
101    let mut input = fft.dft(input.to_vec());
102
103    // Convolution theorem: point-wise multiply in frequency domain.
104    for (&coefficient, value) in freq_column.iter().zip(&mut input) {
105        *value = coefficient * *value;
106    }
107
108    // Transform back to the time domain to get the circulant product.
109    let output = fft.idft(input);
110    output.try_into().unwrap()
111}
112
113/// Dense matrix-vector product, applied in place to a fixed-width state vector.
114///
115/// # Overview
116///
117/// - Generic O(t^2) fallback for any dense square matrix.
118/// - Circulant matrices have faster paths in this module (Karatsuba, FFT).
119/// - Sparse or diagonal layouts can skip full-row scans entirely.
120///
121/// # Arguments
122///
123/// - The state vector, overwritten with the product on return.
124/// - The matrix, indexed row-first as `m[row][col]`.
125///
126/// # Performance
127///
128/// - Runtime: O(t^2) ring operations for a width-t state.
129/// - Allocations: one stack snapshot of the input state.
130#[inline]
131pub fn mds_multiply<F, A, const WIDTH: usize>(state: &mut [A; WIDTH], matrix: &[[F; WIDTH]; WIDTH])
132where
133    F: PrimeCharacteristicRing,
134    A: Algebra<F>,
135{
136    // Snapshot inputs so in-place writes don't corrupt later row reads.
137    let input = state.clone();
138
139    //     output[i] = sum_{j=0..t} matrix[i][j] * snapshot[j]
140    for (out, row) in state.iter_mut().zip(matrix.iter()) {
141        *out = A::mixed_dot_product(&input, row);
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use p3_baby_bear::BabyBear;
148    use p3_dft::NaiveDft;
149    use p3_field::PrimeCharacteristicRing;
150    use proptest::prelude::*;
151
152    use super::*;
153
154    type F = BabyBear;
155
156    fn arb_f() -> impl Strategy<Value = F> {
157        prop::num::u32::ANY.prop_map(F::from_u32)
158    }
159
160    #[test]
161    fn first_row_to_first_col_even_length() {
162        let input = [0, 1, 2, 3, 4, 5];
163        assert_eq!(first_row_to_first_col(&input), [0, 5, 4, 3, 2, 1]);
164    }
165
166    #[test]
167    fn first_row_to_first_col_odd_length() {
168        let input = [10, 20, 30, 40, 50];
169        assert_eq!(first_row_to_first_col(&input), [10, 50, 40, 30, 20]);
170    }
171
172    #[test]
173    fn first_row_to_first_col_single_element() {
174        assert_eq!(first_row_to_first_col(&[42]), [42]);
175    }
176
177    #[test]
178    fn first_row_to_first_col_two_elements() {
179        assert_eq!(first_row_to_first_col(&[1, 2]), [1, 2]);
180    }
181
182    #[test]
183    fn apply_circulant_identity() {
184        // The identity circulant [1, 0, 0, ...] must return the input unchanged.
185        let identity_row: [u64; 4] = [1, 0, 0, 0];
186        let input: [F; 4] = [5, 10, 15, 20].map(F::from_u32);
187        assert_eq!(apply_circulant(&identity_row, &input), input);
188    }
189
190    #[test]
191    fn apply_circulant_all_ones() {
192        // An all-ones circulant sums every input element into every output slot.
193        let ones: [u64; 4] = [1, 1, 1, 1];
194        let input: [F; 4] = [1, 2, 3, 4].map(F::from_u32);
195        let sum = F::from_u32(10);
196        assert_eq!(apply_circulant(&ones, &input), [sum; 4]);
197    }
198
199    #[test]
200    fn apply_circulant_scalar() {
201        // A scalar circulant [k, 0, 0, ...] multiplies each element by k.
202        let row: [u64; 4] = [7, 0, 0, 0];
203        let input: [F; 4] = [1, 2, 3, 4].map(F::from_u32);
204        let expected: [F; 4] = [7, 14, 21, 28].map(F::from_u32);
205        assert_eq!(apply_circulant(&row, &input), expected);
206    }
207
208    #[test]
209    fn apply_circulant_size_1() {
210        // A 1x1 circulant is just scalar multiplication.
211        let row: [u64; 1] = [5];
212        let input: [F; 1] = [F::from_u32(3)];
213        assert_eq!(apply_circulant(&row, &input), [F::from_u32(15)]);
214    }
215
216    #[test]
217    fn apply_circulant_fft_matches_naive_4() {
218        // The FFT-based path must agree with the naive O(N^2) path.
219        let row: [u64; 4] = [2, 3, 5, 7];
220        let col = first_row_to_first_col(&row);
221        let input: [F; 4] = [1, 2, 3, 4].map(F::from_u32);
222
223        let naive = apply_circulant(&row, &input);
224        let fft_result = apply_circulant_fft(&NaiveDft, col, &input);
225        assert_eq!(naive, fft_result);
226    }
227
228    #[test]
229    fn apply_circulant_fft_identity() {
230        // The FFT-based identity circulant must also return the input unchanged.
231        let row: [u64; 4] = [1, 0, 0, 0];
232        let col = first_row_to_first_col(&row);
233        let input: [F; 4] = [5, 10, 15, 20].map(F::from_u32);
234        assert_eq!(apply_circulant_fft(&NaiveDft, col, &input), input);
235    }
236
237    proptest! {
238        #[test]
239        fn first_row_to_first_col_involution(v in prop::array::uniform4(0u64..1000)) {
240            let col = first_row_to_first_col(&v);
241            let back = first_row_to_first_col(&col);
242            prop_assert_eq!(back, v);
243        }
244
245        #[test]
246        fn apply_circulant_fft_matches_naive(
247            row in prop::array::uniform4(0u64..1000),
248            input in prop::array::uniform4(arb_f()),
249        ) {
250            let col = first_row_to_first_col(&row);
251            let naive = apply_circulant(&row, &input);
252            let fft_result = apply_circulant_fft(&NaiveDft, col, &input);
253            prop_assert_eq!(naive, fft_result);
254        }
255
256        #[test]
257        fn apply_circulant_linearity(
258            row in prop::array::uniform4(0u64..100),
259            a in prop::array::uniform4(arb_f()),
260            b in prop::array::uniform4(arb_f()),
261        ) {
262            let sum_input: [F; 4] = core::array::from_fn(|i| a[i] + b[i]);
263            let ca = apply_circulant(&row, &a);
264            let cb = apply_circulant(&row, &b);
265            let c_sum = apply_circulant(&row, &sum_input);
266            for i in 0..4 {
267                prop_assert_eq!(c_sum[i], ca[i] + cb[i]);
268            }
269        }
270
271        #[test]
272        fn apply_circulant_zero_matrix(input in prop::array::uniform4(arb_f())) {
273            let zeros: [u64; 4] = [0; 4];
274            let result = apply_circulant(&zeros, &input);
275            prop_assert_eq!(result, [F::ZERO; 4]);
276        }
277    }
278}