Skip to main content

binius_math/
univariate.rs

1// Copyright 2024-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use binius_field::{BinaryField, Field, field::FieldOps};
5use itertools::izip;
6
7use super::{BinarySubspace, FieldBuffer};
8
9/// Evaluate a univariate polynomial specified by its monomial coefficients.
10///
11/// # Arguments
12/// * `coeffs` - Slice of coefficients ordered from low-degree terms to high-degree terms
13/// * `x` - Point at which to evaluate the polynomial
14pub fn evaluate_univariate<F: FieldOps>(coeffs: &[F], x: &F) -> F {
15	let Some((highest_degree, rest)) = coeffs.split_last() else {
16		return F::zero();
17	};
18
19	// Evaluate using Horner's method
20	rest.iter()
21		.rev()
22		.fold(highest_degree.clone(), |acc, coeff| acc * x + coeff)
23}
24
25/// Optimized Lagrange evaluation for power-of-2 domains in binary fields.
26///
27/// Computes the Lagrange polynomial evaluations L̃(z, i) for a power-of-2 domain at point `z`.
28/// Uses the provided binary subspace as the evaluation domain.
29///
30/// # Key Optimization
31/// For power-of-2 domains, all barycentric weights are identical due to the additive group
32/// structure. For each i ∈ {0, ..., 2^k - 1}, the set {i ⊕ j | j ≠ i} = {1, ..., 2^k - 1}.
33/// This allows us to:
34/// 1. Compute a single barycentric weight w = 1 / ∏_{j=1}^{n-1} j
35/// 2. Use prefix/suffix products to avoid redundant computation
36/// 3. Replace inversions with multiplications for better performance
37///
38/// # Complexity
39/// - Time: O(n) where n = subspace size, using 4n - 2 multiplications and 1 inversion
40/// - Space: O(n) for prefix/suffix arrays
41///
42/// # Parameters
43/// - `subspace`: The binary subspace defining the evaluation domain
44/// - `z`: The evaluation point
45///
46/// # Returns
47/// A vector of Lagrange polynomial evaluations, one for each domain element
48pub fn lagrange_evals<F: BinaryField>(subspace: &BinarySubspace<F>, z: F) -> FieldBuffer<F> {
49	let result = lagrange_evals_scalars(subspace, &z);
50	FieldBuffer::new(subspace.dim(), result)
51}
52
53/// Scalar variant of [`lagrange_evals`] that returns a `Vec<E>` instead of a `FieldBuffer`.
54///
55/// Computes Lagrange polynomial evaluations for a binary subspace domain, converting domain
56/// points from `F` to `E` and performing all arithmetic in `E`.
57///
58/// # Parameters
59/// - `subspace`: The binary subspace defining the evaluation domain (over `F`)
60/// - `z`: The evaluation point (in `E`)
61///
62/// # Returns
63/// A vector of Lagrange polynomial evaluations, one for each domain element
64pub fn lagrange_evals_scalars<F: BinaryField, E: FieldOps + From<F>>(
65	subspace: &BinarySubspace<F>,
66	z: &E,
67) -> Vec<E> {
68	let domain: Vec<E> = subspace.iter().map(E::from).collect();
69	let n = domain.len();
70
71	// Compute single barycentric weight for the additive subgroup
72	let w = domain[1..]
73		.iter()
74		.fold(E::one(), |acc, d| acc * d)
75		.invert_or_zero();
76
77	// Compute prefix products: prefix[i] = ∏_{j=0}^{i-1} (z - domain[j])
78	let mut prefixes = vec![E::one(); n];
79	for i in 1..n {
80		prefixes[i] = prefixes[i - 1].clone() * (z.clone() - domain[i - 1].clone());
81	}
82
83	// Compute suffix products: suffix[i] = ∏_{j=i+1}^{n-1} (z - domain[j])
84	let mut suffixes = vec![E::one(); n];
85	for i in (0..n - 1).rev() {
86		suffixes[i] = suffixes[i + 1].clone() * (z.clone() - domain[i + 1].clone());
87	}
88
89	// Combine prefix, suffix, and weight: L_i(z) = prefix[i] * suffix[i] * w
90	izip!(prefixes, suffixes)
91		.map(|(p, s)| p * s * w.clone())
92		.collect()
93}
94
95/// Extrapolate a polynomial from its evaluations over a binary subspace to a point.
96///
97/// Given evaluations of a polynomial on all points of a binary subspace, computes the polynomial's
98/// value at an arbitrary point `z` using Lagrange interpolation. This is equivalent to computing
99/// the inner product of `values` with the Lagrange basis evaluations at `z`, but avoids
100/// materializing the full vector of Lagrange evaluations.
101///
102/// # Algorithm
103///
104/// Exploits the additive group structure of binary subspaces: all barycentric weights are
105/// identical, so a single weight `w = (∏_{j=1}^{n-1} domain[j])^{-1}` is computed once. The
106/// interpolated value is then `w * Σ_i values[i] * ∏_{j≠i} (z - domain[j])`, evaluated via a
107/// single linear pass using a prefix-product accumulator (same technique as
108/// [`EvaluationDomain::extrapolate`]).
109///
110/// # Complexity
111/// - Time: O(n) where n = subspace size
112/// - Space: O(1) beyond the input
113pub fn extrapolate_over_subspace<F: BinaryField, E: FieldOps + From<F>>(
114	subspace: &BinarySubspace<F>,
115	values: &[E],
116	z: &E,
117) -> E {
118	let n = 1 << subspace.dim();
119	assert_eq!(values.len(), n);
120
121	// Compute single barycentric weight for the additive subgroup.
122	let w = subspace
123		.iter()
124		.skip(1)
125		.map(E::from)
126		.fold(E::one(), |acc, d| acc * d)
127		.invert_or_zero();
128
129	// Accumulate Σ_i values[i] * ∏_{j≠i} (z - domain[j]) using a prefix-product fold.
130	let (acc, _) = izip!(values, subspace.iter()).fold(
131		(E::zero(), E::one()),
132		|(acc, prod), (value, point)| {
133			let term = z.clone() - E::from(point);
134			let next_acc = acc * &term + prod.clone() * value;
135			(next_acc, prod * term)
136		},
137	);
138
139	acc * w
140}
141
142/// A domain that univariate polynomials may be evaluated on.
143///
144/// An evaluation domain of size d + 1 together with polynomial values on that domain uniquely
145/// defines a degree <= d polynomial.
146#[derive(Debug, Clone)]
147pub struct EvaluationDomain<F: Field> {
148	points: Vec<F>,
149	weights: Vec<F>,
150}
151
152impl<F: Field> EvaluationDomain<F> {
153	/// Create a new evaluation domain from a set of points.
154	///
155	/// # Arguments
156	/// * `points` - The points that define the domain
157	///
158	/// # Panics
159	/// * If any points are repeated (not distinct)
160	pub fn from_points(points: Vec<F>) -> Self {
161		let weights = compute_barycentric_weights(&points);
162		Self { points, weights }
163	}
164
165	pub const fn size(&self) -> usize {
166		self.points.len()
167	}
168
169	pub const fn points(&self) -> &[F] {
170		self.points.as_slice()
171	}
172
173	/// Compute a vector of Lagrange polynomial evaluations in $O(N)$ at a given point `x`.
174	///
175	/// For an evaluation domain consisting of points $x_i$ Lagrange polynomials $L_i(x)$
176	/// are defined by
177	///
178	/// $$L_i(x) = \prod_{j \neq i}\frac{x - \pi_j}{\pi_i - \pi_j}$$
179	pub fn lagrange_evals(&self, x: F) -> Vec<F> {
180		let n = self.size();
181
182		let mut result = vec![F::ONE; n];
183
184		// Multiply the product suffixes
185		for i in (1..n).rev() {
186			result[i - 1] = result[i] * (x - self.points[i]);
187		}
188
189		let mut prefix = F::ONE;
190
191		// Multiply the product prefixes and weights
192		for (result_i, &point, &weight) in izip!(&mut result, &self.points, &self.weights) {
193			*result_i *= prefix * weight;
194			prefix *= x - point;
195		}
196
197		result
198	}
199
200	/// Evaluate the unique interpolated polynomial at any point `x`.
201	///
202	/// Computational complexity is $O(n)$, for a domain of size $n$.
203	pub fn extrapolate(&self, values: &[F], x: F) -> F {
204		assert_eq!(values.len(), self.size()); // precondition
205
206		let (ret, _) = izip!(values, &self.points, &self.weights).fold(
207			(F::ZERO, F::ONE),
208			|(acc, prod), (&value, &point, &weight)| {
209				let term = x - point;
210				let next_acc = acc * term + prod * value * weight;
211				(next_acc, prod * term)
212			},
213		);
214
215		ret
216	}
217}
218
219/// Compute the Barycentric weights for a sequence of unique points.
220///
221/// The [Barycentric] weight $w_i$ for point $x_i$ is calculated as:
222/// $$w_i = \prod_{j \neq i} \frac{1}{x_i - x_j}$$
223///
224/// These weights are used in the Lagrange interpolation formula:
225/// $$L(x) = \sum_{i=0}^{n-1} f(x_i) \cdot \frac{w_i}{x - x_i} \cdot \prod_{j=0}^{n-1} (x - x_j)$$
226///
227/// # Preconditions
228/// * All points in the input slice must be distinct, otherwise this function panics.
229///
230/// [Barycentric]: <https://en.wikipedia.org/wiki/Lagrange_polynomial#Barycentric_form>
231fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
232	let n = points.len();
233	(0..n)
234		.map(|i| {
235			// TODO: We could use batch inversion here, but it's not a bottleneck
236			let product = (0..n)
237				.filter(|&j| j != i)
238				.map(|j| points[i] - points[j])
239				.product::<F>();
240			// Safety: precondition — all points are distinct, so every difference (and thus the
241			// product) is non-zero.
242			unsafe { product.invert() }
243		})
244		.collect()
245}
246
247#[cfg(test)]
248mod tests {
249	use binius_field::{BinaryField128bGhash, Field, Random, util::powers};
250	use rand::prelude::*;
251
252	use super::*;
253	use crate::{
254		BinarySubspace,
255		inner_product::inner_product,
256		line::extrapolate_line_packed,
257		test_utils::{B128, random_scalars},
258	};
259
260	fn evaluate_univariate_with_powers<F: Field>(coeffs: &[F], x: F) -> F {
261		inner_product(coeffs.iter().copied(), powers(x).take(coeffs.len()))
262	}
263
264	type F = BinaryField128bGhash;
265
266	#[test]
267	fn test_evaluate_univariate_against_reference() {
268		let mut rng = StdRng::seed_from_u64(0);
269
270		for n_coeffs in [0, 1, 2, 5, 10] {
271			let coeffs = random_scalars(&mut rng, n_coeffs);
272			let x = F::random(&mut rng);
273			assert_eq!(
274				evaluate_univariate(&coeffs, &x),
275				evaluate_univariate_with_powers(&coeffs, x)
276			);
277		}
278	}
279
280	#[test]
281	fn test_lagrange_evals() {
282		let mut rng = StdRng::seed_from_u64(0);
283
284		// Test mathematical properties across different domain sizes
285		for log_domain_size in [3, 4, 5, 6] {
286			// Create subspace for this test
287			let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
288			let domain: Vec<F> = subspace.iter().collect();
289
290			// Test 1: Partition of Unity - Lagrange polynomials sum to 1
291			let eval_point = F::random(&mut rng);
292			let lagrange_coeffs = lagrange_evals(&subspace, eval_point);
293			let sum: F = lagrange_coeffs.as_ref().iter().copied().sum();
294			assert_eq!(
295				sum,
296				F::ONE,
297				"Partition of unity failed for domain size {}",
298				1 << log_domain_size
299			);
300
301			// Test 2: Interpolation Property - L_i(x_j) = δ_ij
302			for (j, &domain_point) in domain.iter().enumerate() {
303				let lagrange_at_domain = lagrange_evals(&subspace, domain_point);
304				for (i, &coeff) in lagrange_at_domain.as_ref().iter().enumerate() {
305					let expected = if i == j { F::ONE } else { F::ZERO };
306					assert_eq!(
307						coeff, expected,
308						"Interpolation property failed: L_{i}({j}) ≠ {expected}"
309					);
310				}
311			}
312		}
313
314		// Test 3: Polynomial Interpolation Accuracy
315		let log_domain_size = 6;
316		let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
317		let domain: Vec<F> = subspace.iter().collect();
318		let coeffs = random_scalars(&mut rng, 10);
319
320		// Evaluate polynomial at domain points
321		let domain_evals: Vec<F> = domain
322			.iter()
323			.map(|&point| evaluate_univariate(&coeffs, &point))
324			.collect();
325
326		// Test interpolation at random point
327		let test_point = F::random(&mut rng);
328		let lagrange_coeffs = lagrange_evals(&subspace, test_point);
329		let interpolated =
330			inner_product(domain_evals.iter().copied(), lagrange_coeffs.iter_scalars());
331		let direct = evaluate_univariate(&coeffs, &test_point);
332
333		assert_eq!(interpolated, direct, "Polynomial interpolation accuracy failed");
334	}
335
336	#[test]
337	fn test_random_extrapolate() {
338		let mut rng = StdRng::seed_from_u64(0);
339		let degree = 6;
340
341		let domain = EvaluationDomain::from_points(random_scalars(&mut rng, degree + 1));
342
343		let coeffs = random_scalars(&mut rng, degree + 1);
344
345		let values = domain
346			.points()
347			.iter()
348			.map(|&x| evaluate_univariate(&coeffs, &x))
349			.collect::<Vec<_>>();
350
351		let x = B128::random(&mut rng);
352		let expected_y = evaluate_univariate(&coeffs, &x);
353		assert_eq!(domain.extrapolate(&values, x), expected_y);
354	}
355
356	#[test]
357	fn test_extrapolate_line() {
358		let mut rng = StdRng::seed_from_u64(0);
359		for _ in 0..10 {
360			let x0 = B128::random(&mut rng);
361			let x1 = B128::random(&mut rng);
362			// Use a smaller field element for z to test the subfield scalar multiplication
363			let z = B128::from(rng.next_u64() as u128);
364			assert_eq!(extrapolate_line_packed(x0, x1, z), x0 + (x1 - x0) * z);
365		}
366	}
367
368	#[test]
369	fn test_extrapolate_over_subspace_against_evaluate_univariate() {
370		let mut rng = StdRng::seed_from_u64(0);
371
372		for log_domain_size in 0..=6 {
373			let n = 1 << log_domain_size;
374			let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
375
376			// Random polynomial of degree < n
377			let coeffs: Vec<F> = random_scalars(&mut rng, n);
378
379			// Evaluate at all domain points
380			let values: Vec<F> = subspace
381				.iter()
382				.map(|point| evaluate_univariate(&coeffs, &point))
383				.collect();
384
385			// Extrapolate at a random point
386			let z = F::random(&mut rng);
387			let extrapolated = extrapolate_over_subspace(&subspace, &values, &z);
388			let expected = evaluate_univariate(&coeffs, &z);
389
390			assert_eq!(extrapolated, expected, "Mismatch for log_domain_size={log_domain_size}");
391		}
392	}
393
394	#[test]
395	fn test_extrapolate_over_subspace_against_lagrange_evals() {
396		let mut rng = StdRng::seed_from_u64(0);
397
398		for log_domain_size in 0..=6 {
399			let n = 1 << log_domain_size;
400			let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
401
402			// Random values (not necessarily from a polynomial)
403			let values: Vec<F> = random_scalars(&mut rng, n);
404
405			let z = F::random(&mut rng);
406			let extrapolated = extrapolate_over_subspace(&subspace, &values, &z);
407			let lagrange = lagrange_evals_scalars(&subspace, &z);
408			let expected = inner_product(values.iter().copied(), lagrange);
409
410			assert_eq!(extrapolated, expected, "Mismatch for log_domain_size={log_domain_size}");
411		}
412	}
413
414	#[test]
415	fn test_evaluation_domain_lagrange_evals() {
416		let mut rng = StdRng::seed_from_u64(0);
417
418		// Create a small domain
419		let domain_points: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
420		let evaluation_domain = EvaluationDomain::from_points(domain_points);
421
422		// Create random values for interpolation
423		let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
424
425		// Test point
426		let z = B128::random(&mut rng);
427
428		// Compute extrapolation
429		let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
430
431		// Compute using Lagrange coefficients
432		let lagrange_coeffs = evaluation_domain.lagrange_evals(z);
433		let lagrange_eval = inner_product(lagrange_coeffs, values);
434
435		assert_eq!(lagrange_eval, extrapolated);
436	}
437}