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