//! # Swarm Interference //! //! Agents don't vote -- they *interfere*. Each agent contributes a complex //! amplitude toward one or more actions. Conflicting agents cancel //! (destructive interference). Reinforcing agents amplify (constructive //! interference). The decision emerges from the interference pattern, not //! from a majority vote or consensus protocol. //! //! ## Model //! //! - **Action**: something the swarm can do, identified by an `id` string. //! - **Agent contribution**: a complex amplitude per action. The *magnitude* //! encodes confidence; the *phase* encodes stance (0 = support, pi = oppose). //! - **Decision**: for each action, sum all contributing amplitudes. The //! resulting probability |sum|^2 determines the action's strength. //! //! Destructive interference naturally resolves conflicts: an action backed //! by 3 agents at phase 0 and opposed by 3 agents at phase pi has zero net //! amplitude, so it is detected as a deadlock. use ruqu_core::types::Complex; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; use std::collections::HashMap; use std::f64::consts::PI; // --------------------------------------------------------------------------- // Public types // --------------------------------------------------------------------------- /// An action that agents can support or oppose. #[derive(Debug, Clone, PartialEq)] pub struct Action { pub id: String, pub description: String, } /// An agent's complex-amplitude contribution to one or more actions. /// /// The amplitude encodes both confidence (magnitude) and stance (phase). /// Phase 0 = full support, phase pi = full opposition. pub struct AgentContribution { pub agent_id: String, pub amplitudes: Vec<(Action, Complex)>, } impl AgentContribution { /// Create a contribution where the agent supports or opposes a single /// action with the given confidence. /// /// - `confidence` in `[0, 1]` sets the magnitude. /// - `support = true` => phase 0 (constructive with other supporters). /// - `support = false` => phase pi (destructive against supporters). pub fn new(agent_id: &str, action: Action, confidence: f64, support: bool) -> Self { let phase = if support { 0.0 } else { PI }; let amplitude = Complex::from_polar(confidence, phase); Self { agent_id: agent_id.to_string(), amplitudes: vec![(action, amplitude)], } } /// Create a contribution spanning multiple actions with explicit complex /// amplitudes. pub fn multi(agent_id: &str, amplitudes: Vec<(Action, Complex)>) -> Self { Self { agent_id: agent_id.to_string(), amplitudes, } } } /// The swarm decision engine using quantum interference. pub struct SwarmInterference { contributions: Vec, } /// Result of swarm interference for a single action. #[derive(Debug)] pub struct SwarmDecision { /// The action evaluated. pub action: Action, /// |total_amplitude|^2 after interference. pub probability: f64, /// Number of agents whose phase reinforced the net amplitude. pub constructive_count: usize, /// Number of agents whose phase opposed the net amplitude. pub destructive_count: usize, } // --------------------------------------------------------------------------- // Implementation // --------------------------------------------------------------------------- impl SwarmInterference { /// Create an empty swarm interference engine. pub fn new() -> Self { Self { contributions: Vec::new(), } } /// Add an agent's contribution to the interference pattern. pub fn contribute(&mut self, contribution: AgentContribution) { self.contributions.push(contribution); } /// Compute the interference pattern across all agents for all actions. /// /// For each unique action (matched by `action.id`): /// 1. Sum all agent amplitudes (complex addition). /// 2. Compute probability = |sum|^2. /// 3. Classify each contributing agent as constructive or destructive /// relative to the net amplitude's phase. /// /// Returns actions sorted by probability (descending). pub fn decide(&self) -> Vec { let (action_map, amplitude_map, agent_phases_map) = self.aggregate(); let mut decisions: Vec = amplitude_map .into_iter() .map(|(id, total)| { let probability = total.norm_sq(); let net_phase = total.arg(); // Count constructive vs destructive contributors. let phases = agent_phases_map.get(&id).unwrap(); let mut constructive = 0usize; let mut destructive = 0usize; for &agent_phase in phases { let delta = Self::phase_distance(agent_phase, net_phase); if delta <= PI / 2.0 { constructive += 1; } else { destructive += 1; } } SwarmDecision { action: action_map[&id].clone(), probability, constructive_count: constructive, destructive_count: destructive, } }) .collect(); decisions.sort_by(|a, b| { b.probability .partial_cmp(&a.probability) .unwrap_or(std::cmp::Ordering::Equal) }); decisions } /// Return the winning action (highest probability after interference). pub fn winner(&self) -> Option { let decisions = self.decide(); decisions.into_iter().next() } /// Run `num_trials` decisions with additive quantum noise and return /// win counts: `Vec<(Action, wins)>` sorted by wins descending. /// /// Each trial adds a small random complex perturbation to every agent /// amplitude before summing. This models environmental noise and shows /// the stability of the interference pattern. pub fn decide_with_noise( &self, noise_level: f64, num_trials: usize, seed: u64, ) -> Vec<(Action, usize)> { let mut rng = StdRng::seed_from_u64(seed); let mut win_counts: HashMap = HashMap::new(); for _ in 0..num_trials { // Aggregate with noise. let mut amplitude_map: HashMap = HashMap::new(); let mut action_map: HashMap = HashMap::new(); for contrib in &self.contributions { for (action, amp) in &contrib.amplitudes { action_map .entry(action.id.clone()) .or_insert_with(|| action.clone()); // Add noise: random complex perturbation with magnitude up // to `noise_level`. let noise_r = rng.gen::() * noise_level; let noise_theta = rng.gen::() * 2.0 * PI; let noise = Complex::from_polar(noise_r, noise_theta); let noisy_amp = *amp + noise; let entry = amplitude_map .entry(action.id.clone()) .or_insert(Complex::ZERO); *entry = *entry + noisy_amp; } } // Find winner for this trial. if let Some((winner_id, _)) = amplitude_map.iter().max_by(|a, b| { a.1.norm_sq() .partial_cmp(&b.1.norm_sq()) .unwrap_or(std::cmp::Ordering::Equal) }) { let entry = win_counts .entry(winner_id.clone()) .or_insert_with(|| (action_map[winner_id].clone(), 0)); entry.1 += 1; } } let mut result: Vec<(Action, usize)> = win_counts.into_values().collect(); result.sort_by(|a, b| b.1.cmp(&a.1)); result } /// Check if the decision is deadlocked. /// /// A deadlock is detected when the top two actions have probabilities /// within `epsilon` of each other. pub fn is_deadlocked(&self, epsilon: f64) -> bool { let decisions = self.decide(); if decisions.len() < 2 { return false; } (decisions[0].probability - decisions[1].probability).abs() <= epsilon } /// Clear all contributions, resetting the engine. pub fn reset(&mut self) { self.contributions.clear(); } // ----------------------------------------------------------------------- // Private helpers // ----------------------------------------------------------------------- /// Aggregate all contributions by action id. /// /// Returns: /// - action_map: id -> canonical Action /// - amplitude_map: id -> summed complex amplitude /// - agent_phases_map: id -> list of each agent's contributing phase fn aggregate( &self, ) -> ( HashMap, HashMap, HashMap>, ) { let mut action_map: HashMap = HashMap::new(); let mut amplitude_map: HashMap = HashMap::new(); let mut agent_phases_map: HashMap> = HashMap::new(); for contrib in &self.contributions { for (action, amp) in &contrib.amplitudes { action_map .entry(action.id.clone()) .or_insert_with(|| action.clone()); let entry = amplitude_map .entry(action.id.clone()) .or_insert(Complex::ZERO); *entry = *entry + *amp; agent_phases_map .entry(action.id.clone()) .or_insert_with(Vec::new) .push(amp.arg()); } } (action_map, amplitude_map, agent_phases_map) } /// Absolute angular distance between two phases, in [0, pi]. fn phase_distance(a: f64, b: f64) -> f64 { let mut d = (a - b).abs() % (2.0 * PI); if d > PI { d = 2.0 * PI - d; } d } } impl Default for SwarmInterference { fn default() -> Self { Self::new() } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; fn action(id: &str) -> Action { Action { id: id.to_string(), description: id.to_string(), } } #[test] fn single_agent_support() { let mut swarm = SwarmInterference::new(); swarm.contribute(AgentContribution::new("alice", action("deploy"), 0.8, true)); let decisions = swarm.decide(); assert_eq!(decisions.len(), 1); assert_eq!(decisions[0].action.id, "deploy"); // probability = |0.8|^2 = 0.64 assert!((decisions[0].probability - 0.64).abs() < 1e-10); assert_eq!(decisions[0].constructive_count, 1); assert_eq!(decisions[0].destructive_count, 0); } #[test] fn constructive_interference() { let mut swarm = SwarmInterference::new(); // 3 agents all support "deploy" with confidence 1.0 swarm.contribute(AgentContribution::new("a", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("c", action("deploy"), 1.0, true)); let decisions = swarm.decide(); // Net amplitude = 3.0, probability = 9.0 assert!((decisions[0].probability - 9.0).abs() < 1e-10); assert_eq!(decisions[0].constructive_count, 3); assert_eq!(decisions[0].destructive_count, 0); } #[test] fn destructive_interference_cancels() { let mut swarm = SwarmInterference::new(); // 2 agents support, 2 oppose with equal confidence swarm.contribute(AgentContribution::new("a", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("c", action("deploy"), 1.0, false)); swarm.contribute(AgentContribution::new("d", action("deploy"), 1.0, false)); let decisions = swarm.decide(); // Net amplitude ~ 0, probability ~ 0 assert!(decisions[0].probability < 1e-10); } #[test] fn partial_cancellation() { let mut swarm = SwarmInterference::new(); // 3 support, 1 opposes => net amplitude ~ 2.0 swarm.contribute(AgentContribution::new("a", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("c", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("d", action("deploy"), 1.0, false)); let decisions = swarm.decide(); // Net amplitude = 3 - 1 = 2, probability = 4.0 assert!((decisions[0].probability - 4.0).abs() < 1e-10); assert_eq!(decisions[0].constructive_count, 3); assert_eq!(decisions[0].destructive_count, 1); } #[test] fn multiple_actions_sorted_by_probability() { let mut swarm = SwarmInterference::new(); // Action "deploy": 2 supporters swarm.contribute(AgentContribution::new("a", action("deploy"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("deploy"), 1.0, true)); // Action "rollback": 3 supporters swarm.contribute(AgentContribution::new("c", action("rollback"), 1.0, true)); swarm.contribute(AgentContribution::new("d", action("rollback"), 1.0, true)); swarm.contribute(AgentContribution::new("e", action("rollback"), 1.0, true)); let decisions = swarm.decide(); assert_eq!(decisions.len(), 2); assert_eq!(decisions[0].action.id, "rollback"); // P=9 assert_eq!(decisions[1].action.id, "deploy"); // P=4 } #[test] fn winner_returns_highest() { let mut swarm = SwarmInterference::new(); swarm.contribute(AgentContribution::new("a", action("A"), 0.5, true)); swarm.contribute(AgentContribution::new("b", action("B"), 1.0, true)); let w = swarm.winner().unwrap(); assert_eq!(w.action.id, "B"); } #[test] fn winner_empty_swarm() { let swarm = SwarmInterference::new(); assert!(swarm.winner().is_none()); } #[test] fn deadlock_detection() { let mut swarm = SwarmInterference::new(); // Two actions with exactly equal support swarm.contribute(AgentContribution::new("a", action("A"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("B"), 1.0, true)); assert!(swarm.is_deadlocked(1e-10)); } #[test] fn no_deadlock_with_clear_winner() { let mut swarm = SwarmInterference::new(); swarm.contribute(AgentContribution::new("a", action("A"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("A"), 1.0, true)); swarm.contribute(AgentContribution::new("c", action("B"), 0.1, true)); assert!(!swarm.is_deadlocked(0.01)); } #[test] fn reset_clears_contributions() { let mut swarm = SwarmInterference::new(); swarm.contribute(AgentContribution::new("a", action("X"), 1.0, true)); assert_eq!(swarm.decide().len(), 1); swarm.reset(); assert!(swarm.decide().is_empty()); } #[test] fn multi_contribution() { let mut swarm = SwarmInterference::new(); swarm.contribute(AgentContribution::multi( "alice", vec![ (action("A"), Complex::new(0.5, 0.0)), (action("B"), Complex::new(0.0, 0.3)), ], )); let decisions = swarm.decide(); assert_eq!(decisions.len(), 2); } #[test] fn noise_trials_are_reproducible() { let mut swarm = SwarmInterference::new(); swarm.contribute(AgentContribution::new("a", action("X"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("Y"), 0.5, true)); let r1 = swarm.decide_with_noise(0.1, 100, 42); let r2 = swarm.decide_with_noise(0.1, 100, 42); // Same seed -> same results. assert_eq!(r1.len(), r2.len()); for i in 0..r1.len() { assert_eq!(r1[i].0.id, r2[i].0.id); assert_eq!(r1[i].1, r2[i].1); } } #[test] fn noise_preserves_strong_winner() { let mut swarm = SwarmInterference::new(); // Action "A" has overwhelming support. swarm.contribute(AgentContribution::new("a", action("A"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("A"), 1.0, true)); swarm.contribute(AgentContribution::new("c", action("A"), 1.0, true)); // Action "B" has weak support. swarm.contribute(AgentContribution::new("d", action("B"), 0.1, true)); let results = swarm.decide_with_noise(0.05, 200, 7); // "A" should win the vast majority of trials. assert_eq!(results[0].0.id, "A"); assert!(results[0].1 > 150, "A should win most trials"); } #[test] fn default_trait() { let swarm = SwarmInterference::default(); assert!(swarm.decide().is_empty()); } #[test] fn complete_cancellation_detects_deadlock() { let mut swarm = SwarmInterference::new(); // Perfect cancellation on a single action. swarm.contribute(AgentContribution::new("a", action("X"), 1.0, true)); swarm.contribute(AgentContribution::new("b", action("X"), 1.0, false)); let decisions = swarm.decide(); assert_eq!(decisions.len(), 1); assert!(decisions[0].probability < 1e-10); } }