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