1use binius_field::{BinaryField, Field, field::FieldOps};
5use itertools::izip;
6
7use super::{BinarySubspace, FieldBuffer};
8
9pub 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 rest.iter()
21 .rev()
22 .fold(highest_degree.clone(), |acc, coeff| acc * x + coeff)
23}
24
25pub 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
53pub 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 let w = domain[1..]
73 .iter()
74 .fold(E::one(), |acc, d| acc * d)
75 .invert_or_zero();
76
77 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 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 izip!(prefixes, suffixes)
91 .map(|(p, s)| p * s * w.clone())
92 .collect()
93}
94
95pub 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 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 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#[derive(Debug, Clone)]
147pub struct EvaluationDomain<F: Field> {
148 points: Vec<F>,
149 weights: Vec<F>,
150}
151
152impl<F: Field> EvaluationDomain<F> {
153 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 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 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 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 pub fn extrapolate(&self, values: &[F], x: F) -> F {
204 assert_eq!(values.len(), self.size()); 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
219fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
232 let n = points.len();
233 (0..n)
234 .map(|i| {
235 let product = (0..n)
237 .filter(|&j| j != i)
238 .map(|j| points[i] - points[j])
239 .product::<F>();
240 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 for log_domain_size in [3, 4, 5, 6] {
286 let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
288 let domain: Vec<F> = subspace.iter().collect();
289
290 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 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 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 let domain_evals: Vec<F> = domain
322 .iter()
323 .map(|&point| evaluate_univariate(&coeffs, &point))
324 .collect();
325
326 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 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 let coeffs: Vec<F> = random_scalars(&mut rng, n);
378
379 let values: Vec<F> = subspace
381 .iter()
382 .map(|point| evaluate_univariate(&coeffs, &point))
383 .collect();
384
385 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 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 let domain_points: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
420 let evaluation_domain = EvaluationDomain::from_points(domain_points);
421
422 let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
424
425 let z = B128::random(&mut rng);
427
428 let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
430
431 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}