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}