binius_math/
univariate.rs1use binius_field::{BinaryField, Field};
4use itertools::izip;
5
6use super::{BinarySubspace, FieldBuffer};
7
8pub 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 rest.iter()
20 .rev()
21 .fold(highest_degree, |acc, &coeff| acc * x + coeff)
22}
23
24pub 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 let w = domain[1..]
54 .iter()
55 .fold(F::ONE, |acc, &d| acc * d)
56 .invert()
57 .unwrap_or(F::ONE);
58
59 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 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 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#[derive(Debug, Clone)]
86pub struct EvaluationDomain<F: Field> {
87 points: Vec<F>,
88 weights: Vec<F>,
89}
90
91impl<F: Field> EvaluationDomain<F> {
92 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 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 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 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 pub fn extrapolate(&self, values: &[F], x: F) -> F {
143 assert_eq!(values.len(), self.size()); 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
158fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
171 let n = points.len();
172 (0..n)
173 .map(|i| {
174 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 for log_domain_size in [3, 4, 5, 6] {
225 let subspace = BinarySubspace::<F>::with_dim(log_domain_size).unwrap();
227 let domain: Vec<F> = subspace.iter().collect();
228
229 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 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 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 let domain_evals: Vec<F> = domain
261 .iter()
262 .map(|&point| evaluate_univariate(&coeffs, point))
263 .collect();
264
265 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 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 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 let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
317
318 let z = B128::random(&mut rng);
320
321 let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
323
324 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}