snarkvm_algorithms/fft/polynomial/
sparse.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! A sparse polynomial represented in coefficient form.
17
18use crate::fft::{EvaluationDomain, Evaluations, Polynomial};
19use snarkvm_fields::{Field, PrimeField};
20use snarkvm_utilities::serialize::*;
21
22use std::{collections::BTreeMap, fmt};
23
24/// Stores a sparse polynomial in coefficient form.
25#[derive(Clone, PartialEq, Eq, Hash, Default, CanonicalSerialize, CanonicalDeserialize)]
26#[must_use]
27pub struct SparsePolynomial<F: Field> {
28    /// The coefficient a_i of `x^i` is stored as (i, a_i) in `self.coeffs`.
29    /// the entries in `self.coeffs` are sorted in increasing order of `i`.
30    coeffs: BTreeMap<usize, F>,
31}
32
33impl<F: Field> fmt::Debug for SparsePolynomial<F> {
34    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
35        for (i, coeff) in self.coeffs.iter().filter(|(_, c)| !c.is_zero()) {
36            if *i == 0 {
37                write!(f, "\n{coeff:?}")?;
38            } else if *i == 1 {
39                write!(f, " + \n{coeff:?} * x")?;
40            } else {
41                write!(f, " + \n{coeff:?} * x^{i}")?;
42            }
43        }
44        Ok(())
45    }
46}
47
48impl<F: Field> SparsePolynomial<F> {
49    /// Returns the zero polynomial.
50    pub fn zero() -> Self {
51        Self { coeffs: BTreeMap::new() }
52    }
53
54    /// Checks if the given polynomial is zero.
55    pub fn is_zero(&self) -> bool {
56        self.coeffs.is_empty() || self.coeffs.iter().all(|(_, c)| c.is_zero())
57    }
58
59    /// Constructs a new polynomial from a list of coefficients.
60    pub fn from_coefficients_slice(coeffs: &[(usize, F)]) -> Self {
61        Self::from_coefficients(coeffs.iter().copied())
62    }
63
64    /// Constructs a new polynomial from a list of coefficients.
65    pub fn from_coefficients(coeffs: impl IntoIterator<Item = (usize, F)>) -> Self {
66        let coeffs: BTreeMap<_, _> = coeffs.into_iter().filter(|(_, c)| !c.is_zero()).collect();
67        Self { coeffs }
68    }
69
70    pub fn coeffs(&self) -> impl Iterator<Item = (&usize, &F)> {
71        self.coeffs.iter()
72    }
73
74    /// Returns the degree of the polynomial.
75    pub fn degree(&self) -> usize {
76        if self.is_zero() {
77            0
78        } else {
79            let last = self.coeffs.iter().max();
80            assert!(last.map_or(false, |(_, c)| !c.is_zero()));
81            *last.unwrap().0
82        }
83    }
84
85    /// Evaluates `self` at the given `point` in the field.
86    pub fn evaluate(&self, point: F) -> F {
87        if self.is_zero() {
88            return F::zero();
89        }
90        let mut total = F::zero();
91        for (i, c) in &self.coeffs {
92            total += *c * point.pow([*i as u64]);
93        }
94        total
95    }
96
97    /// Perform a naive n^2 multiplication of `self` by `other`.
98    pub fn mul(&self, other: &Self) -> Self {
99        if self.is_zero() || other.is_zero() {
100            SparsePolynomial::zero()
101        } else {
102            let mut result = std::collections::BTreeMap::new();
103            for (i, self_coeff) in self.coeffs.iter() {
104                for (j, other_coeff) in other.coeffs.iter() {
105                    let cur_coeff = result.entry(i + j).or_insert_with(F::zero);
106                    *cur_coeff += *self_coeff * other_coeff;
107                }
108            }
109            SparsePolynomial::from_coefficients(result)
110        }
111    }
112}
113
114impl<F: PrimeField> SparsePolynomial<F> {
115    /// Evaluate `self` over `domain`.
116    pub fn evaluate_over_domain_by_ref(&self, domain: EvaluationDomain<F>) -> Evaluations<F> {
117        let poly: Polynomial<'_, F> = self.into();
118        Polynomial::<F>::evaluate_over_domain(poly, domain)
119        // unimplemented!("current implementation does not produce evals in
120        // correct order")
121    }
122
123    /// Evaluate `self` over `domain`.
124    pub fn evaluate_over_domain(self, domain: EvaluationDomain<F>) -> Evaluations<F> {
125        let poly: Polynomial<'_, F> = self.into();
126        Polynomial::<F>::evaluate_over_domain(poly, domain)
127        // unimplemented!("current implementation does not produce evals in
128        // correct order")
129    }
130}
131impl<F: PrimeField> core::ops::MulAssign<F> for SparsePolynomial<F> {
132    fn mul_assign(&mut self, other: F) {
133        if other.is_zero() {
134            *self = Self::zero()
135        } else {
136            for coeff in self.coeffs.values_mut() {
137                *coeff *= other;
138            }
139        }
140    }
141}
142
143impl<F: PrimeField> core::ops::Mul<F> for &'_ SparsePolynomial<F> {
144    type Output = SparsePolynomial<F>;
145
146    fn mul(self, other: F) -> Self::Output {
147        let mut result = self.clone();
148        result *= other;
149        result
150    }
151}
152
153impl<'a, F: PrimeField> core::ops::AddAssign<&'a Self> for SparsePolynomial<F> {
154    fn add_assign(&mut self, other: &'a Self) {
155        let mut result = other.clone();
156        for (i, coeff) in self.coeffs.iter() {
157            let cur_coeff = result.coeffs.entry(*i).or_insert_with(F::zero);
158            *cur_coeff += coeff;
159        }
160        *self = Self::from_coefficients(result.coeffs.into_iter().filter(|(_, f)| !f.is_zero()));
161    }
162}
163
164impl<'a, F: PrimeField> core::ops::AddAssign<(F, &'a Self)> for SparsePolynomial<F> {
165    fn add_assign(&mut self, (f, other): (F, &'a Self)) {
166        let mut result = other.clone();
167        for (i, coeff) in self.coeffs.iter() {
168            let cur_coeff = result.coeffs.entry(*i).or_insert_with(F::zero);
169            *cur_coeff += f * coeff;
170        }
171        *self = Self::from_coefficients(result.coeffs.into_iter().filter(|(_, f)| !f.is_zero()))
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use crate::fft::{DensePolynomial, EvaluationDomain, SparsePolynomial};
178    use snarkvm_curves::bls12_377::Fr;
179    use snarkvm_fields::One;
180
181    #[test]
182    fn evaluate_over_domain() {
183        for size in 2..10 {
184            let domain_size = 1 << size;
185            let domain = EvaluationDomain::new(domain_size).unwrap();
186            let two = Fr::one() + Fr::one();
187            let sparse_poly = SparsePolynomial::from_coefficients(vec![(0, two), (1, two)]);
188            let evals1 = sparse_poly.evaluate_over_domain_by_ref(domain);
189
190            let dense_poly: DensePolynomial<Fr> = sparse_poly.into();
191            let evals2 = dense_poly.clone().evaluate_over_domain(domain);
192            assert_eq!(evals1.clone().interpolate(), evals2.clone().interpolate());
193            assert_eq!(evals1.interpolate(), dense_poly);
194            assert_eq!(evals2.interpolate(), dense_poly);
195        }
196    }
197}