oxiz_solver/conflict/
explanation_cache.rs1use oxiz_sat::Lit;
17use rustc_hash::FxHashMap;
18use std::collections::VecDeque;
19
20pub type CacheKey = u64;
22
23#[derive(Debug, Clone)]
25pub struct Explanation {
26 pub literals: Vec<Lit>,
28 pub proof: Option<String>,
30 timestamp: u64,
32}
33
34#[derive(Debug, Clone)]
36pub struct CacheConfig {
37 pub max_entries: usize,
39 pub cache_proofs: bool,
41 pub use_lru: bool,
43}
44
45impl Default for CacheConfig {
46 fn default() -> Self {
47 Self {
48 max_entries: 10_000,
49 cache_proofs: true,
50 use_lru: true,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct CacheStats {
58 pub hits: u64,
60 pub misses: u64,
62 pub evictions: u64,
64 pub cached: u64,
66}
67
68impl CacheStats {
69 pub fn hit_rate(&self) -> f64 {
71 let total = self.hits + self.misses;
72 if total == 0 {
73 0.0
74 } else {
75 self.hits as f64 / total as f64
76 }
77 }
78}
79
80#[derive(Debug)]
82pub struct ExplanationCache {
83 cache: FxHashMap<CacheKey, Explanation>,
85 lru_queue: VecDeque<CacheKey>,
87 timestamp: u64,
89 config: CacheConfig,
91 stats: CacheStats,
93}
94
95impl ExplanationCache {
96 pub fn new(config: CacheConfig) -> Self {
98 Self {
99 cache: FxHashMap::default(),
100 lru_queue: VecDeque::new(),
101 timestamp: 0,
102 config,
103 stats: CacheStats::default(),
104 }
105 }
106
107 pub fn default_config() -> Self {
109 Self::new(CacheConfig::default())
110 }
111
112 pub fn insert(&mut self, key: CacheKey, literals: Vec<Lit>, proof: Option<String>) {
114 if self.cache.len() >= self.config.max_entries && !self.cache.contains_key(&key) {
116 self.evict_lru();
117 }
118
119 let explanation = Explanation {
120 literals,
121 proof: if self.config.cache_proofs {
122 proof
123 } else {
124 None
125 },
126 timestamp: self.timestamp,
127 };
128
129 self.cache.insert(key, explanation);
130 if self.config.use_lru {
131 self.lru_queue.push_back(key);
132 }
133 self.stats.cached += 1;
134 self.timestamp += 1;
135 }
136
137 pub fn get(&mut self, key: CacheKey) -> Option<&Explanation> {
139 if let Some(explanation) = self.cache.get_mut(&key) {
140 explanation.timestamp = self.timestamp;
142 self.timestamp += 1;
143 self.stats.hits += 1;
144
145 if self.config.use_lru {
147 self.lru_queue.retain(|&k| k != key);
148 self.lru_queue.push_back(key);
149 }
150
151 Some(explanation)
152 } else {
153 self.stats.misses += 1;
154 None
155 }
156 }
157
158 fn evict_lru(&mut self) {
160 if let Some(key) = self.lru_queue.pop_front() {
161 self.cache.remove(&key);
162 self.stats.evictions += 1;
163 }
164 }
165
166 pub fn clear(&mut self) {
168 self.cache.clear();
169 self.lru_queue.clear();
170 self.timestamp = 0;
171 }
172
173 pub fn len(&self) -> usize {
175 self.cache.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.cache.is_empty()
181 }
182
183 pub fn stats(&self) -> &CacheStats {
185 &self.stats
186 }
187
188 pub fn reset_stats(&mut self) {
190 self.stats = CacheStats::default();
191 }
192}
193
194impl Default for ExplanationCache {
195 fn default() -> Self {
196 Self::default_config()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_cache_creation() {
206 let cache = ExplanationCache::default_config();
207 assert!(cache.is_empty());
208 assert_eq!(cache.len(), 0);
209 }
210
211 #[test]
212 fn test_insert_and_get() {
213 use oxiz_sat::Var;
214 let mut cache = ExplanationCache::default_config();
215 let key = 42;
216 let literals = vec![Lit::pos(Var::new(0)), Lit::neg(Var::new(1))];
217
218 cache.insert(key, literals.clone(), None);
219 assert_eq!(cache.len(), 1);
220
221 let retrieved = cache.get(key).unwrap();
222 assert_eq!(retrieved.literals.len(), 2);
223 assert_eq!(cache.stats().hits, 1);
224 }
225
226 #[test]
227 fn test_cache_miss() {
228 let mut cache = ExplanationCache::default_config();
229 let result = cache.get(999);
230 assert!(result.is_none());
231 assert_eq!(cache.stats().misses, 1);
232 }
233
234 #[test]
235 fn test_lru_eviction() {
236 let config = CacheConfig {
237 max_entries: 2,
238 ..Default::default()
239 };
240 let mut cache = ExplanationCache::new(config);
241
242 cache.insert(1, vec![], None);
244 cache.insert(2, vec![], None);
245 cache.insert(3, vec![], None);
246
247 assert_eq!(cache.len(), 2);
248 assert_eq!(cache.stats().evictions, 1);
249
250 assert!(cache.get(1).is_none());
252 assert!(cache.get(2).is_some());
253 assert!(cache.get(3).is_some());
254 }
255
256 #[test]
257 fn test_hit_rate() {
258 let mut cache = ExplanationCache::default_config();
259 cache.insert(1, vec![], None);
260
261 cache.get(1); cache.get(2); cache.get(1); assert_eq!(cache.stats().hits, 2);
266 assert_eq!(cache.stats().misses, 1);
267 assert!((cache.stats().hit_rate() - 0.666).abs() < 0.01);
268 }
269
270 #[test]
271 fn test_clear() {
272 let mut cache = ExplanationCache::default_config();
273 cache.insert(1, vec![], None);
274 cache.insert(2, vec![], None);
275
276 cache.clear();
277 assert!(cache.is_empty());
278 }
279
280 #[test]
281 fn test_proof_caching() {
282 let mut cache = ExplanationCache::default_config();
283 let key = 100;
284 let proof = Some("(proof-term ...)".to_string());
285
286 cache.insert(key, vec![], proof.clone());
287
288 let retrieved = cache.get(key).unwrap();
289 assert_eq!(retrieved.proof, proof);
290 }
291
292 #[test]
293 fn test_stats() {
294 let mut cache = ExplanationCache::default_config();
295 cache.insert(1, vec![], None);
296 cache.get(1);
297
298 assert_eq!(cache.stats().cached, 1);
299 assert_eq!(cache.stats().hits, 1);
300
301 cache.reset_stats();
302 assert_eq!(cache.stats().cached, 0);
303 assert_eq!(cache.stats().hits, 0);
304 }
305}