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