diff --git a/mvpoly/src/lib.rs b/mvpoly/src/lib.rs index 266e11015d..6e8d35b7bf 100644 --- a/mvpoly/src/lib.rs +++ b/mvpoly/src/lib.rs @@ -10,14 +10,12 @@ //! "Expressions", as defined in the [kimchi] crate, can be converted into a //! multi-variate polynomial using the `from_expr` method. -use std::collections::HashMap; - use ark_ff::PrimeField; -use kimchi::circuits::{ - expr::{ConstantExpr, ConstantExprInner, ConstantTerm, Expr, ExprInner, Operations, Variable}, - gate::CurrOrNext, +use kimchi::circuits::expr::{ + ConstantExpr, ConstantExprInner, ConstantTerm, Expr, ExprInner, Operations, Variable, }; use rand::RngCore; +use std::collections::HashMap; pub mod monomials; pub mod pbt; @@ -86,7 +84,6 @@ pub trait MVPoly: /// speed up the computation. fn eval(&self, x: &[F; N]) -> F; - /// Build the univariate polynomial `x_i` from the variable `i`. /// The conversion into the type `usize` is unspecified by this trait. It /// is left to the trait implementation. @@ -95,7 +92,12 @@ pub trait MVPoly: /// used. /// For [crate::monomials], the output must be the index of the variable, /// starting from `0`. - fn from_variable>(var: Column) -> Self; + /// + /// The parameter `offset_next_row` is an optional argument that is used to + /// support the case where the "next row" is used. In this case, the type + /// parameter `N` must include this offset (i.e. if 4 variables are in ued, + /// N should be at least `8 = 2 * 4`). + fn from_variable>(var: Variable, offset_next_row: Option) -> Self; fn from_constant(op: Operations>) -> Self { use kimchi::circuits::expr::Operations::*; @@ -147,7 +149,16 @@ pub trait MVPoly: /// "the expression framework". /// In the near future, the "expression framework" should be moved also into /// this library. - fn from_expr, ChallengeTerm: Clone>(expr: Expr, Column>) -> Self { + /// + /// The mapping from variable to the user is left unspecified by this trait + /// and is left to the implementation. The conversion of a variable into an + /// index is done by the trait requirement `Into` on the column type. + /// + /// The parameter `offset_next_row` is an optional argument that is used to + /// support the case where the "next row" is used. In this case, the type + /// parameter `N` must include this offset (i.e. if 4 variables are in ued, + /// N should be at least `8 = 2 * 4`). + fn from_expr, ChallengeTerm: Clone>(expr: Expr, Column>, offset_next_row: Option) -> Self { use kimchi::circuits::expr::Operations::*; match expr { @@ -160,38 +171,37 @@ pub trait MVPoly: unimplemented!("Not used in this context") } ExprInner::Constant(c) => Self::from_constant(c), - ExprInner::Cell(Variable { col, row }) => { - assert_eq!(row, CurrOrNext::Curr, "Only current row is supported for now. You cannot reference the next row"); - Self::from_variable(col) + ExprInner::Cell(var) => { + Self::from_variable::(var, offset_next_row) } } } Add(e1, e2) => { - let p1 = Self::from_expr(*e1); - let p2 = Self::from_expr(*e2); + let p1 = Self::from_expr::(*e1, offset_next_row); + let p2 = Self::from_expr::(*e2, offset_next_row); p1 + p2 } Sub(e1, e2) => { - let p1 = Self::from_expr(*e1); - let p2 = Self::from_expr(*e2); + let p1 = Self::from_expr::(*e1, offset_next_row); + let p2 = Self::from_expr::(*e2, offset_next_row); p1 - p2 } Mul(e1, e2) => { - let p1 = Self::from_expr(*e1); - let p2 = Self::from_expr(*e2); + let p1 = Self::from_expr::(*e1, offset_next_row); + let p2 = Self::from_expr::(*e2, offset_next_row); p1 * p2 } Double(p) => { - let p = Self::from_expr(*p); + let p = Self::from_expr::(*p, offset_next_row); p.double() } Square(p) => { - let p = Self::from_expr(*p); + let p = Self::from_expr::(*p, offset_next_row); p.clone() * p.clone() } Pow(c, e) => { // FIXME: dummy implementation - let p = Self::from_expr(*c); + let p = Self::from_expr::(*c, offset_next_row); let mut result = p.clone(); for _ in 0..e { result = result.clone() * p.clone(); diff --git a/mvpoly/src/monomials.rs b/mvpoly/src/monomials.rs index ad6abbda0a..8fb495ef02 100644 --- a/mvpoly/src/monomials.rs +++ b/mvpoly/src/monomials.rs @@ -1,4 +1,5 @@ use ark_ff::{One, PrimeField, Zero}; +use kimchi::circuits::{expr::Variable, gate::CurrOrNext}; use num_integer::binomial; use rand::RngCore; use std::{ @@ -295,10 +296,30 @@ impl MVPoly for Sparse>(var: Column) -> Self { - let var_usize: usize = var.into(); + fn from_variable>( + var: Variable, + offset_next_row: Option, + ) -> Self { + let Variable { col, row } = var; + // Manage offset + if row == CurrOrNext::Next { + assert!( + offset_next_row.is_some(), + "The offset must be provided for the next row" + ); + } + let offset = if row == CurrOrNext::Curr { + 0 + } else { + offset_next_row.unwrap() + }; + + // Build the corresponding monomial + let var_usize: usize = col.into(); + let idx = offset + var_usize; + let mut monomials = HashMap::new(); - let exponents: [usize; N] = std::array::from_fn(|i| if i == var_usize { 1 } else { 0 }); + let exponents: [usize; N] = std::array::from_fn(|i| if i == idx { 1 } else { 0 }); monomials.insert(exponents, F::one()); Self { monomials } } diff --git a/mvpoly/src/pbt.rs b/mvpoly/src/pbt.rs index f4a1da5808..b517f0107f 100644 --- a/mvpoly/src/pbt.rs +++ b/mvpoly/src/pbt.rs @@ -563,3 +563,33 @@ pub fn test_is_multilinear>() { + let mut rng = o1_utils::tests::make_test_rng(None); + let c = F::rand(&mut rng); + let p = T::from(c); + assert!(p.is_constant()); + + let p = T::zero(); + assert!(p.is_constant()); + + let p = { + let mut res = T::zero(); + let monomial: [usize; N] = std::array::from_fn(|i| if i == 0 { 1 } else { 0 }); + res.add_monomial(monomial, F::one()); + res + }; + assert!(!p.is_constant()); + + let p = { + let mut res = T::zero(); + let monomial: [usize; N] = std::array::from_fn(|i| if i == 1 { 1 } else { 0 }); + res.add_monomial(monomial, F::one()); + res + }; + assert!(!p.is_constant()); + + // This might be flaky + let p = unsafe { T::random(&mut rng, None) }; + assert!(!p.is_constant()); +} diff --git a/mvpoly/src/prime.rs b/mvpoly/src/prime.rs index 0392b8d1c0..498cc1c7ee 100644 --- a/mvpoly/src/prime.rs +++ b/mvpoly/src/prime.rs @@ -148,6 +148,7 @@ use std::{ }; use ark_ff::{One, PrimeField, Zero}; +use kimchi::circuits::{expr::Variable, gate::CurrOrNext}; use num_integer::binomial; use o1_utils::FieldHelpers; use rand::{Rng, RngCore}; @@ -337,18 +338,38 @@ impl MVPoly for Dense>(var: Column) -> Self { - let mut res = Self::zero(); + fn from_variable>( + var: Variable, + offset_next_row: Option, + ) -> Self { + let Variable { col, row } = var; + if row == CurrOrNext::Next { + assert!( + offset_next_row.is_some(), + "The offset for the next row must be provided" + ); + } + let offset = if row == CurrOrNext::Curr { + 0 + } else { + offset_next_row.unwrap() + }; + let var_usize: usize = col.into(); + let mut prime_gen = PrimeNumberGenerator::new(); let primes = prime_gen.get_first_nth_primes(N); - let var_usize: usize = var.into(); assert!(primes.contains(&var_usize), "The usize representation of the variable must be a prime number, and unique for each variable"); - let inv_var = res + + let prime_idx = primes.iter().position(|&x| x == var_usize).unwrap(); + let idx = prime_gen.get_nth_prime(prime_idx + offset + 1); + + let mut res = Self::zero(); + let inv_idx = res .normalized_indices .iter() - .position(|&x| x == var_usize) + .position(|&x| x == idx) .unwrap(); - res[inv_var] = F::one(); + res[inv_idx] = F::one(); res } @@ -674,20 +695,30 @@ impl Eq for Dense {} impl Debug for Dense { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut prime_gen = PrimeNumberGenerator::new(); - self.coeff.iter().enumerate().for_each(|(i, c)| { + let primes = prime_gen.get_first_nth_primes(N); + let coeff: Vec<_> = self + .coeff + .iter() + .enumerate() + .filter(|(_i, c)| *c != &F::zero()) + .collect(); + // Print 0 if the polynomial is zero + if coeff.is_empty() { + write!(f, "0").unwrap(); + return Ok(()); + } + let l = coeff.len(); + coeff.into_iter().for_each(|(i, c)| { let normalized_idx = self.normalized_indices[i]; - if normalized_idx == 1 { + if normalized_idx == 1 && *c != F::one() { write!(f, "{}", c.to_biguint()).unwrap(); } else { let prime_decomposition = naive_prime_factors(normalized_idx, &mut prime_gen); - write!(f, "{}", c.to_biguint()).unwrap(); + if *c != F::one() { + write!(f, "{}", c.to_biguint()).unwrap(); + } prime_decomposition.iter().for_each(|(p, d)| { - // FIXME: not correct - let inv_p = self - .normalized_indices - .iter() - .position(|&x| x == *p) - .unwrap(); + let inv_p = primes.iter().position(|&x| x == *p).unwrap(); if *d > 1 { write!(f, "x_{}^{}", inv_p, d).unwrap(); } else { @@ -695,7 +726,9 @@ impl Debug for Dense { } }); } - if i != self.coeff.len() - 1 { + // Avoid printing the last `+` or if the polynomial is a single + // monomial + if i != l - 1 && l != 1 { write!(f, " + ").unwrap(); } }); diff --git a/mvpoly/tests/monomials.rs b/mvpoly/tests/monomials.rs index 1848b48958..902a3fcfbc 100644 --- a/mvpoly/tests/monomials.rs +++ b/mvpoly/tests/monomials.rs @@ -101,17 +101,7 @@ fn test_degree_random_degree() { #[test] fn test_is_constant() { - let mut rng = o1_utils::tests::make_test_rng(None); - let c = Fp::rand(&mut rng); - let p = Sparse::::from(c); - assert!(p.is_constant()); - - let p = Sparse::::zero(); - assert!(p.is_constant()); - - // This might be flaky - let p = unsafe { Sparse::::random(&mut rng, None) }; - assert!(!p.is_constant()); + mvpoly::pbt::test_is_constant::>(); } #[test] @@ -476,13 +466,93 @@ fn test_build_from_variable() { let mut rng = o1_utils::tests::make_test_rng(None); let idx: usize = rng.gen_range(0..4); - let p = Sparse::::from_variable(Column::X(idx)); + let p = Sparse::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Curr, + }, + None, + ); let eval: [Fp; 4] = std::array::from_fn(|_i| Fp::rand(&mut rng)); assert_eq!(p.eval(&eval), eval[idx]); } +#[test] +#[should_panic] +fn test_build_from_variable_next_row_without_offset_given() { + #[derive(Clone, Copy, PartialEq)] + enum Column { + X(usize), + } + + impl From for usize { + fn from(val: Column) -> usize { + match val { + Column::X(i) => i, + } + } + } + + let mut rng = o1_utils::tests::make_test_rng(None); + let idx: usize = rng.gen_range(0..4); + let _p = Sparse::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Next, + }, + None, + ); +} + +#[test] +fn test_build_from_variable_next_row_with_offset_given() { + #[derive(Clone, Copy, PartialEq)] + enum Column { + X(usize), + } + + impl From for usize { + fn from(val: Column) -> usize { + match val { + Column::X(i) => i, + } + } + } + + let mut rng = o1_utils::tests::make_test_rng(None); + let idx: usize = rng.gen_range(0..4); + + // Using next + { + let p = Sparse::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Next, + }, + Some(4), + ); + + let eval: [Fp; 8] = std::array::from_fn(|_i| Fp::rand(&mut rng)); + assert_eq!(p.eval(&eval), eval[idx + 4]); + } + + // Still using current + { + let p = Sparse::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Curr, + }, + Some(4), + ); + + let eval: [Fp; 8] = std::array::from_fn(|_i| Fp::rand(&mut rng)); + assert_eq!(p.eval(&eval), eval[idx]); + } +} + /// As a reminder, here are the equations to compute the addition of two /// different points `P1 = (X1, Y1)` and `P2 = (X2, Y2)`. Let `P3 = (X3, /// Y3) = P1 + P2`. @@ -606,7 +676,7 @@ fn test_from_expr_ec_addition() { // - Constraint 1: λ (X1 - X2) - Y1 + Y2 = 0 let expression = lambda.clone() * (x1.clone() - x2.clone()) - (y1.clone() - y2.clone()); - let p = Sparse::::from_expr(expression); + let p = Sparse::::from_expr::(expression, None); let random_evaluation: [Fp; 7] = std::array::from_fn(|_| Fp::rand(&mut rng)); let eval = p.eval(&random_evaluation); let exp_eval = { @@ -619,7 +689,7 @@ fn test_from_expr_ec_addition() { { // - Constraint 2: X3 + X1 + X2 - λ^2 = 0 let expr = x3.clone() + x1.clone() + x2.clone() - lambda.clone() * lambda.clone(); - let p = Sparse::::from_expr(expr); + let p = Sparse::::from_expr::(expr, None); let random_evaluation: [Fp; 7] = std::array::from_fn(|_| Fp::rand(&mut rng)); let eval = p.eval(&random_evaluation); let exp_eval = { @@ -631,7 +701,7 @@ fn test_from_expr_ec_addition() { { // - Constraint 3: Y3 - λ (X1 - X3) + Y1 = 0 let expr = y3.clone() - lambda.clone() * (x1.clone() - x3.clone()) + y1.clone(); - let p = Sparse::::from_expr(expr); + let p = Sparse::::from_expr::(expr, None); let random_evaluation: [Fp; 7] = std::array::from_fn(|_| Fp::rand(&mut rng)); let eval = p.eval(&random_evaluation); let exp_eval = { diff --git a/mvpoly/tests/prime.rs b/mvpoly/tests/prime.rs index 2baacdb935..17cc6a3eb1 100644 --- a/mvpoly/tests/prime.rs +++ b/mvpoly/tests/prime.rs @@ -6,6 +6,7 @@ use kimchi::circuits::{ }; use mina_curves::pasta::Fp; use mvpoly::{prime::Dense, utils::PrimeNumberGenerator, MVPoly}; +use rand::Rng; #[test] fn test_vector_space_dimension() { @@ -246,34 +247,6 @@ fn test_mul_by_scalar_with_from() { assert_eq!(result1, result2); } -#[test] -fn test_from_variable() { - // Test for y variable (index 2) - let y = Dense::::from_variable(2_usize); - assert_eq!(y[1], Fp::one()); - assert_eq!(y[0], Fp::zero()); - assert_eq!(y[2], Fp::zero()); - assert_eq!(y[3], Fp::zero()); - assert_eq!(y[4], Fp::zero()); - assert_eq!(y[5], Fp::zero()); - - // Test for z variable (index 3) - let z = Dense::::from_variable(3_usize); - assert_eq!(z[0], Fp::zero()); - assert_eq!(z[1], Fp::zero()); - assert_eq!(z[2], Fp::one()); - assert_eq!(z[3], Fp::zero()); - assert_eq!(z[4], Fp::zero()); - - // Test for w variable (index 5) - let w = Dense::::from_variable(5_usize); - assert_eq!(w[0], Fp::zero()); - assert_eq!(w[1], Fp::zero()); - assert_eq!(w[2], Fp::zero()); - assert_eq!(w[3], Fp::zero()); - assert_eq!(w[4], Fp::one()); -} - #[test] fn test_from_variable_column() { // Simulate a real usecase @@ -292,7 +265,13 @@ fn test_from_variable_column() { } } - let p = Dense::::from_variable(Column::X(0)); + let p = Dense::::from_variable::( + Variable { + col: Column::X(0), + row: CurrOrNext::Curr, + }, + None, + ); assert_eq!(p[0], Fp::zero()); assert_eq!(p[1], Fp::one()); assert_eq!(p[2], Fp::zero()); @@ -301,7 +280,13 @@ fn test_from_variable_column() { assert_eq!(p[5], Fp::zero()); // Test for z variable (index 3) - let p = Dense::::from_variable(Column::X(1)); + let p = Dense::::from_variable::( + Variable { + col: Column::X(1), + row: CurrOrNext::Curr, + }, + None, + ); assert_eq!(p[0], Fp::zero()); assert_eq!(p[1], Fp::zero()); assert_eq!(p[2], Fp::one()); @@ -309,7 +294,13 @@ fn test_from_variable_column() { assert_eq!(p[4], Fp::zero()); // Test for w variable (index 5) - let p = Dense::::from_variable(Column::X(2)); + let p = Dense::::from_variable::( + Variable { + col: Column::X(2), + row: CurrOrNext::Curr, + }, + None, + ); assert_eq!(p[0], Fp::zero()); assert_eq!(p[1], Fp::zero()); assert_eq!(p[2], Fp::zero()); @@ -478,7 +469,7 @@ fn test_from_expr_ec_addition() { // - Constraint 1: λ (X1 - X2) - Y1 + Y2 = 0 let expression = lambda.clone() * (x1.clone() - x2.clone()) - (y1.clone() - y2.clone()); - let p = Dense::::from_expr(expression); + let p = Dense::::from_expr::(expression, None); let random_evaluation: [Fp; 7] = std::array::from_fn(|_| Fp::rand(&mut rng)); let eval = p.eval(&random_evaluation); let exp_eval = { @@ -491,7 +482,7 @@ fn test_from_expr_ec_addition() { { // - Constraint 2: X3 + X1 + X2 - λ^2 = 0 let expr = x3.clone() + x1.clone() + x2.clone() - lambda.clone() * lambda.clone(); - let p = Dense::::from_expr(expr); + let p = Dense::::from_expr::(expr, None); let random_evaluation: [Fp; 7] = std::array::from_fn(|_| Fp::rand(&mut rng)); let eval = p.eval(&random_evaluation); let exp_eval = { @@ -503,7 +494,7 @@ fn test_from_expr_ec_addition() { { // - Constraint 3: Y3 - λ (X1 - X3) + Y1 = 0 let expr = y3.clone() - lambda.clone() * (x1.clone() - x3.clone()) + y1.clone(); - let p = Dense::::from_expr(expr); + let p = Dense::::from_expr::(expr, None); let random_evaluation: [Fp; 7] = std::array::from_fn(|_| Fp::rand(&mut rng)); let eval = p.eval(&random_evaluation); let exp_eval = { @@ -574,23 +565,7 @@ fn test_degree_random_degree() { #[test] fn test_is_constant() { - let mut rng = o1_utils::tests::make_test_rng(None); - let c = Fp::rand(&mut rng); - let p = Dense::::from(c); - assert!(p.is_constant()); - - let p = Dense::::zero(); - assert!(p.is_constant()); - - let p = Dense::::from_variable(2_usize); - assert!(!p.is_constant()); - - let p = Dense::::from_variable(3_usize); - assert!(!p.is_constant()); - - // This might be flaky - let p = unsafe { Dense::::random(&mut rng, None) }; - assert!(!p.is_constant()); + mvpoly::pbt::test_is_constant::>(); } #[test] @@ -652,3 +627,83 @@ fn test_add_monomial() { fn test_is_multilinear() { mvpoly::pbt::test_is_multilinear::>(); } + +#[test] +#[should_panic] +fn test_build_from_variable_next_row_without_offset_given() { + #[derive(Clone, Copy, PartialEq)] + enum Column { + X(usize), + } + + impl From for usize { + fn from(val: Column) -> usize { + match val { + Column::X(i) => { + let mut prime_gen = PrimeNumberGenerator::new(); + prime_gen.get_nth_prime(i + 1) + } + } + } + } + + let mut rng = o1_utils::tests::make_test_rng(None); + let idx: usize = rng.gen_range(0..4); + let _p = Dense::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Next, + }, + None, + ); +} + +#[test] +fn test_build_from_variable_next_row_with_offset_given() { + #[derive(Clone, Copy, PartialEq)] + enum Column { + X(usize), + } + + impl From for usize { + fn from(val: Column) -> usize { + match val { + Column::X(i) => { + let mut prime_gen = PrimeNumberGenerator::new(); + prime_gen.get_nth_prime(i + 1) + } + } + } + } + + let mut rng = o1_utils::tests::make_test_rng(None); + let idx: usize = rng.gen_range(0..4); + + // Using next + { + let p = Dense::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Next, + }, + Some(4), + ); + + let eval: [Fp; 8] = std::array::from_fn(|_i| Fp::rand(&mut rng)); + assert_eq!(p.eval(&eval), eval[idx + 4]); + } + + // Still using current + { + let p = Dense::::from_variable::( + Variable { + col: Column::X(idx), + row: CurrOrNext::Curr, + }, + Some(4), + ); + + let eval: [Fp; 8] = std::array::from_fn(|_i| Fp::rand(&mut rng)); + assert_eq!(p.eval(&eval), eval[idx]); + } +}