//! # Quantum Collapse Search //! //! Instead of deterministic top-k retrieval, encode search candidates as //! quantum amplitudes. Apply Grover-like iterations biased by query similarity, //! then "measure" to collapse to a single result. Nondeterministic but //! statistically stable -- repeated shots converge to a reproducible //! frequency distribution weighted by relevance. //! //! ## Algorithm //! //! 1. Initialise a uniform superposition over all candidate slots. //! 2. **Oracle**: apply a phase rotation proportional to cosine similarity //! between the query and each candidate embedding. //! 3. **Diffusion**: inversion about the mean amplitude (Grover diffusion). //! 4. Repeat for the requested number of iterations. //! 5. **Collapse**: sample one index from the |amplitude|^2 distribution. //! //! The oracle biases the superposition toward high-similarity candidates. //! Multiple collapses yield a frequency distribution that concentrates on //! the most relevant candidates while still allowing serendipitous discovery. use ruqu_core::types::Complex; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; use std::f64::consts::PI; // --------------------------------------------------------------------------- // Public types // --------------------------------------------------------------------------- /// A quantum search engine that collapses from superposition rather than /// ranking deterministically. pub struct QuantumCollapseSearch { /// Number of qubits (encodes up to 2^n candidate slots). num_qubits: u32, /// Candidate embeddings, padded with zero-vectors to length 2^num_qubits. candidates: Vec>, /// Number of *real* candidates (the rest are zero-padding). num_real: usize, } /// Result of a single collapse measurement. #[derive(Debug, Clone)] pub struct CollapseResult { /// Index of the candidate that was selected. pub index: usize, /// Amplitude magnitude before collapse (acts as confidence). pub amplitude: f64, /// `true` if the collapse landed on a padding slot (no real candidate). pub is_padding: bool, } // --------------------------------------------------------------------------- // Implementation // --------------------------------------------------------------------------- impl QuantumCollapseSearch { /// Number of qubits used to encode the candidate space. pub fn num_qubits(&self) -> u32 { self.num_qubits } /// Number of real (non-padding) candidates. pub fn num_real(&self) -> usize { self.num_real } /// Create a search engine from candidate embeddings. /// /// The candidate list is padded with empty vectors to the next power of two /// so that the amplitude vector has length 2^n. pub fn new(candidates: Vec>) -> Self { let num_real = candidates.len(); // Determine the number of qubits needed. let num_qubits = if num_real <= 1 { 1 } else { (num_real as f64).log2().ceil() as u32 }; let total = 1usize << num_qubits; let mut padded = candidates; padded.resize(total, Vec::new()); Self { num_qubits, candidates: padded, num_real, } } /// Run a single quantum collapse search. /// /// 1. Initialise a uniform superposition over all candidate slots. /// 2. For each iteration, apply the similarity-biased oracle followed by /// the Grover diffusion operator. /// 3. Sample one index from the resulting probability distribution. pub fn search(&self, query: &[f64], iterations: usize, seed: u64) -> CollapseResult { let n = self.candidates.len(); assert!(n > 0, "no candidates"); // --- Uniform superposition --- let amp = 1.0 / (n as f64).sqrt(); let mut amplitudes: Vec = vec![Complex::new(amp, 0.0); n]; // --- Grover-like iterations --- for _ in 0..iterations { // Oracle: phase rotation proportional to similarity self.apply_oracle(query, &mut amplitudes); // Diffusion: inversion about the mean Self::apply_diffusion(&mut amplitudes); } // --- Collapse (sample from |amplitude|^2 distribution) --- self.collapse(&litudes, seed) } /// Run `num_shots` independent collapses and return a frequency /// distribution: `Vec<(index, count)>` sorted by count descending. /// /// This demonstrates statistical stability: the same query produces a /// reproducible distribution over repeated shots. pub fn search_distribution( &self, query: &[f64], iterations: usize, num_shots: usize, seed: u64, ) -> Vec<(usize, usize)> { let n = self.candidates.len(); let mut counts = vec![0usize; n]; for shot in 0..num_shots { // Each shot gets a deterministic but distinct seed. let shot_seed = seed.wrapping_add(shot as u64); let result = self.search(query, iterations, shot_seed); counts[result.index] += 1; } // Collect non-zero counts, sorted descending. let mut distribution: Vec<(usize, usize)> = counts .into_iter() .enumerate() .filter(|&(_, c)| c > 0) .collect(); distribution.sort_by(|a, b| b.1.cmp(&a.1)); distribution } // ----------------------------------------------------------------------- // Private helpers // ----------------------------------------------------------------------- /// Similarity-biased oracle. /// /// For each candidate slot `i`, compute the cosine similarity between the /// query and the candidate embedding. Apply a phase rotation of /// `PI * similarity` to the amplitude, boosting candidates that align with /// the query. fn apply_oracle(&self, query: &[f64], amplitudes: &mut [Complex]) { for (i, candidate) in self.candidates.iter().enumerate() { let sim = Self::similarity(query, candidate); // Phase rotation: amplitude[i] *= e^{i * PI * sim} let phase = Complex::from_polar(1.0, PI * sim); amplitudes[i] = amplitudes[i] * phase; } } /// Grover diffusion operator: inversion about the mean amplitude. /// /// mean = (1/n) * sum(amplitudes) /// amplitudes[i] = 2 * mean - amplitudes[i] fn apply_diffusion(amplitudes: &mut [Complex]) { let n = amplitudes.len(); let inv_n = 1.0 / n as f64; let mut mean = Complex::ZERO; for a in amplitudes.iter() { mean += *a; } mean = mean * inv_n; let two_mean = mean * 2.0; for a in amplitudes.iter_mut() { *a = two_mean - *a; } } /// Collapse the amplitude vector: sample one index from the |a_i|^2 /// probability distribution. fn collapse(&self, amplitudes: &[Complex], seed: u64) -> CollapseResult { let mut rng = StdRng::seed_from_u64(seed); // Build the cumulative probability distribution. let probs: Vec = amplitudes.iter().map(|a| a.norm_sq()).collect(); let total: f64 = probs.iter().sum(); let r: f64 = rng.gen::() * total; let mut cumulative = 0.0; let mut chosen = amplitudes.len() - 1; // fallback to last for (i, &p) in probs.iter().enumerate() { cumulative += p; if r <= cumulative { chosen = i; break; } } CollapseResult { index: chosen, amplitude: amplitudes[chosen].norm(), is_padding: chosen >= self.num_real, } } /// Cosine similarity between two vectors. /// /// Returns 0.0 if either vector is empty or has zero norm. fn similarity(query: &[f64], candidate: &[f64]) -> f64 { if query.is_empty() || candidate.is_empty() { return 0.0; } let len = query.len().min(candidate.len()); let mut dot = 0.0_f64; let mut norm_q = 0.0_f64; let mut norm_c = 0.0_f64; for i in 0..len { dot += query[i] * candidate[i]; norm_q += query[i] * query[i]; norm_c += candidate[i] * candidate[i]; } // Account for any remaining elements in the longer vector. for i in len..query.len() { norm_q += query[i] * query[i]; } for i in len..candidate.len() { norm_c += candidate[i] * candidate[i]; } let denom = norm_q.sqrt() * norm_c.sqrt(); if denom < 1e-15 { 0.0 } else { (dot / denom).clamp(-1.0, 1.0) } } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; /// Helper: create simple 2D embeddings. fn sample_candidates() -> Vec> { vec![ vec![1.0, 0.0], // 0: east vec![0.0, 1.0], // 1: north vec![-1.0, 0.0], // 2: west vec![0.0, -1.0], // 3: south ] } #[test] fn new_pads_to_power_of_two() { // 3 candidates should pad to 4 (2 qubits) let search = QuantumCollapseSearch::new(vec![vec![1.0], vec![2.0], vec![3.0]]); assert_eq!(search.num_qubits, 2); assert_eq!(search.candidates.len(), 4); assert_eq!(search.num_real, 3); } #[test] fn similarity_identical_vectors() { let a = vec![1.0, 2.0, 3.0]; let sim = QuantumCollapseSearch::similarity(&a, &a); assert!((sim - 1.0).abs() < 1e-10); } #[test] fn similarity_orthogonal_vectors() { let a = vec![1.0, 0.0]; let b = vec![0.0, 1.0]; let sim = QuantumCollapseSearch::similarity(&a, &b); assert!(sim.abs() < 1e-10); } #[test] fn similarity_opposite_vectors() { let a = vec![1.0, 0.0]; let b = vec![-1.0, 0.0]; let sim = QuantumCollapseSearch::similarity(&a, &b); assert!((sim + 1.0).abs() < 1e-10); } #[test] fn similarity_empty_returns_zero() { assert_eq!(QuantumCollapseSearch::similarity(&[], &[1.0, 2.0]), 0.0); assert_eq!(QuantumCollapseSearch::similarity(&[1.0], &[]), 0.0); } #[test] fn single_candidate_always_collapses_to_it() { let search = QuantumCollapseSearch::new(vec![vec![1.0, 0.0]]); let query = [1.0, 0.0]; for seed in 0..20 { let result = search.search(&query, 3, seed); // With 1 real candidate and 1 padding, we should almost always // get index 0 after iterations biased toward the real candidate. // At minimum check that the result is valid. assert!(result.index < 2); } } #[test] fn search_favors_similar_candidates() { // Use asymmetric candidates so only one is highly aligned with the query. let candidates = vec![ vec![1.0, 0.0], // 0: very aligned vec![0.3, 0.7], // 1: partially aligned vec![0.0, 1.0], // 2: orthogonal vec![-0.5, 0.5], // 3: partially opposed ]; let search = QuantumCollapseSearch::new(candidates); let query = [1.0, 0.0]; // aligned with candidate 0 // Run many shots to build a distribution. let dist = search.search_distribution(&query, 1, 500, 42); assert!(!dist.is_empty(), "distribution should not be empty"); // The distribution should be non-uniform (oracle has an effect). // We just verify the distribution has variation. let max_count = dist.iter().map(|&(_, c)| c).max().unwrap_or(0); let min_count = dist.iter().map(|&(_, c)| c).min().unwrap_or(0); assert!( max_count > min_count, "distribution should be non-uniform: max {} vs min {}", max_count, min_count ); } #[test] fn search_distribution_is_reproducible() { let search = QuantumCollapseSearch::new(sample_candidates()); let query = [0.7, 0.7]; let d1 = search.search_distribution(&query, 2, 100, 99); let d2 = search.search_distribution(&query, 2, 100, 99); assert_eq!(d1, d2, "same seed should produce identical distributions"); } #[test] fn collapse_result_flags_padding() { // 3 real candidates -> padded to 4 let search = QuantumCollapseSearch::new(vec![vec![0.0, 1.0], vec![1.0, 0.0], vec![0.5, 0.5]]); // Run many shots; any hit on index 3 should have is_padding = true. for seed in 0..50 { let result = search.search(&[0.0, 0.0], 0, seed); if result.index >= 3 { assert!(result.is_padding); } else { assert!(!result.is_padding); } } } #[test] fn zero_iterations_gives_uniform_distribution() { let search = QuantumCollapseSearch::new(sample_candidates()); let query = [1.0, 0.0]; // With 0 iterations, the superposition stays uniform. // Each of the 4 candidates should get roughly 25% of 1000 shots. let dist = search.search_distribution(&query, 0, 1000, 7); for &(_, count) in &dist { // Should be roughly 250 +/- some variance assert!(count > 100, "expected roughly uniform: got {count}"); } } #[test] fn amplitude_is_positive() { let search = QuantumCollapseSearch::new(sample_candidates()); let result = search.search(&[1.0, 0.0], 2, 0); assert!(result.amplitude >= 0.0); } }