Skip to content

Commit 3a04406

Browse files
Feat/fast division (#1001)
* feat(fft): add fast multiplication * chore: add fast_mul benchmark * impl suggestion * chore: impl suggestion * chore: add fast_mul benchmark * feat(fft): fast division * refacto: proper error handling * refacto: rename inversion function * test: add tests for fast division helper functions * doc: explain where fast division is taken from --------- Co-authored-by: Diego K <[email protected]>
1 parent c9d02b5 commit 3a04406

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

crates/math/benches/polynomials/polynomial.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ pub fn polynomial_benchmarks(c: &mut Criterion) {
5959
bench.iter(|| black_box(&x_poly) * black_box(&y_poly));
6060
});
6161

62+
let y_poly = rand_complex_mersenne_poly(big_order - 2);
63+
64+
group.bench_function("fast div big poly", |bench| {
65+
bench
66+
.iter(|| black_box(&x_poly).fast_division::<Degree2ExtensionField>(black_box(&y_poly)));
67+
});
68+
69+
group.bench_function("slow div big poly", |bench| {
70+
bench.iter(|| black_box(x_poly.clone()).long_division_with_remainder(black_box(&y_poly)));
71+
});
72+
6273
group.bench_function("div", |bench| {
6374
let x_poly = rand_poly(order);
6475
let y_poly = rand_poly(order);

crates/math/src/fft/polynomial.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::fft::errors::FFTError;
22

3+
use crate::field::errors::FieldError;
34
use crate::field::traits::{IsField, IsSubFieldOf};
45
use crate::{
56
field::{
@@ -104,6 +105,10 @@ impl<E: IsField> Polynomial<FieldElement<E>> {
104105
/// It's faster than naive multiplication when the degree of the polynomials is large enough (>=2**6).
105106
/// This works best with polynomials whose highest degree is equal to a power of 2 - 1.
106107
/// Will return an error if the degree of the resulting polynomial is greater than 2**63.
108+
///
109+
/// This is an implementation of the fast division algorithm from
110+
/// [Gathen's book](https://www.cambridge.org/core/books/modern-computer-algebra/DB3563D4013401734851CF683D2F03F0)
111+
/// chapter 9
107112
pub fn fast_fft_multiplication<F: IsFFTField + IsSubFieldOf<E>>(
108113
&self,
109114
other: &Self,
@@ -115,6 +120,69 @@ impl<E: IsField> Polynomial<FieldElement<E>> {
115120

116121
Polynomial::interpolate_fft::<F>(&r)
117122
}
123+
124+
/// Divides two polynomials with remainder.
125+
/// This is faster than the naive division if the degree of the divisor
126+
/// is greater than the degree of the dividend and both degrees are large enough.
127+
pub fn fast_division<F: IsSubFieldOf<E> + IsFFTField>(
128+
&self,
129+
divisor: &Self,
130+
) -> Result<(Self, Self), FFTError> {
131+
let n = self.degree();
132+
let m = divisor.degree();
133+
if divisor.coefficients.is_empty()
134+
|| divisor
135+
.coefficients
136+
.iter()
137+
.all(|c| c == &FieldElement::zero())
138+
{
139+
return Err(FieldError::DivisionByZero.into());
140+
}
141+
if n < m {
142+
return Ok((Self::zero(), self.clone()));
143+
}
144+
let d = n - m; // Degree of the quotient
145+
let a_rev = self.reverse(n);
146+
let b_rev = divisor.reverse(m);
147+
let inv_b_rev = b_rev.invert_polynomial_mod::<F>(d + 1)?;
148+
let q = a_rev
149+
.fast_fft_multiplication::<F>(&inv_b_rev)?
150+
.truncate(d + 1)
151+
.reverse(d);
152+
153+
let r = self - q.fast_fft_multiplication::<F>(divisor)?;
154+
Ok((q, r))
155+
}
156+
157+
/// Computes the inverse of polynomial P modulo x^k using Newton iteration.
158+
/// P must have an invertible constant term.
159+
pub fn invert_polynomial_mod<F: IsSubFieldOf<E> + IsFFTField>(
160+
&self,
161+
k: usize,
162+
) -> Result<Self, FFTError> {
163+
if self.coefficients.is_empty()
164+
|| self.coefficients.iter().all(|c| c == &FieldElement::zero())
165+
{
166+
return Err(FieldError::DivisionByZero.into());
167+
}
168+
let mut q = Self::new(&[self.coefficients[0].inv()?]);
169+
let mut current_precision = 1;
170+
171+
let two = Self::new(&[FieldElement::<F>::one() + FieldElement::one()]);
172+
while current_precision < k {
173+
current_precision *= 2;
174+
let temp = self
175+
.fast_fft_multiplication::<F>(&q)?
176+
.truncate(current_precision);
177+
let correction = &two - temp;
178+
q = q
179+
.fast_fft_multiplication::<F>(&correction)?
180+
.truncate(current_precision);
181+
}
182+
183+
// Final truncation to desired degree k
184+
Ok(q.truncate(k))
185+
}
118186
}
119187

120188
pub fn compose_fft<F, E>(
@@ -273,6 +341,11 @@ mod tests {
273341
vec
274342
}
275343
}
344+
prop_compose! {
345+
fn non_empty_field_vec(max_exp: u8)(vec in collection::vec(field_element(), 1 << max_exp)) -> Vec<FE> {
346+
vec
347+
}
348+
}
276349
prop_compose! {
277350
fn non_power_of_two_sized_field_vec(max_exp: u8)(vec in collection::vec(field_element(), 2..1<<max_exp).prop_filter("Avoid polynomials of size power of two", |vec| !vec.len().is_power_of_two())) -> Vec<FE> {
278351
vec
@@ -283,6 +356,11 @@ mod tests {
283356
Polynomial::new(&coeffs)
284357
}
285358
}
359+
prop_compose! {
360+
fn non_zero_poly(max_exp: u8)(coeffs in non_empty_field_vec(max_exp)) -> Polynomial<FE> {
361+
Polynomial::new(&coeffs)
362+
}
363+
}
286364
prop_compose! {
287365
fn poly_with_non_power_of_two_coeffs(max_exp: u8)(coeffs in non_power_of_two_sized_field_vec(max_exp)) -> Polynomial<FE> {
288366
Polynomial::new(&coeffs)
@@ -336,6 +414,17 @@ mod tests {
336414
fn test_fft_multiplication_works(poly in poly(7), other in poly(7)) {
337415
prop_assert_eq!(poly.fast_fft_multiplication::<F>(&other).unwrap(), poly * other);
338416
}
417+
418+
#[test]
419+
fn test_fft_division_works(poly in non_zero_poly(7), other in non_zero_poly(7)) {
420+
prop_assert_eq!(poly.fast_division::<F>(&other).unwrap(), poly.long_division_with_remainder(&other));
421+
}
422+
423+
#[test]
424+
fn test_invert_polynomial_mod_works(poly in non_zero_poly(7), k in powers_of_two(4)) {
425+
let inverted_poly = poly.invert_polynomial_mod::<F>(k).unwrap();
426+
prop_assert_eq!((poly * inverted_poly).truncate(k), Polynomial::new(&[FE::one()]));
427+
}
339428
}
340429

341430
#[test]
@@ -371,6 +460,11 @@ mod tests {
371460
vec
372461
}
373462
}
463+
prop_compose! {
464+
fn non_empty_field_vec(max_exp: u8)(vec in collection::vec(field_element(), 1 << max_exp)) -> Vec<FE> {
465+
vec
466+
}
467+
}
374468
prop_compose! {
375469
fn non_power_of_two_sized_field_vec(max_exp: u8)(vec in collection::vec(field_element(), 2..1<<max_exp).prop_filter("Avoid polynomials of size power of two", |vec| !vec.len().is_power_of_two())) -> Vec<FE> {
376470
vec
@@ -381,6 +475,11 @@ mod tests {
381475
Polynomial::new(&coeffs)
382476
}
383477
}
478+
prop_compose! {
479+
fn non_zero_poly(max_exp: u8)(coeffs in non_empty_field_vec(max_exp)) -> Polynomial<FE> {
480+
Polynomial::new(&coeffs)
481+
}
482+
}
384483
prop_compose! {
385484
fn poly_with_non_power_of_two_coeffs(max_exp: u8)(coeffs in non_power_of_two_sized_field_vec(max_exp)) -> Polynomial<FE> {
386485
Polynomial::new(&coeffs)
@@ -436,6 +535,17 @@ mod tests {
436535
fn test_fft_multiplication_works(poly in poly(7), other in poly(7)) {
437536
prop_assert_eq!(poly.fast_fft_multiplication::<F>(&other).unwrap(), poly * other);
438537
}
538+
539+
#[test]
540+
fn test_fft_division_works(poly in poly(7), other in non_zero_poly(7)) {
541+
prop_assert_eq!(poly.fast_division::<F>(&other).unwrap(), poly.long_division_with_remainder(&other));
542+
}
543+
544+
#[test]
545+
fn test_invert_polynomial_mod_works(poly in non_zero_poly(7), k in powers_of_two(4)) {
546+
let inverted_poly = poly.invert_polynomial_mod::<F>(k).unwrap();
547+
prop_assert_eq!((poly * inverted_poly).truncate(k), Polynomial::new(&[FE::one()]));
548+
}
439549
}
440550
}
441551

crates/math/src/polynomial/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,20 @@ impl<F: IsField> Polynomial<FieldElement<F>> {
335335
.collect(),
336336
}
337337
}
338+
339+
pub fn truncate(&self, k: usize) -> Self {
340+
if k == 0 {
341+
Self::zero()
342+
} else {
343+
Self::new(&self.coefficients[0..k.min(self.coefficients.len())])
344+
}
345+
}
346+
pub fn reverse(&self, d: usize) -> Self {
347+
let mut coeffs = self.coefficients.clone();
348+
coeffs.resize(d + 1, FieldElement::zero());
349+
coeffs.reverse();
350+
Self::new(&coeffs)
351+
}
338352
}
339353

340354
impl<F: IsPrimeField> Polynomial<FieldElement<F>> {
@@ -1277,6 +1291,21 @@ mod tests {
12771291
assert_eq!(dpdx, Polynomial::new(&[FE::new(0)]));
12781292
}
12791293

1294+
#[test]
1295+
fn test_reverse() {
1296+
let p = Polynomial::new(&[FE::new(3), FE::new(2), FE::new(1)]);
1297+
assert_eq!(
1298+
p.reverse(3),
1299+
Polynomial::new(&[FE::new(0), FE::new(1), FE::new(2), FE::new(3)])
1300+
);
1301+
}
1302+
1303+
#[test]
1304+
fn test_truncate() {
1305+
let p = Polynomial::new(&[FE::new(3), FE::new(2), FE::new(1)]);
1306+
assert_eq!(p.truncate(2), Polynomial::new(&[FE::new(3), FE::new(2)]));
1307+
}
1308+
12801309
#[test]
12811310
fn test_print_as_sage_poly() {
12821311
let p = Polynomial::new(&[FE::new(1), FE::new(2), FE::new(3)]);

0 commit comments

Comments
 (0)