Skip to main content

openvm_stark_backend/prover/
poly.rs

1use core::ops::Mul;
2use std::iter::zip;
3
4use getset::Getters;
5use itertools::Itertools;
6use p3_dft::{Radix2Bowers, TwoAdicSubgroupDft};
7use p3_field::{ExtensionField, Field, TwoAdicField};
8use p3_matrix::{dense::RowMajorMatrix, Matrix};
9use p3_maybe_rayon::prelude::*;
10use p3_util::{log2_ceil_usize, log2_strict_usize};
11use tracing::instrument;
12
13use crate::{
14    dft::Radix2BowersSerial,
15    poly_common::{eval_eq_uni, evals_eq_hypercube_serial, UnivariatePoly},
16    prover::{ColMajorMatrix, ColMajorMatrixView, MatrixDimensions},
17    utils::batch_multiplicative_inverse_serial,
18};
19
20/// Multilinear extension polynomial, in coefficient form.
21///
22/// Length of `coeffs` is `2^n` where `n` is hypercube dimension.
23/// Indexing of `coeffs` is to use the little-endian encoding of integer index as the powers of
24/// variables.
25#[derive(Getters)]
26pub struct Mle<F> {
27    #[getset(get = "pub")]
28    coeffs: Vec<F>,
29}
30
31impl<F: Field> Mle<F> {
32    pub fn from_coeffs(coeffs: Vec<F>) -> Self {
33        Self { coeffs }
34    }
35
36    /// Create MLE from evaluations on the hypercube.
37    ///
38    /// Takes evaluations of the polynomial at all points in {0,1}^n and converts
39    /// them to coefficient form.
40    ///
41    /// The input `evals` should have length 2^n where n is the number of variables.
42    /// The evaluation at index i corresponds to the point whose binary representation
43    /// is i (with bit 0 being the least significant).
44    pub fn from_evaluations(evals: &[F]) -> Self {
45        assert!(!evals.is_empty(), "Evaluations cannot be empty");
46        let mut coeffs = evals.to_vec();
47        Self::evals_to_coeffs_inplace(&mut coeffs);
48        Self { coeffs }
49    }
50
51    pub fn into_coeffs(self) -> Vec<F> {
52        self.coeffs
53    }
54
55    /// Evaluate with `O(1)` extra memory via naive algorithm.
56    ///
57    /// Performs `N*log(N)/2` multiplications when `N = x.len()` is a power of two.
58    pub fn eval_at_point<F2: Field, EF: ExtensionField<F> + Mul<F2, Output = EF>>(
59        &self,
60        x: &[F2],
61    ) -> EF {
62        debug_assert_eq!(log2_strict_usize(self.coeffs.len()), x.len());
63        let mut res = EF::ZERO;
64        for (i, coeff) in self.coeffs.iter().enumerate() {
65            let mut term = EF::from(*coeff);
66            for (j, x_j) in x.iter().enumerate() {
67                if (i >> j) & 1 == 1 {
68                    term = term * *x_j;
69                }
70            }
71            res += term;
72        }
73        res
74    }
75
76    /// Evaluate with `O(1)` extra memory but consuming `self`.
77    ///
78    /// Performs `N - 1` multiplications for `N = x.len()`.
79    pub fn eval_at_point_inplace<F2>(self, x: &[F2]) -> F
80    where
81        F2: Field,
82        F: ExtensionField<F2>,
83    {
84        let mut buf = self.coeffs;
85        debug_assert_eq!(buf.len(), 1 << x.len());
86        let mut len = 1usize << x.len();
87        // Assumes caller ensured buf[..len] is initialized with the current coefficients.
88        for &xj in x.iter().rev() {
89            len >>= 1;
90            let (left, right) = buf.split_at_mut(len);
91            for (li, &ri) in zip(left.iter_mut(), right.iter()) {
92                *li += ri * xj;
93            }
94        }
95        buf[0]
96    }
97
98    pub fn evals_to_coeffs_inplace(a: &mut [F]) {
99        let n = log2_strict_usize(a.len());
100        // Go through coordinates X_1, ..., X_n and interpolate each one from s(0), s(1) -> s(0) +
101        // (s(1) - s(0)) X_i
102        for log_step in 0..n {
103            let step = 1usize << log_step;
104            let span = step << 1;
105            a.par_chunks_exact_mut(span).for_each(|chunk| {
106                let (first_half, second_half) = chunk.split_at_mut(step);
107                first_half
108                    .par_iter()
109                    .zip(second_half.par_iter_mut())
110                    .for_each(|(u, v)| {
111                        *v -= *u;
112                    });
113            });
114        }
115    }
116
117    pub fn coeffs_to_evals_inplace(a: &mut [F]) {
118        let n = log2_strict_usize(a.len());
119
120        for b in 0..n {
121            let step = 1usize << b;
122            let span = step << 1;
123            for i in (0..a.len()).step_by(span) {
124                for j in 0..step {
125                    let u = i + j;
126                    let v = u + step;
127                    a[v] += a[u];
128                }
129            }
130        }
131    }
132}
133
134/// Given vector `x` in `F^n`, populates `out` with `eq_n(x, y)` for `y` on hypercube `H_n`.
135///
136/// The multilinear equality polynomial is defined as:
137/// ```text
138///     eq(x, z) = \prod_{i=0}^{n-1} (x_i z_i + (1 - x_i)(1 - z_i)).
139/// ```
140// Reference: <https://github.com/Plonky3/Plonky3/blob/main/multilinear-util/src/eq.rs>
141pub fn evals_eq_hypercube<F: Field>(x: &[F]) -> Vec<F> {
142    let n = x.len();
143    let mut out = F::zero_vec(1 << n);
144    out[0] = F::ONE;
145    for (i, &x_i) in x.iter().enumerate() {
146        let (los, his) = out[..2 << i].split_at_mut(1 << i);
147        los.par_iter_mut()
148            .zip(his.par_iter_mut())
149            .for_each(|(lo, hi)| {
150                *hi = *lo * x_i;
151                *lo *= F::ONE - x_i;
152            })
153    }
154    out
155}
156
157/// Given vector `u_tilde` in `F^n`, populates `out` with `mobius_eq_kernel(u_tilde, b)` for
158/// `b` on hypercube `H_n`.
159///
160/// For boolean `b_i ∈ {0,1}` the per-coordinate kernel is:
161/// - `K_i(0) = 1 - 2 * u_tilde_i`
162/// - `K_i(1) = u_tilde_i`
163///
164/// The output ordering matches [`evals_eq_hypercube`]: mask bit `i` corresponds to `u_tilde[i]`.
165pub fn evals_mobius_eq_hypercube<F: Field>(u_tilde: &[F]) -> Vec<F> {
166    let n = u_tilde.len();
167    let mut out = F::zero_vec(1 << n);
168    out[0] = F::ONE;
169    for (i, &u_i) in u_tilde.iter().enumerate() {
170        let w0 = F::ONE - u_i.double();
171        let w1 = u_i;
172        let (los, his) = out[..2 << i].split_at_mut(1 << i);
173        los.par_iter_mut()
174            .zip(his.par_iter_mut())
175            .for_each(|(lo, hi)| {
176                let prev = *lo;
177                *hi = prev * w1;
178                *lo = prev * w0;
179            })
180    }
181    out
182}
183
184/// Given vector `x` in `F^n`, returns a concatenation of `evals_eq_hypercube(x[..n])` for all valid
185/// `n` in order. Also, the order of masks is of different endianness.
186pub fn evals_eq_hypercubes<'a, F: Field>(n: usize, x: impl IntoIterator<Item = &'a F>) -> Vec<F> {
187    let mut out = F::zero_vec((2 << n) - 1);
188    out[0] = F::ONE;
189    for (i, &x_i) in x.into_iter().enumerate() {
190        for y in 0..(1 << i) {
191            out[(1 << (i + 1)) - 1 + (2 * y + 1)] = out[(1 << i) - 1 + y] * x_i;
192            out[(1 << (i + 1)) - 1 + (2 * y)] = out[(1 << i) - 1 + y] * (F::ONE - x_i);
193        }
194    }
195    out
196}
197
198/// Given vector `(z,x)` in `F^{n+1}`, populates `out` with `eq_{l_skip,n}(x, y)` for `y` on
199/// hyperprism `D_n`.
200pub fn evals_eq_hyperprism<F: TwoAdicField, EF: ExtensionField<F>>(
201    omega_pows: &[F],
202    z: EF,
203    x: &[EF],
204) -> Vec<EF> {
205    // Size of D
206    let d_size = omega_pows.len();
207    let l_skip = log2_strict_usize(d_size);
208    let n = x.len();
209    let mut out = EF::zero_vec(d_size << n);
210    for (omega_pow, eq_uni) in zip(omega_pows, out.iter_mut()) {
211        *eq_uni = eval_eq_uni(l_skip, z, EF::from(*omega_pow));
212    }
213    for (i, &x_i) in x.iter().enumerate() {
214        for y in (0..d_size << i).rev() {
215            let eq_prev = out[y];
216            // Don't overwrite in y = 0 case
217            out[y | (d_size << i)] = eq_prev * x_i;
218            out[y] = eq_prev * (EF::ONE - x_i);
219        }
220    }
221    out
222}
223
224pub fn eq_sharp_uni_poly<EF: TwoAdicField>(xi_1: &[EF]) -> UnivariatePoly<EF> {
225    let evals = evals_eq_hypercube_serial(xi_1);
226    UnivariatePoly::from_evals_idft(&evals)
227}
228
229/// Prismalinear extension polynomial, in coefficient form.
230///
231/// Depends on implicit univariate skip parameter `l_skip`.
232/// Length of `coeffs` is `2^{l_skip + n}` where `n` is hypercube dimension.
233/// Indexing is to decompose `i = i_0 + 2^{l_skip} * (i_1 + 2 * i_2 .. + 2^{n-1} * i_n)` and let
234/// `coeffs[i]` be the coefficient of `Z^{i_0} X_1^{i_1} .. X_n^{i_n}`.
235#[derive(Getters)]
236pub struct Ple<F> {
237    #[getset(get = "pub")]
238    pub(crate) coeffs: Vec<F>,
239}
240
241impl<F: TwoAdicField> Ple<F> {
242    /// Create PLE from evaluations on the hyperprism D x H_n for n >= 0.
243    ///
244    /// Takes evaluations at 2^{l_skip + n} points and converts them to coefficient form
245    /// for a polynomial in n+1 variables: degree < 2^l_skip in the first variable,
246    /// degree < 2 (linear) in the other n variables.
247    ///
248    /// The input `evals` should have length 2^{l_skip + n}.
249    /// The evaluation at index i corresponds to:
250    /// - bits 0 to l_skip-1: univariate point index
251    /// - bits l_skip to l_skip+n-1: multilinear variable assignments
252    pub fn from_evaluations(l_skip: usize, evals: &[F]) -> Self {
253        let prism_dim = log2_strict_usize(evals.len());
254        assert!(
255            prism_dim >= l_skip,
256            "Total variables must be at least l_skip"
257        );
258        // Go through coordinates Z, X_1, ..., X_n and interpolate each one
259        // For first Z coordinate, we do parallel iDFT on each 2^l_skip sized chunk
260        let mut buf: Vec<_> = evals
261            .par_chunks_exact(1 << l_skip)
262            .flat_map(|chunk| {
263                let dft = Radix2Bowers;
264                dft.idft(chunk.to_vec())
265            })
266            .collect();
267
268        let n = prism_dim - l_skip;
269        // Go through coordinates X_1, ..., X_n and interpolate each one from s(0), s(1) -> s(0) +
270        // (s(1) - s(0)) X_i
271        for i in 0..n {
272            let step = 1usize << (l_skip + i);
273            let span = step << 1;
274            buf.par_chunks_exact_mut(span).for_each(|chunk| {
275                let (first_half, second_half) = chunk.split_at_mut(step);
276                first_half
277                    .par_iter()
278                    .zip(second_half.par_iter_mut())
279                    .for_each(|(u, v)| {
280                        *v -= *u;
281                    });
282            });
283        }
284        Self { coeffs: buf }
285    }
286
287    pub fn eval_at_point<EF: ExtensionField<F>>(&self, l_skip: usize, z: EF, x: &[EF]) -> EF {
288        let n = x.len();
289        debug_assert_eq!(l_skip + n, log2_strict_usize(self.coeffs.len()));
290        let mut res = EF::ZERO;
291        let mut z_pow = EF::ONE;
292        for (i, coeff) in self.coeffs.iter().enumerate() {
293            if i.trailing_zeros() >= l_skip as u32 {
294                z_pow = EF::ONE;
295            }
296            let i_x = i >> l_skip;
297            let mut term = z_pow * *coeff;
298            for (j, x_j) in x.iter().enumerate() {
299                if (i_x >> j) & 1 == 1 {
300                    term *= *x_j;
301                }
302            }
303            z_pow *= z;
304            res += term;
305        }
306        res
307    }
308
309    pub fn into_coeffs(self) -> Vec<F> {
310        self.coeffs
311    }
312}
313
314/// Convert evaluations of a prismalinear polynomial on `D × {0,1}^n` into the RS coefficient
315/// vector for **eval-to-coeff** encoding.
316///
317/// - `|D| = 2^l_skip`, and `evals` must be ordered so the lower `l_skip` bits of the index select
318///   the point in `D`, and higher bits select the boolean assignment.
319/// - The output ordering matches the same convention: `idx = z_mask + (1 << l_skip) * x_mask`.
320///
321/// This avoids computing full prismalinear monomial coefficients in the boolean variables (which
322/// would later be re-zeta-transformed), by:
323/// 1) Performing an iDFT in `Z` for each boolean assignment.
324/// 2) Applying the subset-zeta transform only over the `Z`-mask bits.
325pub fn eval_to_coeff_rs_message<F: TwoAdicField>(l_skip: usize, evals: &[F]) -> Vec<F> {
326    assert!(!evals.is_empty(), "Evaluations cannot be empty");
327    let prism_dim = log2_strict_usize(evals.len());
328    assert!(
329        prism_dim >= l_skip,
330        "Total variables must be at least l_skip"
331    );
332
333    let chunk_len = 1usize << l_skip;
334    let mut buf: Vec<_> = evals
335        .par_chunks_exact(chunk_len)
336        .flat_map(|chunk| {
337            let dft = Radix2Bowers;
338            dft.idft(chunk.to_vec())
339        })
340        .collect();
341
342    // For each fixed boolean assignment, convert Z-monomial coefficients into hypercube
343    // evaluations over the Z-bit variables.
344    buf.par_chunks_exact_mut(chunk_len)
345        .for_each(Mle::coeffs_to_evals_inplace);
346
347    buf
348}
349
350pub struct MleMatrix<F> {
351    pub columns: Vec<Mle<F>>,
352}
353
354impl<F: Field> MleMatrix<F> {
355    pub fn from_evaluations(evals: &ColMajorMatrix<F>) -> Self {
356        let width = evals.width();
357        let columns = (0..width)
358            .into_par_iter()
359            .map(|j| Mle::from_evaluations(evals.column(j)))
360            .collect();
361        Self { columns }
362    }
363}
364
365pub struct PleMatrix<F> {
366    pub columns: Vec<Ple<F>>,
367}
368
369impl<F: TwoAdicField> PleMatrix<F> {
370    pub fn from_evaluations(l_skip: usize, evals: &ColMajorMatrixView<F>) -> Self {
371        let width = evals.width();
372        let columns = (0..width)
373            .into_par_iter()
374            .map(|j| Ple::from_evaluations(l_skip, evals.column(j)))
375            .collect();
376        Self { columns }
377    }
378}
379
380impl<F: Field> UnivariatePoly<F> {
381    #[instrument(level = "debug", skip_all)]
382    pub fn lagrange_interpolate<BF: Field>(points: &[BF], evals: &[F]) -> Self
383    where
384        F: ExtensionField<BF>,
385    {
386        assert_eq!(points.len(), evals.len());
387        let len = points.len();
388
389        // Special case: empty or single evaluation
390        if len == 0 {
391            return Self(vec![]);
392        }
393        if len == 1 {
394            return Self(vec![evals[0]]);
395        }
396
397        // Lagrange interpolation algorithm
398        // P(x) = sum_{i=0}^{len-1} evals[i] * L_i(x)
399        // where L_i(x) = prod_{j != i} (x - points[j]) / (points[i] - points[j])
400
401        // Step 1: Compute all denominators (points[i] - points[j]) for i != j
402        let mut denominators = Vec::with_capacity(len * (len - 1));
403        for i in 0..len {
404            for j in 0..len {
405                if i != j {
406                    denominators.push(points[i] - points[j]);
407                }
408            }
409        }
410
411        // Step 2: Batch invert all denominators
412        let inv_denominators = batch_multiplicative_inverse_serial(&denominators);
413
414        // Step 3: Build coefficient form by accumulating Lagrange basis polynomials
415        let mut coeffs = vec![F::ZERO; len];
416
417        // Reusable workspace for Lagrange polynomial computation
418        let mut lagrange_poly = Vec::with_capacity(len);
419
420        #[allow(clippy::needless_range_loop)]
421        for i in 0..len {
422            // Skip if evaluation is zero (optimization)
423            if evals[i] == F::ZERO {
424                continue;
425            }
426
427            // Build L_i(x) in coefficient form using polynomial multiplication
428            // L_i(x) = prod_{j != i} (x - points[j]) / (points[i] - points[j])
429
430            // Start with constant polynomial 1
431            lagrange_poly.clear();
432            lagrange_poly.push(F::ONE);
433
434            // Get the precomputed inverse denominators for this i
435            let inv_denom_start = i * (len - 1);
436            let mut inv_idx = 0;
437
438            // Multiply by (x - points[j]) / (points[i] - points[j]) for each j != i
439            #[allow(clippy::needless_range_loop)]
440            for j in 0..len {
441                if i != j {
442                    let scale = inv_denominators[inv_denom_start + inv_idx];
443                    inv_idx += 1;
444
445                    // Multiply lagrange_poly by (x - points[j]) * scale in place
446                    // This is equivalent to: lagrange_poly * (x - points[j]) * scale
447                    // = lagrange_poly * x * scale - lagrange_poly * points[j] * scale
448
449                    lagrange_poly.push(F::ZERO); // Extend by one for the new highest degree term
450                    for k in (1..lagrange_poly.len()).rev() {
451                        let prev_coeff = lagrange_poly[k - 1] * scale;
452                        lagrange_poly[k] += prev_coeff;
453                        lagrange_poly[k - 1] = -prev_coeff * points[j];
454                    }
455                }
456            }
457
458            // Add evals[i] * L_i(x) to the result
459            for (k, &coeff) in lagrange_poly.iter().enumerate() {
460                coeffs[k] += evals[i] * coeff;
461            }
462        }
463
464        Self(coeffs)
465    }
466}
467
468impl<F: TwoAdicField> UnivariatePoly<F> {
469    /// Computes P(1), P(omega), ..., P(omega^{n-1}).
470    fn chirp_z(poly: &[F], omega: F, n: usize) -> Vec<F> {
471        if n == 0 {
472            return Vec::new();
473        }
474        if poly.is_empty() {
475            return vec![F::ZERO; n];
476        }
477        let s = poly.len() + n;
478        let omega_powers = (0..(s as u64))
479            .map(|i| omega.exp_u64(i * (i.saturating_sub(1)) / 2))
480            .collect_vec();
481        let omega_powers_inv = batch_multiplicative_inverse_serial(&omega_powers);
482        let mut p = zip(poly, &omega_powers_inv)
483            .map(|(&c, &inv)| c * inv)
484            .collect_vec();
485        let mut q = omega_powers.iter().rev().copied().collect_vec();
486
487        let dft_deg = (p.len() + q.len() - 1).next_power_of_two();
488        p.resize(dft_deg, F::ZERO);
489        q.resize(dft_deg, F::ZERO);
490        let dft = Radix2BowersSerial;
491        p = dft.dft(p);
492        q = dft.dft(q);
493        for (x, y) in p.iter_mut().zip(q.iter()) {
494            *x *= *y;
495        }
496        p = dft.idft(p);
497        zip(p.into_iter().skip(n).take(n).rev(), omega_powers_inv)
498            .map(|(x, inv)| x * inv)
499            .collect()
500    }
501
502    /// Given z and n, find product (1 - x)(1 - zx)...(1 - z^{n-1}x).
503    /// If n is odd, this can be trivially computed from n - 1.
504    /// Otherwise, F_n(x) = F_{n/2}(x) * F_{n/2}(x * z^{n/2}).
505    fn geometric_sequence_linear_product_helper(
506        dft: &Radix2BowersSerial,
507        z: F,
508        n: usize,
509    ) -> Vec<F> {
510        if n == 1 {
511            vec![F::ONE, F::NEG_ONE]
512        } else if n % 2 == 1 {
513            let mut prev = Self::geometric_sequence_linear_product_helper(dft, z, n - 1);
514            let zp = z.exp_u64((n - 1) as u64);
515            prev.push(F::ZERO);
516            for i in (1..prev.len()).rev() {
517                let value = prev[i - 1] * zp;
518                prev[i] -= value;
519            }
520            prev
521        } else {
522            let mut prev = Self::geometric_sequence_linear_product_helper(dft, z, n / 2);
523            let zp = z.exp_u64((n / 2) as u64);
524            let mut another = prev
525                .iter()
526                .zip(zp.powers())
527                .map(|(a, b)| *a * b)
528                .collect_vec();
529            let len = prev.len().next_power_of_two() * 2;
530            prev.resize(len, F::ZERO);
531            another.resize(len, F::ZERO);
532            prev = dft.dft(prev);
533            another = dft.dft(another);
534            for (x, y) in prev.iter_mut().zip(another.into_iter()) {
535                *x *= y;
536            }
537            prev = dft.idft(prev);
538            prev.truncate(n + 1);
539            prev
540        }
541    }
542
543    /// Constructs the polynomial in coefficient form from its evaluations on
544    /// `{omega^0,...,omega^d}` where `d` is the degree of the polynomial. Here `omega` is a
545    /// (fixed) generator of the two-adic subgroup of order `(d+1).next_power_of_two()`.
546    #[instrument(level = "debug", skip_all)]
547    pub fn from_evals(evals: &[F]) -> Self {
548        let n = evals.len();
549        let log_n = log2_ceil_usize(n);
550        let omega = F::two_adic_generator(log_n);
551        let omega_pows = omega.powers().take((1 << log_n) + 1).collect_vec();
552        if n == 0 {
553            return Self(Vec::new());
554        }
555        if n == 1 {
556            return Self(vec![evals[0]]);
557        }
558
559        // We know that, by Lagrange interpolation,
560        // P(x) = \sum_i evals[i] * \prod_{j\neq i} (x - omega^j) / (omega^i - omega^j).
561        // Let y[i] = evals[i] / (omega^{(n-1) * i} * prod_{j < n - 1 - i}(1 - omega^j) * prod_{j <
562        // i}(1 - omega^{-j})). Then P(x) = \sum_i y[i] * \prod_{j\neq i} (x - omega^j).
563
564        let mut positive_denoms = vec![F::ONE; n];
565        let mut negative_denoms = vec![F::ONE; n];
566        for i in 0..(n - 1) {
567            positive_denoms[i + 1] = positive_denoms[i] / (F::ONE - omega_pows[i + 1]);
568            negative_denoms[i + 1] =
569                negative_denoms[i] / (F::ONE - omega_pows[(1 << log_n) - 1 - i]);
570        }
571        let omega_inv = omega_pows[(1 << log_n) - 1];
572        let y = (0..n)
573            .map(|i| {
574                evals[i]
575                    * omega_inv.exp_u64(((n - 1) * i) as u64)
576                    * negative_denoms[i]
577                    * positive_denoms[n - 1 - i]
578            })
579            .collect_vec();
580
581        // If we reverse both P and replace all (x - a) with (1 - ax), we'll still have an equality.
582        // So from now we assume that P(x) = \sum_i y[i] * \prod_{j\neq i} (1 - omega^j * x).
583        // If we divide everything by Q(x) = \prod_i (1 - omega^i * x), then we'll have
584        // P(x) / Q(x) = \sum_i y[i] / (1 - omega^i * x).
585
586        // We want to find the first n coefficients of the right-hand side.
587        // [x^k](\sum_i y[i] / (1 - x * omega^i)) = \sum_i y[i] / (omega^{ik}) = Y(omega^k).
588
589        let mut rhs = Self::chirp_z(&y, omega, n);
590
591        // Now we need the denominator in the left-hand side.
592        let dft = Radix2BowersSerial;
593        let mut denom = Self::geometric_sequence_linear_product_helper(&dft, omega, n);
594
595        let len = (denom.len() + rhs.len() - 1).next_power_of_two();
596        denom.resize(len, F::ZERO);
597        rhs.resize(len, F::ZERO);
598        denom = dft.dft(denom);
599        rhs = dft.dft(rhs);
600        let res = denom.into_iter().zip(rhs).map(|(a, b)| a * b).collect_vec();
601        let mut res = dft.idft(res);
602        res.truncate(n);
603        // Remember that P(x) is reversed
604        res.reverse();
605        Self(res)
606    }
607
608    /// Constructs the polynomial in coefficient form from its evaluations on a smooth subgroup of
609    /// `F^*` by performing inverse DFT.
610    ///
611    /// Requires that `evals.len()` is a power of 2.
612    pub fn from_evals_idft(evals: &[F]) -> Self {
613        // NOTE[jpw]: Use Bowers instead of Dit to avoid RefCell
614        let dft = Radix2BowersSerial;
615        let coeffs = dft.idft(evals.to_vec());
616        Self(coeffs)
617    }
618
619    /// Interpolates from evaluations on cosets `init * g^i D` for `i = 0,..,width-1` where `D` is
620    /// a smooth subgroup of F.
621    pub fn from_geometric_cosets_evals_idft<BF: Field>(
622        evals: RowMajorMatrix<F>,
623        shift: BF,
624        init: BF,
625    ) -> Self
626    where
627        F: ExtensionField<BF>,
628    {
629        let height = Matrix::height(&evals);
630        let width = Matrix::width(&evals);
631        if height == 0 || width == 0 {
632            return Self(Vec::new());
633        }
634
635        let log_height = log2_strict_usize(height);
636        let dft = Radix2BowersSerial;
637
638        // First interpolate within each coset (size `height`) to get the remainder
639        // modulo `X^height - shift^height`, then unshift coefficients by `(init * shift^i)^{-t}`.
640        let mut coeffs_mat = dft.idft_batch(evals);
641        let shift_inv = shift.inverse();
642        let init_inv = init.inverse();
643        // shift_invs[i] = (init * shift^i)^{-1} = init^{-1} * shift^{-i}
644        let shift_invs = (0..width)
645            .fold(
646                (Vec::with_capacity(width), init_inv),
647                |(mut acc, pow), _| {
648                    acc.push(pow);
649                    (acc, pow * shift_inv)
650                },
651            )
652            .0;
653        let mut shift_pows = vec![F::ONE; width];
654        for row in coeffs_mat.rows_mut() {
655            for (col, value) in row.iter_mut().enumerate() {
656                *value *= shift_pows[col];
657                shift_pows[col] *= shift_invs[col];
658            }
659        }
660
661        // Interpolate across cosets for each coefficient degree.
662        // Points are init^height, init^height * shift^height, ..., init^height *
663        // shift^{(width-1)*height}
664        let coset_base = shift.exp_power_of_2(log_height);
665        let init_base = init.exp_power_of_2(log_height);
666        let lagrange_basis = lagrange_basis_from_geometric_points(coset_base, width, init_base);
667        let mut coeffs = vec![F::ZERO; height * width];
668        for (row_idx, row_vals) in coeffs_mat.row_slices().enumerate() {
669            let mut poly_coeffs = vec![F::ZERO; width];
670            for (i, &value) in row_vals.iter().enumerate() {
671                if value == F::ZERO {
672                    continue;
673                }
674                for (k, basis_coeff) in lagrange_basis[i].iter().enumerate() {
675                    poly_coeffs[k] += value * *basis_coeff;
676                }
677            }
678            for (coset_idx, coeff) in poly_coeffs.into_iter().enumerate() {
679                coeffs[coset_idx * height + row_idx] = coeff;
680            }
681        }
682        Self(coeffs)
683    }
684}
685
686/// Precompute Lagrange basis polynomials for interpolation at `init * base^i` for i=0..width-1.
687fn lagrange_basis_from_geometric_points<F: Field>(base: F, width: usize, init: F) -> Vec<Vec<F>> {
688    if width == 0 {
689        return Vec::new();
690    }
691    if width == 1 {
692        return vec![vec![F::ONE]];
693    }
694
695    // Points are init, init * base, init * base^2, ..., init * base^{width-1}
696    let points = (0..width)
697        .fold((Vec::with_capacity(width), init), |(mut acc, pow), _| {
698            acc.push(pow);
699            (acc, pow * base)
700        })
701        .0;
702
703    // Build the monic polynomial P(x) = ∏(x - points[i]).
704    let mut root_poly = vec![F::ONE];
705    for &x in &points {
706        root_poly.push(F::ZERO);
707        for k in (1..root_poly.len()).rev() {
708            let prev = root_poly[k - 1];
709            root_poly[k] = prev - x * root_poly[k];
710        }
711        root_poly[0] = -x * root_poly[0];
712    }
713
714    // Precompute products of (1 - base^k) for k=1..width-1.
715    let mut prefix = vec![F::ONE; width];
716    for (i, base_pow) in base.powers().skip(1).take(width - 1).enumerate() {
717        prefix[i + 1] = prefix[i] * (F::ONE - base_pow);
718    }
719
720    // Compute P(x)/(x - points[i]) for each i and scale by the inverse denominator.
721    let mut quotients = Vec::with_capacity(width);
722    for (i, &x) in points.iter().enumerate() {
723        let mut q = vec![F::ZERO; width];
724        q[width - 1] = root_poly[width];
725        for k in (0..width - 1).rev() {
726            q[k] = root_poly[k + 1] + x * q[k + 1];
727        }
728
729        // Denominator is prod_{j != i} (points[i] - points[j])
730        // = prod_{j != i} (init * base^i - init * base^j)
731        // = init^{width-1} * base^{i*(width-1)} * prod_{j != i} (1 - base^{j-i})
732        // = init^{width-1} * base^{i*(width-1)} * prod_{k=1}^{i} (1 - base^{-k}) *
733        // prod_{k=1}^{width-1-i} (1 - base^k) For k=1..i: (1 - base^{-k}) = -base^{-k} * (1
734        // - base^k) So prod_{k=1}^{i} (1 - base^{-k}) = (-1)^i * base^{-i*(i+1)/2} *
735        // prefix[i]
736        let sign = if i % 2 == 0 { F::ONE } else { F::NEG_ONE };
737        let exp = i * (width - 1) - (i * (i + 1) / 2);
738        let pow = base.exp_u64(exp as u64);
739        let init_pow = init.exp_u64((width - 1) as u64);
740        let denom = sign * init_pow * pow * prefix[i] * prefix[width - 1 - i];
741        let inv_denom = denom.inverse();
742        for coeff in q.iter_mut() {
743            *coeff *= inv_denom;
744        }
745        quotients.push(q);
746    }
747    quotients
748}
749
750#[cfg(test)]
751mod tests {
752    use std::iter::zip;
753
754    use itertools::Itertools;
755    use openvm_stark_sdk::config::baby_bear_poseidon2::*;
756    use p3_field::{PrimeCharacteristicRing, TwoAdicField};
757    use p3_util::{log2_ceil_usize, log2_strict_usize};
758    use rand::{rngs::StdRng, Rng, SeedableRng};
759
760    use super::*;
761    use crate::poly_common::horner_eval;
762
763    #[test]
764    fn test_evals_mobius_eq_hypercube_matches_naive() {
765        let mut rng = StdRng::seed_from_u64(0);
766
767        for n in 0..=10usize {
768            let u_tilde = (0..n)
769                .map(|_| F::from_u64(rng.random()))
770                .collect::<Vec<_>>();
771            let evals = evals_mobius_eq_hypercube(&u_tilde);
772            assert_eq!(evals.len(), 1 << n);
773
774            for (mask, &eval) in evals.iter().enumerate() {
775                let mut expected = F::ONE;
776                for (i, &w) in u_tilde.iter().enumerate() {
777                    expected *= if (mask >> i) & 1 == 0 {
778                        F::ONE - w.double()
779                    } else {
780                        w
781                    };
782                }
783                assert_eq!(eval, expected);
784            }
785        }
786    }
787
788    #[test]
789    fn test_decoder_kernel_identity() {
790        let mut rng = StdRng::seed_from_u64(1);
791
792        for m in 0..=10usize {
793            let a = (0..(1usize << m))
794                .map(|_| F::from_u64(rng.random()))
795                .collect::<Vec<_>>();
796
797            // RS coefficients under eval-to-coeff encoding are the hypercube evaluations of f.
798            let mut rs_coeffs = a.clone();
799            Mle::coeffs_to_evals_inplace(&mut rs_coeffs);
800
801            // WHIR works with the associated MLE HatF whose coefficients are `rs_coeffs`.
802            // We need HatF evaluations on the hypercube.
803            let mut hatf_evals = rs_coeffs.clone();
804            Mle::coeffs_to_evals_inplace(&mut hatf_evals);
805
806            let u_tilde = (0..m)
807                .map(|_| F::from_u64(rng.random()))
808                .collect::<Vec<_>>();
809
810            let mobius_eq_evals = evals_mobius_eq_hypercube(&u_tilde);
811            let lhs = zip(&hatf_evals, &mobius_eq_evals).fold(F::ZERO, |acc, (&f, &g)| acc + f * g);
812
813            // Naive evaluation of f(u_tilde) from its coefficient table.
814            let rhs = a.iter().enumerate().fold(F::ZERO, |acc, (mask, &coeff)| {
815                let mut term = coeff;
816                for (i, &w) in u_tilde.iter().enumerate() {
817                    if (mask >> i) & 1 == 1 {
818                        term *= w;
819                    }
820                }
821                acc + term
822            });
823
824            assert_eq!(lhs, rhs);
825        }
826    }
827
828    #[test]
829    fn test_lagrange_interpolation_round_trip() {
830        let mut rng = StdRng::seed_from_u64(0);
831        // Test various polynomial degrees
832        for degree in 0..8usize {
833            let num_evals: usize = degree + 1;
834
835            // Create random coefficients for a polynomial of given degree
836            let mut original_coeffs = vec![];
837            for _ in 0..num_evals {
838                // Use deterministic values for reproducibility
839                original_coeffs.push(F::from_u32(rng.random()));
840            }
841
842            // Generate evaluation points (powers of omega)
843            let log_domain_size = log2_ceil_usize(num_evals);
844            let omega = F::two_adic_generator(log_domain_size);
845
846            // Evaluate polynomial at these points
847            let mut evals = vec![];
848            let points = omega.powers().take(num_evals).collect_vec();
849            for &x in &points {
850                evals.push(horner_eval(&original_coeffs, x));
851            }
852
853            // Reconstruct polynomial from evaluations using Lagrange interpolation
854            let reconstructed_poly = UnivariatePoly::lagrange_interpolate(&points, &evals);
855
856            // Verify coefficients match (up to the original degree)
857            for (i, (coeff, reconstructed_coeff)) in
858                zip(original_coeffs, reconstructed_poly.0).enumerate()
859            {
860                assert_eq!(
861                    coeff, reconstructed_coeff,
862                    "Coefficient mismatch at index {} for degree {} polynomial",
863                    i, degree
864                );
865            }
866        }
867    }
868
869    #[test]
870    fn test_chirp_z() {
871        let mut rng = StdRng::seed_from_u64(0);
872        for degree in 0..8usize {
873            let num_evals: usize = degree + 1;
874
875            // Create random coefficients for a polynomial of given degree
876            let mut original_coeffs = vec![];
877            for _ in 0..num_evals {
878                // Use deterministic values for reproducibility
879                original_coeffs.push(F::from_u32(rng.random()));
880            }
881
882            let log_domain_size = log2_ceil_usize(num_evals);
883            let omega = F::two_adic_generator(log_domain_size);
884
885            // Evaluate polynomial at these points
886            let mut evals = vec![];
887            let points = omega.powers().take(num_evals).collect_vec();
888            for &x in &points {
889                evals.push(horner_eval(&original_coeffs, x));
890            }
891
892            assert_eq!(
893                evals,
894                UnivariatePoly::chirp_z(&original_coeffs, omega, num_evals)
895            );
896
897            let reconstructed_poly = UnivariatePoly::from_evals(&evals);
898
899            // Verify coefficients match (up to the original degree)
900            for (i, (coeff, reconstructed_coeff)) in
901                zip(original_coeffs, reconstructed_poly.0).enumerate()
902            {
903                assert_eq!(
904                    coeff, reconstructed_coeff,
905                    "Coefficient mismatch at index {} for degree {} polynomial",
906                    i, degree
907                );
908            }
909        }
910    }
911
912    #[test]
913    fn test_from_geometric_cosets_evals_idft_round_trip() {
914        let mut rng = StdRng::seed_from_u64(0);
915        let height = 8usize;
916        let log_height = log2_strict_usize(height);
917        let omega = F::two_adic_generator(log_height);
918
919        let configs = [
920            (F::GENERATOR, F::ONE),
921            (F::two_adic_generator(log_height + 2), F::GENERATOR),
922        ];
923
924        for (shift, init) in configs {
925            for width in 2..=4usize {
926                let coeffs = (0..height * width)
927                    .map(|_| F::from_u32(rng.random()))
928                    .collect_vec();
929                let coeffs_ref = &coeffs;
930
931                // Evaluations on cosets init * shift^i * D for i = 0, ..., width - 1
932                let evals: Vec<F> = (0..height)
933                    .flat_map(|row| {
934                        let omega_pow = omega.exp_u64(row as u64);
935                        (0..width).map(move |col| {
936                            let coset_shift = init * shift.exp_u64(col as u64);
937                            horner_eval(coeffs_ref, coset_shift * omega_pow)
938                        })
939                    })
940                    .collect_vec();
941
942                let evals_mat = RowMajorMatrix::new(evals, width);
943                let poly = UnivariatePoly::from_geometric_cosets_evals_idft(evals_mat, shift, init);
944                assert_eq!(
945                    poly.into_coeffs(),
946                    coeffs,
947                    "width {width} round-trip failed"
948                );
949            }
950        }
951    }
952}