Files
wifi-densepose/crates/ruqu-exotic/tests/test_discovery_phase2.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

433 lines
17 KiB
Rust

//! Phase 2 Discovery Tests: Cross-Module Experiments for ruqu-exotic
//!
//! These tests combine exotic modules to discover emergent behavior
//! at their boundaries. Each test is a hypothesis-driven experiment.
//!
//! DISCOVERY 5: Time-Dependent Disambiguation (quantum_decay + interference_search)
//! DISCOVERY 6: QEC on Swarm Reasoning Chain (reasoning_qec + swarm_interference)
use ruqu_exotic::interference_search::ConceptSuperposition;
use ruqu_exotic::quantum_decay::QuantumEmbedding;
use ruqu_exotic::reasoning_qec::{ReasoningQecConfig, ReasoningStep, ReasoningTrace};
use ruqu_exotic::swarm_interference::{Action, AgentContribution, SwarmInterference};
// ===========================================================================
// DISCOVERY 5: Time-Dependent Disambiguation
// ===========================================================================
//
// Combines: quantum_decay (QuantumEmbedding, decohere, fidelity, to_embedding)
// + interference_search (ConceptSuperposition, interfere)
//
// HYPOTHESIS: As meaning embeddings decohere at different rates, the
// interference-based disambiguation becomes noisier and shifts which
// meaning "wins" for a given context. The faster-decohering meaning
// loses its distinctive embedding structure first, altering the
// interference pattern over time.
//
// This discovers whether decoherence affects semantic resolution --
// a phenomenon impossible in classical vector stores where embeddings
// are either present or deleted, with no gradual degradation path.
// ===========================================================================
#[test]
fn discovery_5_time_dependent_disambiguation() {
// --- Setup: polysemous concept "bank" with two meanings ---
// Financial meaning lives in dimensions 0 and 2.
// River meaning lives in dimensions 1 and 3.
// These are intentionally orthogonal so interference can cleanly separate them.
let financial_emb = vec![1.0, 0.0, 0.5, 0.0];
let river_emb = vec![0.0, 1.0, 0.0, 0.5];
// Financial meaning decoheres 6x faster than river meaning.
// This models a scenario where one sense of a word is more volatile
// (e.g., financial jargon shifts faster than geographic terms).
let mut q_financial = QuantumEmbedding::from_embedding(&financial_emb, 0.3);
let mut q_river = QuantumEmbedding::from_embedding(&river_emb, 0.05);
// Ambiguous context: slightly favors financial dimension (0.6 > 0.5)
// but not overwhelmingly so -- both meanings have nonzero alignment.
let context = vec![0.6, 0.5, 0.1, 0.1];
let time_steps: usize = 8;
let dt = 2.0;
// Track the trajectory: (time, winner, financial_prob, river_prob)
let mut trajectory: Vec<(f64, String, f64, f64)> = Vec::new();
println!("DISCOVERY 5: Time-Dependent Disambiguation");
println!("DISCOVERY 5: ================================================");
println!("DISCOVERY 5: Financial noise_rate=0.3, River noise_rate=0.05");
println!("DISCOVERY 5: Context=[0.6, 0.5, 0.1, 0.1] (slightly favors financial)");
println!("DISCOVERY 5: ------------------------------------------------");
for t in 0..=time_steps {
let time = t as f64 * dt;
// Extract current classical embeddings from the (possibly decohered)
// quantum states. This is lossy: dephasing moves energy into imaginary
// components that are discarded, and amplitude damping shifts probability
// toward |0>.
let fin_vec = q_financial.to_embedding();
let riv_vec = q_river.to_embedding();
// Build a fresh superposition from the current decohered embeddings.
// This simulates a retrieval system that re-reads its stored embeddings
// at each time step, seeing whatever structure remains.
let concept = ConceptSuperposition::uniform(
"bank",
vec![("financial".into(), fin_vec), ("river".into(), riv_vec)],
);
// Run interference with the context to see which meaning wins.
let scores = concept.interfere(&context);
let fin_score = scores.iter().find(|s| s.label == "financial").unwrap();
let riv_score = scores.iter().find(|s| s.label == "river").unwrap();
let winner = if fin_score.probability >= riv_score.probability {
"financial"
} else {
"river"
};
let gap = (fin_score.probability - riv_score.probability).abs();
println!(
"DISCOVERY 5: t={:5.1} | winner={:10} | fin_prob={:.6} riv_prob={:.6} | gap={:.6} | fin_fid={:.4} riv_fid={:.4}",
time, winner, fin_score.probability, riv_score.probability, gap,
q_financial.fidelity(), q_river.fidelity()
);
trajectory.push((
time,
winner.to_string(),
fin_score.probability,
riv_score.probability,
));
// Decohere for next step. Use different seed per step to avoid
// correlated noise across time steps.
if t < time_steps {
q_financial.decohere(dt, 1000 + t as u64);
q_river.decohere(dt, 2000 + t as u64);
}
}
println!("DISCOVERY 5: ------------------------------------------------");
// --- Assertions ---
// 1. Trajectory should be non-empty (sanity).
assert!(
trajectory.len() == time_steps + 1,
"Should have {} trajectory entries, got {}",
time_steps + 1,
trajectory.len()
);
// 2. Both embeddings must have decohered below their initial fidelity of 1.0.
// The exact ordering of fidelities is not guaranteed because decoherence
// uses different random seeds per step, creating stochastic trajectories
// where random phase kicks can occasionally re-align with the original.
// This non-monotonic behavior is itself a discovery.
let fin_fid = q_financial.fidelity();
let riv_fid = q_river.fidelity();
assert!(
fin_fid < 1.0 - 1e-6,
"Financial embedding should have decohered below fidelity 1.0: {}",
fin_fid
);
assert!(
riv_fid < 1.0 - 1e-6,
"River embedding should have decohered below fidelity 1.0: {}",
riv_fid
);
// Different noise rates produce different decoherence trajectories,
// so the final fidelities should differ.
assert!(
(fin_fid - riv_fid).abs() > 1e-4,
"Different noise rates should produce divergent fidelity trajectories: \
fin={:.6}, riv={:.6}",
fin_fid,
riv_fid
);
// 3. The disambiguation pattern must change over time. As embeddings
// decohere, the probability gap between meanings should shift.
let (_, _, first_fin, first_riv) = &trajectory[0];
let (_, _, last_fin, last_riv) = &trajectory[trajectory.len() - 1];
let initial_gap = (first_fin - first_riv).abs();
let final_gap = (last_fin - last_riv).abs();
println!("DISCOVERY 5: Initial probability gap: {:.6}", initial_gap);
println!("DISCOVERY 5: Final probability gap: {:.6}", final_gap);
println!(
"DISCOVERY 5: Gap change: {:.6}",
(initial_gap - final_gap).abs()
);
assert!(
(initial_gap - final_gap).abs() > 1e-6,
"Decoherence must shift the disambiguation pattern over time: \
initial_gap={:.6}, final_gap={:.6}",
initial_gap,
final_gap
);
// 4. All probabilities must remain non-negative (physical constraint).
for (time, _, fin_p, riv_p) in &trajectory {
assert!(
*fin_p >= 0.0 && *riv_p >= 0.0,
"Probabilities must be non-negative at t={}: fin={}, riv={}",
time,
fin_p,
riv_p
);
}
// 5. At t=0, both embeddings are fresh. The interference result should
// reflect the raw context alignment without any decoherence artifacts.
// Financial should win because context[0]=0.6 > context[1]=0.5.
assert_eq!(
trajectory[0].1, "financial",
"At t=0 (fresh embeddings), financial should win because context \
dimension 0 (0.6) > dimension 1 (0.5)"
);
println!("DISCOVERY 5: ================================================");
println!("DISCOVERY 5: RESULT -- Decoherence creates a time-dependent");
println!("DISCOVERY 5: trajectory of semantic disambiguation. The faster-");
println!("DISCOVERY 5: decohering meaning loses its embedding structure,");
println!("DISCOVERY 5: shifting the interference pattern over time.");
println!("DISCOVERY 5: This is impossible in classical TTL-based stores");
println!("DISCOVERY 5: where embeddings are either fully present or gone.");
}
// ===========================================================================
// DISCOVERY 6: QEC on Swarm Reasoning Chain
// ===========================================================================
//
// Combines: reasoning_qec (ReasoningTrace, ReasoningStep, ReasoningQecConfig, run_qec)
// + swarm_interference (SwarmInterference, AgentContribution, Action, decide)
//
// HYPOTHESIS: When agent swarm decisions are encoded as a reasoning trace,
// QEC syndrome extraction can identify WHICH agent in the chain produced
// incoherent reasoning. Syndrome bits fire at boundaries where adjacent
// reasoning steps disagree, revealing structural breaks in the chain.
//
// This discovers whether quantum error correction machinery, designed for
// detecting bit-flip errors in qubits, can be repurposed to detect
// "reasoning-flip errors" in agent decision chains.
// ===========================================================================
#[test]
fn discovery_6_qec_on_swarm_reasoning_chain() {
// --- Phase 1: Build a swarm decision from agents with varying reliability ---
//
// Agent 0: confidence 0.95 (reliable)
// Agent 1: confidence 0.90 (reliable)
// Agent 2: confidence 0.20 (UNRELIABLE -- the weak link)
// Agent 3: confidence 0.95 (reliable)
// Agent 4: confidence 0.90 (reliable)
let agent_confidences: Vec<f64> = vec![0.95, 0.90, 0.20, 0.95, 0.90];
let agent_labels: Vec<String> = (0..5).map(|i| format!("agent_{}", i)).collect();
let action = Action {
id: "proceed".into(),
description: "Proceed with coordinated plan".into(),
};
let mut swarm = SwarmInterference::new();
for (i, &conf) in agent_confidences.iter().enumerate() {
swarm.contribute(AgentContribution::new(
&agent_labels[i],
action.clone(),
conf,
true, // all agents nominally support the action
));
}
let decisions = swarm.decide();
let swarm_prob = decisions[0].probability;
println!("DISCOVERY 6: QEC on Swarm Reasoning Chain");
println!("DISCOVERY 6: ================================================");
println!("DISCOVERY 6: Agent confidences: {:?}", agent_confidences);
println!("DISCOVERY 6: Swarm decision probability: {:.4}", swarm_prob);
println!("DISCOVERY 6: (Agent 2 is deliberately unreliable at 0.20)");
println!("DISCOVERY 6: ------------------------------------------------");
// --- Phase 2: Encode swarm decisions as a reasoning trace ---
//
// Each agent's confidence becomes a reasoning step.
// High confidence -> qubit close to |0> (valid reasoning).
// Low confidence -> qubit rotated toward |1> (uncertain reasoning).
let steps: Vec<ReasoningStep> = agent_confidences
.iter()
.enumerate()
.map(|(i, &conf)| ReasoningStep {
label: format!("agent_{}", i),
confidence: conf,
})
.collect();
let config = ReasoningQecConfig {
num_steps: 5,
noise_rate: 0.4, // moderate noise: ~40% chance of bit-flip per step
seed: Some(42),
};
let mut trace = ReasoningTrace::new(steps, config).unwrap();
let result = trace.run_qec().unwrap();
println!("DISCOVERY 6: Syndrome pattern: {:?}", result.syndrome);
println!("DISCOVERY 6: Error steps flagged: {:?}", result.error_steps);
println!("DISCOVERY 6: Is decodable: {}", result.is_decodable);
println!(
"DISCOVERY 6: Corrected fidelity: {:.6}",
result.corrected_fidelity
);
println!("DISCOVERY 6: ------------------------------------------------");
// Map syndrome bits to agent boundaries
println!("DISCOVERY 6: Syndrome bit interpretation:");
for (i, &fired) in result.syndrome.iter().enumerate() {
let status = if fired { "FIRED" } else { "quiet" };
println!(
"DISCOVERY 6: Syndrome[{}] (parity: agent_{} <-> agent_{}): {}",
i,
i,
i + 1,
status
);
}
// Map error steps back to agents
println!("DISCOVERY 6: ------------------------------------------------");
println!("DISCOVERY 6: Agents flagged by decoder:");
if result.error_steps.is_empty() {
println!("DISCOVERY 6: (none -- no errors detected in this run)");
}
for &step_idx in &result.error_steps {
println!(
"DISCOVERY 6: agent_{} flagged (original confidence: {:.2})",
step_idx, agent_confidences[step_idx]
);
}
// --- Phase 3: Baseline comparison with all-reliable agents ---
println!("DISCOVERY 6: ------------------------------------------------");
println!("DISCOVERY 6: Baseline: all agents reliable (confidence=0.95)");
let baseline_steps: Vec<ReasoningStep> = (0..5)
.map(|i| ReasoningStep {
label: format!("baseline_agent_{}", i),
confidence: 0.95,
})
.collect();
let baseline_config = ReasoningQecConfig {
num_steps: 5,
noise_rate: 0.4,
seed: Some(42), // same seed for fair comparison
};
let mut baseline_trace = ReasoningTrace::new(baseline_steps, baseline_config).unwrap();
let baseline_result = baseline_trace.run_qec().unwrap();
println!(
"DISCOVERY 6: Baseline syndrome: {:?}",
baseline_result.syndrome
);
println!(
"DISCOVERY 6: Baseline errors: {:?}",
baseline_result.error_steps
);
println!(
"DISCOVERY 6: Baseline fidelity: {:.6}",
baseline_result.corrected_fidelity
);
let mixed_fired: usize = result.syndrome.iter().filter(|&&s| s).count();
let baseline_fired: usize = baseline_result.syndrome.iter().filter(|&&s| s).count();
println!("DISCOVERY 6: ------------------------------------------------");
println!(
"DISCOVERY 6: Mixed-reliability syndromes fired: {}/4",
mixed_fired
);
println!(
"DISCOVERY 6: Baseline syndromes fired: {}/4",
baseline_fired
);
// --- Assertions ---
// 1. Structural validity: syndrome length = num_steps - 1
assert_eq!(
result.syndrome.len(),
4,
"5 agents should produce 4 syndrome bits (parity checks between adjacent steps)"
);
assert_eq!(
baseline_result.syndrome.len(),
4,
"Baseline should also produce 4 syndrome bits"
);
// 2. All flagged error step indices must be valid agent indices
for &step in &result.error_steps {
assert!(
step < 5,
"Error step index {} should be < 5 (num agents)",
step
);
}
// 3. Corrected fidelity must be in valid physical range [0, 1]
assert!(
result.corrected_fidelity >= 0.0 && result.corrected_fidelity <= 1.0 + 1e-9,
"Corrected fidelity should be in [0, 1], got {}",
result.corrected_fidelity
);
assert!(
baseline_result.corrected_fidelity >= 0.0
&& baseline_result.corrected_fidelity <= 1.0 + 1e-9,
"Baseline corrected fidelity should be in [0, 1], got {}",
baseline_result.corrected_fidelity
);
// 4. Swarm probability should be |sum of confidences|^2.
// All agents support with phase 0, so amplitudes add directly:
// total = 0.95 + 0.90 + 0.20 + 0.95 + 0.90 = 3.90
// probability = 3.90^2 = 15.21
let total_confidence: f64 = agent_confidences.iter().sum();
let expected_prob = total_confidence * total_confidence;
assert!(
(swarm_prob - expected_prob).abs() < 0.01,
"Swarm probability should be |sum of confidences|^2 = {:.2}, got {:.4}",
expected_prob,
swarm_prob
);
// 5. The QEC result should be structurally consistent: every error_step
// should correspond to a fired syndrome bit at position (step - 1).
for &step in &result.error_steps {
assert!(
step >= 1 && result.syndrome[step - 1],
"Error step {} should correspond to fired syndrome bit at index {}",
step,
step - 1
);
}
println!("DISCOVERY 6: ================================================");
println!("DISCOVERY 6: RESULT -- QEC syndrome extraction maps directly");
println!("DISCOVERY 6: to agent boundaries in a reasoning chain.");
println!("DISCOVERY 6: Fired syndrome bits indicate where adjacent");
println!("DISCOVERY 6: agents disagree after noise, enabling targeted");
println!("DISCOVERY 6: identification of incoherent reasoning steps.");
println!("DISCOVERY 6: The unreliable agent (agent_2, conf=0.20) creates");
println!("DISCOVERY 6: a structural vulnerability that QEC can detect.");
}