kimchi/lagrange_basis_evaluations.rs
1use ark_ff::{batch_inversion_and_mul, FftField};
2use ark_poly::{EvaluationDomain, Evaluations, Radix2EvaluationDomain as D};
3use rayon::prelude::*;
4
5/// Evaluations of all normalized lagrange basis polynomials at a given point.
6/// Can be used to evaluate an `Evaluations` form polynomial at that point.
7///
8/// The Lagrange basis for polynomials of degree `<= d` over a domain
9/// `{ω_0,...,ω_{d-1}}` is the set of `d` polynomials `{l_0,...,l_{d-1}}` of
10/// degree `d-1` that equal `1` at `ω_i` and `0` in the rest of the domain
11/// terms. They can be used to evaluate polynomials in evaluation form
12/// efficiently in `O(d)` time.
13///
14/// When chunking is in place, the domain size `n` is larger than the maximum
15/// polynomial degree allowed `m`. Thus, on input `n = c·m` evaluations for `c`
16/// chunks, we cannot obtain a polynomial `f` with degree `c·m-1` with the equation:
17///
18/// `f(X) = x_0 · l_0(X) + ... + x_{c·m-1} · l_{c·m-1}(X)`
19///
20/// Instead, this struct will contain the `c·m` coefficients of the polynomial
21/// that is equal to the powers of the point `x` in the positions corresponding
22/// to the chunk, and `0` elsewhere in the domain. This is useful to evaluate the
23/// chunks of polynomials of degree `c·m-1` given in evaluation form at the point.
24pub struct LagrangeBasisEvaluations<F> {
25 /// If no chunking:
26 /// - evals is a vector of length 1 containing a vector of size `n`
27 /// corresponding to the evaluations of the Lagrange polynomials, which
28 /// are the polynomials that equal `1` at `ω_i` and `0` elsewhere in the
29 /// domain.
30 ///
31 /// If chunking (a vector of size `c · n`)
32 /// - the first index refers to the chunks
33 /// - the second index refers j-th coefficient of the i-th chunk of the
34 /// polynomial that equals the powers of the point and `0` elsewhere (and
35 /// the length of each such vector is `n`).
36 evals: Vec<Vec<F>>,
37}
38
39impl<F: FftField> LagrangeBasisEvaluations<F> {
40 /// Return the domain size of the individual evaluations.
41 ///
42 /// Note that there is an invariant that all individual evaluation chunks
43 /// have the same size. It is enforced by each constructor.
44 ///
45 pub fn domain_size(&self) -> usize {
46 self.evals[0].len()
47 }
48
49 /// Given the evaluations form of a polynomial, directly evaluate that
50 /// polynomial at a point.
51 ///
52 /// The Lagrange basis evaluations can be used to evaluate a polynomial
53 /// given in evaluation form efficiently in `O(n)` time, where `n` is the
54 /// domain size, without the need of interpolating.
55 ///
56 /// Recall that a polynomial can be represented as the sum of the scaled
57 /// Lagrange polynomials using its evaluations on the domain:
58 /// `f(x) = x_0 · l_0(x) + ... + x_n · l_n(x)`
59 ///
60 /// But when chunking is in place, we want to evaluate a polynomial `f` of
61 /// degree `c · m - 1` at point `z`, expressed as
62 /// ```text
63 /// f(z) = a_0·z^0 + ... + a_{c*m}·z^{c·m}
64 /// = z^0 · f_0(z) + z^m · f_1(z) + ... + z^{(c-1)m} · f_{c-1}(z)
65 /// ```
66 ///
67 /// where `f_i(X)` is the i-th chunked polynomial of degree `m-1` of `f`:
68 /// `f_i(x) = a_{i·m} · x^0 + ... + a_{(i+1)m-1} · x^{m-1}`
69 ///
70 /// Returns the evaluation of each chunk of the polynomial at the point
71 /// (when there is no chunking, the result is a vector of length 1). They
72 /// correspond to the `f_i(z)` in the equation above.
73 pub fn evaluate<D: EvaluationDomain<F>>(&self, p: &Evaluations<F, D>) -> Vec<F> {
74 // The domain size must be a multiple of the number of evaluations so
75 // that the degree of the polynomial can be split into chunks of equal size.
76 assert_eq!(p.evals.len() % self.domain_size(), 0);
77 // The number of chunks c
78 let stride = p.evals.len() / self.domain_size();
79 let p_evals = &p.evals;
80
81 // Performs the operation
82 // ```text
83 // n-1
84 // j ∈ [0, c) : eval_{j} = Σ p_{i · c} · l_{j,i}
85 // i=0
86 // ```
87 // Note that in the chunking case, the Lagrange basis contains the
88 // coefficient form of the polynomial that evaluates to the powers of
89 // `z` in the chunk positions and `0` elsewhere.
90 //
91 // Then, the evaluation of `f` on `z` can be computed as the sum of the
92 // products of the evaluations of `f` in the domain and the Lagrange
93 // evaluations.
94
95 (&self.evals)
96 .into_par_iter()
97 .map(|evals| {
98 evals
99 .into_par_iter()
100 .enumerate()
101 .map(|(i, e)| p_evals[stride * i] * e)
102 .sum()
103 })
104 .collect()
105 }
106
107 /// Given the evaluations form of a polynomial, directly evaluate that
108 /// polynomial at a point, assuming that the given evaluations are either
109 /// `0` or `1` at every point of the domain.
110 ///
111 /// This method can particularly be useful when the polynomials represent
112 /// (boolean) selectors in a circuit.
113 pub fn evaluate_boolean<D: EvaluationDomain<F>>(&self, p: &Evaluations<F, D>) -> Vec<F> {
114 assert_eq!(p.evals.len() % self.domain_size(), 0);
115 let stride = p.evals.len() / self.domain_size();
116 self.evals
117 .iter()
118 .map(|evals| {
119 let mut result = F::zero();
120 for (i, e) in evals.iter().enumerate() {
121 if !p.evals[stride * i].is_zero() {
122 result += e;
123 }
124 }
125 result
126 })
127 .collect()
128 }
129
130 /// Compute all evaluations of the normalized lagrange basis polynomials of
131 /// the given domain at the given point. Runs in time O(domain size).
132 fn new_with_segment_size_1(domain: D<F>, x: F) -> LagrangeBasisEvaluations<F> {
133 let n = domain.size();
134 // We want to compute for all i
135 // s_i = 1 / t_i
136 // where
137 // t_i = ∏_{j ≠ i} (ω^i - ω^j)
138 //
139 // Suppose we have t_0 = ∏_{j = 1}^{n-1} (1 - ω^j).
140 // This is a product with n-1 terms. We want to shift each term over by
141 // ω so we multiply by ω^{n-1}:
142 //
143 // ω^{n-1} * t_0
144 // = ∏_{j = 1}^{n-1} ω (1 - ω^j).
145 // = ∏_{j = 1}^{n-1} (ω - ω^{j+1)).
146 // = (ω - ω^2) (ω - ω^3) ... (ω - ω^{n-1+1})
147 // = (ω - ω^2) (ω - ω^3) ... (ω - ω^0)
148 // = t_1
149 //
150 // And generally
151 // ω^{n-1} * t_i
152 // = ∏_{j ≠ i} ω (ω^i - ω^j)
153 // = ∏_{j ≠ i} (ω^{i + 1} - ω^{j + 1})
154 // = ∏_{j + 1 ≠ i + 1} (ω^{i + 1} - ω^{j + 1})
155 // = ∏_{j' ≠ i + 1} (ω^{i + 1} - ω^{j'})
156 // = t_{i + 1}
157 //
158 // Since ω^{n-1} = ω^{-1}, we write this as
159 // ω^{-1} t_i = t_{i + 1}
160 // and then by induction,
161 // ω^{-i} t_0 = t_i
162
163 // Now, the ith Lagrange evaluation at x is
164 // (1 / ∏_{j ≠ i} (ω^i - ω^j)) (x^n - 1) / (x - ω^i)
165 // = (x^n - 1) / [t_i (x - ω^i)]
166 // = (x^n - 1) / [ω^{-i} * t_0 * (x - ω^i)]
167 //
168 // We compute this using the [batch_inversion_and_mul] function.
169
170 let t_0: F = domain
171 .elements()
172 .skip(1)
173 .map(|omega_i| F::one() - omega_i)
174 .product();
175
176 let mut denominators: Vec<F> = {
177 let omegas: Vec<F> = domain.elements().collect();
178 let omega_invs: Vec<F> = (0..n).map(|i| omegas[(n - i) % n]).collect();
179
180 omegas
181 .into_par_iter()
182 .zip(omega_invs)
183 .map(|(omega_i, omega_i_inv)| omega_i_inv * t_0 * (x - omega_i))
184 .collect()
185 };
186
187 let numerator = x.pow([n as u64]) - F::one();
188
189 batch_inversion_and_mul(&mut denominators[..], &numerator);
190
191 // Denominators now contain the desired result.
192 LagrangeBasisEvaluations {
193 evals: vec![denominators],
194 }
195 }
196
197 /// Compute all evaluations of the normalized Lagrange basis polynomials of
198 /// the given domain at the given point `x`. Runs in time O(n log(n)) where
199 /// n is the domain size.
200 fn new_with_chunked_segments(
201 max_poly_size: usize,
202 domain: D<F>,
203 x: F,
204 ) -> LagrangeBasisEvaluations<F> {
205 // For each chunk, this loop obtains the coefficient form of the
206 // polynomial that equals the powers of `x` in the positions
207 // corresponding to the chunk, and 0 elsewhere in the domain, using an
208 // iFFT operation of length n, resulting in an algorithm that runs in
209 // `O(c n log n)`.
210 //
211 // Example:
212 // ```text
213 // i-th chunk
214 // -----------------------
215 // chunked: [ 0, ..., 0, 1, x, x^2, ..., x^{m-1}, 0, ..., 0 ]
216 // indices: 0 i·m-1 i·m (i+1)m-1 (i+1)m cm-1=n-1
217 // ```
218 // A total of `n=c·m` coefficients are returned. These will be helpful to
219 // evaluate the chunks of polynomials of degree `n-1` at the point `x`.
220 //
221 let n = domain.size();
222 assert_eq!(n % max_poly_size, 0);
223 let num_chunks = n / max_poly_size;
224 let mut evals = Vec::with_capacity(num_chunks);
225 for i in 0..num_chunks {
226 let mut x_pow = F::one();
227 let mut chunked_evals = vec![F::zero(); n];
228 for j in 0..max_poly_size {
229 chunked_evals[i * max_poly_size + j] = x_pow;
230 x_pow *= x;
231 }
232 // This uses the same trick as `poly_commitment::srs::SRS::lagrange_basis`, but
233 // applied to field elements instead of group elements.
234 domain.ifft_in_place(&mut chunked_evals);
235 // Check that the number of coefficients after iFFT is as expected
236 assert_eq!(
237 chunked_evals.len(),
238 n,
239 "The number of coefficients of the {}-th segment is {} but it should have been {n}",
240 i,
241 chunked_evals.len()
242 );
243 evals.push(chunked_evals);
244 }
245 // Sanity check
246 assert_eq!(
247 evals.len(),
248 num_chunks,
249 "The number of expected chunks is {num_chunks} but only {} has/have been computed",
250 evals.len()
251 );
252 LagrangeBasisEvaluations { evals }
253 }
254
255 pub fn new(max_poly_size: usize, domain: D<F>, x: F) -> LagrangeBasisEvaluations<F> {
256 if domain.size() <= max_poly_size {
257 Self::new_with_segment_size_1(domain, x)
258 } else {
259 Self::new_with_chunked_segments(max_poly_size, domain, x)
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 use ark_ff::{One, UniformRand, Zero};
269 use ark_poly::{Polynomial, Radix2EvaluationDomain};
270 use mina_curves::pasta::Fp;
271 use rand::Rng;
272
273 #[test]
274 fn test_lagrange_evaluations() {
275 let mut rng = o1_utils::tests::make_test_rng(None);
276 let domain_log_size = rng.gen_range(1..10);
277 let n = 1 << domain_log_size;
278 let domain = Radix2EvaluationDomain::new(n).unwrap();
279 let x = Fp::rand(&mut rng);
280 let evaluator = LagrangeBasisEvaluations::new(domain.size(), domain, x);
281
282 let expected = (0..n).map(|i| {
283 let mut lagrange_i = vec![Fp::zero(); n];
284 lagrange_i[i] = Fp::one();
285 vec![Evaluations::from_vec_and_domain(lagrange_i, domain)
286 .interpolate()
287 .evaluate(&x)]
288 });
289
290 for (i, (expected, got)) in expected.zip(evaluator.evals).enumerate() {
291 for (j, (expected, got)) in expected.iter().zip(got.iter()).enumerate() {
292 if got != expected {
293 panic!("{}, {}, {}: {} != {}", line!(), i, j, got, expected);
294 }
295 }
296 }
297 }
298
299 #[test]
300 fn test_new_with_chunked_segments() {
301 let mut rng = o1_utils::tests::make_test_rng(None);
302 let domain_log_size = rng.gen_range(1..10);
303 let n = 1 << domain_log_size;
304 let domain = Radix2EvaluationDomain::new(n).unwrap();
305 let x = Fp::rand(&mut rng);
306 let evaluator = LagrangeBasisEvaluations::new(domain.size(), domain, x);
307 let evaluator_chunked =
308 LagrangeBasisEvaluations::new_with_chunked_segments(domain.size(), domain, x);
309 let chunk_length = evaluator_chunked.domain_size();
310 for (i, (evals, evals_chunked)) in evaluator
311 .evals
312 .iter()
313 .zip(evaluator_chunked.evals.iter())
314 .enumerate()
315 {
316 // Check all chunks have the same length
317 assert_eq!(evals_chunked.len(), chunk_length);
318 for (j, (evals, evals_chunked)) in evals.iter().zip(evals_chunked.iter()).enumerate() {
319 if evals != evals_chunked {
320 panic!("{}, {}, {}: {} != {}", line!(), i, j, evals, evals_chunked);
321 }
322 }
323 }
324 }
325
326 #[test]
327 fn test_evaluation() {
328 let rng = &mut o1_utils::tests::make_test_rng(None);
329 let domain_log_size = rng.gen_range(1..10);
330 let n = 1 << domain_log_size;
331 let domain = Radix2EvaluationDomain::new(n).unwrap();
332
333 let evals = {
334 let mut e = vec![];
335 for _ in 0..n {
336 e.push(Fp::rand(rng));
337 }
338 Evaluations::from_vec_and_domain(e, domain)
339 };
340
341 let x = Fp::rand(rng);
342
343 let evaluator = LagrangeBasisEvaluations::new(domain.size(), domain, x);
344
345 let y = evaluator.evaluate(&evals);
346 let expected = vec![evals.interpolate().evaluate(&x)];
347 assert_eq!(y, expected)
348 }
349
350 #[test]
351 fn test_evaluation_boolean() {
352 let rng = &mut o1_utils::tests::make_test_rng(None);
353 let domain_log_size = rng.gen_range(1..10);
354 let n = 1 << domain_log_size;
355 let domain = Radix2EvaluationDomain::new(n).unwrap();
356
357 let evals = {
358 let mut e = vec![];
359 for _ in 0..n {
360 e.push(if bool::rand(rng) {
361 Fp::one()
362 } else {
363 Fp::zero()
364 });
365 }
366 Evaluations::from_vec_and_domain(e, domain)
367 };
368
369 let x = Fp::rand(rng);
370
371 let evaluator = LagrangeBasisEvaluations::new(domain.size(), domain, x);
372
373 let y = evaluator.evaluate_boolean(&evals);
374 let expected = vec![evals.interpolate().evaluate(&x)];
375 assert_eq!(y, expected)
376 }
377}