Skip to main content

exo_core/backends/
quantum_stub.rs

1//! QuantumStubBackend — feature-gated quantum substrate for EXO-AI.
2//!
3//! When `ruqu` feature is not enabled, provides a classical simulation
4//! that matches the quantum backend's interface. Enables compilation and
5//! testing without ruQu dependency while preserving integration contract.
6//!
7//! ADR-029: ruQu exotic algorithms (interference_search, reasoning_qec,
8//! quantum_decay) are the canonical quantum backend when enabled.
9
10use super::{AdaptResult, SearchResult, SubstrateBackend};
11use std::time::Instant;
12
13/// Quantum measurement outcome (amplitude → probability)
14#[derive(Debug, Clone)]
15pub struct QuantumMeasurement {
16    pub basis_state: u64,
17    pub probability: f64,
18    pub amplitude_re: f64,
19    pub amplitude_im: f64,
20}
21
22/// Quantum decoherence parameters (T1/T2 analog for pattern eviction)
23#[derive(Debug, Clone)]
24pub struct DecoherenceParams {
25    /// T1 relaxation time (ms) — energy loss
26    pub t1_ms: f64,
27    /// T2 dephasing time (ms) — coherence loss
28    pub t2_ms: f64,
29}
30
31impl Default for DecoherenceParams {
32    fn default() -> Self {
33        // Typical superconducting qubit parameters, scaled to cognitive timescales
34        Self {
35            t1_ms: 100.0,
36            t2_ms: 50.0,
37        }
38    }
39}
40
41/// Quantum interference state (2^n basis states, compressed representation)
42struct InterferenceState {
43    #[allow(dead_code)]
44    n_qubits: usize,
45    /// State amplitudes (real, imaginary) — only track non-negligible amplitudes
46    amplitudes: Vec<(u64, f64, f64)>, // (basis_state, re, im)
47    /// Decoherence clock (ms since initialization)
48    age_ms: f64,
49    params: DecoherenceParams,
50}
51
52impl InterferenceState {
53    fn new(n_qubits: usize) -> Self {
54        // Initialize in equal superposition |+⟩^n
55        let n_states = 1usize << n_qubits.min(8); // Cap at 8 qubits for memory
56        let amp = 1.0 / (n_states as f64).sqrt();
57        let amplitudes = (0..n_states as u64).map(|i| (i, amp, 0.0)).collect();
58        Self {
59            n_qubits: n_qubits.min(8),
60            amplitudes,
61            age_ms: 0.0,
62            params: DecoherenceParams::default(),
63        }
64    }
65
66    /// Apply T1/T2 decoherence after dt_ms milliseconds.
67    fn decohere(&mut self, dt_ms: f64) {
68        self.age_ms += dt_ms;
69        let t1_decay = (-self.age_ms / self.params.t1_ms).exp();
70        let t2_decay = (-self.age_ms / self.params.t2_ms).exp();
71        for (_, re, im) in self.amplitudes.iter_mut() {
72            *re *= t1_decay * t2_decay;
73            *im *= t2_decay;
74        }
75    }
76
77    /// Compute coherence (purity measure: Tr(ρ²))
78    fn purity(&self) -> f64 {
79        let norm_sq: f64 = self
80            .amplitudes
81            .iter()
82            .map(|(_, re, im)| re * re + im * im)
83            .sum();
84        norm_sq
85    }
86
87    /// Apply quantum interference: embed classical vector as phase rotations.
88    /// |ψ⟩ → Σ_i v_i e^{iθ_i} |i⟩ (normalized)
89    fn embed_vector(&mut self, vec: &[f32]) {
90        use std::f64::consts::TAU;
91        for (i, (_, re, im)) in self.amplitudes.iter_mut().enumerate() {
92            let v = vec.get(i).copied().unwrap_or(0.0) as f64;
93            let phase = v * TAU; // Map [-1,1] to [-2π, 2π]
94            let magnitude = (*re * *re + *im * *im).sqrt();
95            *re = phase.cos() * magnitude;
96            *im = phase.sin() * magnitude;
97        }
98        // Renormalize
99        let norm = self
100            .amplitudes
101            .iter()
102            .map(|(_, r, i)| r * r + i * i)
103            .sum::<f64>()
104            .sqrt();
105        if norm > 1e-10 {
106            for (_, re, im) in self.amplitudes.iter_mut() {
107                *re /= norm;
108                *im /= norm;
109            }
110        }
111    }
112
113    /// Measure: collapse to basis states, return top-k by probability.
114    #[allow(dead_code)]
115    fn measure_top_k(&self, k: usize) -> Vec<QuantumMeasurement> {
116        let mut measurements: Vec<QuantumMeasurement> = self
117            .amplitudes
118            .iter()
119            .map(|&(basis_state, re, im)| QuantumMeasurement {
120                basis_state,
121                probability: re * re + im * im,
122                amplitude_re: re,
123                amplitude_im: im,
124            })
125            .collect();
126        measurements.sort_unstable_by(|a, b| {
127            b.probability
128                .partial_cmp(&a.probability)
129                .unwrap_or(std::cmp::Ordering::Equal)
130        });
131        measurements.truncate(k);
132        measurements
133    }
134}
135
136/// Quantum stub backend — classical simulation of quantum interference search.
137pub struct QuantumStubBackend {
138    n_qubits: usize,
139    state: InterferenceState,
140    stored_patterns: Vec<(u64, Vec<f32>)>,
141    next_id: u64,
142    decohere_dt_ms: f64,
143}
144
145impl QuantumStubBackend {
146    pub fn new(n_qubits: usize) -> Self {
147        let n = n_qubits.min(8);
148        Self {
149            n_qubits: n,
150            state: InterferenceState::new(n),
151            stored_patterns: Vec::new(),
152            next_id: 0,
153            decohere_dt_ms: 10.0,
154        }
155    }
156
157    /// Quantum decay-based eviction: remove patterns whose T2 coherence is below threshold.
158    pub fn evict_decoherent(&mut self, coherence_threshold: f64) {
159        self.state.decohere(self.decohere_dt_ms);
160        let purity = self.state.purity();
161        if purity < coherence_threshold {
162            // Re-initialize state (decoherence-driven forgetting)
163            self.state = InterferenceState::new(self.n_qubits);
164        }
165    }
166
167    pub fn purity(&self) -> f64 {
168        self.state.purity()
169    }
170
171    pub fn store(&mut self, pattern: &[f32]) -> u64 {
172        let id = self.next_id;
173        self.stored_patterns.push((id, pattern.to_vec()));
174        self.next_id += 1;
175        // Embed into quantum state as interference pattern
176        self.state.embed_vector(pattern);
177        id
178    }
179}
180
181impl SubstrateBackend for QuantumStubBackend {
182    fn name(&self) -> &'static str {
183        "quantum-interference-stub"
184    }
185
186    fn similarity_search(&self, query: &[f32], k: usize) -> Vec<SearchResult> {
187        let t0 = Instant::now();
188        // Classical interference: inner product weighted by quantum amplitudes
189        let mut results: Vec<SearchResult> = self
190            .stored_patterns
191            .iter()
192            .map(|(id, pattern)| {
193                // Score = |⟨ψ|query⟩|² weighted by pattern norm
194                let inner: f32 = pattern
195                    .iter()
196                    .zip(query.iter())
197                    .map(|(a, b)| a * b)
198                    .sum::<f32>();
199                let norm_p = pattern.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-8);
200                let norm_q = query.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-8);
201                // Amplitude-weighted cosine similarity
202                let score = (inner / (norm_p * norm_q)) * self.state.purity() as f32;
203                SearchResult {
204                    id: *id,
205                    score: score.max(0.0),
206                    embedding: pattern.clone(),
207                }
208            })
209            .collect();
210        results.sort_unstable_by(|a, b| {
211            b.score
212                .partial_cmp(&a.score)
213                .unwrap_or(std::cmp::Ordering::Equal)
214        });
215        results.truncate(k);
216        let _elapsed = t0.elapsed();
217        results
218    }
219
220    fn adapt(&mut self, pattern: &[f32], reward: f32) -> AdaptResult {
221        let t0 = Instant::now();
222        if reward.abs() > 0.5 {
223            self.store(pattern);
224        }
225        // Decohere proportional to time (quantum decay = forgetting)
226        self.evict_decoherent(0.5);
227        let delta_norm = pattern.iter().map(|x| x * x).sum::<f32>().sqrt() * reward.abs();
228        AdaptResult {
229            delta_norm,
230            mode: "quantum-decay-adapt",
231            latency_us: t0.elapsed().as_micros() as u64,
232        }
233    }
234
235    fn coherence(&self) -> f32 {
236        self.state.purity() as f32
237    }
238
239    fn reset(&mut self) {
240        self.state = InterferenceState::new(self.n_qubits);
241        self.stored_patterns.clear();
242        self.next_id = 0;
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_quantum_state_initialized() {
252        let backend = QuantumStubBackend::new(4);
253        // Initial purity of pure equal superposition = 1.0
254        assert!(
255            (backend.purity() - 1.0).abs() < 1e-6,
256            "Initial state should be pure"
257        );
258    }
259
260    #[test]
261    fn test_quantum_decoherence() {
262        let mut backend = QuantumStubBackend::new(4);
263        backend.state.params.t1_ms = 10.0;
264        backend.state.params.t2_ms = 5.0;
265        let initial_purity = backend.purity();
266        for _ in 0..50 {
267            backend.evict_decoherent(0.01); // Very low threshold, don't reset
268            backend.state.decohere(2.0);
269        }
270        // Purity should have decreased due to T1/T2 decay
271        assert!(
272            backend.purity() < initial_purity,
273            "Decoherence should reduce purity"
274        );
275    }
276
277    #[test]
278    fn test_quantum_similarity_search() {
279        let mut backend = QuantumStubBackend::new(4);
280        let p1 = vec![1.0f32, 0.0, 0.0, 0.0];
281        let p2 = vec![0.0f32, 1.0, 0.0, 0.0];
282        backend.store(&p1);
283        backend.store(&p2);
284
285        let results = backend.similarity_search(&p1, 2);
286        assert!(!results.is_empty());
287        // p1 should score highest against query p1
288        assert!(results[0].score >= results.get(1).map(|r| r.score).unwrap_or(0.0));
289    }
290
291    #[test]
292    fn test_interference_embedding() {
293        let mut state = InterferenceState::new(4);
294        let vec = vec![0.5f32; 8];
295        state.embed_vector(&vec);
296        // After embedding, state should remain normalized (purity ≤ 1)
297        assert!(
298            state.purity() <= 1.0 + 1e-6,
299            "Quantum state must remain normalized"
300        );
301    }
302}