Skip to main content

oxiz_nlsat/
root_hints.rs

1//! Root approximation hints cache for faster root isolation.
2//!
3//! This module caches approximate root locations to speed up repeated
4//! root isolation operations. When solving similar problems or revisiting
5//! polynomials during backtracking, cached hints provide initial bounds
6//! that dramatically reduce isolation time.
7//!
8//! Key features:
9//! - **Approximate Root Storage**: Store interval approximations for roots
10//! - **Fast Lookup**: Hash-based O(1) lookup by polynomial
11//! - **Refinement Support**: Track refinement levels for progressive precision
12//! - **LRU Eviction**: Automatic eviction of old hints
13//!
14//! Reference: Techniques from numerical root-finding and interval arithmetic
15
16use num_rational::BigRational;
17use oxiz_math::polynomial::{Polynomial, Var};
18use rustc_hash::FxHashMap;
19use std::collections::{VecDeque, hash_map::DefaultHasher};
20use std::hash::{Hash, Hasher};
21
22/// An approximate root location with confidence bounds.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct RootHint {
25    /// Lower bound of root location.
26    pub lower: BigRational,
27    /// Upper bound of root location.
28    pub upper: BigRational,
29    /// Refinement level (higher = more precise).
30    pub refinement_level: u32,
31}
32
33impl RootHint {
34    /// Create a new root hint.
35    pub fn new(lower: BigRational, upper: BigRational, refinement_level: u32) -> Self {
36        Self {
37            lower,
38            upper,
39            refinement_level,
40        }
41    }
42
43    /// Get the interval width.
44    pub fn width(&self) -> BigRational {
45        &self.upper - &self.lower
46    }
47
48    /// Get the midpoint of the hint interval.
49    pub fn midpoint(&self) -> BigRational {
50        (&self.lower + &self.upper) / BigRational::from_integer(2.into())
51    }
52
53    /// Check if this hint contains a value.
54    pub fn contains(&self, value: &BigRational) -> bool {
55        &self.lower <= value && value <= &self.upper
56    }
57}
58
59/// Collection of root hints for a polynomial.
60#[derive(Debug, Clone)]
61pub struct PolynomialRootHints {
62    /// Polynomial hash for identification.
63    #[allow(dead_code)]
64    poly_hash: u64,
65    /// Variable the roots are for.
66    #[allow(dead_code)]
67    variable: Var,
68    /// Ordered list of root hints.
69    hints: Vec<RootHint>,
70    /// Number of times these hints have been used.
71    use_count: u64,
72}
73
74impl PolynomialRootHints {
75    /// Create new polynomial root hints.
76    pub fn new(poly_hash: u64, variable: Var, hints: Vec<RootHint>) -> Self {
77        Self {
78            poly_hash,
79            variable,
80            hints,
81            use_count: 0,
82        }
83    }
84
85    /// Get the number of roots hinted.
86    pub fn num_roots(&self) -> usize {
87        self.hints.len()
88    }
89
90    /// Get a specific hint by index.
91    pub fn get_hint(&self, index: usize) -> Option<&RootHint> {
92        self.hints.get(index)
93    }
94
95    /// Get all hints.
96    pub fn all_hints(&self) -> &[RootHint] {
97        &self.hints
98    }
99
100    /// Increment usage count.
101    fn mark_used(&mut self) {
102        self.use_count += 1;
103    }
104}
105
106/// Statistics for root hints cache.
107#[derive(Debug, Clone, Default)]
108pub struct RootHintsStats {
109    /// Number of hints stored.
110    pub num_hints_stored: u64,
111    /// Number of cache hits.
112    pub num_cache_hits: u64,
113    /// Number of cache misses.
114    pub num_cache_misses: u64,
115    /// Number of hints used for refinement.
116    pub num_refinements: u64,
117}
118
119impl RootHintsStats {
120    /// Calculate cache hit rate.
121    pub fn hit_rate(&self) -> f64 {
122        let total = self.num_cache_hits + self.num_cache_misses;
123        if total == 0 {
124            0.0
125        } else {
126            self.num_cache_hits as f64 / total as f64
127        }
128    }
129}
130
131/// Configuration for root hints cache.
132#[derive(Debug, Clone)]
133pub struct RootHintsConfig {
134    /// Maximum number of polynomial hint sets to cache.
135    pub max_polynomials: usize,
136    /// Maximum number of hints per polynomial.
137    pub max_hints_per_poly: usize,
138    /// Enable hint refinement tracking.
139    pub enable_refinement: bool,
140}
141
142impl Default for RootHintsConfig {
143    fn default() -> Self {
144        Self {
145            max_polynomials: 1000,
146            max_hints_per_poly: 20,
147            enable_refinement: true,
148        }
149    }
150}
151
152/// Root approximation hints cache.
153pub struct RootHintsCache {
154    /// Configuration.
155    config: RootHintsConfig,
156    /// Cache: polynomial hash -> root hints.
157    cache: FxHashMap<u64, PolynomialRootHints>,
158    /// LRU queue for eviction.
159    lru: VecDeque<u64>,
160    /// Statistics.
161    stats: RootHintsStats,
162}
163
164impl RootHintsCache {
165    /// Create a new root hints cache.
166    pub fn new() -> Self {
167        Self::with_config(RootHintsConfig::default())
168    }
169
170    /// Create a new cache with the given configuration.
171    pub fn with_config(config: RootHintsConfig) -> Self {
172        Self {
173            config,
174            cache: FxHashMap::default(),
175            lru: VecDeque::new(),
176            stats: RootHintsStats::default(),
177        }
178    }
179
180    /// Store root hints for a polynomial.
181    pub fn store_hints(&mut self, poly: &Polynomial, variable: Var, hints: Vec<RootHint>) {
182        let hash = Self::hash_polynomial(poly, variable);
183
184        // Limit number of hints per polynomial
185        let limited_hints: Vec<_> = hints
186            .into_iter()
187            .take(self.config.max_hints_per_poly)
188            .collect();
189
190        let poly_hints = PolynomialRootHints::new(hash, variable, limited_hints);
191
192        // Check if we need to evict
193        if self.cache.len() >= self.config.max_polynomials
194            && !self.cache.contains_key(&hash)
195            && let Some(old_hash) = self.lru.pop_back()
196        {
197            self.cache.remove(&old_hash);
198        }
199
200        self.cache.insert(hash, poly_hints);
201        self.lru.retain(|&h| h != hash);
202        self.lru.push_front(hash);
203
204        self.stats.num_hints_stored += 1;
205    }
206
207    /// Lookup root hints for a polynomial.
208    pub fn lookup_hints(&mut self, poly: &Polynomial, variable: Var) -> Option<&[RootHint]> {
209        let hash = Self::hash_polynomial(poly, variable);
210
211        if let Some(hints) = self.cache.get_mut(&hash) {
212            self.stats.num_cache_hits += 1;
213            hints.mark_used();
214
215            // Move to front of LRU
216            self.lru.retain(|&h| h != hash);
217            self.lru.push_front(hash);
218
219            Some(hints.all_hints())
220        } else {
221            self.stats.num_cache_misses += 1;
222            None
223        }
224    }
225
226    /// Refine an existing hint with more precise bounds.
227    pub fn refine_hint(
228        &mut self,
229        poly: &Polynomial,
230        variable: Var,
231        root_index: usize,
232        new_lower: BigRational,
233        new_upper: BigRational,
234    ) -> bool {
235        if !self.config.enable_refinement {
236            return false;
237        }
238
239        let hash = Self::hash_polynomial(poly, variable);
240
241        if let Some(hints) = self.cache.get_mut(&hash)
242            && let Some(hint) = hints.hints.get_mut(root_index)
243        {
244            hint.lower = new_lower;
245            hint.upper = new_upper;
246            hint.refinement_level += 1;
247            self.stats.num_refinements += 1;
248            return true;
249        }
250
251        false
252    }
253
254    /// Get the number of cached root hints for a polynomial.
255    pub fn num_roots(&mut self, poly: &Polynomial, variable: Var) -> Option<usize> {
256        let hash = Self::hash_polynomial(poly, variable);
257        self.cache.get(&hash).map(|h| h.num_roots())
258    }
259
260    /// Hash a polynomial and variable for caching.
261    fn hash_polynomial(poly: &Polynomial, variable: Var) -> u64 {
262        let mut hasher = DefaultHasher::new();
263        format!("{:?}", poly).hash(&mut hasher);
264        variable.hash(&mut hasher);
265        hasher.finish()
266    }
267
268    /// Clear the cache.
269    pub fn clear(&mut self) {
270        self.cache.clear();
271        self.lru.clear();
272    }
273
274    /// Get statistics.
275    pub fn stats(&self) -> &RootHintsStats {
276        &self.stats
277    }
278
279    /// Get cache size.
280    pub fn cache_size(&self) -> usize {
281        self.cache.len()
282    }
283}
284
285impl Default for RootHintsCache {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use num_bigint::BigInt;
295
296    fn rat(n: i32) -> BigRational {
297        BigRational::from_integer(BigInt::from(n))
298    }
299
300    #[test]
301    fn test_root_hint_new() {
302        let hint = RootHint::new(rat(0), rat(10), 1);
303        assert_eq!(hint.lower, rat(0));
304        assert_eq!(hint.upper, rat(10));
305        assert_eq!(hint.refinement_level, 1);
306    }
307
308    #[test]
309    fn test_root_hint_width() {
310        let hint = RootHint::new(rat(0), rat(10), 1);
311        assert_eq!(hint.width(), rat(10));
312    }
313
314    #[test]
315    fn test_root_hint_midpoint() {
316        let hint = RootHint::new(rat(0), rat(10), 1);
317        assert_eq!(hint.midpoint(), rat(5));
318    }
319
320    #[test]
321    fn test_root_hint_contains() {
322        let hint = RootHint::new(rat(0), rat(10), 1);
323        assert!(hint.contains(&rat(5)));
324        assert!(hint.contains(&rat(0)));
325        assert!(hint.contains(&rat(10)));
326        assert!(!hint.contains(&rat(-1)));
327        assert!(!hint.contains(&rat(11)));
328    }
329
330    #[test]
331    fn test_cache_new() {
332        let cache = RootHintsCache::new();
333        assert_eq!(cache.cache_size(), 0);
334    }
335
336    #[test]
337    fn test_store_and_lookup() {
338        let mut cache = RootHintsCache::new();
339        let poly = Polynomial::from_var(0);
340        let hints = vec![RootHint::new(rat(0), rat(1), 1)];
341
342        cache.store_hints(&poly, 0, hints.clone());
343
344        let looked_up = cache.lookup_hints(&poly, 0);
345        assert!(looked_up.is_some());
346        assert_eq!(looked_up.expect("test operation should succeed").len(), 1);
347    }
348
349    #[test]
350    fn test_cache_miss() {
351        let mut cache = RootHintsCache::new();
352        let poly = Polynomial::from_var(0);
353
354        let result = cache.lookup_hints(&poly, 0);
355        assert!(result.is_none());
356        assert_eq!(cache.stats.num_cache_misses, 1);
357    }
358
359    #[test]
360    fn test_cache_hit() {
361        let mut cache = RootHintsCache::new();
362        let poly = Polynomial::from_var(0);
363        let hints = vec![RootHint::new(rat(0), rat(1), 1)];
364
365        cache.store_hints(&poly, 0, hints);
366        let _result = cache.lookup_hints(&poly, 0);
367
368        assert_eq!(cache.stats.num_cache_hits, 1);
369    }
370
371    #[test]
372    fn test_refine_hint() {
373        let mut cache = RootHintsCache::new();
374        let poly = Polynomial::from_var(0);
375        let hints = vec![RootHint::new(rat(0), rat(10), 1)];
376
377        cache.store_hints(&poly, 0, hints);
378
379        let success = cache.refine_hint(&poly, 0, 0, rat(2), rat(8));
380        assert!(success);
381
382        let refined = cache
383            .lookup_hints(&poly, 0)
384            .expect("test operation should succeed");
385        assert_eq!(refined[0].lower, rat(2));
386        assert_eq!(refined[0].upper, rat(8));
387        assert_eq!(refined[0].refinement_level, 2);
388    }
389
390    #[test]
391    fn test_num_roots() {
392        let mut cache = RootHintsCache::new();
393        let poly = Polynomial::from_var(0);
394        let hints = vec![
395            RootHint::new(rat(0), rat(1), 1),
396            RootHint::new(rat(5), rat(6), 1),
397        ];
398
399        cache.store_hints(&poly, 0, hints);
400
401        assert_eq!(cache.num_roots(&poly, 0), Some(2));
402    }
403
404    #[test]
405    fn test_eviction() {
406        let config = RootHintsConfig {
407            max_polynomials: 2,
408            ..Default::default()
409        };
410        let mut cache = RootHintsCache::with_config(config);
411
412        // Add 3 polynomials (should evict the first)
413        for i in 0..3 {
414            let poly = Polynomial::from_var(i);
415            let hints = vec![RootHint::new(rat(0), rat(1), 1)];
416            cache.store_hints(&poly, i, hints);
417        }
418
419        assert_eq!(cache.cache_size(), 2);
420    }
421
422    #[test]
423    fn test_hit_rate() {
424        let mut cache = RootHintsCache::new();
425        let poly = Polynomial::from_var(0);
426        let hints = vec![RootHint::new(rat(0), rat(1), 1)];
427
428        cache.store_hints(&poly, 0, hints);
429
430        // One hit
431        let _result1 = cache.lookup_hints(&poly, 0);
432
433        // One miss
434        let poly2 = Polynomial::from_var(1);
435        let _result2 = cache.lookup_hints(&poly2, 1);
436
437        assert_eq!(cache.stats().hit_rate(), 0.5);
438    }
439
440    #[test]
441    fn test_clear() {
442        let mut cache = RootHintsCache::new();
443        let poly = Polynomial::from_var(0);
444        let hints = vec![RootHint::new(rat(0), rat(1), 1)];
445
446        cache.store_hints(&poly, 0, hints);
447        assert!(cache.cache_size() > 0);
448
449        cache.clear();
450        assert_eq!(cache.cache_size(), 0);
451    }
452
453    #[test]
454    fn test_hints_limit() {
455        let config = RootHintsConfig {
456            max_hints_per_poly: 2,
457            ..Default::default()
458        };
459        let mut cache = RootHintsCache::with_config(config);
460
461        let poly = Polynomial::from_var(0);
462        let hints = vec![
463            RootHint::new(rat(0), rat(1), 1),
464            RootHint::new(rat(2), rat(3), 1),
465            RootHint::new(rat(4), rat(5), 1),
466        ];
467
468        cache.store_hints(&poly, 0, hints);
469
470        // Should only store 2 hints
471        assert_eq!(cache.num_roots(&poly, 0), Some(2));
472    }
473}