oxiz_math/polynomial/
sparse_ops.rs1use super::{Monomial, MonomialOrder, Polynomial, Term, Var};
20#[allow(unused_imports)]
21use crate::prelude::*;
22use num_rational::BigRational;
23use num_traits::{One, Zero};
24
25#[derive(Debug, Clone)]
27pub struct SparseConfig {
28 pub sparsity_threshold: f64,
30 pub enable_fast_mul: bool,
32 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#[derive(Debug, Clone, Default)]
48pub struct SparseStats {
49 pub zero_terms_skipped: u64,
51 pub memory_saved: u64,
53 pub fast_muls: u64,
55}
56
57pub struct SparseOps {
59 config: SparseConfig,
61 stats: SparseStats,
63}
64
65impl SparseOps {
66 pub fn new(config: SparseConfig) -> Self {
68 Self {
69 config,
70 stats: SparseStats::default(),
71 }
72 }
73
74 pub fn default_config() -> Self {
76 Self::new(SparseConfig::default())
77 }
78
79 pub fn is_sparse(&self, p: &Polynomial) -> bool {
81 if p.num_terms() > self.config.max_sparse_terms {
82 return false;
83 }
84
85 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 let approx_dense_size = if num_vars <= 3 {
96 max_degree.pow(num_vars as u32)
97 } else {
98 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 pub fn sparse_mul(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
108 if !self.config.enable_fast_mul {
109 return p * q; }
111
112 self.stats.fast_muls += 1;
113
114 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 let mono = term_p.monomial.mul(&term_q.monomial);
121
122 let coeff = &term_p.coeff * &term_q.coeff;
124
125 if !coeff.is_zero() {
126 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 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 terms.sort_by(|a, b| MonomialOrder::GRevLex.compare(&a.monomial, &b.monomial));
152
153 Polynomial::from_terms(terms, MonomialOrder::GRevLex)
154 }
155
156 pub fn sparse_add(&mut self, p: &Polynomial, q: &Polynomial) -> Polynomial {
158 let mut term_map: FxHashMap<Monomial, BigRational> = FxHashMap::default();
159
160 for term in p.terms() {
162 term_map.insert(term.monomial.clone(), term.coeff.clone());
163 }
164
165 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 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 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 let mut mono_val = BigRational::one();
200
201 for vp in term.monomial.vars() {
202 if let Some(val) = point.get(&vp.var) {
203 let powered = self.power_rational(val, vp.power);
205 mono_val *= powered;
206 } else {
207 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 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 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 pub fn estimate_memory(&self, p: &Polynomial) -> usize {
246 p.num_terms() * 100
248 }
249
250 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 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 pub fn stats(&self) -> &SparseStats {
272 &self.stats
273 }
274
275 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 let sparse = Polynomial::from_coeffs_int(&[(1, &[(0, 5)]), (1, &[(1, 5)])]);
303
304 assert!(ops.is_sparse(&sparse));
305
306 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 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 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 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)); point.insert(1, rat(2)); let result = ops.sparse_eval(&p, &point);
350
351 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 let p = Polynomial::from_coeffs_int(&[
380 (1, &[(0, 10)]), (1, &[]), ]);
383
384 let savings = ops.estimate_savings(&p);
385 assert!(savings > 0);
386 }
387}