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