1use binius_field::{BinaryField, Field, field::FieldOps};
4use itertools::izip;
5
6use super::{BinarySubspace, FieldBuffer};
7
8pub fn evaluate_univariate<F: FieldOps>(coeffs: &[F], x: F) -> F {
14 let Some((highest_degree, rest)) = coeffs.split_last() else {
15 return F::zero();
16 };
17
18 rest.iter()
20 .rev()
21 .fold(highest_degree.clone(), |acc, coeff| acc * &x + coeff)
22}
23
24pub fn lagrange_evals<F: BinaryField>(subspace: &BinarySubspace<F>, z: F) -> FieldBuffer<F> {
48 let result = lagrange_evals_scalars(subspace, z);
49 FieldBuffer::new(subspace.dim(), result.into_boxed_slice())
50}
51
52pub fn lagrange_evals_scalars<F: BinaryField, E: FieldOps + From<F>>(
64 subspace: &BinarySubspace<F>,
65 z: E,
66) -> Vec<E> {
67 let domain: Vec<E> = subspace.iter().map(E::from).collect();
68 let n = domain.len();
69
70 let w = domain[1..]
72 .iter()
73 .fold(E::one(), |acc, d| acc * d)
74 .invert_or_zero();
75
76 let mut prefixes = vec![E::one(); n];
78 for i in 1..n {
79 prefixes[i] = prefixes[i - 1].clone() * (z.clone() - domain[i - 1].clone());
80 }
81
82 let mut suffixes = vec![E::one(); n];
84 for i in (0..n - 1).rev() {
85 suffixes[i] = suffixes[i + 1].clone() * (z.clone() - domain[i + 1].clone());
86 }
87
88 izip!(prefixes, suffixes)
90 .map(|(p, s)| p * s * w.clone())
91 .collect()
92}
93
94pub fn extrapolate_over_subspace<F: BinaryField, E: FieldOps + From<F>>(
113 subspace: &BinarySubspace<F>,
114 values: &[E],
115 z: E,
116) -> E {
117 let n = 1 << subspace.dim();
118 assert_eq!(values.len(), n);
119
120 let w = subspace
122 .iter()
123 .skip(1)
124 .map(E::from)
125 .fold(E::one(), |acc, d| acc * d)
126 .invert_or_zero();
127
128 let (acc, _) = izip!(values, subspace.iter()).fold(
130 (E::zero(), E::one()),
131 |(acc, prod), (value, point)| {
132 let term = z.clone() - E::from(point);
133 let next_acc = acc * &term + prod.clone() * value;
134 (next_acc, prod * term)
135 },
136 );
137
138 acc * w
139}
140
141#[derive(Debug, Clone)]
146pub struct EvaluationDomain<F: Field> {
147 points: Vec<F>,
148 weights: Vec<F>,
149}
150
151impl<F: Field> EvaluationDomain<F> {
152 pub fn from_points(points: Vec<F>) -> Self {
160 let weights = compute_barycentric_weights(&points);
161 Self { points, weights }
162 }
163
164 pub fn size(&self) -> usize {
165 self.points.len()
166 }
167
168 pub fn points(&self) -> &[F] {
169 self.points.as_slice()
170 }
171
172 pub fn lagrange_evals(&self, x: F) -> Vec<F> {
179 let n = self.size();
180
181 let mut result = vec![F::ONE; n];
182
183 for i in (1..n).rev() {
185 result[i - 1] = result[i] * (x - self.points[i]);
186 }
187
188 let mut prefix = F::ONE;
189
190 for (result_i, &point, &weight) in izip!(&mut result, &self.points, &self.weights) {
192 *result_i *= prefix * weight;
193 prefix *= x - point;
194 }
195
196 result
197 }
198
199 pub fn extrapolate(&self, values: &[F], x: F) -> F {
203 assert_eq!(values.len(), self.size()); let (ret, _) = izip!(values, &self.points, &self.weights).fold(
206 (F::ZERO, F::ONE),
207 |(acc, prod), (&value, &point, &weight)| {
208 let term = x - point;
209 let next_acc = acc * term + prod * value * weight;
210 (next_acc, prod * term)
211 },
212 );
213
214 ret
215 }
216}
217
218fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
231 let n = points.len();
232 (0..n)
233 .map(|i| {
234 let product = (0..n)
236 .filter(|&j| j != i)
237 .map(|j| points[i] - points[j])
238 .product::<F>();
239 product
240 .invert()
241 .expect("precondition: all points are distinct; invert only fails on 0")
242 })
243 .collect()
244}
245
246#[cfg(test)]
247mod tests {
248 use binius_field::{BinaryField128bGhash, Field, Random, util::powers};
249 use rand::prelude::*;
250
251 use super::*;
252 use crate::{
253 BinarySubspace,
254 inner_product::inner_product,
255 line::extrapolate_line_packed,
256 test_utils::{B128, random_scalars},
257 };
258
259 fn evaluate_univariate_with_powers<F: Field>(coeffs: &[F], x: F) -> F {
260 inner_product(coeffs.iter().copied(), powers(x).take(coeffs.len()))
261 }
262
263 type F = BinaryField128bGhash;
264
265 #[test]
266 fn test_evaluate_univariate_against_reference() {
267 let mut rng = StdRng::seed_from_u64(0);
268
269 for n_coeffs in [0, 1, 2, 5, 10] {
270 let coeffs = random_scalars(&mut rng, n_coeffs);
271 let x = F::random(&mut rng);
272 assert_eq!(
273 evaluate_univariate(&coeffs, x),
274 evaluate_univariate_with_powers(&coeffs, x)
275 );
276 }
277 }
278
279 #[test]
280 fn test_lagrange_evals() {
281 let mut rng = StdRng::seed_from_u64(0);
282
283 for log_domain_size in [3, 4, 5, 6] {
285 let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
287 let domain: Vec<F> = subspace.iter().collect();
288
289 let eval_point = F::random(&mut rng);
291 let lagrange_coeffs = lagrange_evals(&subspace, eval_point);
292 let sum: F = lagrange_coeffs.as_ref().iter().copied().sum();
293 assert_eq!(
294 sum,
295 F::ONE,
296 "Partition of unity failed for domain size {}",
297 1 << log_domain_size
298 );
299
300 for (j, &domain_point) in domain.iter().enumerate() {
302 let lagrange_at_domain = lagrange_evals(&subspace, domain_point);
303 for (i, &coeff) in lagrange_at_domain.as_ref().iter().enumerate() {
304 let expected = if i == j { F::ONE } else { F::ZERO };
305 assert_eq!(
306 coeff, expected,
307 "Interpolation property failed: L_{i}({j}) ≠ {expected}"
308 );
309 }
310 }
311 }
312
313 let log_domain_size = 6;
315 let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
316 let domain: Vec<F> = subspace.iter().collect();
317 let coeffs = random_scalars(&mut rng, 10);
318
319 let domain_evals: Vec<F> = domain
321 .iter()
322 .map(|&point| evaluate_univariate(&coeffs, point))
323 .collect();
324
325 let test_point = F::random(&mut rng);
327 let lagrange_coeffs = lagrange_evals(&subspace, test_point);
328 let interpolated =
329 inner_product(domain_evals.iter().copied(), lagrange_coeffs.iter_scalars());
330 let direct = evaluate_univariate(&coeffs, test_point);
331
332 assert_eq!(interpolated, direct, "Polynomial interpolation accuracy failed");
333 }
334
335 #[test]
336 fn test_random_extrapolate() {
337 let mut rng = StdRng::seed_from_u64(0);
338 let degree = 6;
339
340 let domain = EvaluationDomain::from_points(random_scalars(&mut rng, degree + 1));
341
342 let coeffs = random_scalars(&mut rng, degree + 1);
343
344 let values = domain
345 .points()
346 .iter()
347 .map(|&x| evaluate_univariate(&coeffs, x))
348 .collect::<Vec<_>>();
349
350 let x = B128::random(&mut rng);
351 let expected_y = evaluate_univariate(&coeffs, x);
352 assert_eq!(domain.extrapolate(&values, x), expected_y);
353 }
354
355 #[test]
356 fn test_extrapolate_line() {
357 let mut rng = StdRng::seed_from_u64(0);
358 for _ in 0..10 {
359 let x0 = B128::random(&mut rng);
360 let x1 = B128::random(&mut rng);
361 let z = B128::from(rng.next_u64() as u128);
363 assert_eq!(extrapolate_line_packed(x0, x1, z), x0 + (x1 - x0) * z);
364 }
365 }
366
367 #[test]
368 fn test_extrapolate_over_subspace_against_evaluate_univariate() {
369 let mut rng = StdRng::seed_from_u64(0);
370
371 for log_domain_size in 0..=6 {
372 let n = 1 << log_domain_size;
373 let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
374
375 let coeffs: Vec<F> = random_scalars(&mut rng, n);
377
378 let values: Vec<F> = subspace
380 .iter()
381 .map(|point| evaluate_univariate(&coeffs, point))
382 .collect();
383
384 let z = F::random(&mut rng);
386 let extrapolated = extrapolate_over_subspace(&subspace, &values, z);
387 let expected = evaluate_univariate(&coeffs, z);
388
389 assert_eq!(extrapolated, expected, "Mismatch for log_domain_size={log_domain_size}");
390 }
391 }
392
393 #[test]
394 fn test_extrapolate_over_subspace_against_lagrange_evals() {
395 let mut rng = StdRng::seed_from_u64(0);
396
397 for log_domain_size in 0..=6 {
398 let n = 1 << log_domain_size;
399 let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
400
401 let values: Vec<F> = random_scalars(&mut rng, n);
403
404 let z = F::random(&mut rng);
405 let extrapolated = extrapolate_over_subspace(&subspace, &values, z);
406 let lagrange = lagrange_evals_scalars(&subspace, z);
407 let expected = inner_product(values.iter().copied(), lagrange);
408
409 assert_eq!(extrapolated, expected, "Mismatch for log_domain_size={log_domain_size}");
410 }
411 }
412
413 #[test]
414 fn test_evaluation_domain_lagrange_evals() {
415 let mut rng = StdRng::seed_from_u64(0);
416
417 let domain_points: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
419 let evaluation_domain = EvaluationDomain::from_points(domain_points.clone());
420
421 let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
423
424 let z = B128::random(&mut rng);
426
427 let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
429
430 let lagrange_coeffs = evaluation_domain.lagrange_evals(z);
432 let lagrange_eval = inner_product(lagrange_coeffs, values);
433
434 assert_eq!(lagrange_eval, extrapolated);
435 }
436}