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}
79
80#[derive(Debug, Clone)]
85pub struct EvaluationDomain<F: Field> {
86 points: Vec<F>,
87 weights: Vec<F>,
88}
89
90impl<F: Field> EvaluationDomain<F> {
91 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 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 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 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 pub fn extrapolate(&self, values: &[F], x: F) -> F {
142 assert_eq!(values.len(), self.size()); 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
157fn compute_barycentric_weights<F: Field>(points: &[F]) -> Vec<F> {
170 let n = points.len();
171 (0..n)
172 .map(|i| {
173 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 for log_domain_size in [3, 4, 5, 6] {
224 let subspace = BinarySubspace::<F>::with_dim(log_domain_size);
226 let domain: Vec<F> = subspace.iter().collect();
227
228 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 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 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 let domain_evals: Vec<F> = domain
260 .iter()
261 .map(|&point| evaluate_univariate(&coeffs, point))
262 .collect();
263
264 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 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 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 let values: Vec<B128> = (0..10).map(|_| B128::random(&mut rng)).collect();
316
317 let z = B128::random(&mut rng);
319
320 let extrapolated = evaluation_domain.extrapolate(values.as_slice(), z);
322
323 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}