Files
wifi-densepose/vendor/ruvector/crates/ruqu-exotic/src/quantum_collapse.rs

401 lines
14 KiB
Rust

//! # 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<Vec<f64>>,
/// 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<Vec<f64>>) -> 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<Complex> = 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(&amplitudes, 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<f64> = amplitudes.iter().map(|a| a.norm_sq()).collect();
let total: f64 = probs.iter().sum();
let r: f64 = rng.gen::<f64>() * 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<f64>> {
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);
}
}