Skip to content

Commit 1296329

Browse files
IAvecillajrchatrucmpaulucci
authored
FFT operation wrappers and evaluation with coset (#93)
* Add wrappers for fft related operations * Add function to scale polynomials * Add property tests for new fft with cosets * Fix error message for invalid polynomial order * Fix test (cooley-tukey implementation did not always work) * Move tests to fft operations file * Add `blowup_factor` to increase the domain evaluations on ftt. * Fix test and parameter type for interpolation * Add newline in gitignore --------- Co-authored-by: Javier Chatruc <[email protected]> Co-authored-by: Martin Paulucci <[email protected]>
1 parent 435447b commit 1296329

File tree

6 files changed

+162
-85
lines changed

6 files changed

+162
-85
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ Cargo.lock
88

99
# These are backup files generated by rustfmt
1010
**/*.rs.bk
11+
12+
**/proptest-regressions

math/src/fft/fft_cooley_tukey.rs

Lines changed: 17 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ use crate::field::{
55

66
use super::{errors::FFTError, helpers::log2};
77

8+
pub fn fft_with_blowup<F: IsField + IsTwoAdicField>(
9+
coeffs: &[FieldElement<F>],
10+
blowup_factor: usize,
11+
) -> Result<Vec<FieldElement<F>>, FFTError> {
12+
let domain_size = coeffs.len() * blowup_factor;
13+
let mut padded_coeffs = coeffs.to_vec();
14+
padded_coeffs.resize(domain_size, FieldElement::zero());
15+
fft(&padded_coeffs[..])
16+
}
17+
818
pub fn fft<F: IsField + IsTwoAdicField>(
919
coeffs: &[FieldElement<F>],
1020
) -> Result<Vec<FieldElement<F>>, FFTError> {
@@ -31,15 +41,16 @@ fn cooley_tukey<F: IsField>(
3141
let coeffs_odd: Vec<FieldElement<F>> = coeffs.iter().skip(1).step_by(2).cloned().collect();
3242

3343
let (y_even, y_odd) = (
34-
cooley_tukey(&coeffs_even, omega),
35-
cooley_tukey(&coeffs_odd, omega),
44+
cooley_tukey(&coeffs_even, &omega.pow(2_usize)),
45+
cooley_tukey(&coeffs_odd, &omega.pow(2_usize)),
3646
);
47+
3748
let mut y = vec![FieldElement::zero(); n];
38-
for i in 0..n / 2 {
39-
let a = &y_even[i];
40-
let b = &(omega.pow(i) * &y_odd[i]);
49+
50+
for i in 0..n {
51+
let a = &y_even[i % (n / 2)];
52+
let b = &(omega.pow(i as u64) * &y_odd[i % (n / 2)]);
4153
y[i] = a + b;
42-
y[i + n / 2] = a - b;
4354
}
4455
y
4556
}
@@ -56,81 +67,3 @@ pub fn inverse_cooley_tukey<F: IsField>(
5667
.map(|coeff| coeff * &inverse_n)
5768
.collect()
5869
}
59-
60-
#[cfg(test)]
61-
mod fft_test {
62-
use crate::field::test_fields::u64_test_field::U64TestField;
63-
use crate::polynomial::Polynomial;
64-
use proptest::prelude::*;
65-
66-
use super::*;
67-
68-
const MODULUS: u64 = 0xFFFFFFFF00000001;
69-
type F = U64TestField<MODULUS>;
70-
type FE = FieldElement<F>;
71-
72-
prop_compose! {
73-
fn powers_of_two(max_exp: u8)(exp in 1..max_exp) -> usize { 1 << exp }
74-
// max_exp cannot be multiple of the bits that represent a usize, generally 64 or 32.
75-
// also it can't exceed the test field's two-adicity.
76-
}
77-
prop_compose! {
78-
fn field_element()(num in any::<u64>()) -> FE { FE::from(num) }
79-
}
80-
prop_compose! {
81-
fn field_vec(max_exp: u8)(elem in field_element(), size in powers_of_two(max_exp)) -> Vec<FE> {
82-
vec![elem; size]
83-
}
84-
}
85-
prop_compose! {
86-
// non-power-of-two sized vector
87-
fn unsuitable_field_vec(max_exp: u8)(elem in field_element(), size in powers_of_two(max_exp)) -> Vec<FE> {
88-
vec![elem; size + 1]
89-
}
90-
}
91-
92-
proptest! {
93-
// Property-based test that ensures FFT gives same result as a naive polynomial evaluation.
94-
#[test]
95-
fn test_fft_matches_naive_evaluation(coeffs in field_vec(8)) {
96-
let poly = Polynomial::new(&coeffs[..]);
97-
let omega = F::get_root_of_unity(log2(poly.coefficients().len()).unwrap()).unwrap();
98-
99-
let result = fft(poly.coefficients()).unwrap();
100-
101-
let twiddles_iter = (0..poly.coefficients().len() as u64).map(|i| omega.pow(i));
102-
let expected: Vec<FE> = twiddles_iter.map(|x| poly.evaluate(&x)).collect();
103-
104-
prop_assert_eq!(result, expected);
105-
}
106-
}
107-
proptest! {
108-
// Property-based test that ensures IFFT is the inverse operation of FFT.
109-
#[test]
110-
fn test_ifft_composed_fft_is_identity(coeffs in field_vec(8)) {
111-
let result = fft(&coeffs).unwrap();
112-
let recovered_poly = inverse_fft(&result).unwrap();
113-
114-
prop_assert_eq!(recovered_poly, coeffs);
115-
}
116-
}
117-
118-
proptest! {
119-
// Property-based test that ensures FFT won't work with a non-power-of-two polynomial.
120-
#[test]
121-
fn test_fft_panics_on_non_power_of_two(coeffs in unsuitable_field_vec(8)) {
122-
let result = fft(&coeffs);
123-
124-
prop_assert!(matches!(result, Err(FFTError::InvalidOrder(_))));
125-
}
126-
}
127-
proptest! {
128-
// Property-based test that ensures FFT won't work with a degree 0 polynomial.
129-
#[test]
130-
fn test_fft_constant_poly(elem in field_element()) {
131-
let result = fft(&[elem]);
132-
133-
prop_assert!(matches!(result, Err(FFTError::RootOfUnityError(_, k)) if k == 0));
134-
}
135-
}
136-
}

math/src/fft/helpers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use super::errors::FFTError;
33
pub fn log2(n: usize) -> Result<u64, FFTError> {
44
if !n.is_power_of_two() {
55
return Err(FFTError::InvalidOrder(
6-
"The order of polynomial should a be power of 2".to_string(),
6+
"The order of polynomial + 1 should a be power of 2".to_string(),
77
));
88
}
99
Ok(n.trailing_zeros() as u64)

math/src/fft/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod errors;
22
pub mod fft_cooley_tukey;
33
mod helpers;
4+
pub mod operations;

math/src/fft/operations.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use crate::{
2+
field::{
3+
element::FieldElement,
4+
traits::{IsField, IsTwoAdicField},
5+
},
6+
polynomial::Polynomial,
7+
};
8+
9+
use super::{
10+
errors::FFTError,
11+
fft_cooley_tukey::{fft, fft_with_blowup, inverse_fft},
12+
};
13+
14+
pub fn evaluate_poly<F: IsField + IsTwoAdicField>(
15+
polynomial: &Polynomial<FieldElement<F>>,
16+
) -> Result<Vec<FieldElement<F>>, FFTError> {
17+
fft(polynomial.coefficients())
18+
}
19+
20+
pub fn interpolate_poly<F: IsField + IsTwoAdicField>(
21+
evaluation_points: &[FieldElement<F>],
22+
) -> Result<Vec<FieldElement<F>>, FFTError> {
23+
inverse_fft(evaluation_points)
24+
}
25+
26+
pub fn evaluate_poly_with_offset<F: IsField + IsTwoAdicField>(
27+
polynomial: &Polynomial<FieldElement<F>>,
28+
offset: &FieldElement<F>,
29+
blowup_factor: usize,
30+
) -> Result<Vec<FieldElement<F>>, FFTError> {
31+
let scaled_polynomial = polynomial.scale(offset);
32+
fft_with_blowup(scaled_polynomial.coefficients(), blowup_factor)
33+
}
34+
35+
#[cfg(test)]
36+
mod fft_test {
37+
use crate::fft::helpers::log2;
38+
use crate::field::test_fields::u64_test_field::U64TestField;
39+
use crate::polynomial::Polynomial;
40+
use proptest::prelude::*;
41+
42+
use super::*;
43+
44+
const MODULUS: u64 = 0xFFFFFFFF00000001;
45+
type F = U64TestField<MODULUS>;
46+
type FE = FieldElement<F>;
47+
48+
prop_compose! {
49+
fn powers_of_two(max_exp: u8)(exp in 1..max_exp) -> usize { 1 << exp }
50+
// max_exp cannot be multiple of the bits that represent a usize, generally 64 or 32.
51+
// also it can't exceed the test field's two-adicity.
52+
}
53+
prop_compose! {
54+
fn field_element()(num in any::<u64>().prop_filter("Avoid null polynomial", |x| x != &0)) -> FE {
55+
FE::from(num)
56+
}
57+
}
58+
prop_compose! {
59+
fn offset()(num in 1..MODULUS - 1) -> FE { FE::from(num) }
60+
}
61+
prop_compose! {
62+
fn field_vec(max_exp: u8)(elem in field_element(), size in powers_of_two(max_exp)) -> Vec<FE> {
63+
vec![elem; size]
64+
}
65+
}
66+
prop_compose! {
67+
// non-power-of-two sized vector
68+
fn unsuitable_field_vec(max_exp: u8)(elem in field_element(), size in powers_of_two(max_exp)) -> Vec<FE> {
69+
vec![elem; size + 1]
70+
}
71+
}
72+
73+
proptest! {
74+
// Property-based test that ensures FFT gives same result as a naive polynomial evaluation.
75+
#[test]
76+
fn test_fft_matches_naive_evaluation(coeffs in field_vec(8)) {
77+
let poly = Polynomial::new(&coeffs[..]);
78+
let result = evaluate_poly(&poly).unwrap();
79+
80+
let omega = F::get_root_of_unity(log2(poly.coefficients().len()).unwrap()).unwrap();
81+
let twiddles_iter = (0..poly.coefficients().len() as u64).map(|i| omega.pow(i));
82+
let expected: Vec<FE> = twiddles_iter.map(|x| poly.evaluate(&x)).collect();
83+
84+
prop_assert_eq!(result, expected);
85+
}
86+
}
87+
88+
proptest! {
89+
// Property-based test that ensures FFT with coset gives same result as a naive polynomial evaluation.
90+
#[test]
91+
fn test_fft_with_coset_matches_naive_evaluation(coeffs in field_vec(8), blowup_factor in powers_of_two(3)) {
92+
let domain_size = coeffs.len() * blowup_factor;
93+
94+
let poly = Polynomial::new(&coeffs[..]);
95+
96+
let offset = FE::new(2);
97+
let result = evaluate_poly_with_offset(&poly, &offset, blowup_factor).unwrap();
98+
99+
let omega = F::get_root_of_unity(log2(domain_size).unwrap()).unwrap();
100+
let twiddles_iter = (0..domain_size as u64).map(|i| omega.pow(i) * &offset);
101+
let expected: Vec<FE> = twiddles_iter.map(|x| poly.evaluate(&(x))).collect();
102+
103+
prop_assert_eq!(result, expected);
104+
}
105+
}
106+
107+
proptest! {
108+
// Property-based test that ensures IFFT is the inverse operation of FFT.
109+
#[test]
110+
fn test_ifft_composed_fft_is_identity(coeffs in field_vec(8)) {
111+
let poly_to_evaluate = Polynomial::new(&coeffs[..]);
112+
let evaluation_result = evaluate_poly(&poly_to_evaluate).unwrap();
113+
114+
let recovered_coefficients = interpolate_poly(&evaluation_result).unwrap();
115+
prop_assert_eq!(recovered_coefficients, coeffs);
116+
}
117+
}
118+
119+
proptest! {
120+
// Property-based test that ensures FFT won't work with a degree 0 polynomial.
121+
#[test]
122+
fn test_fft_constant_poly(elem in field_element()) {
123+
let poly_to_evaluate = Polynomial::new(&[elem]);
124+
let result = evaluate_poly(&poly_to_evaluate);
125+
126+
prop_assert!(matches!(result, Err(FFTError::RootOfUnityError(_, k)) if k == 0));
127+
}
128+
}
129+
}

math/src/polynomial.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ impl<F: IsField> Polynomial<FieldElement<F>> {
146146
Polynomial::new(&coefficients)
147147
}
148148
}
149+
150+
pub fn scale(&self, factor: &FieldElement<F>) -> Self {
151+
let scaled_coefficients = self
152+
.coefficients
153+
.iter()
154+
.enumerate()
155+
.map(|(i, coeff)| factor.pow(i) * coeff)
156+
.collect();
157+
Self {
158+
coefficients: scaled_coefficients,
159+
}
160+
}
149161
}
150162

151163
impl<F: IsField> ops::Add<&Polynomial<FieldElement<F>>> for &Polynomial<FieldElement<F>> {

0 commit comments

Comments
 (0)