binius_math/
univariate.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use binius_field::{BinaryField, Field};
4use itertools::izip;
5
6use super::{BinarySubspace, FieldBuffer};
7
8/// Evaluate a univariate polynomial specified by its monomial coefficients.
9///
10/// # Arguments
11/// * `coeffs` - Slice of coefficients ordered from low-degree terms to high-degree terms
12/// * `x` - Point at which to evaluate the polynomial
13pub fn evaluate_univariate<F: Field>(coeffs: &[F], x: F) -> F {
14	let Some((&highest_degree, rest)) = coeffs.split_last() else {
15		return F::ZERO;
16	};
17
18	// Evaluate using Horner's method
19	rest.iter()
20		.rev()
21		.fold(highest_degree, |acc, &coeff| acc * x + coeff)
22}
23
24/// Optimized Lagrange evaluation for power-of-2 domains in binary fields.
25///
26/// Computes the Lagrange polynomial evaluations L̃(z, i) for a power-of-2 domain at point `z`.
27/// Uses the provided binary subspace as the evaluation domain.
28///
29/// # Key Optimization
30/// For power-of-2 domains, all barycentric weights are identical due to the additive group
31/// structure. For each i ∈ {0, ..., 2^k - 1}, the set {i ⊕ j | j ≠ i} = {1, ..., 2^k - 1}.
32/// This allows us to:
33/// 1. Compute a single barycentric weight w = 1 / ∏_{j=1}^{n-1} j
34/// 2. Use prefix/suffix products to avoid redundant computation
35/// 3. Replace inversions with multiplications for better performance
36///
37/// # Complexity
38/// - Time: O(n) where n = subspace size, using 4n - 2 multiplications and 1 inversion
39/// - Space: O(n) for prefix/suffix arrays
40///
41/// # Parameters
42/// - `subspace`: The binary subspace defining the evaluation domain
43/// - `z`: The evaluation point
44///
45/// # Returns
46/// A vector of Lagrange polynomial evaluations, one for each domain element
47pub fn lagrange_evals<F: BinaryField>(subspace: &BinarySubspace<F>, z: F) -> FieldBuffer<F> {
48	let domain: Vec<F> = subspace.iter().collect();
49	let n = domain.len();
50
51	// Compute single barycentric weight for the additive subgroup
52	// All points have the same weight due to subgroup structure
53	let w = domain[1..]
54		.iter()
55		.fold(F::ONE, |acc, &d| acc * d)
56		.invert()
57		.unwrap_or(F::ONE);
58
59	// Compute prefix products: prefix[i] = ∏_{j=0}^{i-1} (z - domain[j])
60	let mut prefixes = vec![F::ONE; n];
61	for i in 1..n {
62		prefixes[i] = prefixes[i - 1] * (z - domain[i - 1]);
63	}
64
65	// Compute suffix products: suffix[i] = ∏_{j=i+1}^{n-1} (z - domain[j])
66	let mut suffixes = vec![F::ONE; n];
67	for i in (0..n - 1).rev() {
68		suffixes[i] = suffixes[i + 1] * (z - domain[i + 1]);
69	}
70
71	// Combine prefix, suffix, and weight: L_i(z) = prefix[i] * suffix[i] * w
72	let mut result = vec![F::ZERO; n];
73	for i in 0..n {
74		result[i] = prefixes[i] * suffixes[i] * w;
75	}
76
77	FieldBuffer::new(subspace.dim(), result.into_boxed_slice())
78}
79
80/// A domain that univariate polynomials may be evaluated on.
81///
82/// An evaluation domain of size d + 1 together with polynomial values on that domain uniquely
83/// defines a degree <= d polynomial.
84#[derive(Debug, Clone)]
85pub struct EvaluationDomain<F: Field> {
86	points: Vec<F>,
87	weights: Vec<F>,
88}
89
90impl<F: Field> EvaluationDomain<F> {
91	/// Create a new evaluation domain from a set of points.
92	///
93	/// # Arguments
94	/// * `points` - The points that define the domain
95	///
96	/// # Panics
97	/// * If any points are repeated (not distinct)
98	pub fn from_points(points: Vec<F>) -> Self {
99		let weights = compute_barycentric_weights(&points);
100		Self { points, weights }
101	}
102
103	pub fn size(&self) -> usize {
104		self.points.len()
105	}
106
107	pub fn points(&self) -> &[F] {
108		self.points.as_slice()
109	}
110
111	/// Compute a vector of Lagrange polynomial evaluations in $O(N)$ at a given point `x`.
112	///
113	/// For an evaluation domain consisting of points $x_i$ Lagrange polynomials $L_i(x)$
114	/// are defined by
115	///
116	/// $$L_i(x) = \prod_{j \neq i}\frac{x - \pi_j}{\pi_i - \pi_j}$$
117	pub fn lagrange_evals(&self, x: F) -> Vec<F> {
118		let n = self.size();
119
120		let mut result = vec![F::ONE; n];
121
122		// Multiply the product suffixes
123		for i in (1..n).rev() {
124			result[i - 1] = result[i] * (x - self.points[i]);
125		}
126
127		let mut prefix = F::ONE;
128
129		// Multiply the product prefixes and weights
130		for (result_i, &point, &weight) in izip!(&mut result, &self.points, &self.weights) {
131			*result_i *= prefix * weight;
132			prefix *= x - point;
133		}
134
135		result
136	}
137
138	/// Evaluate the unique interpolated polynomial at any point `x`.
139	///
140	/// Computational complexity is $O(n)$, for a domain of size $n$.
141	pub fn extrapolate(&self, values: &[F], x: F) -> F {
142		assert_eq!(values.len(), self.size()); // precondition
143
144		let (ret, _) = izip!(values, &self.points, &self.weights).fold(
145			(F::ZERO, F::ONE),
146			|(acc, prod), (&value, &point, &weight)| {
147				let term = x - point;
148				let next_acc = acc * term + prod * value * weight;
149				(next_acc, prod * term)
150			},
151		);
152
153		ret
154	}
155}
156
157/// Compute the Barycentric weights for a sequence of unique points.
158///
159/// The [Barycentric] weight $w_i$ for point $x_i$ is calculated as:
160/// $$w_i = \prod_{j \neq i} \frac{1}{x_i - x_j}$$
161///
162/// These weights are used in the Lagrange interpolation formula:
163/// $$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)$$
164///
165/// # Preconditions
166/// * All points in the input slice must be distinct, otherwise this function panics.
167///
168/// [Barycentric]: <https://en.wikipedia.org/wiki/Lagrange_polynomial#Barycentric_form>
169fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
170	let n = points.len();
171	(0..n)
172		.map(|i| {
173			// TODO: We could use batch inversion here, but it's not a bottleneck
174			let product = (0..n)
175				.filter(|&j| j != i)
176				.map(|j| points[i] - points[j])
177				.product::<F>();
178			product
179				.invert()
180				.expect("precondition: all points are distinct; invert only fails on 0")
181		})
182		.collect()
183}
184
185#[cfg(test)]
186mod tests {
187	use binius_field::{BinaryField128bGhash, Field, Random, util::powers};
188	use rand::prelude::*;
189
190	use super::*;
191	use crate::{
192		BinarySubspace,
193		inner_product::inner_product,
194		line::extrapolate_line_packed,
195		test_utils::{B128, random_scalars},
196	};
197
198	fn evaluate_univariate_with_powers<F: Field>(coeffs: &[F], x: F) -> F {
199		inner_product(coeffs.iter().copied(), powers(x).take(coeffs.len()))
200	}
201
202	type F = BinaryField128bGhash;
203
204	#[test]
205	fn test_evaluate_univariate_against_reference() {
206		let mut rng = StdRng::seed_from_u64(0);
207
208		for n_coeffs in [0, 1, 2, 5, 10] {
209			let coeffs = random_scalars(&mut rng, n_coeffs);
210			let x = F::random(&mut rng);
211			assert_eq!(
212				evaluate_univariate(&coeffs, x),
213				evaluate_univariate_with_powers(&coeffs, x)
214			);
215		}
216	}
217
218	#[test]
219	fn test_lagrange_evals() {
220		let mut rng = StdRng::seed_from_u64(0);
221
222		// Test mathematical properties across different domain sizes
223		for log_domain_size in [3, 4, 5, 6] {
224			// Create subspace for this test
225			let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
226			let domain: Vec<F> = subspace.iter().collect();
227
228			// Test 1: Partition of Unity - Lagrange polynomials sum to 1
229			let eval_point = F::random(&mut rng);
230			let lagrange_coeffs = lagrange_evals(&subspace, eval_point);
231			let sum: F = lagrange_coeffs.as_ref().iter().copied().sum();
232			assert_eq!(
233				sum,
234				F::ONE,
235				"Partition of unity failed for domain size {}",
236				1 << log_domain_size
237			);
238
239			// Test 2: Interpolation Property - L_i(x_j) = δ_ij
240			for (j, &domain_point) in domain.iter().enumerate() {
241				let lagrange_at_domain = lagrange_evals(&subspace, domain_point);
242				for (i, &coeff) in lagrange_at_domain.as_ref().iter().enumerate() {
243					let expected = if i == j { F::ONE } else { F::ZERO };
244					assert_eq!(
245						coeff, expected,
246						"Interpolation property failed: L_{i}({j}) ≠ {expected}"
247					);
248				}
249			}
250		}
251
252		// Test 3: Polynomial Interpolation Accuracy
253		let log_domain_size = 6;
254		let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
255		let domain: Vec<F> = subspace.iter().collect();
256		let coeffs = random_scalars(&mut rng, 10);
257
258		// Evaluate polynomial at domain points
259		let domain_evals: Vec<F> = domain
260			.iter()
261			.map(|&point| evaluate_univariate(&coeffs, point))
262			.collect();
263
264		// Test interpolation at random point
265		let test_point = F::random(&mut rng);
266		let lagrange_coeffs = lagrange_evals(&subspace, test_point);
267		let interpolated =
268			inner_product(domain_evals.iter().copied(), lagrange_coeffs.iter_scalars());
269		let direct = evaluate_univariate(&coeffs, test_point);
270
271		assert_eq!(interpolated, direct, "Polynomial interpolation accuracy failed");
272	}
273
274	#[test]
275	fn test_random_extrapolate() {
276		let mut rng = StdRng::seed_from_u64(0);
277		let degree = 6;
278
279		let domain = EvaluationDomain::from_points(random_scalars(&mut rng, degree + 1));
280
281		let coeffs = random_scalars(&mut rng, degree + 1);
282
283		let values = domain
284			.points()
285			.iter()
286			.map(|&x| evaluate_univariate(&coeffs, x))
287			.collect::<Vec<_>>();
288
289		let x = B128::random(&mut rng);
290		let expected_y = evaluate_univariate(&coeffs, x);
291		assert_eq!(domain.extrapolate(&values, x), expected_y);
292	}
293
294	#[test]
295	fn test_extrapolate_line() {
296		let mut rng = StdRng::seed_from_u64(0);
297		for _ in 0..10 {
298			let x0 = B128::random(&mut rng);
299			let x1 = B128::random(&mut rng);
300			// Use a smaller field element for z to test the subfield scalar multiplication
301			let z = B128::from(rng.next_u64() as u128);
302			assert_eq!(extrapolate_line_packed(x0, x1, z), x0 + (x1 - x0) * z);
303		}
304	}
305
306	#[test]
307	fn test_evaluation_domain_lagrange_evals() {
308		let mut rng = StdRng::seed_from_u64(0);
309
310		// Create a small domain
311		let domain_points: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
312		let evaluation_domain = EvaluationDomain::from_points(domain_points.clone());
313
314		// Create random values for interpolation
315		let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
316
317		// Test point
318		let z = B128::random(&mut rng);
319
320		// Compute extrapolation
321		let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
322
323		// Compute using Lagrange coefficients
324		let lagrange_coeffs = evaluation_domain.lagrange_evals(z);
325		let lagrange_eval = inner_product(lagrange_coeffs, values);
326
327		assert_eq!(lagrange_eval, extrapolated);
328	}
329}