//! # Interference Search //! //! Concepts interfere during retrieval. Each concept can exist in a //! superposition of multiple meanings, each with a complex amplitude. //! When a search context is applied, the amplitudes interfere -- //! meanings aligned with the context get constructively boosted, //! while misaligned meanings destructively cancel. //! //! This replaces simple cosine reranking with a quantum-inspired //! interference model where polysemous concepts naturally resolve //! to context-appropriate meanings. use ruqu_core::types::Complex; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; // --------------------------------------------------------------------------- // Public types // --------------------------------------------------------------------------- /// A single meaning within a superposition: a label, an embedding, and a /// complex amplitude. #[derive(Debug, Clone)] pub struct Meaning { pub label: String, pub embedding: Vec, pub amplitude: Complex, } /// A concept in superposition of multiple meanings. #[derive(Debug, Clone)] pub struct ConceptSuperposition { pub concept_id: String, pub meanings: Vec, } /// Score for a single meaning after interference with a context. #[derive(Debug, Clone)] pub struct InterferenceScore { pub label: String, pub probability: f64, pub amplitude: Complex, } /// A concept with its interference-computed relevance score. #[derive(Debug, Clone)] pub struct ConceptScore { pub concept_id: String, pub relevance: f64, pub dominant_meaning: String, } // --------------------------------------------------------------------------- // Implementation // --------------------------------------------------------------------------- impl ConceptSuperposition { /// Create a uniform superposition: all meanings get equal amplitude /// with zero phase. pub fn uniform(concept_id: &str, meanings: Vec<(String, Vec)>) -> Self { let n = meanings.len(); let amp = if n > 0 { 1.0 / (n as f64).sqrt() } else { 0.0 }; let meanings = meanings .into_iter() .map(|(label, embedding)| Meaning { label, embedding, amplitude: Complex::new(amp, 0.0), }) .collect(); Self { concept_id: concept_id.to_string(), meanings, } } /// Create a superposition with explicit complex amplitudes. pub fn with_amplitudes(concept_id: &str, meanings: Vec<(String, Vec, Complex)>) -> Self { let meanings = meanings .into_iter() .map(|(label, embedding, amplitude)| Meaning { label, embedding, amplitude, }) .collect(); Self { concept_id: concept_id.to_string(), meanings, } } /// Compute interference scores for each meaning given a context embedding. /// /// For each meaning, the context modifies the amplitude: /// effective_amplitude = original_amplitude * (1 + similarity(meaning, context)) /// /// Meanings aligned with the context get amplified; orthogonal meanings /// stay the same; opposing meanings get attenuated. /// /// Returns scores sorted by probability (descending). pub fn interfere(&self, context: &[f64]) -> Vec { let mut scores: Vec = self .meanings .iter() .map(|m| { let sim = cosine_similarity(&m.embedding, context); // Scale amplitude by (1 + sim). For sim in [-1, 1], this gives // a factor in [0, 2]. Negative similarity attenuates. let scale = (1.0 + sim).max(0.0); let effective = m.amplitude * scale; InterferenceScore { label: m.label.clone(), probability: effective.norm_sq(), amplitude: effective, } }) .collect(); scores.sort_by(|a, b| { b.probability .partial_cmp(&a.probability) .unwrap_or(std::cmp::Ordering::Equal) }); scores } /// Collapse the superposition to a single meaning by sampling from /// the interference-weighted probability distribution. pub fn collapse(&self, context: &[f64], seed: u64) -> String { let scores = self.interfere(context); let total: f64 = scores.iter().map(|s| s.probability).sum(); if total < 1e-15 { // Degenerate case: return first meaning if available return scores.first().map(|s| s.label.clone()).unwrap_or_default(); } let mut rng = StdRng::seed_from_u64(seed); let r: f64 = rng.gen::() * total; let mut cumulative = 0.0; for score in &scores { cumulative += score.probability; if r <= cumulative { return score.label.clone(); } } scores.last().map(|s| s.label.clone()).unwrap_or_default() } /// Return the dominant meaning: the one with the largest |amplitude|^2 /// (before any context is applied). pub fn dominant(&self) -> Option<&Meaning> { self.meanings.iter().max_by(|a, b| { a.amplitude .norm_sq() .partial_cmp(&b.amplitude.norm_sq()) .unwrap_or(std::cmp::Ordering::Equal) }) } } /// Run an interference search across multiple concepts, ranking them by /// relevance to the given query context. /// /// Returns concepts sorted by relevance (descending). pub fn interference_search( concepts: &[ConceptSuperposition], context: &[f64], ) -> Vec { let mut results: Vec = concepts .iter() .map(|concept| { let scores = concept.interfere(context); let relevance: f64 = scores.iter().map(|s| s.probability).sum(); let dominant_meaning = scores.first().map(|s| s.label.clone()).unwrap_or_default(); ConceptScore { concept_id: concept.concept_id.clone(), relevance, dominant_meaning, } }) .collect(); results.sort_by(|a, b| { b.relevance .partial_cmp(&a.relevance) .unwrap_or(std::cmp::Ordering::Equal) }); results } /// Cosine similarity between two vectors. fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 { if a.is_empty() || b.is_empty() { return 0.0; } let len = a.len().min(b.len()); let mut dot = 0.0_f64; let mut norm_a = 0.0_f64; let mut norm_b = 0.0_f64; for i in 0..len { dot += a[i] * b[i]; norm_a += a[i] * a[i]; norm_b += b[i] * b[i]; } let denom = norm_a.sqrt() * norm_b.sqrt(); if denom < 1e-15 { 0.0 } else { (dot / denom).clamp(-1.0, 1.0) } }