mvpoly/lib.rs
1//! This module contains the definition of the `MVPoly` trait, which is used to
2//! represent multi-variate polynomials.
3//!
4//! Different representations are provided in the sub-modules:
5//! - `monomials`: a representation based on monomials
6//! - `prime`: a representation based on a mapping from variables to prime
7//! numbers. This representation is unmaintained for now. We leave it
8//! for interested users.
9//!
10//! "Expressions", as defined in the [kimchi] crate, can be converted into a
11//! multi-variate polynomial using the `from_expr` method.
12
13use ark_ff::PrimeField;
14use kimchi::circuits::expr::{
15 ConstantExpr, ConstantExprInner, ConstantTerm, Expr, ExprInner, Operations, Variable,
16};
17use rand::RngCore;
18use std::collections::HashMap;
19
20pub mod monomials;
21pub mod pbt;
22pub mod prime;
23pub mod utils;
24
25/// Generic trait to represent a multi-variate polynomial
26pub trait MVPoly<F: PrimeField, const N: usize, const D: usize>:
27 // Addition
28 std::ops::Add<Self, Output = Self>
29 + for<'a> std::ops::Add<&'a Self, Output = Self>
30 // Mul
31 + std::ops::Mul<Self, Output = Self>
32 // Negation
33 + std::ops::Neg<Output = Self>
34 // Sub
35 + std::ops::Sub<Self, Output = Self>
36 + for<'a> std::ops::Sub<&'a Self, Output = Self>
37 + ark_ff::One
38 + ark_ff::Zero
39 + std::fmt::Debug
40 + Clone
41 // Comparison operators
42 + PartialEq
43 + Eq
44 // Useful conversions
45 + From<F>
46 + Sized
47{
48 /// Generate a random polynomial of maximum degree `max_degree`.
49 ///
50 /// If `None` is provided as the maximum degree, the polynomial will be
51 /// generated with a maximum degree of `D`.
52 ///
53 /// # Safety
54 ///
55 /// Marked as unsafe to warn the user to use it with caution and to not
56 /// necessarily rely on it for security/randomness in cryptographic
57 /// protocols. The user is responsible for providing its own secure
58 /// polynomial random generator, if needed.
59 ///
60 /// For now, the function is only used for testing.
61 unsafe fn random<RNG: RngCore>(rng: &mut RNG, max_degree: Option<usize>) -> Self;
62
63 fn double(&self) -> Self;
64
65 fn is_constant(&self) -> bool;
66
67 fn mul_by_scalar(&self, scalar: F) -> Self;
68
69 /// Returns the degree of the polynomial.
70 ///
71 /// The degree of the polynomial is the maximum degree of the monomials
72 /// that have a non-zero coefficient.
73 ///
74 /// # Safety
75 ///
76 /// The zero polynomial as a degree equals to 0, as the degree of the
77 /// constant polynomials. We do use the `unsafe` keyword to warn the user
78 /// for this specific case.
79 unsafe fn degree(&self) -> usize;
80
81 /// Evaluate the polynomial at the vector point `x`.
82 ///
83 /// This is a dummy implementation. A cache can be used for the monomials to
84 /// speed up the computation.
85 fn eval(&self, x: &[F; N]) -> F;
86
87 /// Build the univariate polynomial `x_i` from the variable `i`.
88 /// The conversion into the type `usize` is unspecified by this trait. It
89 /// is left to the trait implementation.
90 /// For instance, in the case of [crate::prime], the output must be a prime
91 /// number, starting at `2`. [crate::utils::PrimeNumberGenerator] can be
92 /// used.
93 /// For [crate::monomials], the output must be the index of the variable,
94 /// starting from `0`.
95 ///
96 /// The parameter `offset_next_row` is an optional argument that is used to
97 /// support the case where the "next row" is used. In this case, the type
98 /// parameter `N` must include this offset (i.e. if 4 variables are in ued,
99 /// N should be at least `8 = 2 * 4`).
100 fn from_variable<Column: Into<usize>>(var: Variable<Column>, offset_next_row: Option<usize>) -> Self;
101
102 fn from_constant<ChallengeTerm: Clone>(op: Operations<ConstantExprInner<F, ChallengeTerm>>) -> Self {
103 use kimchi::circuits::expr::Operations::*;
104 match op {
105 Atom(op_const) => {
106 match op_const {
107 ConstantExprInner::Challenge(_) => {
108 unimplemented!("Challenges are not supposed to be used in this context for now")
109 }
110 ConstantExprInner::Constant(ConstantTerm::EndoCoefficient) => {
111 unimplemented!(
112 "The constant EndoCoefficient is not supposed to be used in this context"
113 )
114 }
115 ConstantExprInner::Constant(ConstantTerm::Mds {
116 row: _row,
117 col: _col,
118 }) => {
119 unimplemented!("The constant Mds is not supposed to be used in this context")
120 }
121 ConstantExprInner::Constant(ConstantTerm::Literal(c)) => Self::from(c),
122 }
123 }
124 Add(c1, c2) => Self::from_constant(*c1) + Self::from_constant(*c2),
125 Sub(c1, c2) => Self::from_constant(*c1) - Self::from_constant(*c2),
126 Mul(c1, c2) => Self::from_constant(*c1) * Self::from_constant(*c2),
127 Square(c) => Self::from_constant(*c.clone()) * Self::from_constant(*c),
128 Double(c1) => Self::from_constant(*c1).double(),
129 Pow(c, e) => {
130 // FIXME: dummy implementation
131 let p = Self::from_constant(*c);
132 let mut result = p.clone();
133 for _ in 0..e {
134 result = result.clone() * p.clone();
135 }
136 result
137 }
138 Cache(_c, _) => {
139 unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
140 }
141 IfFeature(_c, _t, _f) => {
142 unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
143 }
144 }
145 }
146
147 /// Build a value from an expression.
148 /// This method aims to be used to be retro-compatible with what we call
149 /// "the expression framework".
150 /// In the near future, the "expression framework" should be moved also into
151 /// this library.
152 ///
153 /// The mapping from variable to the user is left unspecified by this trait
154 /// and is left to the implementation. The conversion of a variable into an
155 /// index is done by the trait requirement `Into<usize>` on the column type.
156 ///
157 /// The parameter `offset_next_row` is an optional argument that is used to
158 /// support the case where the "next row" is used. In this case, the type
159 /// parameter `N` must include this offset (i.e. if 4 variables are in used,
160 /// N should be at least `8 = 2 * 4`).
161 fn from_expr<Column: Into<usize>, ChallengeTerm: Clone>(expr: Expr<ConstantExpr<F, ChallengeTerm>, Column>, offset_next_row: Option<usize>) -> Self {
162 use kimchi::circuits::expr::Operations::*;
163
164 match expr {
165 Atom(op_const) => {
166 match op_const {
167 ExprInner::UnnormalizedLagrangeBasis(_) => {
168 unimplemented!("Not used in this context")
169 }
170 ExprInner::VanishesOnZeroKnowledgeAndPreviousRows => {
171 unimplemented!("Not used in this context")
172 }
173 ExprInner::Constant(c) => Self::from_constant(c),
174 ExprInner::Cell(var) => {
175 Self::from_variable::<Column>(var, offset_next_row)
176 }
177 }
178 }
179 Add(e1, e2) => {
180 let p1 = Self::from_expr::<Column, ChallengeTerm>(*e1, offset_next_row);
181 let p2 = Self::from_expr::<Column, ChallengeTerm>(*e2, offset_next_row);
182 p1 + p2
183 }
184 Sub(e1, e2) => {
185 let p1 = Self::from_expr::<Column, ChallengeTerm>(*e1, offset_next_row);
186 let p2 = Self::from_expr::<Column, ChallengeTerm>(*e2, offset_next_row);
187 p1 - p2
188 }
189 Mul(e1, e2) => {
190 let p1 = Self::from_expr::<Column, ChallengeTerm>(*e1, offset_next_row);
191 let p2 = Self::from_expr::<Column, ChallengeTerm>(*e2, offset_next_row);
192 p1 * p2
193 }
194 Double(p) => {
195 let p = Self::from_expr::<Column, ChallengeTerm>(*p, offset_next_row);
196 p.double()
197 }
198 Square(p) => {
199 let p = Self::from_expr::<Column, ChallengeTerm>(*p, offset_next_row);
200 p.clone() * p.clone()
201 }
202 Pow(c, e) => {
203 // FIXME: dummy implementation
204 let p = Self::from_expr::<Column, ChallengeTerm>(*c, offset_next_row);
205 let mut result = p.clone();
206 for _ in 0..e {
207 result = result.clone() * p.clone();
208 }
209 result
210 }
211 Cache(_c, _) => {
212 unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
213 }
214 IfFeature(_c, _t, _f) => {
215 unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
216 }
217 }
218 }
219
220 /// Returns true if the polynomial is homogeneous (of degree `D`).
221 /// As a reminder, a polynomial is homogeneous if all its monomials have the
222 /// same degree.
223 fn is_homogeneous(&self) -> bool;
224
225 /// Evaluate the polynomial at the vector point `x` and the extra variable
226 /// `u` using its homogeneous form of degree D.
227 fn homogeneous_eval(&self, x: &[F; N], u: F) -> F;
228
229 /// Add the monomial `coeff * x_1^{e_1} * ... * x_N^{e_N}` to the
230 /// polynomial, where `e_i` are the values given by the array `exponents`.
231 ///
232 /// For instance, to add the monomial `3 * x_1^2 * x_2^3` to the polynomial,
233 /// one would call `add_monomial([2, 3], 3)`.
234 fn add_monomial(&mut self, exponents: [usize; N], coeff: F);
235
236 /// Compute the cross-terms as described in [Behind Nova: cross-terms
237 /// computation for high degree
238 /// gates](https://hackmd.io/@dannywillems/Syo5MBq90)
239 ///
240 /// The polynomial must not necessarily be homogeneous. For this reason, the
241 /// values `u1` and `u2` represents the extra variable that is used to make
242 /// the polynomial homogeneous.
243 ///
244 /// The homogeneous degree is supposed to be the one defined by the type of
245 /// the polynomial, i.e. `D`.
246 ///
247 /// The output is a map of `D - 1` values that represents the cross-terms
248 /// for each power of `r`.
249 fn compute_cross_terms(
250 &self,
251 eval1: &[F; N],
252 eval2: &[F; N],
253 u1: F,
254 u2: F,
255 ) -> HashMap<usize, F>;
256
257 /// Compute the cross-terms of the given polynomial, scaled by the given
258 /// scalar.
259 ///
260 /// More explicitly, given a polynomial `P(X1, ..., Xn)` and a scalar α, the
261 /// method computes the cross-terms of the polynomial `Q(X1, ..., Xn, α)
262 /// = α * P(X1, ..., Xn)`. For this reason, the method takes as input the
263 /// two different scalars `scalar1` and `scalar2` as we are considering the
264 /// scaling factor as a variable.
265 ///
266 /// This method is particularly useful when you need to compute a
267 /// (possibly random) combinaison of polynomials `P1(X1, ..., Xn), ...,
268 /// Pm(X1, ..., Xn)`, like when computing a quotient polynomial in the PlonK
269 /// PIOP, as the result is the sum of individual "scaled" polynomials:
270 /// ```text
271 /// Q(X_1, ..., X_n, α_1, ..., α_m) =
272 /// α_1 P1(X_1, ..., X_n) +
273 /// ...
274 /// α_m Pm(X_1, ..., X_n) +
275 /// ```
276 ///
277 /// The polynomial must not necessarily be homogeneous. For this reason, the
278 /// values `u1` and `u2` represents the extra variable that is used to make
279 /// the polynomial homogeneous.
280 ///
281 /// The homogeneous degree is supposed to be the one defined by the type of
282 /// the polynomial `P`, i.e. `D`.
283 ///
284 /// The output is a map of `D` values that represents the cross-terms
285 /// for each power of `r`.
286 fn compute_cross_terms_scaled(
287 &self,
288 eval1: &[F; N],
289 eval2: &[F; N],
290 u1: F,
291 u2: F,
292 scalar1: F,
293 scalar2: F,
294 ) -> HashMap<usize, F>;
295
296 /// Modify the monomial in the polynomial to the new value `coeff`.
297 fn modify_monomial(&mut self, exponents: [usize; N], coeff: F);
298
299 /// Return true if the multi-variate polynomial is multilinear, i.e. if each
300 /// variable in each monomial is of maximum degree 1.
301 fn is_multilinear(&self) -> bool;
302}
303
304/// Compute the cross terms of a list of polynomials. The polynomials are
305/// linearly combined using the power of a combiner, often called `α`.
306pub fn compute_combined_cross_terms<
307 F: PrimeField,
308 const N: usize,
309 const D: usize,
310 T: MVPoly<F, N, D>,
311>(
312 polys: Vec<T>,
313 eval1: [F; N],
314 eval2: [F; N],
315 u1: F,
316 u2: F,
317 combiner1: F,
318 combiner2: F,
319) -> HashMap<usize, F> {
320 // These should never happen as they should be random
321 // It also makes the code cleaner as we do not need to handle 0^0
322 assert!(combiner1 != F::zero());
323 assert!(combiner2 != F::zero());
324 assert!(u1 != F::zero());
325 assert!(u2 != F::zero());
326 polys
327 .into_iter()
328 .enumerate()
329 .fold(HashMap::new(), |mut acc, (i, poly)| {
330 let scalar1 = combiner1.pow([i as u64]);
331 let scalar2 = combiner2.pow([i as u64]);
332 let res = poly.compute_cross_terms_scaled(&eval1, &eval2, u1, u2, scalar1, scalar2);
333 res.iter().for_each(|(p, r)| {
334 acc.entry(*p).and_modify(|e| *e += r).or_insert(*r);
335 });
336 acc
337 })
338}