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 scirs2_core::ndarray::{Array1, Array2};
7use scirs2_core::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            Self::SingleQubitMatrix(_) => 4 * std::mem::size_of::<Complex64>(),
85            Self::TwoQubitMatrix(_) => 16 * std::mem::size_of::<Complex64>(),
86            Self::Matrix(m) => m.len() * std::mem::size_of::<Complex64>(),
87            Self::StateVector(v) => v.len() * std::mem::size_of::<Complex64>(),
88            Self::ExpectationValue(_) => std::mem::size_of::<Complex64>(),
89            Self::Probabilities(p) => p.len() * std::mem::size_of::<f64>(),
90            Self::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 = self
184            .average_access_time_ns
185            .mul_add((self.total_requests - 1) as f64, access_time_ns as f64);
186        self.average_access_time_ns = total_time / self.total_requests as f64;
187
188        // Update efficiency by operation type
189        let type_stats = self
190            .efficiency_by_type
191            .entry(operation_type.to_string())
192            .or_insert(0.0);
193        // Simplified efficiency calculation
194        if hit {
195            *type_stats = f64::midpoint(*type_stats, 1.0);
196        } else {
197            *type_stats /= 2.0;
198        }
199    }
200}
201
202/// Advanced quantum operation cache
203#[derive(Debug)]
204pub struct QuantumOperationCache {
205    /// Main cache storage
206    cache: RwLock<HashMap<OperationKey, CachedOperation>>,
207    /// Access order for LRU
208    access_order: Mutex<VecDeque<OperationKey>>,
209    /// Configuration
210    config: CacheConfig,
211    /// Statistics
212    stats: Arc<Mutex<CacheStats>>,
213    /// Last cleanup time
214    last_cleanup: Mutex<Instant>,
215}
216
217impl QuantumOperationCache {
218    /// Create new quantum operation cache
219    pub fn new(config: CacheConfig) -> Self {
220        Self {
221            cache: RwLock::new(HashMap::new()),
222            access_order: Mutex::new(VecDeque::new()),
223            config,
224            stats: Arc::new(Mutex::new(CacheStats::default())),
225            last_cleanup: Mutex::new(Instant::now()),
226        }
227    }
228
229    /// Get cached operation result
230    pub fn get(&self, key: &OperationKey) -> Option<CachedData> {
231        let start_time = Instant::now();
232
233        let result = {
234            let entry_data = {
235                let cache = self.cache.read().unwrap();
236                cache.get(key).map(|entry| entry.data.clone())
237            };
238
239            if entry_data.is_some() {
240                self.update_access_stats(key);
241            }
242
243            entry_data
244        };
245
246        let access_time = start_time.elapsed().as_nanos() as u64;
247
248        // Update statistics
249        if self.config.enable_stats {
250            if let Ok(mut stats) = self.stats.lock() {
251                stats.record_access(&key.operation_type, result.is_some(), access_time);
252            }
253        }
254
255        result
256    }
257
258    /// Store operation result in cache
259    pub fn put(&self, key: OperationKey, data: CachedData) {
260        let size = data.estimate_size();
261        let entry = CachedOperation {
262            data,
263            created_at: Instant::now(),
264            access_count: 0,
265            last_accessed: Instant::now(),
266            size_bytes: size,
267        };
268
269        // Check if we need to evict entries first
270        self.maybe_evict(&key, size);
271
272        // Insert the new entry
273        {
274            let mut cache = self.cache.write().unwrap();
275            cache.insert(key.clone(), entry);
276        }
277
278        // Update access order
279        {
280            let mut access_order = self.access_order.lock().unwrap();
281            access_order.push_back(key);
282        }
283
284        // Update statistics
285        if self.config.enable_stats {
286            if let Ok(mut stats) = self.stats.lock() {
287                stats.current_entries = self.cache.read().unwrap().len();
288                stats.current_memory_bytes += size;
289                if stats.current_memory_bytes > stats.peak_memory_bytes {
290                    stats.peak_memory_bytes = stats.current_memory_bytes;
291                }
292            }
293        }
294
295        // Trigger cleanup if needed
296        self.maybe_cleanup();
297    }
298
299    /// Update access statistics for an entry
300    fn update_access_stats(&self, key: &OperationKey) {
301        let mut cache = self.cache.write().unwrap();
302        if let Some(entry) = cache.get_mut(key) {
303            entry.access_count += 1;
304            entry.last_accessed = Instant::now();
305        }
306
307        // Update access order for LRU
308        if self.config.eviction_policy == EvictionPolicy::LRU {
309            let mut access_order = self.access_order.lock().unwrap();
310            // Remove key from current position and add to back
311            if let Some(pos) = access_order.iter().position(|k| k == key) {
312                access_order.remove(pos);
313            }
314            access_order.push_back(key.clone());
315        }
316    }
317
318    /// Check if eviction is needed and perform it
319    fn maybe_evict(&self, _new_key: &OperationKey, new_size: usize) {
320        let (current_entries, current_memory) = {
321            let cache = self.cache.read().unwrap();
322            (cache.len(), self.get_current_memory_usage())
323        };
324
325        let needs_eviction = current_entries >= self.config.max_entries
326            || current_memory + new_size > self.config.max_memory_bytes;
327
328        if needs_eviction {
329            self.evict_entries();
330        }
331    }
332
333    /// Evict entries based on configured policy
334    fn evict_entries(&self) {
335        match self.config.eviction_policy {
336            EvictionPolicy::LRU => self.evict_lru(),
337            EvictionPolicy::LFU => self.evict_lfu(),
338            EvictionPolicy::TTL => self.evict_expired(),
339            EvictionPolicy::FIFO => self.evict_fifo(),
340            EvictionPolicy::Hybrid => self.evict_hybrid(),
341        }
342    }
343
344    /// Evict least recently used entries
345    fn evict_lru(&self) {
346        let keys_to_evict: Vec<OperationKey> = {
347            let mut access_order = self.access_order.lock().unwrap();
348            let mut keys = Vec::new();
349
350            // Evict 25% of entries or until memory constraint is satisfied
351            let max_evictions = (self.config.max_entries / 4).max(1);
352
353            for _ in 0..max_evictions {
354                if let Some(key) = access_order.pop_front() {
355                    keys.push(key);
356                } else {
357                    break;
358                }
359            }
360            keys
361        };
362
363        self.remove_entries(keys_to_evict);
364    }
365
366    /// Evict least frequently used entries
367    fn evict_lfu(&self) {
368        let keys_to_evict: Vec<OperationKey> = {
369            let cache = self.cache.read().unwrap();
370            let mut entries: Vec<_> = cache.iter().collect();
371
372            // Sort by access count (ascending)
373            entries.sort_by_key(|(_, entry)| entry.access_count);
374
375            // Take first 25% for eviction
376            let max_evictions = (cache.len() / 4).max(1);
377            entries
378                .into_iter()
379                .take(max_evictions)
380                .map(|(key, _)| key.clone())
381                .collect()
382        };
383
384        self.remove_entries(keys_to_evict);
385    }
386
387    /// Evict expired entries based on TTL
388    fn evict_expired(&self) {
389        let now = Instant::now();
390        let keys_to_evict: Vec<OperationKey> = {
391            let cache = self.cache.read().unwrap();
392            cache
393                .iter()
394                .filter(|(_, entry)| now.duration_since(entry.created_at) > self.config.ttl)
395                .map(|(key, _)| key.clone())
396                .collect()
397        };
398
399        self.remove_entries(keys_to_evict);
400    }
401
402    /// Evict in FIFO order
403    fn evict_fifo(&self) {
404        // Similar to LRU but based on creation time
405        let keys_to_evict: Vec<OperationKey> = {
406            let cache = self.cache.read().unwrap();
407            let mut entries: Vec<_> = cache.iter().collect();
408
409            // Sort by creation time (ascending)
410            entries.sort_by_key(|(_, entry)| entry.created_at);
411
412            // Take first 25% for eviction
413            let max_evictions = (cache.len() / 4).max(1);
414            entries
415                .into_iter()
416                .take(max_evictions)
417                .map(|(key, _)| key.clone())
418                .collect()
419        };
420
421        self.remove_entries(keys_to_evict);
422    }
423
424    /// Hybrid eviction combining LRU and LFU
425    fn evict_hybrid(&self) {
426        let keys_to_evict: Vec<OperationKey> = {
427            let cache = self.cache.read().unwrap();
428            let mut entries: Vec<_> = cache.iter().collect();
429
430            // Hybrid score: combine recency and frequency
431            entries.sort_by_key(|(_, entry)| {
432                let recency_score = entry.last_accessed.elapsed().as_secs();
433                let frequency_score = 1000 / (entry.access_count + 1); // Inverse frequency
434                recency_score + frequency_score
435            });
436
437            // Take first 25% for eviction
438            let max_evictions = (cache.len() / 4).max(1);
439            entries
440                .into_iter()
441                .take(max_evictions)
442                .map(|(key, _)| key.clone())
443                .collect()
444        };
445
446        self.remove_entries(keys_to_evict);
447    }
448
449    /// Remove specified entries from cache
450    fn remove_entries(&self, keys: Vec<OperationKey>) {
451        let mut total_freed_memory = 0usize;
452
453        {
454            let mut cache = self.cache.write().unwrap();
455            for key in &keys {
456                if let Some(entry) = cache.remove(key) {
457                    total_freed_memory += entry.size_bytes;
458                }
459            }
460        }
461
462        // Update access order
463        {
464            let mut access_order = self.access_order.lock().unwrap();
465            access_order.retain(|key| !keys.contains(key));
466        }
467
468        // Update statistics
469        if self.config.enable_stats {
470            if let Ok(mut stats) = self.stats.lock() {
471                stats.evictions += keys.len() as u64;
472                stats.current_entries = self.cache.read().unwrap().len();
473                stats.current_memory_bytes = stats
474                    .current_memory_bytes
475                    .saturating_sub(total_freed_memory);
476            }
477        }
478    }
479
480    /// Get current memory usage
481    fn get_current_memory_usage(&self) -> usize {
482        let cache = self.cache.read().unwrap();
483        cache.values().map(|entry| entry.size_bytes).sum()
484    }
485
486    /// Periodic cleanup of expired entries
487    fn maybe_cleanup(&self) {
488        if let Ok(mut last_cleanup) = self.last_cleanup.try_lock() {
489            if last_cleanup.elapsed() > self.config.cleanup_interval {
490                self.evict_expired();
491                *last_cleanup = Instant::now();
492            }
493        }
494    }
495
496    /// Clear all cached entries
497    pub fn clear(&self) {
498        let mut cache = self.cache.write().unwrap();
499        cache.clear();
500
501        let mut access_order = self.access_order.lock().unwrap();
502        access_order.clear();
503
504        if self.config.enable_stats {
505            if let Ok(mut stats) = self.stats.lock() {
506                stats.current_entries = 0;
507                stats.current_memory_bytes = 0;
508            }
509        }
510    }
511
512    /// Get cache statistics
513    pub fn get_stats(&self) -> CacheStats {
514        self.stats.lock().unwrap().clone()
515    }
516
517    /// Get cache configuration
518    pub const fn get_config(&self) -> &CacheConfig {
519        &self.config
520    }
521}
522
523/// Specialized cache for gate matrices
524pub struct GateMatrixCache {
525    /// Internal operation cache
526    cache: QuantumOperationCache,
527}
528
529impl Default for GateMatrixCache {
530    fn default() -> Self {
531        Self::new()
532    }
533}
534
535impl GateMatrixCache {
536    /// Create new gate matrix cache
537    pub fn new() -> Self {
538        let config = CacheConfig {
539            max_entries: 5000,
540            max_memory_bytes: 64 * 1024 * 1024, // 64 MB
541            eviction_policy: EvictionPolicy::LRU,
542            ttl: Duration::from_secs(1800), // 30 minutes
543            enable_stats: true,
544            cleanup_interval: Duration::from_secs(120), // 2 minutes
545        };
546
547        Self {
548            cache: QuantumOperationCache::new(config),
549        }
550    }
551
552    /// Get single-qubit gate matrix
553    pub fn get_single_qubit_gate(
554        &self,
555        gate_name: &str,
556        parameters: &[f64],
557    ) -> Option<Array2<Complex64>> {
558        let key = OperationKey {
559            operation_type: format!("single_qubit_{gate_name}"),
560            parameters: parameters.to_vec(),
561            qubits: vec![],
562            metadata: HashMap::new(),
563        };
564
565        match self.cache.get(&key) {
566            Some(CachedData::SingleQubitMatrix(matrix)) => Some(matrix),
567            Some(CachedData::Matrix(matrix)) if matrix.shape() == [2, 2] => Some(matrix),
568            _ => None,
569        }
570    }
571
572    /// Cache single-qubit gate matrix
573    pub fn put_single_qubit_gate(
574        &self,
575        gate_name: &str,
576        parameters: &[f64],
577        matrix: Array2<Complex64>,
578    ) {
579        let key = OperationKey {
580            operation_type: format!("single_qubit_{gate_name}"),
581            parameters: parameters.to_vec(),
582            qubits: vec![],
583            metadata: HashMap::new(),
584        };
585
586        self.cache.put(key, CachedData::SingleQubitMatrix(matrix));
587    }
588
589    /// Get two-qubit gate matrix
590    pub fn get_two_qubit_gate(
591        &self,
592        gate_name: &str,
593        parameters: &[f64],
594    ) -> Option<Array2<Complex64>> {
595        let key = OperationKey {
596            operation_type: format!("two_qubit_{gate_name}"),
597            parameters: parameters.to_vec(),
598            qubits: vec![],
599            metadata: HashMap::new(),
600        };
601
602        match self.cache.get(&key) {
603            Some(CachedData::TwoQubitMatrix(matrix)) => Some(matrix),
604            Some(CachedData::Matrix(matrix)) if matrix.shape() == [4, 4] => Some(matrix),
605            _ => None,
606        }
607    }
608
609    /// Cache two-qubit gate matrix
610    pub fn put_two_qubit_gate(
611        &self,
612        gate_name: &str,
613        parameters: &[f64],
614        matrix: Array2<Complex64>,
615    ) {
616        let key = OperationKey {
617            operation_type: format!("two_qubit_{gate_name}"),
618            parameters: parameters.to_vec(),
619            qubits: vec![],
620            metadata: HashMap::new(),
621        };
622
623        self.cache.put(key, CachedData::TwoQubitMatrix(matrix));
624    }
625
626    /// Get cache statistics
627    pub fn get_stats(&self) -> CacheStats {
628        self.cache.get_stats()
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use scirs2_core::ndarray::array;
636
637    #[test]
638    fn test_operation_cache() {
639        let config = CacheConfig::default();
640        let cache = QuantumOperationCache::new(config);
641
642        let key = OperationKey {
643            operation_type: "test_op".to_string(),
644            parameters: vec![0.5, 1.0],
645            qubits: vec![0, 1],
646            metadata: HashMap::new(),
647        };
648
649        let matrix = array![
650            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
651            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]
652        ];
653
654        // Test cache miss
655        assert!(cache.get(&key).is_none());
656
657        // Test cache put and hit
658        cache.put(key.clone(), CachedData::Matrix(matrix.clone()));
659
660        match cache.get(&key) {
661            Some(CachedData::Matrix(cached_matrix)) => {
662                assert_eq!(cached_matrix, matrix);
663            }
664            _ => panic!("Expected cached matrix"),
665        }
666
667        let stats = cache.get_stats();
668        assert_eq!(stats.total_requests, 2);
669        assert_eq!(stats.hits, 1);
670        assert_eq!(stats.misses, 1);
671    }
672
673    #[test]
674    fn test_gate_matrix_cache() {
675        let cache = GateMatrixCache::new();
676
677        let pauli_x = array![
678            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
679            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]
680        ];
681
682        // Test cache miss
683        assert!(cache.get_single_qubit_gate("X", &[]).is_none());
684
685        // Test cache put and hit
686        cache.put_single_qubit_gate("X", &[], pauli_x.clone());
687
688        let cached_matrix = cache.get_single_qubit_gate("X", &[]).unwrap();
689        assert_eq!(cached_matrix, pauli_x);
690    }
691
692    #[test]
693    fn test_eviction_policies() {
694        let mut config = CacheConfig::default();
695        config.max_entries = 2;
696
697        let cache = QuantumOperationCache::new(config);
698
699        // Fill cache to capacity
700        for i in 0..3 {
701            let key = OperationKey {
702                operation_type: format!("test_{i}"),
703                parameters: vec![i as f64],
704                qubits: vec![i],
705                metadata: HashMap::new(),
706            };
707            cache.put(
708                key,
709                CachedData::ExpectationValue(Complex64::new(i as f64, 0.0)),
710            );
711        }
712
713        // Should have evicted first entry
714        let stats = cache.get_stats();
715        assert_eq!(stats.current_entries, 2);
716        assert!(stats.evictions > 0);
717    }
718}