Skip to main content

chematic_chem/
cache.rs

1//! Descriptor caching for improved performance.
2//!
3//! Simple LRU-like cache for descriptors keyed by molecule canonical SMILES.
4//! Improves performance when computing same molecules repeatedly.
5
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9/// Descriptor cache entry: stores computed descriptor values.
10#[derive(Clone, Debug, PartialEq, Default)]
11pub struct DescriptorEntry {
12    /// Molecular weight (Ø = computed on demand)
13    pub mw: Option<f64>,
14    /// LogP (Ø)
15    pub logp: Option<f64>,
16    /// TPSA (Ø)
17    pub tpsa: Option<f64>,
18    /// HBA count (Ø)
19    pub hba: Option<usize>,
20    /// HBD count (Ø)
21    pub hbd: Option<usize>,
22    /// Rotatable bond count (Ø)
23    pub rotb: Option<usize>,
24}
25
26/// Thread-safe descriptor cache with max_size limit.
27#[derive(Clone, Debug)]
28pub struct DescriptorCache {
29    cache: Arc<Mutex<HashMap<String, DescriptorEntry>>>,
30    max_size: usize,
31}
32
33impl DescriptorCache {
34    /// Create new cache with given max size.
35    pub fn new(max_size: usize) -> Self {
36        Self {
37            cache: Arc::new(Mutex::new(HashMap::new())),
38            max_size,
39        }
40    }
41
42    /// Get cached entry for molecule (keyed by canonical SMILES).
43    pub fn get(&self, smiles: &str) -> Option<DescriptorEntry> {
44        self.cache.lock().ok().and_then(|c| c.get(smiles).cloned())
45    }
46
47    /// Store/update cached entry.
48    pub fn put(&self, smiles: String, entry: DescriptorEntry) {
49        if let Ok(mut cache) = self.cache.lock() {
50            // Simple eviction: clear if full and adding new entry
51            if !cache.contains_key(&smiles) && cache.len() >= self.max_size {
52                // Remove arbitrary entry (FIFO-like behavior in practice)
53                if let Some(key) = cache.keys().next().cloned() {
54                    cache.remove(&key);
55                }
56            }
57            cache.insert(smiles, entry);
58        }
59    }
60
61    /// Clear all cached entries.
62    pub fn clear(&self) {
63        if let Ok(mut cache) = self.cache.lock() {
64            cache.clear();
65        }
66    }
67
68    /// Get cache size.
69    pub fn len(&self) -> usize {
70        self.cache.lock().map(|c| c.len()).unwrap_or(0)
71    }
72
73    /// Check if cache is empty.
74    pub fn is_empty(&self) -> bool {
75        self.len() == 0
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn cache_put_get() {
85        let cache = DescriptorCache::new(100);
86        let entry = DescriptorEntry {
87            mw: Some(46.0),
88            ..DescriptorEntry::default()
89        };
90
91        cache.put("CC".to_string(), entry.clone());
92        let retrieved = cache.get("CC").unwrap();
93        assert_eq!(retrieved.mw, Some(46.0));
94    }
95
96    #[test]
97    fn cache_miss() {
98        let cache = DescriptorCache::new(100);
99        assert_eq!(cache.get("CC"), None);
100    }
101
102    #[test]
103    fn cache_eviction() {
104        let cache = DescriptorCache::new(2);
105        let entry = DescriptorEntry::default();
106
107        cache.put("C1".to_string(), entry.clone());
108        cache.put("C2".to_string(), entry.clone());
109        cache.put("C3".to_string(), entry.clone());
110
111        // Cache size should be 2 (evicted oldest)
112        assert!(cache.len() <= 2);
113    }
114
115    #[test]
116    fn cache_clear() {
117        let cache = DescriptorCache::new(100);
118        let entry = DescriptorEntry::default();
119
120        cache.put("CC".to_string(), entry);
121        assert!(!cache.is_empty());
122
123        cache.clear();
124        assert!(cache.is_empty());
125    }
126}