Skip to main content

p3_matrix/
interpolation.rs

1//! Lagrange interpolation over structured (two-adic coset) and arbitrary evaluation domains.
2//!
3//! Evaluates polynomials at out-of-domain points given their evaluations on the chosen domain.
4//!
5//! # Mathematical background (two-adic coset path)
6//!
7//! Slight variation of this approach: <https://hackmd.io/@vbuterin/barycentric_evaluation>.
8//!
9//! We start with the evaluations of a polynomial `f` over a coset `gH` of size `N`
10//! and want to compute `f(z)`.
11//!
12//! Observe that `z^N - g^N` is equal to `0` at all points in the coset.
13//! Thus `(z^N - g^N)/(z - gh^i)` is equal to `0` at all points except for `gh^i`
14//! where it is equal to:
15//! ```text
16//!   N * (gh^i)^{N - 1} = N * g^N * (gh^i)^{-1}.
17//! ```
18//!
19//! Hence `L_i(z) = h^i * (z^N - g^N)/(N * g^{N - 1} * (z - gh^i))` will be equal
20//! to `1` at `gh^i` and `0` at all other points in the coset. This means that we
21//! can compute `f(z)` as:
22//! ```text
23//!   sum_i L_i(z) f(gh^i) = (z^N - g^N)/(N * g^N) * sum_i gh^i/(z - gh^i) * f(gh^i)
24//!                        = z * (z^N - g^N)/(N * g^N) * sum_i (1/(z - gh^i) - 1/z) * f(gh^i).
25//! ```
26//!
27//! This second equality lets us trade off N extension-by-base multiplications for
28//! a single extension-by-extension multiplication, an extension inversion and N
29//! extension-by-extension subtractions. For large N this is worth it.
30//!
31//! Thus we define the **adjusted weights** to be `(1/(z - g*h^i) - 1/z)` and work with
32//! these instead.
33//!
34//! # Arbitrary-domain path
35//!
36//! For evaluation domains that are not a two-adic coset we fall back to the standard
37//! second-form barycentric formula with precomputed weights `w_i = 1/prod_{j != i}(x_i - x_j)`.
38//! See [`InterpolateArbitrary`] for the matrix-level entry points and
39//! [`interpolate_lagrange`] for a single-polynomial convenience helper.
40
41use alloc::vec;
42use alloc::vec::Vec;
43
44use p3_field::coset::TwoAdicMultiplicativeCoset;
45use p3_field::{
46    ExtensionField, Field, TwoAdicField, batch_multiplicative_inverse,
47    scale_slice_in_place_single_core,
48};
49use p3_maybe_rayon::prelude::*;
50use p3_util::log2_strict_usize;
51
52use crate::Matrix;
53use crate::dense::RowMajorMatrix;
54
55/// Subtract z^{-1} from each inverse denominator to produce adjusted barycentric weights.
56///
57/// # Overview
58///
59/// Converts raw 1/(z - x_i) values into the form needed by the
60/// zero-allocation interpolation path.
61///
62/// Intended to be called once per opening point z, then reused across
63/// every matrix opened at that point.
64///
65/// # Performance
66///
67/// One extension-field inversion + N parallel extension-field subtractions.
68///
69/// # Panics
70///
71/// Panics when the evaluation point is zero.
72///
73/// The adjusted form factors 1/z out of every weight, so it is undefined there.
74/// Coset interpolation handles that point before ever reaching this function.
75pub fn compute_adjusted_weights<EF: Field>(point: EF, diff_invs: &[EF]) -> Vec<EF> {
76    // The adjusted form divides by the evaluation point, so zero has no representation.
77    // Reporting it here is clearer than the generic inversion failure from the field layer.
78    assert!(
79        !point.is_zero(),
80        "the adjusted form divides by the evaluation point, so zero is not representable"
81    );
82    // Single inversion of z, amortised over all N weights.
83    let point_inv = point.inverse();
84    // Subtract z^{-1} from each 1/(z - x_i).
85    //
86    // One item reads one weight and writes one, so it moves two elements.
87    //
88    // The rate behind a byte charge is calibrated on a fold, whose multiplication dominates.
89    //
90    // A lone subtraction is far cheaper per byte, so the raw count overprices this body.
91    //
92    // Over a degree-four extension of a 31-bit prime, one item takes 0.37 ns on one Linux core.
93    //
94    // A build that vectorizes the subtraction runs it in 0.18 ns instead.
95    //
96    // The raw count charges 3.2 ns either way, so the gate splits work not worth splitting.
97    //
98    // Against the same loop run whole, on 32 workers, whose gate is 20 us of serial work:
99    //
100    //     charged as 32 bytes : splits from 2^13, and loses 3.4x there and 1.7x at 2^14
101    //     charged as  4 bytes : splits from 2^16, where the split first pays
102    //
103    // A vectorized build loses 9x and 4.7x on those first two rows instead.
104    //
105    // Dividing by eight is what lands the gate on that break-even.
106    //
107    // It also cuts the split eight times coarser, which costs up to 1.3x from 2^18 to 2^20.
108    //
109    // That band is one where the split already wins 3x, so the coarser cut is the cheaper side.
110    //
111    // The floor also keeps a short table off rayon's bridge, which costs as much as the body.
112    let item_bytes = (2 * size_of::<EF>()).div_ceil(8);
113    diff_invs
114        .par_iter()
115        .map_collect_min_task_bytes(item_bytes, |&d| d - point_inv)
116}
117
118/// Barycentric Lagrange interpolation over two-adic cosets.
119///
120/// Blanket-implemented for every matrix over a two-adic field.
121/// Import the trait, then call the methods directly on any matrix.
122pub trait Interpolate<F: TwoAdicField>: Matrix<F> {
123    /// Evaluate a batch of polynomials at a point outside the canonical subgroup.
124    ///
125    /// Convenience wrapper that uses shift = 1.
126    ///
127    /// If the point lies in the subgroup, returns the matching row directly.
128    fn interpolate_subgroup<EF: ExtensionField<F>>(&self, point: EF) -> Vec<EF> {
129        // Canonical subgroup has unit shift.
130        self.interpolate_coset(F::ONE, point)
131    }
132
133    /// Evaluate a batch of polynomials at a point outside a shifted coset.
134    ///
135    /// Builds the coset, batch-inverts the denominators, converts to adjusted
136    /// weights, and evaluates — all in one call.
137    ///
138    /// Evaluations must be in standard (not bit-reversed) order.
139    ///
140    /// If the point lies on the coset, returns the matching row directly.
141    fn interpolate_coset<EF: ExtensionField<F>>(&self, shift: F, point: EF) -> Vec<EF> {
142        let log_height = log2_strict_usize(self.height());
143
144        // Materialise the coset so the diff computation can use parallel iteration.
145        let coset: Vec<F> = TwoAdicMultiplicativeCoset::new(shift, log_height)
146            .unwrap()
147            .iter()
148            .collect();
149
150        // Compute z - x_i, then batch-invert in one shot
151        // (Montgomery's trick: single field inversion + O(N) multiplications).
152        //
153        // One item reads one coset element and writes one difference.
154        let item_bytes = size_of::<F>() + size_of::<EF>();
155        let diffs: Vec<EF> = coset
156            .par_iter()
157            .map_collect_min_task_bytes(item_bytes, |&g| point - g);
158
159        // If point lies on the coset, return that row directly.
160        // Detected by scanning the already-computed diffs to keep the off-domain path parallel.
161        if let Some(i) = diffs.iter().position(|d| d.is_zero()) {
162            return self.row(i).unwrap().into_iter().map(EF::from).collect();
163        }
164
165        // At z = 0 every Lagrange basis polynomial of the coset takes the value 1/N.
166        // The evaluation is therefore the mean of each column, whatever the shift.
167        // The adjusted-weight form below factors 1/z out, so it cannot express this point.
168        if point.is_zero() {
169            // A Fiat-Shamir challenge lands on zero with probability 2^{-|EF|}, so this is cold.
170            // An all-ones dot product is cheaper to maintain than a dedicated column sum.
171            let ones = vec![EF::ONE; self.height()];
172            let mut evals = self.columnwise_dot_product(&ones);
173            scale_slice_in_place_single_core(&mut evals, EF::ONE.div_2exp_u64(log_height as u64));
174            return evals;
175        }
176
177        let diff_invs = batch_multiplicative_inverse(&diffs);
178
179        // Convert to adjusted weights and delegate to the zero-allocation hot path.
180        let adjusted = compute_adjusted_weights(point, &diff_invs);
181        self.interpolate_coset_with_precomputation(shift, point, &adjusted)
182    }
183
184    /// Fastest interpolation path — zero allocation beyond the result vector.
185    ///
186    /// Given evaluations of a batch of polynomials over the given coset of the canonical
187    /// power-of-two subgroup, evaluate the polynomials at `point`.
188    ///
189    /// This method takes the precomputed `adjusted_weights` and should
190    /// be preferred over [`interpolate_coset`](Interpolate::interpolate_coset) when repeatedly
191    /// called with the same subgroup and/or point.
192    ///
193    /// # Overview
194    ///
195    /// Each adjusted weight encodes the identity:
196    ///
197    /// ```text
198    ///   g*h^i / (z - g*h^i)  =  z * adjusted_i
199    /// ```
200    ///
201    /// so the full barycentric formula becomes:
202    ///
203    /// ```text
204    ///   f(z)  =  z * (z^N - g^N) / (N * g^N)  *  sum_i  adjusted_i * f(g*h^i)
205    /// ```
206    ///
207    /// The inner sum is a single SIMD-optimized column-wise dot product.
208    /// The outer scalar is computed with one base-field inversion.
209    ///
210    /// # Correctness requirements
211    ///
212    /// - The evaluation point must not lie in the coset and must not be zero.
213    ///   At z = 0 the shared scalar z * (z^N - g^N) / (N * g^N) vanishes.
214    ///   Every column would then come back as zero, whatever the weights.
215    /// - Each weight must equal 1/(z - x_i) - 1/z for the corresponding coset element.
216    ///
217    /// # Performance
218    ///
219    /// - One base-field inversion (for N * g^N).
220    /// - log_2(N) extension-field squarings (for z^N).
221    /// - log_2(N) base-field squarings (for g^N).
222    /// - One SIMD-parallel column-wise dot product over the full matrix.
223    /// - No heap allocation except the result vector.
224    fn interpolate_coset_with_precomputation<EF: ExtensionField<F>>(
225        &self,
226        shift: F,
227        point: EF,
228        adjusted_weights: &[EF],
229    ) -> Vec<EF> {
230        debug_assert_eq!(adjusted_weights.len(), self.height());
231        // The global scaling factor carries a leading factor of the evaluation point.
232        // A zero point would silently return all zeros instead of failing.
233        // Checked in debug only, to leave the release path untouched.
234        debug_assert!(
235            !point.is_zero(),
236            "the global scaling factor vanishes at a zero evaluation point"
237        );
238
239        let log_height = log2_strict_usize(self.height());
240
241        // Phase 1: Global scaling factor
242        //
243        //   s = z * (z^N - g^N) / (N * g^N)
244        //
245        // z^N via extension-field repeated squaring (expensive).
246        let z_pow_n = point.exp_power_of_2(log_height);
247        // g^N via base-field repeated squaring (cheap — single-word ops).
248        let g_pow_n = shift.exp_power_of_2(log_height);
249        // Combine denominator N * g^N and invert once (only base-field inversion).
250        let denom_inv = g_pow_n.mul_2exp_u64(log_height as u64).inverse();
251        // Assemble: z * (z^N - g^N) * 1/(N * g^N).
252        let scaling_factor = point * (z_pow_n - g_pow_n) * denom_inv;
253
254        // Phase 2: Weighted column sums via the SIMD-optimized dot product.
255        //
256        // Computes M^T * adjusted_weights, yielding one extension-field
257        // result per column (polynomial).
258        let mut evals = self.columnwise_dot_product(adjusted_weights);
259
260        // Phase 3: Apply the global scalar to every column result.
261        scale_slice_in_place_single_core(&mut evals, scaling_factor);
262        evals
263    }
264}
265
266impl<F: TwoAdicField, M: Matrix<F>> Interpolate<F> for M {}
267
268/// Computes barycentric weights w_i = 1 / prod_{j != i} (x_i - x_j).
269///
270/// These weights depend only on the domain points, not on polynomial values.
271/// Precompute them once and reuse across many evaluation targets.
272///
273/// # Performance
274///
275/// - n(n-1)/2 field subtractions (upper-triangle symmetry trick).
276/// - One batch inversion via Montgomery's trick.
277///
278/// # Returns
279///
280/// `None` if any two domain points coincide.
281pub fn barycentric_weights<F: Field>(x_coords: &[F]) -> Option<Vec<F>> {
282    let n = x_coords.len();
283    if n == 0 {
284        return Some(Vec::new());
285    }
286
287    // Accumulate denom_i = prod_{j != i} (x_i - x_j) for every point.
288    let mut denoms = alloc::vec![F::ONE; n];
289    for i in 0..n {
290        // Only iterate j < i (strict upper triangle).
291        //
292        // Antisymmetry: (x_i - x_j) = -(x_j - x_i);
293        // So one subtraction updates both denom_i and denom_j.
294        for j in 0..i {
295            let diff = x_coords[i] - x_coords[j];
296            // Zero difference means a duplicate domain point.
297            if diff.is_zero() {
298                return None;
299            }
300            denoms[i] *= diff;
301            denoms[j] *= -diff;
302        }
303    }
304
305    // Invert all n denominators in one shot: O(n) muls + 1 inversion.
306    Some(batch_multiplicative_inverse(&denoms))
307}
308
309/// Lagrange interpolation over arbitrary evaluation domains.
310///
311/// General-domain counterpart of the structured-domain trait.
312///
313/// Blanket-implemented for every matrix over a field — just import and call.
314pub trait InterpolateArbitrary<F: Field>: Matrix<F> {
315    /// Evaluates every column polynomial at `point` via barycentric interpolation.
316    ///
317    /// Each row holds evaluations at the corresponding domain point.
318    ///
319    /// # Performance
320    ///
321    /// O(n^2) weight computation + O(n * width) evaluation.
322    ///
323    /// # Returns
324    ///
325    /// - `None` if any domain points coincide.
326    /// - The matching row directly when the target equals a domain point.
327    fn interpolate_arbitrary_point<EF: ExtensionField<F>>(
328        &self,
329        x_coords: &[F],
330        point: EF,
331    ) -> Option<Vec<EF>> {
332        debug_assert_eq!(x_coords.len(), self.height());
333
334        // Order matters: reject duplicates BEFORE the on-domain shortcut.
335        //
336        // Otherwise the shortcut fires on ill-posed input whenever the
337        // target equals ANY domain point — duplicate value or not.
338        let weights = barycentric_weights(x_coords)?;
339
340        // If the target matches a domain point, return that row directly.
341        // This also avoids a zero in the difference vector below.
342        for (i, &x) in x_coords.iter().enumerate() {
343            if point == EF::from(x) {
344                return Some(self.row(i).unwrap().into_iter().map(EF::from).collect());
345            }
346        }
347
348        // Batch-invert all (point - x_i). Safe: coincidence was ruled out above.
349        let diffs: Vec<EF> = x_coords.iter().map(|&x| point - x).collect();
350        let diff_invs = batch_multiplicative_inverse(&diffs);
351
352        Some(self.interpolate_arbitrary_with_precomputation(&weights, &diff_invs))
353    }
354
355    /// Evaluates every column polynomial at a target point with precomputed data.
356    ///
357    /// Hot path: O(n * width) per call when weights are reused across targets.
358    ///
359    /// # Correctness requirements
360    ///
361    /// - The evaluation point `z` must not equal any domain point `x_i`.
362    /// - `weights[i]` must be the barycentric weight for `x_i`,
363    ///   i.e. `1 / prod_{j != i} (x_i - x_j)`.
364    /// - `diff_invs[i]` must be `1 / (z - x_i)`.
365    ///
366    /// # Panics
367    ///
368    /// Debug-panics if the slices differ in length from the matrix height.
369    fn interpolate_arbitrary_with_precomputation<EF: ExtensionField<F>>(
370        &self,
371        weights: &[F],
372        diff_invs: &[EF],
373    ) -> Vec<EF> {
374        debug_assert_eq!(weights.len(), self.height());
375        debug_assert_eq!(diff_invs.len(), self.height());
376
377        // Empty domain -> undetermined polynomial. Return zero per column.
378        // Handling this explicitly keeps the loud panic on the standard path for caller-side contract violations.
379        if self.height() == 0 {
380            return EF::zero_vec(self.width());
381        }
382
383        // Barycentric second form:
384        //
385        //     s_i    = w_i / (z - x_i)
386        //     f_j(z) = [sum_i  s_i * M[i][j]]  /  [sum_i  s_i]
387        //
388        // The numerator vector is M^T * col_scale (one dot product per column).
389        // The denominator is a single scalar shared across all columns.
390
391        // Per-row scale factor: s_i = w_i * diff_inv_i.
392        let col_scale: Vec<EF> = weights
393            .iter()
394            .zip(diff_invs)
395            .map(|(&w, &d)| d * w)
396            .collect();
397
398        // Denominator: sum of all scale factors.
399        let denominator = col_scale.iter().copied().fold(EF::ZERO, |a, b| a + b);
400        let denom_inv = denominator.inverse();
401
402        // Numerator per column via SIMD-packed M^T * col_scale.
403        let mut evals = self.columnwise_dot_product(&col_scale);
404
405        // Divide every column result by the shared denominator.
406        scale_slice_in_place_single_core(&mut evals, denom_inv);
407        evals
408    }
409
410    /// Recovers coefficient vectors for every column via batched Newton interpolation.
411    ///
412    /// Each row of `self` holds evaluations at the corresponding domain point.
413    /// Returns an n * width matrix where row i holds degree-i coefficients.
414    ///
415    /// # Performance
416    ///
417    /// - O(n^2 * width) field operations.
418    /// - O(n + width) auxiliary memory, zero allocations inside the main loop.
419    ///
420    /// # Returns
421    ///
422    /// `None` if any domain points coincide.
423    fn recover_coefficients(&self, x_coords: &[F]) -> Option<RowMajorMatrix<F>> {
424        let n = self.height();
425        let w = self.width();
426        debug_assert_eq!(x_coords.len(), n);
427
428        if n == 0 {
429            return Some(RowMajorMatrix::new(Vec::new(), w.max(1)));
430        }
431
432        // Row i of result will hold the degree-i coefficients for all w polynomials.
433        let mut result = RowMajorMatrix::new(F::zero_vec(n * w), w);
434
435        // Shared Newton basis polynomial B_k(x) = prod_{i<k} (x - x_i).
436        // Stored in expanded coefficient form; starts as the constant 1.
437        let mut basis = F::zero_vec(n);
438        basis[0] = F::ONE;
439
440        // Per-column scratch buffer, reused every iteration to avoid allocations.
441        let mut scratch = F::zero_vec(w);
442
443        for k in 0..n {
444            let x_k = x_coords[k];
445
446            // Evaluate B_k(x_k) directly from the roots: prod_{i<k} (x_k - x_i).
447            // Cheaper than Horner on the expanded coefficients because it
448            // touches only the domain array (sequential access, no dependency
449            // chain on the basis coefficient array).
450            let mut b_xk = F::ONE;
451            for &x_i in &x_coords[..k] {
452                b_xk *= x_k - x_i;
453            }
454            // Zero means x_k duplicates an earlier domain point.
455            let b_xk_inv = b_xk.try_inverse()?;
456
457            // Horner-evaluate all w result polynomials at x_k.
458            // Process whole rows (= ascending degree) for row-major cache locality.
459            //
460            //     scratch_j = result[k-1][j] * x_k + result[k-2][j] * x_k + ...
461            //               = P_j(x_k)
462            scratch.fill(F::ZERO);
463            for i in (0..k).rev() {
464                let row = result.row_slice(i).unwrap();
465                for j in 0..w {
466                    scratch[j] = scratch[j] * x_k + row[j];
467                }
468            }
469
470            // Newton correction: c_j = (y_{k,j} - P_j(x_k)) / B_k(x_k).
471            // Stream the evaluation row via its iterator — no heap allocation.
472            for (j, y_kj) in self.row(k).unwrap().into_iter().enumerate() {
473                scratch[j] = (y_kj - scratch[j]) * b_xk_inv;
474            }
475
476            // Accumulate: result[i][j] += c_j * basis[i].
477            for (i, &b_i) in basis.iter().enumerate().take(k + 1) {
478                let row = result.row_mut(i);
479                for j in 0..w {
480                    row[j] += scratch[j] * b_i;
481                }
482            }
483
484            // Extend basis: B_{k+1}(x) = B_k(x) * (x - x_k).
485            // Process high-to-low so each coefficient is read before overwritten.
486            //
487            //     new[k+1] = b_k
488            //     new[i]   = b_{i-1} - x_k * b_i    for i = k, ..., 1
489            //     new[0]   = -x_k * b_0
490            if k + 1 < n {
491                basis[k + 1] = basis[k];
492            }
493            for i in (1..=k).rev() {
494                basis[i] = basis[i - 1] - x_k * basis[i];
495            }
496            basis[0] = -x_k * basis[0];
497        }
498
499        Some(result)
500    }
501}
502
503impl<F: Field, M: Matrix<F>> InterpolateArbitrary<F> for M {}
504
505/// Interpolates a single polynomial from (x, y) pairs.
506///
507/// Returns coefficients in ascending degree order (index i = coefficient of x^i).
508/// Convenience wrapper that builds a one-column matrix and delegates to the
509/// batched Newton implementation.
510///
511/// # Performance
512///
513/// O(n^2) field operations, O(n) auxiliary memory.
514///
515/// # Returns
516///
517/// `None` if any two x-coordinates coincide.
518pub fn interpolate_lagrange<F: Field>(points: &[(F, F)]) -> Option<Vec<F>> {
519    if points.is_empty() {
520        return Some(Vec::new());
521    }
522    // Split into separate domain and evaluation vectors.
523    let (xs, ys): (Vec<F>, Vec<F>) = points.iter().copied().unzip();
524    // Build a single-column matrix and recover coefficients via Newton.
525    let evals = RowMajorMatrix::new_col(ys);
526    Some(evals.recover_coefficients(&xs)?.values)
527}
528
529#[cfg(test)]
530mod tests {
531    use alloc::vec;
532    use alloc::vec::Vec;
533
534    use p3_baby_bear::BabyBear;
535    use p3_field::extension::BinomialExtensionField;
536    use p3_field::{
537        BasedVectorSpace, ExtensionField, Field, HornerIter, PrimeCharacteristicRing, TwoAdicField,
538        batch_multiplicative_inverse,
539    };
540    use p3_util::log2_strict_usize;
541    use proptest::prelude::*;
542
543    use super::*;
544    use crate::dense::RowMajorMatrix;
545
546    type F = BabyBear;
547    type EF4 = BinomialExtensionField<BabyBear, 4>;
548
549    /// Evaluate a polynomial (given by coefficients) at a point using Horner's method.
550    ///
551    /// Horner's method: `f(z) = c_0 + z*(c_1 + z*(c_2 + ...))`.
552    /// Processes coefficients from highest degree down to constant term.
553    fn eval_poly<EF: ExtensionField<F>>(coeffs: &[F], point: EF) -> EF {
554        coeffs.iter().copied().horner(point)
555    }
556
557    fn eval_poly_on_coset<EF: ExtensionField<F>>(coeffs: &[F], shift: F, log_n: usize) -> Vec<EF> {
558        let n = 1 << log_n;
559        // Build the coset {shift * h^0, shift * h^1, ..., shift * h^{n-1}}.
560        let subgroup_gen = F::two_adic_generator(log_n);
561        (0..n)
562            .map(|i| {
563                // Coset element: shift * subgroup_gen^i.
564                let coset_elem = shift * subgroup_gen.exp_u64(i as u64);
565                eval_poly(coeffs, EF::from(coset_elem))
566            })
567            .collect()
568    }
569
570    #[test]
571    fn test_interpolate_subgroup() {
572        // Polynomial: f(x) = x^2 + 2x + 3, evaluated over the 8-point two-adic subgroup.
573        // Known answer: f(100) = 10000 + 200 + 3 = 10203.
574
575        // Pre-computed evaluations of f over the canonical 8-point subgroup {h^0, ..., h^7}.
576        let evals = [
577            6, 886605102, 1443543107, 708307799, 2, 556938009, 569722818, 1874680944,
578        ]
579        .map(F::from_u32);
580
581        // Single column matrix: one polynomial, 8 evaluation rows.
582        let evals_mat = RowMajorMatrix::new(evals.to_vec(), 1);
583
584        // Interpolate at z = 100, which lies outside the subgroup.
585        let point = F::from_u16(100);
586        let result = evals_mat.interpolate_subgroup(point);
587
588        // Verify the known answer: f(100) = 10203.
589        assert_eq!(result, vec![F::from_u16(10203)]);
590    }
591
592    #[test]
593    fn test_interpolate_coset() {
594        // Polynomial: f(x) = x^2 + 2x + 3, evaluated over an 8-point coset shifted
595        // by the field generator. Known answer: f(100) = 10203.
596
597        // Coset shift: the multiplicative generator of the field.
598        let shift = F::GENERATOR;
599
600        // Pre-computed evaluations of f over the coset {shift * h^0, ..., shift * h^7}.
601        let evals = [
602            1026, 129027310, 457985035, 994890337, 902, 1988942953, 1555278970, 913671254,
603        ]
604        .map(F::from_u32);
605
606        // Single column matrix: one polynomial, 8 rows.
607        let evals_mat = RowMajorMatrix::new(evals.to_vec(), 1);
608
609        // Part 1: test the standard coset interpolation path.
610        let point = F::from_u16(100);
611        let result = evals_mat.interpolate_coset(shift, point);
612        assert_eq!(result, vec![F::from_u16(10203)]);
613
614        // Part 2: test the precomputation path, which should give the same result.
615        // Manually build the coset elements and adjusted weights.
616        let n = evals.len();
617        let k = log2_strict_usize(n);
618
619        // Coset elements: {shift * h^0, shift * h^1, ..., shift * h^{N-1}}.
620        let coset = F::two_adic_generator(k).shifted_powers(shift).collect_n(n);
621
622        // Inverse denominators: 1/(z - coset_i) for each coset element.
623        let denom: Vec<_> = coset.iter().map(|&w| point - w).collect();
624        let denom = batch_multiplicative_inverse(&denom);
625
626        // Adjusted weights: 1/(z - coset_i) - 1/z.
627        let adjusted = compute_adjusted_weights(point, &denom);
628
629        // The precomputation variant must produce the same result.
630        let result = evals_mat.interpolate_coset_with_precomputation(shift, point, &adjusted);
631        assert_eq!(result, vec![F::from_u16(10203)]);
632    }
633
634    #[test]
635    fn test_interpolate_coset_single_point_identity() {
636        // Invariant: a constant polynomial f(x) = c evaluates to c everywhere.
637        // Interpolation at any external point must recover exactly c.
638        let c = F::from_u32(42);
639
640        // 8 identical evaluations => constant polynomial of degree 0.
641        let evals = vec![c; 8];
642        let evals_mat = RowMajorMatrix::new(evals, 1);
643
644        let shift = F::GENERATOR;
645        let point = F::from_u16(1337);
646
647        let result = evals_mat.interpolate_coset(shift, point);
648        assert_eq!(result, vec![c]);
649    }
650
651    #[test]
652    fn test_interpolate_coset_at_zero() {
653        // Invariant: evaluating at zero returns the constant term of each column.
654        //
655        // Fixture state: f(x) = 3 + 2x + 5x^2 + 7x^3 sampled on a shifted coset of size 8.
656        //
657        //     f(0)      = 3, the constant term
658        //     shift != 0 -> zero is not a coset element, so no on-domain shortcut fires
659        let coeffs = [3, 2, 5, 7].map(F::from_u32);
660        let shift = F::GENERATOR;
661        let evals: Vec<F> = eval_poly_on_coset(&coeffs, shift, 3);
662        let m = RowMajorMatrix::new(evals, 1);
663
664        assert_eq!(m.interpolate_coset(shift, F::ZERO), vec![coeffs[0]]);
665        assert_eq!(
666            m.interpolate_coset(shift, EF4::ZERO),
667            vec![EF4::from(coeffs[0])]
668        );
669
670        let on_subgroup: Vec<F> = eval_poly_on_coset(&coeffs, F::ONE, 3);
671        let m = RowMajorMatrix::new(on_subgroup, 1);
672        assert_eq!(m.interpolate_subgroup(F::ZERO), vec![coeffs[0]]);
673
674        // Fixture state: height 1, width 2, which is the degenerate end of the size range.
675        //
676        //     coset      : {shift}
677        //     1/N factor : 1/1 = 1
678        //     -> the single row comes back unchanged, column by column
679        let m = RowMajorMatrix::new(vec![F::from_u32(11), F::from_u32(22)], 2);
680        assert_eq!(
681            m.interpolate_coset(shift, EF4::ZERO),
682            vec![EF4::from_u32(11), EF4::from_u32(22)]
683        );
684    }
685
686    #[test]
687    #[should_panic(expected = "adjusted form")]
688    fn test_compute_adjusted_weights_rejects_zero_point() {
689        // Invariant: the adjusted weight form rejects a zero evaluation point itself.
690        //
691        // Fixture state: evaluation point 0, one inverse denominator.
692        //
693        // The message must name the barycentric precondition, not a generic inversion failure.
694        let _ = compute_adjusted_weights(EF4::ZERO, &[EF4::ONE]);
695    }
696
697    #[test]
698    fn test_interpolate_coset_point_on_coset() {
699        // On-domain target must return the matching row, never panic on 0.inverse().
700        let log_n = 3;
701        let n = 1usize << log_n;
702        let shift = F::GENERATOR;
703        let h = F::two_adic_generator(log_n);
704
705        let coset: Vec<F> = (0..n).map(|i| shift * h.exp_u64(i as u64)).collect();
706        let evals: Vec<F> = (0..n as u32).map(|i| F::from_u32(100 + i)).collect();
707        let m = RowMajorMatrix::new(evals.clone(), 1);
708
709        // Sweep every coset element so off-by-one in the index would surface.
710        for (i, &x) in coset.iter().enumerate() {
711            let result = m.interpolate_coset(shift, x);
712            assert_eq!(result, vec![evals[i]]);
713        }
714    }
715
716    #[test]
717    fn test_interpolate_coset_point_on_coset_extension() {
718        // Same shortcut, but the target is a coset element lifted into EF4.
719        // Two columns to verify the lift covers every column entry of the row.
720        let log_n = 3;
721        let n = 1usize << log_n;
722        let shift = F::GENERATOR;
723        let h = F::two_adic_generator(log_n);
724
725        let coset: Vec<F> = (0..n).map(|i| shift * h.exp_u64(i as u64)).collect();
726
727        let mut evals: Vec<F> = Vec::with_capacity(n * 2);
728        for i in 0..n {
729            evals.push(F::from_u32(200 + i as u32));
730            evals.push(F::from_u32(300 + i as u32));
731        }
732        let m = RowMajorMatrix::new(evals, 2);
733
734        // Non-zero index avoids hitting the i=0 corner.
735        let i = 3;
736        let result = m.interpolate_coset(shift, EF4::from(coset[i]));
737        assert_eq!(
738            result,
739            vec![
740                EF4::from(F::from_u32(200 + i as u32)),
741                EF4::from(F::from_u32(300 + i as u32)),
742            ]
743        );
744    }
745
746    #[test]
747    fn test_interpolate_subgroup_point_on_subgroup() {
748        // Subgroup wrapper is the shift=1 coset; on-subgroup target must short-circuit too.
749        let log_n = 2;
750        let n = 1usize << log_n;
751        let h = F::two_adic_generator(log_n);
752
753        let evals: Vec<F> = (0..n as u32).map(|i| F::from_u32(10 + i)).collect();
754        let m = RowMajorMatrix::new(evals.clone(), 1);
755
756        // h^2 is non-identity, so the test also exercises a non-trivial coset element.
757        let result = m.interpolate_subgroup(h.exp_u64(2));
758        assert_eq!(result, vec![evals[2]]);
759    }
760
761    #[test]
762    fn test_interpolate_subgroup_degree_3_correctness() {
763        // Invariant: a degree-3 polynomial over a quartic extension field is
764        // uniquely determined by 4 = 2^2 evaluation points.
765        // Interpolation must match direct evaluation.
766
767        // f(x) = x^3 + 2*x^2 + 3*x + 4
768        let poly = |x: EF4| x * x * x + x * x * F::TWO + x * F::from_u32(3) + F::from_u32(4);
769
770        // Evaluate at the 4 elements of the canonical 2^2-subgroup.
771        let subgroup = EF4::two_adic_generator(2).powers().collect_n(4);
772        let evals: Vec<_> = subgroup.iter().map(|&x| poly(x)).collect();
773        let evals_mat = RowMajorMatrix::new(evals, 1);
774
775        // Interpolate at z = 5 and compare against direct Horner evaluation.
776        let point = EF4::from_u16(5);
777        let result = evals_mat.interpolate_subgroup(point);
778        let expected = poly(point);
779        assert_eq!(result[0], expected);
780    }
781
782    #[test]
783    fn test_interpolate_coset_multiple_polynomials() {
784        // Verify batch interpolation: two polynomials evaluated over the same coset
785        // are interpolated simultaneously using a 2-column matrix.
786        //
787        //     f_1(x) = x^2 + 2x + 3
788        //     f_2(x) = 4x^2 + 5x + 6
789        //
790        //     Matrix layout (8 rows x 2 columns):
791        //         row i = [ f_1(coset[i]), f_2(coset[i]) ]
792
793        // Build the 8-point coset shifted by the extension field generator.
794        let shift = EF4::GENERATOR;
795        let coset = EF4::two_adic_generator(3)
796            .shifted_powers(shift)
797            .collect_n(8);
798
799        let f1 = |x: EF4| x * x + x * F::TWO + F::from_u32(3);
800        let f2 = |x: EF4| x * x * F::from_u32(4) + x * F::from_u32(5) + F::from_u32(6);
801
802        // Interleave evaluations: [f1(c0), f2(c0), f1(c1), f2(c1), ...].
803        let evals: Vec<_> = coset.iter().flat_map(|&x| vec![f1(x), f2(x)]).collect();
804
805        // Two-column matrix: each column is one polynomial's evaluations.
806        let evals_mat = RowMajorMatrix::new(evals, 2);
807
808        // Interpolate both polynomials at z = 77.
809        let point = EF4::from_u32(77);
810        let result = evals_mat.interpolate_coset(shift, point);
811
812        // Compare against direct evaluation of each polynomial at the same point.
813        let expected_f1 = f1(point);
814        let expected_f2 = f2(point);
815
816        assert_eq!(result[0], expected_f1);
817        assert_eq!(result[1], expected_f2);
818    }
819
820    #[test]
821    fn test_interpolate_subgroup_multiple_columns() {
822        // Same as the coset multi-polynomial test, but over the canonical subgroup
823        // (shift = 1). Verifies that the subgroup path correctly delegates to
824        // the coset path and produces identical results.
825        //
826        //     f_1(x) = x^2 + 2x + 3
827        //     f_2(x) = 4x^2 + 5x + 6
828
829        let f1 = |x: EF4| x * x + x * F::TWO + F::from_u32(3);
830        let f2 = |x: EF4| x * x * F::from_u32(4) + x * F::from_u32(5) + F::from_u32(6);
831
832        // Evaluation domain: the canonical 2^3 = 8-point subgroup {h^0, ..., h^7}.
833        let subgroup_iter = EF4::two_adic_generator(3).powers().take(8);
834
835        // Evaluate both polynomials on the subgroup, interleaved.
836        let evals: Vec<_> = subgroup_iter.flat_map(|x| vec![f1(x), f2(x)]).collect();
837
838        // 8 rows x 2 columns: column 0 holds f_1 evaluations, column 1 holds f_2.
839        let evals_mat = RowMajorMatrix::new(evals, 2);
840
841        // Interpolate at z = 77, which lies outside the subgroup.
842        let point = EF4::from_u32(77);
843        let result = evals_mat.interpolate_subgroup(point);
844
845        // Compare against direct evaluation of each polynomial.
846        let expected_f1 = f1(point);
847        let expected_f2 = f2(point);
848
849        assert_eq!(result, vec![expected_f1, expected_f2]);
850    }
851
852    proptest! {
853        // Correctness: subgroup round-trip
854        #[test]
855        fn prop_roundtrip_subgroup(
856            log_n in 1usize..=4,
857            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=16),
858            point_raw in 1u32..2013265921u32,
859        ) {
860            // Invariant: evaluate f on 2^log_n-subgroup, interpolate at z → must equal f(z).
861            //
862            //     coeffs  →  eval on {h^0, ..., h^{N-1}}  →  interpolate at z
863            //     coeffs  →  Horner at z
864            //     Both must agree.
865
866            // Truncate to degree < N so the polynomial is uniquely determined.
867            let n = 1usize << log_n;
868            let coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
869
870            // Evaluate on canonical subgroup (shift = 1).
871            let evals: Vec<F> = eval_poly_on_coset(&coeffs, F::ONE, log_n);
872            let evals_mat = RowMajorMatrix::new(evals, 1);
873
874            let point = EF4::from_u32(point_raw);
875
876            // Compare interpolation against direct Horner evaluation.
877            let result = evals_mat.interpolate_subgroup(point);
878            let expected = eval_poly(&coeffs, point);
879            prop_assert_eq!(result[0], expected);
880        }
881
882        // Correctness: evaluation at zero returns the constant term of every column
883        #[test]
884        fn prop_interpolate_coset_at_zero_is_constant_term(
885            log_n in 0usize..=5,
886            width in 1usize..=4,
887            coeffs_raw in prop::collection::vec(0u32..2013265921, 4 * 32),
888            shift_raw in 1u32..2013265921u32,
889        ) {
890            // Invariant: every Lagrange basis polynomial of the coset equals 1/N at zero.
891            //
892            // The evaluation at zero is therefore the mean of each column.
893            // That mean is the constant term of the polynomial sampled in that column.
894            //
895            // A log height of zero covers the degenerate single-row coset.
896            // A sampled shift covers the independence from the coset shift.
897
898            // Coset size, from one point up to thirty-two.
899            let n = 1usize << log_n;
900            // Non-zero shift, so the coset never contains the evaluation point zero.
901            let shift = F::from_u32(shift_raw);
902
903            // One polynomial of degree below N per column, held column by column.
904            // Each column reads its own 32-coefficient slice, so no two columns share one.
905            let coeffs: Vec<Vec<F>> = (0..width)
906                .map(|j| coeffs_raw[j * 32..j * 32 + n].iter().map(|&v| F::from_u32(v)).collect())
907                .collect();
908
909            // Sample every polynomial on the coset shift * H, one matrix row per coset point.
910            //
911            //     row i : [ f_0(shift * h^i), f_1(shift * h^i), ... ]
912            let subgroup_gen = F::two_adic_generator(log_n);
913            let mut rows = Vec::with_capacity(n * width);
914            for i in 0..n {
915                let x = shift * subgroup_gen.exp_u64(i as u64);
916                for c in &coeffs {
917                    rows.push(eval_poly::<F>(c, x));
918                }
919            }
920
921            // Column j of the result must be the constant term of the j-th polynomial.
922            let result = RowMajorMatrix::new(rows, width).interpolate_coset(shift, EF4::ZERO);
923            for (j, c) in coeffs.iter().enumerate() {
924                prop_assert_eq!(result[j], EF4::from(c[0]));
925            }
926        }
927
928        // Correctness: coset round-trip (shift = GENERATOR)
929        #[test]
930        fn prop_roundtrip_coset(
931            log_n in 1usize..=4,
932            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=16),
933            point_raw in 1u32..2013265921u32,
934        ) {
935            // Same round-trip as above, but over a shifted coset {g*h^i}.
936            let n = 1usize << log_n;
937            let coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
938            let shift = F::GENERATOR;
939
940            let evals: Vec<F> = eval_poly_on_coset(&coeffs, shift, log_n);
941            let evals_mat = RowMajorMatrix::new(evals, 1);
942            let point = EF4::from_u32(point_raw);
943
944            let result = evals_mat.interpolate_coset(shift, point);
945            let expected = eval_poly(&coeffs, point);
946            prop_assert_eq!(result[0], expected);
947        }
948
949        // Path equivalence: standard vs precomputation
950        #[test]
951        fn prop_precomputation_equivalence(
952            log_n in 1usize..=4,
953            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=16),
954            point_raw in 1u32..2013265921u32,
955        ) {
956            // Invariant: both code paths compute the same barycentric formula.
957            //
958            //     interpolate_coset                     (builds coset + adjusted weights internally)
959            //     interpolate_coset_with_precomputation (caller provides adjusted weights)
960            //     → must be bit-identical.
961            let n = 1usize << log_n;
962            let coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
963            let shift = F::GENERATOR;
964
965            let evals: Vec<F> = eval_poly_on_coset(&coeffs, shift, log_n);
966            let evals_mat = RowMajorMatrix::new(evals, 1);
967            let point = EF4::from_u32(point_raw);
968
969            // Standard path.
970            let result_standard = evals_mat.interpolate_coset(shift, point);
971
972            // Manual precomputation path.
973            let subgroup_gen = F::two_adic_generator(log_n);
974            let coset: Vec<F> =
975                (0..n).map(|i| shift * subgroup_gen.exp_u64(i as u64)).collect();
976            let diffs: Vec<EF4> = coset.iter().map(|&c| point - c).collect();
977            let diff_invs = batch_multiplicative_inverse(&diffs);
978            let adjusted = compute_adjusted_weights(point, &diff_invs);
979            let result_precomp = evals_mat
980                .interpolate_coset_with_precomputation(shift, point, &adjusted);
981
982            prop_assert_eq!(result_standard, result_precomp);
983        }
984
985        // Constant polynomial: f(x) = c → interpolation at any z must return c.
986        #[test]
987        fn prop_constant_polynomial(
988            log_n in 1usize..=4,
989            c_raw in 0u32..2013265921u32,
990            point_raw in 1u32..2013265921u32,
991        ) {
992            let n = 1usize << log_n;
993            let c = F::from_u32(c_raw);
994
995            // N identical evaluations → constant polynomial.
996            let evals = vec![c; n];
997            let evals_mat = RowMajorMatrix::new(evals, 1);
998            let point = EF4::from_u32(point_raw);
999
1000            let result = evals_mat.interpolate_subgroup(point);
1001            prop_assert_eq!(result[0], EF4::from(c));
1002        }
1003
1004        // Linearity: interp(a*f + b*g) == a*interp(f) + b*interp(g)
1005        #[test]
1006        fn prop_linearity(
1007            log_n in 1usize..=3,
1008            f_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1009            g_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1010            a_raw in 0u32..2013265921u32,
1011            b_raw in 0u32..2013265921u32,
1012            point_raw in 1u32..2013265921u32,
1013        ) {
1014            // Invariant: barycentric interpolation is linear over the evaluation column.
1015            let n = 1usize << log_n;
1016            let f_coeffs: Vec<F> = f_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1017            let g_coeffs: Vec<F> = g_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1018            let a = F::from_u32(a_raw);
1019            let b = F::from_u32(b_raw);
1020
1021            // Evaluate f, g, and (a*f + b*g) on the canonical subgroup.
1022            let f_evals: Vec<F> = eval_poly_on_coset(&f_coeffs, F::ONE, log_n);
1023            let g_evals: Vec<F> = eval_poly_on_coset(&g_coeffs, F::ONE, log_n);
1024            let combined_evals: Vec<F> = f_evals
1025                .iter()
1026                .zip(&g_evals)
1027                .map(|(&fe, &ge)| a * fe + b * ge)
1028                .collect();
1029
1030            let f_mat = RowMajorMatrix::new(f_evals, 1);
1031            let g_mat = RowMajorMatrix::new(g_evals, 1);
1032            let combined_mat = RowMajorMatrix::new(combined_evals, 1);
1033            let point = EF4::from_u32(point_raw);
1034
1035            // Interpolate individually and as a linear combination.
1036            let interp_f = f_mat.interpolate_subgroup(point)[0];
1037            let interp_g = g_mat.interpolate_subgroup(point)[0];
1038            let interp_combined = combined_mat.interpolate_subgroup(point)[0];
1039
1040            let expected = EF4::from(a) * interp_f + EF4::from(b) * interp_g;
1041            prop_assert_eq!(interp_combined, expected);
1042        }
1043
1044        // Batch equivalence: 2-column matrix vs two 1-column matrices
1045        #[test]
1046        fn prop_batch_equals_individual(
1047            log_n in 1usize..=3,
1048            f_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1049            g_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1050            point_raw in 1u32..2013265921u32,
1051        ) {
1052            // Invariant: batch[col_j] == individual[col_j] for all j.
1053            //
1054            //     batch_mat (N×2):       [f(c_0) g(c_0)]     → interpolate → [f(z), g(z)]
1055            //                            [f(c_1) g(c_1)]
1056            //                            ...
1057            //     f_mat (N×1), g_mat (N×1) → interpolate each → f(z), g(z)
1058            let n = 1usize << log_n;
1059            let f_coeffs: Vec<F> = f_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1060            let g_coeffs: Vec<F> = g_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1061            let shift = F::GENERATOR;
1062
1063            let f_evals: Vec<F> = eval_poly_on_coset(&f_coeffs, shift, log_n);
1064            let g_evals: Vec<F> = eval_poly_on_coset(&g_coeffs, shift, log_n);
1065
1066            // Interleave into 2-column batch matrix.
1067            let batch_evals: Vec<F> = f_evals
1068                .iter()
1069                .zip(&g_evals)
1070                .flat_map(|(&fe, &ge)| vec![fe, ge])
1071                .collect();
1072            let batch_mat = RowMajorMatrix::new(batch_evals, 2);
1073
1074            // Individual single-column matrices.
1075            let f_mat = RowMajorMatrix::new(f_evals, 1);
1076            let g_mat = RowMajorMatrix::new(g_evals, 1);
1077            let point = EF4::from_u32(point_raw);
1078
1079            let batch_result = batch_mat.interpolate_coset(shift, point);
1080            let f_result = f_mat.interpolate_coset(shift, point)[0];
1081            let g_result = g_mat.interpolate_coset(shift, point)[0];
1082
1083            prop_assert_eq!(batch_result[0], f_result);
1084            prop_assert_eq!(batch_result[1], g_result);
1085        }
1086    }
1087
1088    #[test]
1089    fn test_barycentric_weights_empty() {
1090        assert_eq!(barycentric_weights::<F>(&[]), Some(vec![]));
1091    }
1092
1093    #[test]
1094    fn test_barycentric_weights_duplicates() {
1095        let xs = [F::from_u32(1), F::from_u32(2), F::from_u32(1)];
1096        assert_eq!(barycentric_weights(&xs), None);
1097    }
1098
1099    #[test]
1100    fn test_barycentric_weights_known() {
1101        // For x = {0, 1, 2}:
1102        //   w_0 = 1/((0-1)(0-2)) = 1/2
1103        //   w_1 = 1/((1-0)(1-2)) = -1
1104        //   w_2 = 1/((2-0)(2-1)) = 1/2
1105        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1106        let ws = barycentric_weights(&xs).unwrap();
1107        let half = F::TWO.inverse();
1108        assert_eq!(ws, vec![half, -F::ONE, half]);
1109    }
1110
1111    #[test]
1112    fn test_interpolate_arbitrary_known_quadratic() {
1113        // f(x) = x^2 + 2x + 3.  Evaluate at x = 0, 1, 2 → y = 3, 6, 11.
1114        // Then interpolate at x = 100 → f(100) = 10203.
1115        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1116        let evals = RowMajorMatrix::new(vec![F::from_u32(3), F::from_u32(6), F::from_u32(11)], 1);
1117        let result = evals.interpolate_arbitrary_point(&xs, F::from_u32(100));
1118        assert_eq!(result, Some(vec![F::from_u32(10203)]));
1119    }
1120
1121    #[test]
1122    fn test_interpolate_arbitrary_point_on_domain() {
1123        // If we evaluate at a domain point, should return that row directly.
1124        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1125        let evals = RowMajorMatrix::new(vec![F::from_u32(3), F::from_u32(6), F::from_u32(11)], 1);
1126        let result = evals.interpolate_arbitrary_point(&xs, F::from_u32(1));
1127        assert_eq!(result, Some(vec![F::from_u32(6)]));
1128    }
1129
1130    #[test]
1131    fn test_interpolate_arbitrary_duplicates() {
1132        let xs = [F::from_u32(1), F::from_u32(1)];
1133        let evals = RowMajorMatrix::new(vec![F::from_u32(5), F::from_u32(7)], 1);
1134        assert_eq!(
1135            evals.interpolate_arbitrary_point(&xs, F::from_u32(42)),
1136            None
1137        );
1138    }
1139
1140    #[test]
1141    fn test_interpolate_arbitrary_duplicates_target_on_duplicate() {
1142        // Invariant:
1143        // Barycentric Lagrange interpolation requires pairwise-distinct domain points.
1144        // A duplicate makes the problem ill-posed → contract returns `None`.
1145        //
1146        // Fixture state: 3 evaluations, collision at indices 0 and 2.
1147        //
1148        //     i:    0     1     2
1149        //     x:    1     2     1     ← duplicate at indices 0 and 2
1150        //     y:    10    20    30    ← rows disagree at the duplicate
1151        //
1152        // Mutation: target = 1, hitting the duplicate value.
1153        //
1154        // The first-match-on-domain shortcut would return row 0 = [10];
1155        // duplicate detection must beat the shortcut and yield `None`.
1156        let xs = [F::ONE, F::TWO, F::ONE];
1157        let evals = RowMajorMatrix::new(vec![F::from_u32(10), F::from_u32(20), F::from_u32(30)], 1);
1158        assert_eq!(evals.interpolate_arbitrary_point(&xs, F::ONE), None);
1159    }
1160
1161    #[test]
1162    fn test_interpolate_arbitrary_duplicates_target_on_unique() {
1163        // Invariant:
1164        // Barycentric Lagrange interpolation requires pairwise-distinct domain points.
1165        // A duplicate makes the problem ill-posed → contract returns `None`.
1166        //
1167        // Fixture state: 3 evaluations, collision at indices 0 and 2.
1168        //
1169        //     i:    0     1     2
1170        //     x:    1     2     1     ← duplicate at indices 0 and 2
1171        //     y:    10    20    30
1172        //
1173        // Mutation: target = 2, hitting the unique value at i=1.
1174        //
1175        // The first-match-on-domain shortcut would return row 1 = [20];
1176        // duplicate detection must beat the shortcut and yield `None`.
1177        let xs = [F::ONE, F::TWO, F::ONE];
1178        let evals = RowMajorMatrix::new(vec![F::from_u32(10), F::from_u32(20), F::from_u32(30)], 1);
1179        assert_eq!(evals.interpolate_arbitrary_point(&xs, F::TWO), None);
1180    }
1181
1182    #[test]
1183    fn test_interpolate_arbitrary_multi_column() {
1184        // f1(x) = x^2 + 2x + 3,  f2(x) = 4x^2 + 5x + 6.
1185        // Evaluate both at x = 0, 1, 2.
1186        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1187        let evals = RowMajorMatrix::new(
1188            vec![
1189                F::from_u32(3),
1190                F::from_u32(6), // row 0: f1(0)=3, f2(0)=6
1191                F::from_u32(6),
1192                F::from_u32(15), // row 1: f1(1)=6, f2(1)=15
1193                F::from_u32(11),
1194                F::from_u32(32), // row 2: f1(2)=11, f2(2)=32
1195            ],
1196            2,
1197        );
1198        let result = evals
1199            .interpolate_arbitrary_point(&xs, F::from_u32(100))
1200            .unwrap();
1201        // f1(100) = 10203, f2(100) = 40506
1202        assert_eq!(result, vec![F::from_u32(10203), F::from_u32(40506)]);
1203    }
1204
1205    #[test]
1206    fn test_interpolate_arbitrary_with_precomputation_equivalence() {
1207        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1208        let evals = RowMajorMatrix::new(vec![F::from_u32(3), F::from_u32(6), F::from_u32(11)], 1);
1209
1210        let point = F::from_u32(100);
1211        let standard = evals.interpolate_arbitrary_point(&xs, point).unwrap();
1212
1213        let weights = barycentric_weights(&xs).unwrap();
1214        let diffs: Vec<F> = xs.iter().map(|&x| point - x).collect();
1215        let diff_invs = batch_multiplicative_inverse(&diffs);
1216        let precomp = evals.interpolate_arbitrary_with_precomputation(&weights, &diff_invs);
1217
1218        assert_eq!(standard, precomp);
1219    }
1220
1221    #[test]
1222    fn test_interpolate_arbitrary_extension_point() {
1223        // f(x) = x^2 + 2x + 3, evaluated at x = 0, 1, 2.
1224        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1225        let evals = RowMajorMatrix::new(vec![F::from_u32(3), F::from_u32(6), F::from_u32(11)], 1);
1226
1227        // Evaluate at a non-trivial extension point and compare against direct Horner.
1228        let point = EF4::GENERATOR;
1229        let result = evals.interpolate_arbitrary_point(&xs, point).unwrap();
1230
1231        let expected = point * point + point * F::TWO + EF4::from(F::from_u32(3));
1232        assert_eq!(result, vec![expected]);
1233    }
1234
1235    #[test]
1236    fn test_interpolate_arbitrary_extension_point_on_domain() {
1237        // EF4 target lies in the base field domain. Must return the matching row directly.
1238        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1239        let evals = RowMajorMatrix::new(vec![F::from_u32(3), F::from_u32(6), F::from_u32(11)], 1);
1240
1241        let point = EF4::from(F::from_u32(1));
1242        let result = evals.interpolate_arbitrary_point(&xs, point).unwrap();
1243        assert_eq!(result, vec![EF4::from(F::from_u32(6))]);
1244    }
1245
1246    #[test]
1247    fn test_recover_coefficients_known_quadratic() {
1248        // f(x) = x^2 + 2x + 3 → coefficients [3, 2, 1].
1249        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1250        let evals = RowMajorMatrix::new(vec![F::from_u32(3), F::from_u32(6), F::from_u32(11)], 1);
1251        let coeffs = evals.recover_coefficients(&xs).unwrap();
1252        assert_eq!(
1253            coeffs.values,
1254            vec![F::from_u32(3), F::from_u32(2), F::from_u32(1)]
1255        );
1256    }
1257
1258    #[test]
1259    fn test_recover_coefficients_multi_column() {
1260        // f1(x) = x^2 + 2x + 3,  f2(x) = 4x^2 + 5x + 6.
1261        let xs = [F::from_u32(0), F::from_u32(1), F::from_u32(2)];
1262        let evals = RowMajorMatrix::new(
1263            vec![
1264                F::from_u32(3),
1265                F::from_u32(6), // x=0
1266                F::from_u32(6),
1267                F::from_u32(15), // x=1
1268                F::from_u32(11),
1269                F::from_u32(32), // x=2
1270            ],
1271            2,
1272        );
1273        let coeffs = evals.recover_coefficients(&xs).unwrap();
1274        // Row 0 (constant): [3, 6], Row 1 (linear): [2, 5], Row 2 (quadratic): [1, 4]
1275        assert_eq!(
1276            coeffs.values,
1277            vec![
1278                F::from_u32(3),
1279                F::from_u32(6),
1280                F::from_u32(2),
1281                F::from_u32(5),
1282                F::from_u32(1),
1283                F::from_u32(4),
1284            ]
1285        );
1286    }
1287
1288    #[test]
1289    fn test_interpolate_arbitrary_empty_matrix() {
1290        // height=0 -> col_scale is empty -> denominator folds to EF::ZERO
1291        // must NOT panic, must return a zero vector of length == width
1292        let xs: Vec<F> = vec![];
1293        let evals = RowMajorMatrix::<F>::new(vec![], 3);
1294        let result = evals.interpolate_arbitrary_point(&xs, F::from_u32(42));
1295        assert_eq!(result, Some(vec![F::ZERO, F::ZERO, F::ZERO]));
1296    }
1297
1298    #[test]
1299    fn test_interpolate_arbitrary_with_precomputation_empty_direct() {
1300        // Precomputed hot path is on the public trait surface; empty domain must not panic.
1301        let weights: Vec<F> = vec![];
1302        let diff_invs: Vec<F> = vec![];
1303        let evals = RowMajorMatrix::<F>::new(vec![], 5);
1304        let result = evals.interpolate_arbitrary_with_precomputation(&weights, &diff_invs);
1305        assert_eq!(result, vec![F::ZERO; 5]);
1306    }
1307
1308    #[test]
1309    fn test_lagrange_empty() {
1310        assert_eq!(interpolate_lagrange::<F>(&[]), Some(vec![]));
1311    }
1312
1313    #[test]
1314    fn test_lagrange_single_point() {
1315        let points = [(F::from_u32(7), F::from_u32(42))];
1316        assert_eq!(interpolate_lagrange(&points), Some(vec![F::from_u32(42)]));
1317    }
1318
1319    #[test]
1320    fn test_lagrange_known_quadratic() {
1321        let points = [
1322            (F::from_u32(0), F::from_u32(3)),
1323            (F::from_u32(1), F::from_u32(6)),
1324            (F::from_u32(2), F::from_u32(11)),
1325        ];
1326        let coeffs = interpolate_lagrange(&points).unwrap();
1327        assert_eq!(coeffs, vec![F::from_u32(3), F::from_u32(2), F::from_u32(1)]);
1328    }
1329
1330    #[test]
1331    fn test_lagrange_duplicate_x_returns_none() {
1332        let points = [
1333            (F::from_u32(1), F::from_u32(5)),
1334            (F::from_u32(1), F::from_u32(7)),
1335        ];
1336        assert_eq!(interpolate_lagrange(&points), None);
1337    }
1338
1339    proptest! {
1340        #[test]
1341        fn prop_lagrange_roundtrip(
1342            n in 1usize..=8,
1343            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1344        ) {
1345            let mut coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1346            coeffs.resize(n, F::ZERO);
1347
1348            let points: Vec<(F, F)> = (0..n)
1349                .map(|i| {
1350                    let x = F::from_u32(i as u32);
1351                    let y = eval_poly(&coeffs, x);
1352                    (x, y)
1353                })
1354                .collect();
1355
1356            let recovered = interpolate_lagrange(&points).unwrap();
1357            prop_assert_eq!(recovered, coeffs);
1358        }
1359
1360        #[test]
1361        fn prop_arbitrary_roundtrip(
1362            n in 1usize..=8,
1363            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1364            point_raw in 1u32..2013265921u32,
1365        ) {
1366            // Evaluate polynomial at n distinct domain points.
1367            //
1368            // Then use the trait method to evaluate at a separate target point.
1369            let mut coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1370            coeffs.resize(n, F::ZERO);
1371
1372            let xs: Vec<F> = (0..n).map(|i| F::from_u32(i as u32)).collect();
1373            let ys: Vec<F> = xs.iter().map(|&x| eval_poly(&coeffs, x)).collect();
1374            let evals = RowMajorMatrix::new(ys, 1);
1375
1376            let point = F::from_u32(point_raw);
1377            let result = evals.interpolate_arbitrary_point(&xs, point).unwrap();
1378            let expected = eval_poly(&coeffs, point);
1379            prop_assert_eq!(result[0], expected);
1380        }
1381
1382        #[test]
1383        fn prop_recover_coefficients_roundtrip(
1384            n in 1usize..=8,
1385            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1386        ) {
1387            let mut coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1388            coeffs.resize(n, F::ZERO);
1389
1390            let xs: Vec<F> = (0..n).map(|i| F::from_u32(i as u32)).collect();
1391            let ys: Vec<F> = xs.iter().map(|&x| eval_poly(&coeffs, x)).collect();
1392            let evals = RowMajorMatrix::new(ys, 1);
1393
1394            let recovered = evals.recover_coefficients(&xs).unwrap();
1395            prop_assert_eq!(recovered.values, coeffs);
1396        }
1397
1398        #[test]
1399        fn prop_arbitrary_batch_equals_individual(
1400            n in 1usize..=6,
1401            f_raw in prop::collection::vec(0u32..2013265921, 1..=6),
1402            g_raw in prop::collection::vec(0u32..2013265921, 1..=6),
1403            point_raw in 1u32..2013265921u32,
1404        ) {
1405            // A 2-column batch must agree with two 1-column evaluations.
1406            let mut f_coeffs: Vec<F> = f_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1407            let mut g_coeffs: Vec<F> = g_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1408            f_coeffs.resize(n, F::ZERO);
1409            g_coeffs.resize(n, F::ZERO);
1410
1411            let xs: Vec<F> = (0..n).map(|i| F::from_u32(i as u32)).collect();
1412            let f_ys: Vec<F> = xs.iter().map(|&x| eval_poly(&f_coeffs, x)).collect();
1413            let g_ys: Vec<F> = xs.iter().map(|&x| eval_poly(&g_coeffs, x)).collect();
1414
1415            // Build 2-column batch matrix.
1416            let batch_vals: Vec<F> = f_ys.iter().zip(&g_ys)
1417                .flat_map(|(&f, &g)| vec![f, g])
1418                .collect();
1419            let batch_mat = RowMajorMatrix::new(batch_vals, 2);
1420            let f_mat = RowMajorMatrix::new(f_ys, 1);
1421            let g_mat = RowMajorMatrix::new(g_ys, 1);
1422
1423            let point = F::from_u32(point_raw);
1424            let batch_result = batch_mat.interpolate_arbitrary_point(&xs, point).unwrap();
1425            let f_result = f_mat.interpolate_arbitrary_point(&xs, point).unwrap()[0];
1426            let g_result = g_mat.interpolate_arbitrary_point(&xs, point).unwrap()[0];
1427
1428            prop_assert_eq!(batch_result[0], f_result);
1429            prop_assert_eq!(batch_result[1], g_result);
1430        }
1431
1432        #[test]
1433        fn prop_precomputation_equivalence_arbitrary(
1434            n in 1usize..=8,
1435            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1436            point_raw in 1u32..2013265921u32,
1437        ) {
1438            // Standard path and precomputation path must agree.
1439            let mut coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1440            coeffs.resize(n, F::ZERO);
1441
1442            let xs: Vec<F> = (0..n).map(|i| F::from_u32(i as u32)).collect();
1443            let ys: Vec<F> = xs.iter().map(|&x| eval_poly(&coeffs, x)).collect();
1444            let evals = RowMajorMatrix::new(ys, 1);
1445
1446            let point = F::from_u32(point_raw);
1447            let standard = evals.interpolate_arbitrary_point(&xs, point).unwrap();
1448
1449            let weights = barycentric_weights(&xs).unwrap();
1450            let diffs: Vec<F> = xs.iter().map(|&x| point - x).collect();
1451
1452            // Skip if point coincides with a domain point (diff_invs would panic).
1453            if diffs.iter().any(|d| d.is_zero()) {
1454                return Ok(());
1455            }
1456
1457            let diff_invs = batch_multiplicative_inverse(&diffs);
1458            let precomp = evals.interpolate_arbitrary_with_precomputation(&weights, &diff_invs);
1459            prop_assert_eq!(standard, precomp);
1460        }
1461
1462        #[test]
1463        fn prop_arbitrary_roundtrip_extension_point(
1464            n in 1usize..=8,
1465            coeffs_raw in prop::collection::vec(0u32..2013265921, 1..=8),
1466            point_raw in prop::collection::vec(0u32..2013265921, 4..=4),
1467        ) {
1468            // Round-trip with the target point taken from EF4: evaluate over a base-field
1469            // domain, interpolate at an extension-field point, and compare against direct
1470            // Horner evaluation in the extension.
1471            let mut coeffs: Vec<F> = coeffs_raw.iter().take(n).map(|&v| F::from_u32(v)).collect();
1472            coeffs.resize(n, F::ZERO);
1473
1474            let xs: Vec<F> = (0..n).map(|i| F::from_u32(i as u32)).collect();
1475            let ys: Vec<F> = xs.iter().map(|&x| eval_poly(&coeffs, x)).collect();
1476            let evals = RowMajorMatrix::new(ys, 1);
1477
1478            let point = EF4::from_basis_coefficients_iter(
1479                point_raw.iter().map(|&v| F::from_u32(v)),
1480            ).unwrap();
1481            let result = evals.interpolate_arbitrary_point(&xs, point).unwrap();
1482            let expected: EF4 = eval_poly(&coeffs, point);
1483            prop_assert_eq!(result[0], expected);
1484        }
1485    }
1486}