quantrs2_sim/
operation_cache.rs

1//! Advanced Caching Strategies for Quantum Operations
2//!
3//! This module provides sophisticated caching mechanisms for frequently computed
4//! quantum operations, gate matrices, and intermediate results to optimize performance.
5
6use ndarray::{Array1, Array2};
7use num_complex::Complex64;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::hash::{Hash, Hasher};
11use std::sync::{Arc, Mutex, RwLock};
12use std::time::{Duration, Instant};
13
14/// Cache key for quantum operations
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct OperationKey {
17    /// Operation type identifier
18    pub operation_type: String,
19    /// Operation parameters (angles, targets, etc.)
20    pub parameters: Vec<f64>,
21    /// Qubit indices involved
22    pub qubits: Vec<usize>,
23    /// Additional metadata
24    pub metadata: HashMap<String, String>,
25}
26
27impl Hash for OperationKey {
28    fn hash<H: Hasher>(&self, state: &mut H) {
29        self.operation_type.hash(state);
30        // Hash parameters with precision consideration
31        for param in &self.parameters {
32            // Round to avoid floating point precision issues
33            let rounded = (param * 1e12).round() as i64;
34            rounded.hash(state);
35        }
36        self.qubits.hash(state);
37        for (k, v) in &self.metadata {
38            k.hash(state);
39            v.hash(state);
40        }
41    }
42}
43
44impl Eq for OperationKey {}
45
46/// Cached operation result
47#[derive(Debug, Clone)]
48pub struct CachedOperation {
49    /// The computed gate matrix or result
50    pub data: CachedData,
51    /// When this entry was created
52    pub created_at: Instant,
53    /// How many times this entry has been accessed
54    pub access_count: u64,
55    /// Last access time
56    pub last_accessed: Instant,
57    /// Size in bytes (for memory management)
58    pub size_bytes: usize,
59}
60
61/// Different types of cached data
62#[derive(Debug, Clone)]
63pub enum CachedData {
64    /// 2x2 single-qubit gate matrix
65    SingleQubitMatrix(Array2<Complex64>),
66    /// 4x4 two-qubit gate matrix
67    TwoQubitMatrix(Array2<Complex64>),
68    /// Arbitrary size matrix
69    Matrix(Array2<Complex64>),
70    /// State vector
71    StateVector(Array1<Complex64>),
72    /// Expectation value result
73    ExpectationValue(Complex64),
74    /// Probability distribution
75    Probabilities(Vec<f64>),
76    /// Custom data with serialization
77    Custom(Vec<u8>),
78}
79
80impl CachedData {
81    /// Estimate memory usage of cached data
82    pub fn estimate_size(&self) -> usize {
83        match self {
84            CachedData::SingleQubitMatrix(_) => 4 * std::mem::size_of::<Complex64>(),
85            CachedData::TwoQubitMatrix(_) => 16 * std::mem::size_of::<Complex64>(),
86            CachedData::Matrix(m) => m.len() * std::mem::size_of::<Complex64>(),
87            CachedData::StateVector(v) => v.len() * std::mem::size_of::<Complex64>(),
88            CachedData::ExpectationValue(_) => std::mem::size_of::<Complex64>(),
89            CachedData::Probabilities(p) => p.len() * std::mem::size_of::<f64>(),
90            CachedData::Custom(data) => data.len(),
91        }
92    }
93}
94
95/// Cache eviction policies
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum EvictionPolicy {
98    /// Least Recently Used
99    LRU,
100    /// Least Frequently Used
101    LFU,
102    /// Time-based expiration
103    TTL,
104    /// First In, First Out
105    FIFO,
106    /// Hybrid policy combining LRU and LFU
107    Hybrid,
108}
109
110/// Cache configuration
111#[derive(Debug, Clone)]
112pub struct CacheConfig {
113    /// Maximum number of entries
114    pub max_entries: usize,
115    /// Maximum memory usage in bytes
116    pub max_memory_bytes: usize,
117    /// Eviction policy
118    pub eviction_policy: EvictionPolicy,
119    /// TTL for time-based eviction
120    pub ttl: Duration,
121    /// Enable cache statistics
122    pub enable_stats: bool,
123    /// Cleanup interval
124    pub cleanup_interval: Duration,
125}
126
127impl Default for CacheConfig {
128    fn default() -> Self {
129        Self {
130            max_entries: 10000,
131            max_memory_bytes: 256 * 1024 * 1024, // 256 MB
132            eviction_policy: EvictionPolicy::LRU,
133            ttl: Duration::from_secs(3600), // 1 hour
134            enable_stats: true,
135            cleanup_interval: Duration::from_secs(60), // 1 minute
136        }
137    }
138}
139
140/// Cache statistics
141#[derive(Debug, Clone, Default)]
142pub struct CacheStats {
143    /// Total cache requests
144    pub total_requests: u64,
145    /// Cache hits
146    pub hits: u64,
147    /// Cache misses
148    pub misses: u64,
149    /// Total evictions
150    pub evictions: u64,
151    /// Current number of entries
152    pub current_entries: usize,
153    /// Current memory usage
154    pub current_memory_bytes: usize,
155    /// Peak memory usage
156    pub peak_memory_bytes: usize,
157    /// Average access time (nanoseconds)
158    pub average_access_time_ns: f64,
159    /// Cache efficiency by operation type
160    pub efficiency_by_type: HashMap<String, f64>,
161}
162
163impl CacheStats {
164    /// Calculate hit ratio
165    pub fn hit_ratio(&self) -> f64 {
166        if self.total_requests == 0 {
167            0.0
168        } else {
169            self.hits as f64 / self.total_requests as f64
170        }
171    }
172
173    /// Update statistics for a cache access
174    pub fn record_access(&mut self, operation_type: &str, hit: bool, access_time_ns: u64) {
175        self.total_requests += 1;
176        if hit {
177            self.hits += 1;
178        } else {
179            self.misses += 1;
180        }
181
182        // Update average access time
183        let total_time =
184            self.average_access_time_ns * (self.total_requests - 1) as f64 + access_time_ns as f64;
185        self.average_access_time_ns = total_time / self.total_requests as f64;
186
187        // Update efficiency by operation type
188        let type_stats = self
189            .efficiency_by_type
190            .entry(operation_type.to_string())
191            .or_insert(0.0);
192        // Simplified efficiency calculation
193        if hit {
194            *type_stats = (*type_stats + 1.0) / 2.0;
195        } else {
196            *type_stats = *type_stats / 2.0;
197        }
198    }
199}
200
201/// Advanced quantum operation cache
202#[derive(Debug)]
203pub struct QuantumOperationCache {
204    /// Main cache storage
205    cache: RwLock<HashMap<OperationKey, CachedOperation>>,
206    /// Access order for LRU
207    access_order: Mutex<VecDeque<OperationKey>>,
208    /// Configuration
209    config: CacheConfig,
210    /// Statistics
211    stats: Arc<Mutex<CacheStats>>,
212    /// Last cleanup time
213    last_cleanup: Mutex<Instant>,
214}
215
216impl QuantumOperationCache {
217    /// Create new quantum operation cache
218    pub fn new(config: CacheConfig) -> Self {
219        Self {
220            cache: RwLock::new(HashMap::new()),
221            access_order: Mutex::new(VecDeque::new()),
222            config,
223            stats: Arc::new(Mutex::new(CacheStats::default())),
224            last_cleanup: Mutex::new(Instant::now()),
225        }
226    }
227
228    /// Get cached operation result
229    pub fn get(&self, key: &OperationKey) -> Option<CachedData> {
230        let start_time = Instant::now();
231
232        let result = {
233            let entry_data = {
234                let cache = self.cache.read().unwrap();
235                cache.get(key).map(|entry| entry.data.clone())
236            };
237
238            if entry_data.is_some() {
239                self.update_access_stats(key);
240            }
241
242            entry_data
243        };
244
245        let access_time = start_time.elapsed().as_nanos() as u64;
246
247        // Update statistics
248        if self.config.enable_stats {
249            if let Ok(mut stats) = self.stats.lock() {
250                stats.record_access(&key.operation_type, result.is_some(), access_time);
251            }
252        }
253
254        result
255    }
256
257    /// Store operation result in cache
258    pub fn put(&self, key: OperationKey, data: CachedData) {
259        let size = data.estimate_size();
260        let entry = CachedOperation {
261            data,
262            created_at: Instant::now(),
263            access_count: 0,
264            last_accessed: Instant::now(),
265            size_bytes: size,
266        };
267
268        // Check if we need to evict entries first
269        self.maybe_evict(&key, size);
270
271        // Insert the new entry
272        {
273            let mut cache = self.cache.write().unwrap();
274            cache.insert(key.clone(), entry);
275        }
276
277        // Update access order
278        {
279            let mut access_order = self.access_order.lock().unwrap();
280            access_order.push_back(key);
281        }
282
283        // Update statistics
284        if self.config.enable_stats {
285            if let Ok(mut stats) = self.stats.lock() {
286                stats.current_entries = self.cache.read().unwrap().len();
287                stats.current_memory_bytes += size;
288                if stats.current_memory_bytes > stats.peak_memory_bytes {
289                    stats.peak_memory_bytes = stats.current_memory_bytes;
290                }
291            }
292        }
293
294        // Trigger cleanup if needed
295        self.maybe_cleanup();
296    }
297
298    /// Update access statistics for an entry
299    fn update_access_stats(&self, key: &OperationKey) {
300        let mut cache = self.cache.write().unwrap();
301        if let Some(entry) = cache.get_mut(key) {
302            entry.access_count += 1;
303            entry.last_accessed = Instant::now();
304        }
305
306        // Update access order for LRU
307        if self.config.eviction_policy == EvictionPolicy::LRU {
308            let mut access_order = self.access_order.lock().unwrap();
309            // Remove key from current position and add to back
310            if let Some(pos) = access_order.iter().position(|k| k == key) {
311                access_order.remove(pos);
312            }
313            access_order.push_back(key.clone());
314        }
315    }
316
317    /// Check if eviction is needed and perform it
318    fn maybe_evict(&self, _new_key: &OperationKey, new_size: usize) {
319        let (current_entries, current_memory) = {
320            let cache = self.cache.read().unwrap();
321            (cache.len(), self.get_current_memory_usage())
322        };
323
324        let needs_eviction = current_entries >= self.config.max_entries
325            || current_memory + new_size > self.config.max_memory_bytes;
326
327        if needs_eviction {
328            self.evict_entries();
329        }
330    }
331
332    /// Evict entries based on configured policy
333    fn evict_entries(&self) {
334        match self.config.eviction_policy {
335            EvictionPolicy::LRU => self.evict_lru(),
336            EvictionPolicy::LFU => self.evict_lfu(),
337            EvictionPolicy::TTL => self.evict_expired(),
338            EvictionPolicy::FIFO => self.evict_fifo(),
339            EvictionPolicy::Hybrid => self.evict_hybrid(),
340        }
341    }
342
343    /// Evict least recently used entries
344    fn evict_lru(&self) {
345        let keys_to_evict: Vec<OperationKey> = {
346            let mut access_order = self.access_order.lock().unwrap();
347            let mut keys = Vec::new();
348
349            // Evict 25% of entries or until memory constraint is satisfied
350            let max_evictions = (self.config.max_entries / 4).max(1);
351
352            for _ in 0..max_evictions {
353                if let Some(key) = access_order.pop_front() {
354                    keys.push(key);
355                } else {
356                    break;
357                }
358            }
359            keys
360        };
361
362        self.remove_entries(keys_to_evict);
363    }
364
365    /// Evict least frequently used entries
366    fn evict_lfu(&self) {
367        let keys_to_evict: Vec<OperationKey> = {
368            let cache = self.cache.read().unwrap();
369            let mut entries: Vec<_> = cache.iter().collect();
370
371            // Sort by access count (ascending)
372            entries.sort_by_key(|(_, entry)| entry.access_count);
373
374            // Take first 25% for eviction
375            let max_evictions = (cache.len() / 4).max(1);
376            entries
377                .into_iter()
378                .take(max_evictions)
379                .map(|(key, _)| key.clone())
380                .collect()
381        };
382
383        self.remove_entries(keys_to_evict);
384    }
385
386    /// Evict expired entries based on TTL
387    fn evict_expired(&self) {
388        let now = Instant::now();
389        let keys_to_evict: Vec<OperationKey> = {
390            let cache = self.cache.read().unwrap();
391            cache
392                .iter()
393                .filter(|(_, entry)| now.duration_since(entry.created_at) > self.config.ttl)
394                .map(|(key, _)| key.clone())
395                .collect()
396        };
397
398        self.remove_entries(keys_to_evict);
399    }
400
401    /// Evict in FIFO order
402    fn evict_fifo(&self) {
403        // Similar to LRU but based on creation time
404        let keys_to_evict: Vec<OperationKey> = {
405            let cache = self.cache.read().unwrap();
406            let mut entries: Vec<_> = cache.iter().collect();
407
408            // Sort by creation time (ascending)
409            entries.sort_by_key(|(_, entry)| entry.created_at);
410
411            // Take first 25% for eviction
412            let max_evictions = (cache.len() / 4).max(1);
413            entries
414                .into_iter()
415                .take(max_evictions)
416                .map(|(key, _)| key.clone())
417                .collect()
418        };
419
420        self.remove_entries(keys_to_evict);
421    }
422
423    /// Hybrid eviction combining LRU and LFU
424    fn evict_hybrid(&self) {
425        let keys_to_evict: Vec<OperationKey> = {
426            let cache = self.cache.read().unwrap();
427            let mut entries: Vec<_> = cache.iter().collect();
428
429            // Hybrid score: combine recency and frequency
430            entries.sort_by_key(|(_, entry)| {
431                let recency_score = entry.last_accessed.elapsed().as_secs();
432                let frequency_score = 1000 / (entry.access_count + 1); // Inverse frequency
433                recency_score + frequency_score
434            });
435
436            // Take first 25% for eviction
437            let max_evictions = (cache.len() / 4).max(1);
438            entries
439                .into_iter()
440                .take(max_evictions)
441                .map(|(key, _)| key.clone())
442                .collect()
443        };
444
445        self.remove_entries(keys_to_evict);
446    }
447
448    /// Remove specified entries from cache
449    fn remove_entries(&self, keys: Vec<OperationKey>) {
450        let mut total_freed_memory = 0usize;
451
452        {
453            let mut cache = self.cache.write().unwrap();
454            for key in &keys {
455                if let Some(entry) = cache.remove(key) {
456                    total_freed_memory += entry.size_bytes;
457                }
458            }
459        }
460
461        // Update access order
462        {
463            let mut access_order = self.access_order.lock().unwrap();
464            access_order.retain(|key| !keys.contains(key));
465        }
466
467        // Update statistics
468        if self.config.enable_stats {
469            if let Ok(mut stats) = self.stats.lock() {
470                stats.evictions += keys.len() as u64;
471                stats.current_entries = self.cache.read().unwrap().len();
472                stats.current_memory_bytes = stats
473                    .current_memory_bytes
474                    .saturating_sub(total_freed_memory);
475            }
476        }
477    }
478
479    /// Get current memory usage
480    fn get_current_memory_usage(&self) -> usize {
481        let cache = self.cache.read().unwrap();
482        cache.values().map(|entry| entry.size_bytes).sum()
483    }
484
485    /// Periodic cleanup of expired entries
486    fn maybe_cleanup(&self) {
487        if let Ok(mut last_cleanup) = self.last_cleanup.try_lock() {
488            if last_cleanup.elapsed() > self.config.cleanup_interval {
489                self.evict_expired();
490                *last_cleanup = Instant::now();
491            }
492        }
493    }
494
495    /// Clear all cached entries
496    pub fn clear(&self) {
497        let mut cache = self.cache.write().unwrap();
498        cache.clear();
499
500        let mut access_order = self.access_order.lock().unwrap();
501        access_order.clear();
502
503        if self.config.enable_stats {
504            if let Ok(mut stats) = self.stats.lock() {
505                stats.current_entries = 0;
506                stats.current_memory_bytes = 0;
507            }
508        }
509    }
510
511    /// Get cache statistics
512    pub fn get_stats(&self) -> CacheStats {
513        self.stats.lock().unwrap().clone()
514    }
515
516    /// Get cache configuration
517    pub fn get_config(&self) -> &CacheConfig {
518        &self.config
519    }
520}
521
522/// Specialized cache for gate matrices
523pub struct GateMatrixCache {
524    /// Internal operation cache
525    cache: QuantumOperationCache,
526}
527
528impl GateMatrixCache {
529    /// Create new gate matrix cache
530    pub fn new() -> Self {
531        let config = CacheConfig {
532            max_entries: 5000,
533            max_memory_bytes: 64 * 1024 * 1024, // 64 MB
534            eviction_policy: EvictionPolicy::LRU,
535            ttl: Duration::from_secs(1800), // 30 minutes
536            enable_stats: true,
537            cleanup_interval: Duration::from_secs(120), // 2 minutes
538        };
539
540        Self {
541            cache: QuantumOperationCache::new(config),
542        }
543    }
544
545    /// Get single-qubit gate matrix
546    pub fn get_single_qubit_gate(
547        &self,
548        gate_name: &str,
549        parameters: &[f64],
550    ) -> Option<Array2<Complex64>> {
551        let key = OperationKey {
552            operation_type: format!("single_qubit_{}", gate_name),
553            parameters: parameters.to_vec(),
554            qubits: vec![],
555            metadata: HashMap::new(),
556        };
557
558        match self.cache.get(&key) {
559            Some(CachedData::SingleQubitMatrix(matrix)) => Some(matrix),
560            Some(CachedData::Matrix(matrix)) if matrix.shape() == [2, 2] => Some(matrix),
561            _ => None,
562        }
563    }
564
565    /// Cache single-qubit gate matrix
566    pub fn put_single_qubit_gate(
567        &self,
568        gate_name: &str,
569        parameters: &[f64],
570        matrix: Array2<Complex64>,
571    ) {
572        let key = OperationKey {
573            operation_type: format!("single_qubit_{}", gate_name),
574            parameters: parameters.to_vec(),
575            qubits: vec![],
576            metadata: HashMap::new(),
577        };
578
579        self.cache.put(key, CachedData::SingleQubitMatrix(matrix));
580    }
581
582    /// Get two-qubit gate matrix
583    pub fn get_two_qubit_gate(
584        &self,
585        gate_name: &str,
586        parameters: &[f64],
587    ) -> Option<Array2<Complex64>> {
588        let key = OperationKey {
589            operation_type: format!("two_qubit_{}", gate_name),
590            parameters: parameters.to_vec(),
591            qubits: vec![],
592            metadata: HashMap::new(),
593        };
594
595        match self.cache.get(&key) {
596            Some(CachedData::TwoQubitMatrix(matrix)) => Some(matrix),
597            Some(CachedData::Matrix(matrix)) if matrix.shape() == [4, 4] => Some(matrix),
598            _ => None,
599        }
600    }
601
602    /// Cache two-qubit gate matrix
603    pub fn put_two_qubit_gate(
604        &self,
605        gate_name: &str,
606        parameters: &[f64],
607        matrix: Array2<Complex64>,
608    ) {
609        let key = OperationKey {
610            operation_type: format!("two_qubit_{}", gate_name),
611            parameters: parameters.to_vec(),
612            qubits: vec![],
613            metadata: HashMap::new(),
614        };
615
616        self.cache.put(key, CachedData::TwoQubitMatrix(matrix));
617    }
618
619    /// Get cache statistics
620    pub fn get_stats(&self) -> CacheStats {
621        self.cache.get_stats()
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use ndarray::array;
629
630    #[test]
631    fn test_operation_cache() {
632        let config = CacheConfig::default();
633        let cache = QuantumOperationCache::new(config);
634
635        let key = OperationKey {
636            operation_type: "test_op".to_string(),
637            parameters: vec![0.5, 1.0],
638            qubits: vec![0, 1],
639            metadata: HashMap::new(),
640        };
641
642        let matrix = array![
643            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
644            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]
645        ];
646
647        // Test cache miss
648        assert!(cache.get(&key).is_none());
649
650        // Test cache put and hit
651        cache.put(key.clone(), CachedData::Matrix(matrix.clone()));
652
653        match cache.get(&key) {
654            Some(CachedData::Matrix(cached_matrix)) => {
655                assert_eq!(cached_matrix, matrix);
656            }
657            _ => panic!("Expected cached matrix"),
658        }
659
660        let stats = cache.get_stats();
661        assert_eq!(stats.total_requests, 2);
662        assert_eq!(stats.hits, 1);
663        assert_eq!(stats.misses, 1);
664    }
665
666    #[test]
667    fn test_gate_matrix_cache() {
668        let cache = GateMatrixCache::new();
669
670        let pauli_x = array![
671            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
672            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]
673        ];
674
675        // Test cache miss
676        assert!(cache.get_single_qubit_gate("X", &[]).is_none());
677
678        // Test cache put and hit
679        cache.put_single_qubit_gate("X", &[], pauli_x.clone());
680
681        let cached_matrix = cache.get_single_qubit_gate("X", &[]).unwrap();
682        assert_eq!(cached_matrix, pauli_x);
683    }
684
685    #[test]
686    fn test_eviction_policies() {
687        let mut config = CacheConfig::default();
688        config.max_entries = 2;
689
690        let cache = QuantumOperationCache::new(config);
691
692        // Fill cache to capacity
693        for i in 0..3 {
694            let key = OperationKey {
695                operation_type: format!("test_{}", i),
696                parameters: vec![i as f64],
697                qubits: vec![i],
698                metadata: HashMap::new(),
699            };
700            cache.put(
701                key,
702                CachedData::ExpectationValue(Complex64::new(i as f64, 0.0)),
703            );
704        }
705
706        // Should have evicted first entry
707        let stats = cache.get_stats();
708        assert_eq!(stats.current_entries, 2);
709        assert!(stats.evictions > 0);
710    }
711}