Skip to main content

exo_temporal/
quantum_decay.rs

1//! Quantum Decay Memory Eviction — ADR-029 temporal memory extension.
2//!
3//! Replaces hard TTL expiry with T1/T2-inspired decoherence-based eviction.
4//! Patterns decohere with time constants proportional to their retrieval
5//! frequency and IIT Φ value — high-Φ, often-retrieved patterns have longer
6//! coherence times (Φ-stabilized memory).
7//!
8//! Key insight: T2 < T1 always (dephasing faster than relaxation), matching
9//! the empirical observation that memory detail fades before memory existence.
10
11use std::time::{Duration, Instant};
12
13/// Per-pattern decoherence state
14#[derive(Debug, Clone)]
15pub struct PatternDecoherence {
16    /// Pattern id
17    pub id: u64,
18    /// T1 relaxation time (energy/existence decay)
19    pub t1: Duration,
20    /// T2 dephasing time (detail/coherence decay)
21    pub t2: Duration,
22    /// Initial creation time
23    pub created_at: Instant,
24    /// Last retrieval time (refreshes coherence)
25    pub last_retrieved: Instant,
26    /// Φ value at creation — high Φ → longer coherence
27    pub phi: f64,
28    /// Retrieval count (higher count → refreshed T1)
29    pub retrieval_count: u32,
30}
31
32impl PatternDecoherence {
33    pub fn new(id: u64, phi: f64) -> Self {
34        let now = Instant::now();
35        // Base times: T1 = 60s, T2 = 30s (T2 < T1 always)
36        // Φ-scaling: high Φ extends both times
37        let phi_factor = (1.0 + phi * 0.5).min(10.0); // max 10x extension
38        let t1 = Duration::from_millis((60_000.0 * phi_factor) as u64);
39        let t2 = Duration::from_millis((30_000.0 * phi_factor) as u64);
40        Self {
41            id,
42            t1,
43            t2,
44            created_at: now,
45            last_retrieved: now,
46            phi,
47            retrieval_count: 0,
48        }
49    }
50
51    /// Refresh coherence on retrieval (use-dependent plasticity analog)
52    pub fn refresh(&mut self) {
53        self.last_retrieved = Instant::now();
54        self.retrieval_count += 1;
55        // Hebbian refreshing: each retrieval extends T2 by 10%
56        self.t2 = Duration::from_millis(
57            (self.t2.as_millis() as f64 * 1.1).min(self.t1.as_millis() as f64) as u64,
58        );
59    }
60
61    /// Current T2 coherence amplitude (1.0 = fully coherent, 0.0 = decoherent)
62    pub fn coherence_amplitude(&self) -> f64 {
63        let elapsed = self.last_retrieved.elapsed().as_millis() as f64;
64        let t2_ms = self.t2.as_millis() as f64;
65        (-elapsed / t2_ms).exp().max(0.0)
66    }
67
68    /// Current T1 existence probability (1.0 = exists, 0.0 = relaxed/forgotten)
69    pub fn existence_probability(&self) -> f64 {
70        let elapsed = self.created_at.elapsed().as_millis() as f64;
71        let t1_ms = self.t1.as_millis() as f64;
72        (-elapsed / t1_ms).exp().max(0.0)
73    }
74
75    /// Combined decoherence score for eviction decisions.
76    /// Low score → candidate for eviction.
77    pub fn decoherence_score(&self) -> f64 {
78        self.coherence_amplitude() * self.existence_probability()
79    }
80
81    /// Should this pattern be evicted?
82    pub fn should_evict(&self, threshold: f64) -> bool {
83        self.decoherence_score() < threshold
84    }
85}
86
87/// Quantum decay memory manager: tracks decoherence for a pool of patterns
88pub struct QuantumDecayPool {
89    pub patterns: Vec<PatternDecoherence>,
90    /// Eviction threshold (patterns below this decoherence score are evicted)
91    pub eviction_threshold: f64,
92    /// Maximum pool size (hard cap)
93    pub max_size: usize,
94}
95
96impl QuantumDecayPool {
97    pub fn new(max_size: usize) -> Self {
98        Self {
99            patterns: Vec::with_capacity(max_size),
100            eviction_threshold: 0.1,
101            max_size,
102        }
103    }
104
105    /// Register a pattern with its Φ value.
106    pub fn register(&mut self, id: u64, phi: f64) {
107        if self.patterns.len() >= self.max_size {
108            self.evict_weakest();
109        }
110        self.patterns.push(PatternDecoherence::new(id, phi));
111    }
112
113    /// Record retrieval — refreshes coherence.
114    pub fn on_retrieve(&mut self, id: u64) {
115        if let Some(p) = self.patterns.iter_mut().find(|p| p.id == id) {
116            p.refresh();
117        }
118    }
119
120    /// Get decoherence-weighted score for search results.
121    pub fn weighted_score(&self, id: u64, base_score: f64) -> f64 {
122        self.patterns
123            .iter()
124            .find(|p| p.id == id)
125            .map(|p| base_score * (0.3 + 0.7 * p.decoherence_score()))
126            .unwrap_or(base_score * 0.5) // Unknown patterns get 50% weight
127    }
128
129    /// Evict decoherent patterns, return count evicted.
130    pub fn evict_decoherent(&mut self) -> usize {
131        let threshold = self.eviction_threshold;
132        let before = self.patterns.len();
133        self.patterns.retain(|p| !p.should_evict(threshold));
134        before - self.patterns.len()
135    }
136
137    /// Evict the weakest pattern (lowest decoherence score).
138    fn evict_weakest(&mut self) {
139        if let Some(idx) = self
140            .patterns
141            .iter()
142            .enumerate()
143            .min_by(|a, b| {
144                a.1.decoherence_score()
145                    .partial_cmp(&b.1.decoherence_score())
146                    .unwrap_or(std::cmp::Ordering::Equal)
147            })
148            .map(|(i, _)| i)
149        {
150            self.patterns.remove(idx);
151        }
152    }
153
154    pub fn len(&self) -> usize {
155        self.patterns.len()
156    }
157    pub fn is_empty(&self) -> bool {
158        self.patterns.is_empty()
159    }
160
161    /// Statistics for monitoring
162    pub fn stats(&self) -> DecayPoolStats {
163        if self.patterns.is_empty() {
164            return DecayPoolStats::default();
165        }
166        let scores: Vec<f64> = self
167            .patterns
168            .iter()
169            .map(|p| p.decoherence_score())
170            .collect();
171        let mean = scores.iter().sum::<f64>() / scores.len() as f64;
172        let min = scores.iter().cloned().fold(f64::INFINITY, f64::min);
173        let max = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
174        DecayPoolStats {
175            count: self.patterns.len(),
176            mean_score: mean,
177            min_score: min,
178            max_score: max,
179        }
180    }
181}
182
183#[derive(Debug, Default)]
184pub struct DecayPoolStats {
185    pub count: usize,
186    pub mean_score: f64,
187    pub min_score: f64,
188    pub max_score: f64,
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_phi_extends_coherence_time() {
197        let low_phi = PatternDecoherence::new(0, 0.1);
198        let high_phi = PatternDecoherence::new(1, 5.0);
199        // High Φ pattern should have longer T1 and T2
200        assert!(high_phi.t1 > low_phi.t1, "High Φ should extend T1");
201        assert!(high_phi.t2 > low_phi.t2, "High Φ should extend T2");
202    }
203
204    #[test]
205    fn test_t2_less_than_t1() {
206        let pattern = PatternDecoherence::new(0, 1.0);
207        assert!(
208            pattern.t2 <= pattern.t1,
209            "T2 must never exceed T1 (physical constraint)"
210        );
211    }
212
213    #[test]
214    fn test_retrieval_refreshes_coherence() {
215        let mut pattern = PatternDecoherence::new(0, 1.0);
216        let initial_t2 = pattern.t2;
217        pattern.refresh();
218        assert!(pattern.t2 >= initial_t2, "Retrieval should not decrease T2");
219        assert_eq!(pattern.retrieval_count, 1);
220    }
221
222    #[test]
223    fn test_pool_evicts_decoherent() {
224        let mut pool = QuantumDecayPool::new(100);
225        // Add pattern with very short T2 (will decohere fast)
226        let mut fast_decoh = PatternDecoherence::new(99, 0.0001);
227        fast_decoh.t1 = Duration::from_micros(1);
228        fast_decoh.t2 = Duration::from_micros(1);
229        pool.patterns.push(fast_decoh);
230        // High-Φ pattern should survive
231        pool.register(1, 10.0);
232        std::thread::sleep(Duration::from_millis(5));
233        let evicted = pool.evict_decoherent();
234        assert!(evicted > 0, "Fast-decoherent pattern should be evicted");
235        assert!(
236            pool.patterns.iter().any(|p| p.id == 1),
237            "High-Φ pattern should survive"
238        );
239    }
240
241    #[test]
242    fn test_decoherence_weighted_score() {
243        let mut pool = QuantumDecayPool::new(10);
244        pool.register(5, 2.0);
245        let weighted = pool.weighted_score(5, 1.0);
246        // Should be between 0.3 and 1.0 (decoherence_score is in [0,1])
247        assert!(
248            weighted > 0.0 && weighted <= 1.0,
249            "Weighted score should be in (0,1]"
250        );
251    }
252}