Skip to main content

oxiz_math/polynomial/
sparse_ops.rs

1//! Sparse Polynomial Operations.
2//!
3//! Optimized operations for sparse multivariate polynomials where most
4//! coefficients are zero. Uses efficient data structures and algorithms
5//! tailored for sparsity.
6//!
7//! ## Optimizations
8//!
9//! - **Sparse storage**: Only store non-zero terms
10//! - **Fast lookups**: Hash-based term access
11//! - **Efficient multiplication**: Skip zero terms
12//! - **Memory efficiency**: Minimal allocation for sparse inputs
13//!
14//! ## References
15//!
16//! - Monagan & Pearce: "Sparse Polynomial Division" (2011)
17//! - Z3's `math/polynomial/polynomial.cpp` (sparse mode)
18
19use super::{Monomial, MonomialOrder, Polynomial, Term, Var};
20#[allow(unused_imports)]
21use crate::prelude::*;
22use num_rational::BigRational;
23use num_traits::{One, Zero};
24
25/// Configuration for sparse operations.
26#[derive(Debug, Clone)]
27pub struct SparseConfig {
28    /// Threshold for considering a polynomial sparse (ratio of non-zero to total possible terms).
29    pub sparsity_threshold: f64,
30    /// Enable fast multiplication for very sparse polynomials.
31    pub enable_fast_mul: bool,
32    /// Maximum terms before switching to dense representation.
33    pub max_sparse_terms: usize,
34}
35
36impl Default for SparseConfig {
37    fn default() -> Self {
38        Self {
39            sparsity_threshold: 0.1,
40            enable_fast_mul: true,
41            max_sparse_terms: 10000,
42        }
43    }
44}
45
46/// Statistics for sparse operations.
47#[derive(Debug, Clone, Default)]
48pub struct SparseStats {
49    /// Number of zero terms skipped.
50    pub zero_terms_skipped: u64,
51    /// Memory saved (estimated bytes).
52    pub memory_saved: u64,
53    /// Fast multiplications performed.
54    pub fast_muls: u64,
55}
56
57/// Sparse polynomial operations engine.
58pub struct SparseOps {
59    /// Configuration.
60    config: SparseConfig,
61    /// Statistics.
62    stats: SparseStats,
63}
64
65impl SparseOps {
66    /// Create a new sparse operations engine.
67    pub fn new(config: SparseConfig) -> Self {
68        Self {
69            config,
70            stats: SparseStats::default(),
71        }
72    }
73
74    /// Create with default configuration.
75    pub fn default_config() -> Self {
76        Self::new(SparseConfig::default())
77    }
78
79    /// Check if a polynomial is sparse according to configuration.
80    pub fn is_sparse(&self, p: &Polynomial) -> bool {
81        if p.num_terms() > self.config.max_sparse_terms {
82            return false;
83        }
84
85        // Estimate total possible terms based on max degree and variables
86        let num_vars = p.vars().len();
87        let max_degree = p.total_degree() as usize;
88
89        if num_vars == 0 {
90            return false;
91        }
92
93        // For multivariate, approximate: (degree + vars choose vars)
94        // Simplified: if fewer than threshold * degree^vars terms
95        let approx_dense_size = if num_vars <= 3 {
96            max_degree.pow(num_vars as u32)
97        } else {
98            // For many variables, just use linear estimate
99            max_degree * num_vars * 100
100        };
101
102        let sparsity = p.num_terms() as f64 / approx_dense_size as f64;
103        sparsity < self.config.sparsity_threshold
104    }
105
106    /// Sparse multiplication optimized for very sparse inputs.
107    pub fn sparse_mul(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
108        if !self.config.enable_fast_mul {
109            return p * q; // Use standard multiplication
110        }
111
112        self.stats.fast_muls += 1;
113
114        // Use hash map for efficient term collection
115        let mut term_map: FxHashMap<Monomial, BigRational> = FxHashMap::default();
116
117        for term_p in p.terms() {
118            for term_q in q.terms() {
119                // Multiply monomials
120                let mono = term_p.monomial.mul(&term_q.monomial);
121
122                // Multiply coefficients
123                let coeff = &term_p.coeff * &term_q.coeff;
124
125                if !coeff.is_zero() {
126                    // Add to existing term or insert new
127                    term_map
128                        .entry(mono)
129                        .and_modify(|c| *c = c.clone() + &coeff)
130                        .or_insert(coeff);
131                } else {
132                    self.stats.zero_terms_skipped += 1;
133                }
134            }
135        }
136
137        // Convert map to term vector, filtering zeros
138        let mut terms: Vec<Term> = term_map
139            .iter()
140            .filter_map(|(mono, coeff)| {
141                if !coeff.is_zero() {
142                    Some(Term::new(coeff.clone(), mono.clone()))
143                } else {
144                    self.stats.zero_terms_skipped += 1;
145                    None
146                }
147            })
148            .collect();
149
150        // Sort by monomial order
151        terms.sort_by(|a, b| MonomialOrder::GRevLex.compare(&a.monomial, &b.monomial));
152
153        Polynomial::from_terms(terms, MonomialOrder::GRevLex)
154    }
155
156    /// Sparse addition that skips zero terms.
157    pub fn sparse_add(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
158        let mut term_map: FxHashMap<Monomial, BigRational> = FxHashMap::default();
159
160        // Add terms from p
161        for term in p.terms() {
162            term_map.insert(term.monomial.clone(), term.coeff.clone());
163        }
164
165        // Add terms from q
166        for term in q.terms() {
167            term_map
168                .entry(term.monomial.clone())
169                .and_modify(|c| *c = c.clone() + &term.coeff)
170                .or_insert(term.coeff.clone());
171        }
172
173        // Filter zeros and convert
174        let terms: Vec<Term> = term_map
175            .iter()
176            .filter_map(|(mono, coeff)| {
177                if !coeff.is_zero() {
178                    Some(Term::new(coeff.clone(), mono.clone()))
179                } else {
180                    self.stats.zero_terms_skipped += 1;
181                    None
182                }
183            })
184            .collect();
185
186        Polynomial::from_terms(terms, MonomialOrder::GRevLex)
187    }
188
189    /// Evaluate sparse polynomial at given point (hash-based).
190    pub fn sparse_eval(
191        &mut self,
192        p: &Polynomial,
193        point: &FxHashMap<Var, BigRational>,
194    ) -> BigRational {
195        let mut result = BigRational::zero();
196
197        for term in p.terms() {
198            // Evaluate monomial
199            let mut mono_val = BigRational::one();
200
201            for vp in term.monomial.vars() {
202                if let Some(val) = point.get(&vp.var) {
203                    // Compute val^power
204                    let powered = self.power_rational(val, vp.power);
205                    mono_val *= powered;
206                } else {
207                    // Variable not in point - use 0 (makes monomial zero)
208                    self.stats.zero_terms_skipped += 1;
209                    mono_val = BigRational::zero();
210                    break;
211                }
212            }
213
214            result += &term.coeff * &mono_val;
215        }
216
217        result
218    }
219
220    /// Raise rational to integer power.
221    fn power_rational(&self, base: &BigRational, exp: u32) -> BigRational {
222        if exp == 0 {
223            BigRational::one()
224        } else if exp == 1 {
225            base.clone()
226        } else {
227            let mut result = BigRational::one();
228            let mut b = base.clone();
229            let mut e = exp;
230
231            // Fast exponentiation
232            while e > 0 {
233                if e % 2 == 1 {
234                    result *= &b;
235                }
236                b = &b * &b;
237                e /= 2;
238            }
239
240            result
241        }
242    }
243
244    /// Estimate memory usage of polynomial.
245    pub fn estimate_memory(&self, p: &Polynomial) -> usize {
246        // Rough estimate: each term is ~100 bytes (monomial + coeff + overhead)
247        p.num_terms() * 100
248    }
249
250    /// Estimate memory savings from sparse representation.
251    pub fn estimate_savings(&mut self, p: &Polynomial) -> usize {
252        let num_vars = p.vars().len();
253        let max_degree = p.total_degree() as usize;
254
255        // Dense representation would have degree^vars terms
256        let dense_terms = if num_vars <= 3 {
257            max_degree.pow(num_vars as u32)
258        } else {
259            max_degree * num_vars * 100
260        };
261
262        let sparse_memory = self.estimate_memory(p);
263        let dense_memory = dense_terms * 100;
264
265        let savings = dense_memory.saturating_sub(sparse_memory);
266        self.stats.memory_saved += savings as u64;
267        savings
268    }
269
270    /// Get statistics.
271    pub fn stats(&self) -> &SparseStats {
272        &self.stats
273    }
274
275    /// Reset statistics.
276    pub fn reset_stats(&mut self) {
277        self.stats = SparseStats::default();
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use num_bigint::BigInt;
285
286    fn rat(n: i64) -> BigRational {
287        BigRational::from_integer(BigInt::from(n))
288    }
289
290    #[test]
291    fn test_sparse_ops_creation() {
292        let ops = SparseOps::default_config();
293        assert_eq!(ops.stats().fast_muls, 0);
294    }
295
296    #[test]
297    fn test_is_sparse() {
298        let ops = SparseOps::default_config();
299
300        // Sparse: x^5 + y^5 (2 terms in 2 variables, degree 5)
301        // With degree 5 and 2 vars, approx_dense = 5^2 = 25, sparsity = 2/25 = 0.08 < 0.1
302        let sparse = Polynomial::from_coeffs_int(&[(1, &[(0, 5)]), (1, &[(1, 5)])]);
303
304        assert!(ops.is_sparse(&sparse));
305
306        // Constant is not considered sparse (num_vars = 0)
307        let constant = Polynomial::constant(BigRational::from_integer(BigInt::from(5)));
308        assert!(!ops.is_sparse(&constant));
309    }
310
311    #[test]
312    fn test_sparse_mul() {
313        let mut ops = SparseOps::default_config();
314
315        // x * y
316        let p = Polynomial::from_var(0);
317        let q = Polynomial::from_var(1);
318
319        let result = ops.sparse_mul(&p, &q);
320
321        assert_eq!(result.total_degree(), 2);
322        assert_eq!(ops.stats().fast_muls, 1);
323    }
324
325    #[test]
326    fn test_sparse_add() {
327        let mut ops = SparseOps::default_config();
328
329        // x + y
330        let p = Polynomial::from_var(0);
331        let q = Polynomial::from_var(1);
332
333        let result = ops.sparse_add(&p, &q);
334
335        assert_eq!(result.num_terms(), 2);
336    }
337
338    #[test]
339    fn test_sparse_eval() {
340        let mut ops = SparseOps::default_config();
341
342        // 2x + 3y
343        let p = Polynomial::from_coeffs_int(&[(2, &[(0, 1)]), (3, &[(1, 1)])]);
344
345        let mut point = FxHashMap::default();
346        point.insert(0, rat(5)); // x = 5
347        point.insert(1, rat(2)); // y = 2
348
349        let result = ops.sparse_eval(&p, &point);
350
351        // 2*5 + 3*2 = 16
352        assert_eq!(result, rat(16));
353    }
354
355    #[test]
356    fn test_power_rational() {
357        let ops = SparseOps::default_config();
358
359        assert_eq!(ops.power_rational(&rat(2), 0), rat(1));
360        assert_eq!(ops.power_rational(&rat(2), 1), rat(2));
361        assert_eq!(ops.power_rational(&rat(2), 3), rat(8));
362    }
363
364    #[test]
365    fn test_estimate_memory() {
366        let ops = SparseOps::default_config();
367
368        let p = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[(1, 1)])]);
369
370        let memory = ops.estimate_memory(&p);
371        assert!(memory > 0);
372    }
373
374    #[test]
375    fn test_estimate_savings() {
376        let mut ops = SparseOps::default_config();
377
378        // Very sparse polynomial
379        let p = Polynomial::from_coeffs_int(&[
380            (1, &[(0, 10)]), // x^10
381            (1, &[]),        // constant
382        ]);
383
384        let savings = ops.estimate_savings(&p);
385        assert!(savings > 0);
386    }
387}