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		.expect("result.len() == 2^subspace.dim()")
79}
80
81/// A domain that univariate polynomials may be evaluated on.
82///
83/// An evaluation domain of size d + 1 together with polynomial values on that domain uniquely
84/// defines a degree <= d polynomial.
85#[derive(Debug, Clone)]
86pub struct EvaluationDomain<F: Field> {
87	points: Vec<F>,
88	weights: Vec<F>,
89}
90
91impl<F: Field> EvaluationDomain<F> {
92	/// Create a new evaluation domain from a set of points.
93	///
94	/// # Arguments
95	/// * `points` - The points that define the domain
96	///
97	/// # Panics
98	/// * If any points are repeated (not distinct)
99	pub fn from_points(points: Vec<F>) -> Self {
100		let weights = compute_barycentric_weights(&points);
101		Self { points, weights }
102	}
103
104	pub fn size(&self) -> usize {
105		self.points.len()
106	}
107
108	pub fn points(&self) -> &[F] {
109		self.points.as_slice()
110	}
111
112	/// Compute a vector of Lagrange polynomial evaluations in $O(N)$ at a given point `x`.
113	///
114	/// For an evaluation domain consisting of points $x_i$ Lagrange polynomials $L_i(x)$
115	/// are defined by
116	///
117	/// $$L_i(x) = \prod_{j \neq i}\frac{x - \pi_j}{\pi_i - \pi_j}$$
118	pub fn lagrange_evals(&self, x: F) -> Vec<F> {
119		let n = self.size();
120
121		let mut result = vec![F::ONE; n];
122
123		// Multiply the product suffixes
124		for i in (1..n).rev() {
125			result[i - 1] = result[i] * (x - self.points[i]);
126		}
127
128		let mut prefix = F::ONE;
129
130		// Multiply the product prefixes and weights
131		for (result_i, &point, &weight) in izip!(&mut result, &self.points, &self.weights) {
132			*result_i *= prefix * weight;
133			prefix *= x - point;
134		}
135
136		result
137	}
138
139	/// Evaluate the unique interpolated polynomial at any point `x`.
140	///
141	/// Computational complexity is $O(n)$, for a domain of size $n$.
142	pub fn extrapolate(&self, values: &[F], x: F) -> F {
143		assert_eq!(values.len(), self.size()); // precondition
144
145		let (ret, _) = izip!(values, &self.points, &self.weights).fold(
146			(F::ZERO, F::ONE),
147			|(acc, prod), (&value, &point, &weight)| {
148				let term = x - point;
149				let next_acc = acc * term + prod * value * weight;
150				(next_acc, prod * term)
151			},
152		);
153
154		ret
155	}
156}
157
158/// Compute the Barycentric weights for a sequence of unique points.
159///
160/// The [Barycentric] weight $w_i$ for point $x_i$ is calculated as:
161/// $$w_i = \prod_{j \neq i} \frac{1}{x_i - x_j}$$
162///
163/// These weights are used in the Lagrange interpolation formula:
164/// $$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)$$
165///
166/// # Preconditions
167/// * All points in the input slice must be distinct, otherwise this function panics.
168///
169/// [Barycentric]: <https://en.wikipedia.org/wiki/Lagrange_polynomial#Barycentric_form>
170fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
171	let n = points.len();
172	(0..n)
173		.map(|i| {
174			// TODO: We could use batch inversion here, but it's not a bottleneck
175			let product = (0..n)
176				.filter(|&j| j != i)
177				.map(|j| points[i] - points[j])
178				.product::<F>();
179			product
180				.invert()
181				.expect("precondition: all points are distinct; invert only fails on 0")
182		})
183		.collect()
184}
185
186#[cfg(test)]
187mod tests {
188	use binius_field::{BinaryField128bGhash, Field, Random, util::powers};
189	use rand::prelude::*;
190
191	use super::*;
192	use crate::{
193		BinarySubspace,
194		inner_product::inner_product,
195		line::extrapolate_line_packed,
196		test_utils::{B128, random_scalars},
197	};
198
199	fn evaluate_univariate_with_powers<F: Field>(coeffs: &[F], x: F) -> F {
200		inner_product(coeffs.iter().copied(), powers(x).take(coeffs.len()))
201	}
202
203	type F = BinaryField128bGhash;
204
205	#[test]
206	fn test_evaluate_univariate_against_reference() {
207		let mut rng = StdRng::seed_from_u64(0);
208
209		for n_coeffs in [0, 1, 2, 5, 10] {
210			let coeffs = random_scalars(&mut rng, n_coeffs);
211			let x = F::random(&mut rng);
212			assert_eq!(
213				evaluate_univariate(&coeffs, x),
214				evaluate_univariate_with_powers(&coeffs, x)
215			);
216		}
217	}
218
219	#[test]
220	fn test_lagrange_evals() {
221		let mut rng = StdRng::seed_from_u64(0);
222
223		// Test mathematical properties across different domain sizes
224		for log_domain_size in [3, 4, 5, 6] {
225			// Create subspace for this test
226			let subspace = BinarySubspace::<F>::with_dim(log_domain_size).unwrap();
227			let domain: Vec<F> = subspace.iter().collect();
228
229			// Test 1: Partition of Unity - Lagrange polynomials sum to 1
230			let eval_point = F::random(&mut rng);
231			let lagrange_coeffs = lagrange_evals(&subspace, eval_point);
232			let sum: F = lagrange_coeffs.as_ref().iter().copied().sum();
233			assert_eq!(
234				sum,
235				F::ONE,
236				"Partition of unity failed for domain size {}",
237				1 << log_domain_size
238			);
239
240			// Test 2: Interpolation Property - L_i(x_j) = δ_ij
241			for (j, &domain_point) in domain.iter().enumerate() {
242				let lagrange_at_domain = lagrange_evals(&subspace, domain_point);
243				for (i, &coeff) in lagrange_at_domain.as_ref().iter().enumerate() {
244					let expected = if i == j { F::ONE } else { F::ZERO };
245					assert_eq!(
246						coeff, expected,
247						"Interpolation property failed: L_{i}({j}) ≠ {expected}"
248					);
249				}
250			}
251		}
252
253		// Test 3: Polynomial Interpolation Accuracy
254		let log_domain_size = 6;
255		let subspace = BinarySubspace::<F>::with_dim(log_domain_size).unwrap();
256		let domain: Vec<F> = subspace.iter().collect();
257		let coeffs = random_scalars(&mut rng, 10);
258
259		// Evaluate polynomial at domain points
260		let domain_evals: Vec<F> = domain
261			.iter()
262			.map(|&point| evaluate_univariate(&coeffs, point))
263			.collect();
264
265		// Test interpolation at random point
266		let test_point = F::random(&mut rng);
267		let lagrange_coeffs = lagrange_evals(&subspace, test_point);
268		let interpolated =
269			inner_product(domain_evals.iter().copied(), lagrange_coeffs.iter_scalars());
270		let direct = evaluate_univariate(&coeffs, test_point);
271
272		assert_eq!(interpolated, direct, "Polynomial interpolation accuracy failed");
273	}
274
275	#[test]
276	fn test_random_extrapolate() {
277		let mut rng = StdRng::seed_from_u64(0);
278		let degree = 6;
279
280		let domain = EvaluationDomain::from_points(random_scalars(&mut rng, degree + 1));
281
282		let coeffs = random_scalars(&mut rng, degree + 1);
283
284		let values = domain
285			.points()
286			.iter()
287			.map(|&x| evaluate_univariate(&coeffs, x))
288			.collect::<Vec<_>>();
289
290		let x = B128::random(&mut rng);
291		let expected_y = evaluate_univariate(&coeffs, x);
292		assert_eq!(domain.extrapolate(&values, x), expected_y);
293	}
294
295	#[test]
296	fn test_extrapolate_line() {
297		let mut rng = StdRng::seed_from_u64(0);
298		for _ in 0..10 {
299			let x0 = B128::random(&mut rng);
300			let x1 = B128::random(&mut rng);
301			// Use a smaller field element for z to test the subfield scalar multiplication
302			let z = B128::from(rng.next_u64() as u128);
303			assert_eq!(extrapolate_line_packed(x0, x1, z), x0 + (x1 - x0) * z);
304		}
305	}
306
307	#[test]
308	fn test_evaluation_domain_lagrange_evals() {
309		let mut rng = StdRng::seed_from_u64(0);
310
311		// Create a small domain
312		let domain_points: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
313		let evaluation_domain = EvaluationDomain::from_points(domain_points.clone());
314
315		// Create random values for interpolation
316		let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
317
318		// Test point
319		let z = B128::random(&mut rng);
320
321		// Compute extrapolation
322		let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
323
324		// Compute using Lagrange coefficients
325		let lagrange_coeffs = evaluation_domain.lagrange_evals(z);
326		let lagrange_eval = inner_product(lagrange_coeffs, values);
327
328		assert_eq!(lagrange_eval, extrapolated);
329	}
330}