500 lines
18 KiB
Rust
500 lines
18 KiB
Rust
//! # 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<AgentContribution>,
|
|
}
|
|
|
|
/// 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<SwarmDecision> {
|
|
let (action_map, amplitude_map, agent_phases_map) = self.aggregate();
|
|
|
|
let mut decisions: Vec<SwarmDecision> = 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<SwarmDecision> {
|
|
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<String, (Action, usize)> = HashMap::new();
|
|
|
|
for _ in 0..num_trials {
|
|
// Aggregate with noise.
|
|
let mut amplitude_map: HashMap<String, Complex> = HashMap::new();
|
|
let mut action_map: HashMap<String, Action> = 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::<f64>() * noise_level;
|
|
let noise_theta = rng.gen::<f64>() * 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<String, Action>,
|
|
HashMap<String, Complex>,
|
|
HashMap<String, Vec<f64>>,
|
|
) {
|
|
let mut action_map: HashMap<String, Action> = HashMap::new();
|
|
let mut amplitude_map: HashMap<String, Complex> = HashMap::new();
|
|
let mut agent_phases_map: HashMap<String, Vec<f64>> = 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);
|
|
}
|
|
}
|