#[allow(unused_imports)]
use crate::prelude::*;
use oxiz_sat::Lit;
pub type CacheKey = u64;
#[derive(Debug, Clone)]
pub struct Explanation {
pub literals: Vec<Lit>,
pub proof: Option<String>,
timestamp: u64,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_entries: usize,
pub cache_proofs: bool,
pub use_lru: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_entries: 10_000,
cache_proofs: true,
use_lru: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub cached: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
#[derive(Debug)]
pub struct ExplanationCache {
cache: FxHashMap<CacheKey, Explanation>,
lru_queue: VecDeque<CacheKey>,
timestamp: u64,
config: CacheConfig,
stats: CacheStats,
}
impl ExplanationCache {
pub fn new(config: CacheConfig) -> Self {
Self {
cache: FxHashMap::default(),
lru_queue: VecDeque::new(),
timestamp: 0,
config,
stats: CacheStats::default(),
}
}
pub fn default_config() -> Self {
Self::new(CacheConfig::default())
}
pub fn insert(&mut self, key: CacheKey, literals: Vec<Lit>, proof: Option<String>) {
if self.cache.len() >= self.config.max_entries && !self.cache.contains_key(&key) {
self.evict_lru();
}
let explanation = Explanation {
literals,
proof: if self.config.cache_proofs {
proof
} else {
None
},
timestamp: self.timestamp,
};
self.cache.insert(key, explanation);
if self.config.use_lru {
self.lru_queue.push_back(key);
}
self.stats.cached += 1;
self.timestamp += 1;
}
pub fn get(&mut self, key: CacheKey) -> Option<&Explanation> {
if let Some(explanation) = self.cache.get_mut(&key) {
explanation.timestamp = self.timestamp;
self.timestamp += 1;
self.stats.hits += 1;
if self.config.use_lru {
self.lru_queue.retain(|&k| k != key);
self.lru_queue.push_back(key);
}
Some(explanation)
} else {
self.stats.misses += 1;
None
}
}
fn evict_lru(&mut self) {
if let Some(key) = self.lru_queue.pop_front() {
self.cache.remove(&key);
self.stats.evictions += 1;
}
}
pub fn clear(&mut self) {
self.cache.clear();
self.lru_queue.clear();
self.timestamp = 0;
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn stats(&self) -> &CacheStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = CacheStats::default();
}
}
impl Default for ExplanationCache {
fn default() -> Self {
Self::default_config()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_creation() {
let cache = ExplanationCache::default_config();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_insert_and_get() {
use oxiz_sat::Var;
let mut cache = ExplanationCache::default_config();
let key = 42;
let literals = vec![Lit::pos(Var::new(0)), Lit::neg(Var::new(1))];
cache.insert(key, literals.clone(), None);
assert_eq!(cache.len(), 1);
let retrieved = cache.get(key).expect("key should exist in map");
assert_eq!(retrieved.literals.len(), 2);
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_cache_miss() {
let mut cache = ExplanationCache::default_config();
let result = cache.get(999);
assert!(result.is_none());
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_lru_eviction() {
let config = CacheConfig {
max_entries: 2,
..Default::default()
};
let mut cache = ExplanationCache::new(config);
cache.insert(1, vec![], None);
cache.insert(2, vec![], None);
cache.insert(3, vec![], None);
assert_eq!(cache.len(), 2);
assert_eq!(cache.stats().evictions, 1);
assert!(cache.get(1).is_none());
assert!(cache.get(2).is_some());
assert!(cache.get(3).is_some());
}
#[test]
fn test_hit_rate() {
let mut cache = ExplanationCache::default_config();
cache.insert(1, vec![], None);
cache.get(1); cache.get(2); cache.get(1);
assert_eq!(cache.stats().hits, 2);
assert_eq!(cache.stats().misses, 1);
assert!((cache.stats().hit_rate() - 0.666).abs() < 0.01);
}
#[test]
fn test_clear() {
let mut cache = ExplanationCache::default_config();
cache.insert(1, vec![], None);
cache.insert(2, vec![], None);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_proof_caching() {
let mut cache = ExplanationCache::default_config();
let key = 100;
let proof = Some("(proof-term ...)".to_string());
cache.insert(key, vec![], proof.clone());
let retrieved = cache.get(key).expect("key should exist in map");
assert_eq!(retrieved.proof, proof);
}
#[test]
fn test_stats() {
let mut cache = ExplanationCache::default_config();
cache.insert(1, vec![], None);
cache.get(1);
assert_eq!(cache.stats().cached, 1);
assert_eq!(cache.stats().hits, 1);
cache.reset_stats();
assert_eq!(cache.stats().cached, 0);
assert_eq!(cache.stats().hits, 0);
}
}