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

352 lines
12 KiB
Rust

//! # Reasoning QEC -- Quantum Error Correction for Reasoning Traces
//!
//! Treats reasoning steps like qubits. Each step is encoded as a quantum state
//! (high confidence = close to |0>, low confidence = rotated toward |1>).
//! Noise is injected to simulate reasoning errors, then a repetition-code-style
//! syndrome extraction detects when adjacent steps become incoherent.
//!
//! This provides **structural** reasoning integrity checks, not semantic ones.
//! The 1D repetition code uses:
//! - N data qubits (one per reasoning step)
//! - N-1 ancilla qubits (parity checks between adjacent steps)
//! - Total: 2N - 1 qubits (maximum N = 13 to stay within 25-qubit limit)
use ruqu_core::error::QuantumError;
use ruqu_core::gate::Gate;
use ruqu_core::state::QuantumState;
use ruqu_core::types::Complex;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
/// A single reasoning step encoded as a quantum state.
/// The step is either "valid" (close to |0>) or "flawed" (close to |1>).
#[derive(Debug, Clone)]
pub struct ReasoningStep {
pub label: String,
pub confidence: f64, // 0.0 = completely uncertain, 1.0 = fully confident
}
/// Configuration for reasoning QEC
pub struct ReasoningQecConfig {
/// Number of reasoning steps (data qubits)
pub num_steps: usize,
/// Noise rate per step (probability of error per step)
pub noise_rate: f64,
/// Seed for reproducibility
pub seed: Option<u64>,
}
/// Result of a reasoning QEC analysis
#[derive(Debug)]
pub struct ReasoningQecResult {
/// Which steps had errors detected (indices)
pub error_steps: Vec<usize>,
/// Syndrome bits (one per stabilizer)
pub syndrome: Vec<bool>,
/// Whether the overall reasoning trace is decodable (correctable)
pub is_decodable: bool,
/// Fidelity of the reasoning trace after correction
pub corrected_fidelity: f64,
/// Number of steps total
pub num_steps: usize,
}
/// A reasoning trace with QEC-style error detection.
///
/// Maps reasoning steps to a 1D repetition code:
/// - Each step is a data qubit
/// - Stabilizers check parity between adjacent steps
/// - If adjacent steps disagree (one flipped, one not), syndrome fires
///
/// This is simpler than a full surface code but captures the key idea:
/// structural detection of reasoning incoherence.
pub struct ReasoningTrace {
steps: Vec<ReasoningStep>,
state: QuantumState,
config: ReasoningQecConfig,
}
impl ReasoningTrace {
/// Create a new reasoning trace from steps.
/// Each step's confidence maps to a rotation: high confidence = close to |0>.
/// Total qubits = num_steps (data) + (num_steps - 1) (ancilla for parity checks)
pub fn new(
steps: Vec<ReasoningStep>,
config: ReasoningQecConfig,
) -> Result<Self, QuantumError> {
let num_steps = steps.len();
if num_steps == 0 {
return Err(QuantumError::CircuitError(
"reasoning trace requires at least one step".into(),
));
}
// Total qubits: data (0..num_steps) + ancillas (num_steps..2*num_steps-1)
let total_qubits = (2 * num_steps - 1) as u32;
// Check qubit limit early (MAX_QUBITS = 25)
if total_qubits > 25 {
return Err(QuantumError::QubitLimitExceeded {
requested: total_qubits,
maximum: 25,
});
}
let seed = config.seed.unwrap_or(42);
let mut state = QuantumState::new_with_seed(total_qubits, seed)?;
// Encode each step: rotate by angle based on confidence
// confidence=1.0 -> |0> (no rotation), confidence=0.0 -> equal superposition (pi/2)
for (i, step) in steps.iter().enumerate() {
let angle = std::f64::consts::FRAC_PI_2 * (1.0 - step.confidence);
if angle.abs() > 1e-15 {
state.apply_gate(&Gate::Ry(i as u32, angle))?;
}
}
Ok(Self {
steps,
state,
config,
})
}
/// Inject noise into the reasoning trace.
/// Each step independently suffers a bit flip (X error) with probability noise_rate.
pub fn inject_noise(&mut self) -> Result<(), QuantumError> {
let seed = self.config.seed.unwrap_or(42).wrapping_add(12345);
let mut rng = StdRng::seed_from_u64(seed);
for i in 0..self.steps.len() {
if rng.gen::<f64>() < self.config.noise_rate {
self.state.apply_gate(&Gate::X(i as u32))?;
}
}
Ok(())
}
/// Extract syndrome by checking parity between adjacent reasoning steps.
/// Uses ancilla qubits to perform non-destructive parity measurement.
/// Syndrome bit i fires if steps i and i+1 disagree (ZZ stabilizer = -1).
pub fn extract_syndrome(&mut self) -> Result<Vec<bool>, QuantumError> {
let num_steps = self.steps.len();
let mut syndrome = Vec::with_capacity(num_steps.saturating_sub(1));
for i in 0..(num_steps - 1) {
let data1 = i as u32;
let data2 = (i + 1) as u32;
let ancilla = (num_steps + i) as u32;
// Reset ancilla to |0>
self.state.reset_qubit(ancilla)?;
// CNOT from data1 to ancilla, CNOT from data2 to ancilla
// Ancilla will be |1> if data1 != data2
self.state.apply_gate(&Gate::CNOT(data1, ancilla))?;
self.state.apply_gate(&Gate::CNOT(data2, ancilla))?;
// Measure ancilla
let outcome = self.state.measure(ancilla)?;
syndrome.push(outcome.result);
}
Ok(syndrome)
}
/// Decode syndrome and attempt correction.
/// Simple decoder: if syndrome\[i\] fires, flip step i+1 (rightmost error assumption).
pub fn decode_and_correct(&mut self, syndrome: &[bool]) -> Result<Vec<usize>, QuantumError> {
let mut corrected = Vec::new();
// Simple decoder: for each fired syndrome, the error is likely
// between the two data qubits. Correct the right one.
for (i, &fired) in syndrome.iter().enumerate() {
if fired {
let step_to_correct = i + 1;
self.state.apply_gate(&Gate::X(step_to_correct as u32))?;
corrected.push(step_to_correct);
}
}
Ok(corrected)
}
/// Run the full QEC pipeline: inject noise, extract syndrome, decode, correct.
pub fn run_qec(&mut self) -> Result<ReasoningQecResult, QuantumError> {
// Save state before noise for fidelity comparison
let clean_sv: Vec<Complex> = self.state.state_vector().to_vec();
let clean_state = QuantumState::from_amplitudes(clean_sv, self.state.num_qubits())?;
// Inject noise
self.inject_noise()?;
// Extract syndrome
let syndrome = self.extract_syndrome()?;
// Determine which steps have errors
let mut error_steps = Vec::new();
for (i, &s) in syndrome.iter().enumerate() {
if s {
error_steps.push(i + 1);
}
}
let is_decodable = error_steps.len() <= self.steps.len() / 2;
// Attempt correction
if is_decodable {
self.decode_and_correct(&syndrome)?;
}
let corrected_fidelity = self.state.fidelity(&clean_state);
Ok(ReasoningQecResult {
error_steps,
syndrome,
is_decodable,
corrected_fidelity,
num_steps: self.steps.len(),
})
}
/// Get the number of reasoning steps
pub fn num_steps(&self) -> usize {
self.steps.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_steps(n: usize, confidence: f64) -> Vec<ReasoningStep> {
(0..n)
.map(|i| ReasoningStep {
label: format!("step_{}", i),
confidence,
})
.collect()
}
#[test]
fn test_new_creates_trace() {
let steps = make_steps(5, 1.0);
let config = ReasoningQecConfig {
num_steps: 5,
noise_rate: 0.0,
seed: Some(42),
};
let trace = ReasoningTrace::new(steps, config);
assert!(trace.is_ok());
assert_eq!(trace.unwrap().num_steps(), 5);
}
#[test]
fn test_empty_steps_rejected() {
let config = ReasoningQecConfig {
num_steps: 0,
noise_rate: 0.0,
seed: Some(42),
};
let result = ReasoningTrace::new(vec![], config);
assert!(result.is_err());
}
#[test]
fn test_qubit_limit_exceeded() {
// 14 steps -> 2*14-1 = 27 qubits > 25
let steps = make_steps(14, 1.0);
let config = ReasoningQecConfig {
num_steps: 14,
noise_rate: 0.0,
seed: Some(42),
};
let result = ReasoningTrace::new(steps, config);
assert!(result.is_err());
}
#[test]
fn test_max_allowed_steps() {
// 13 steps -> 2*13-1 = 25 qubits = MAX_QUBITS (should succeed)
let steps = make_steps(13, 1.0);
let config = ReasoningQecConfig {
num_steps: 13,
noise_rate: 0.0,
seed: Some(42),
};
let result = ReasoningTrace::new(steps, config);
assert!(result.is_ok());
}
#[test]
fn test_no_noise_no_syndrome() {
let steps = make_steps(5, 1.0);
let config = ReasoningQecConfig {
num_steps: 5,
noise_rate: 0.0,
seed: Some(42),
};
let mut trace = ReasoningTrace::new(steps, config).unwrap();
let syndrome = trace.extract_syndrome().unwrap();
// All steps fully confident (|0>) and no noise: parity checks should not fire
assert!(syndrome.iter().all(|&s| !s));
}
#[test]
fn test_run_qec_zero_noise() {
let steps = make_steps(5, 1.0);
let config = ReasoningQecConfig {
num_steps: 5,
noise_rate: 0.0,
seed: Some(42),
};
let mut trace = ReasoningTrace::new(steps, config).unwrap();
let result = trace.run_qec().unwrap();
assert!(result.error_steps.is_empty());
assert!(result.is_decodable);
}
#[test]
fn test_run_qec_with_noise() {
let steps = make_steps(5, 1.0);
let config = ReasoningQecConfig {
num_steps: 5,
noise_rate: 0.5,
seed: Some(100),
};
let mut trace = ReasoningTrace::new(steps, config).unwrap();
let result = trace.run_qec().unwrap();
assert_eq!(result.num_steps, 5);
// Syndrome length = num_steps - 1
assert_eq!(result.syndrome.len(), 4);
}
#[test]
fn test_single_step_trace() {
let steps = make_steps(1, 0.8);
let config = ReasoningQecConfig {
num_steps: 1,
noise_rate: 0.0,
seed: Some(42),
};
let mut trace = ReasoningTrace::new(steps, config).unwrap();
let syndrome = trace.extract_syndrome().unwrap();
// Single step -> no parity checks -> empty syndrome
assert!(syndrome.is_empty());
}
#[test]
fn test_partial_confidence_encoding() {
// Steps with 50% confidence should produce superposition states
let steps = make_steps(3, 0.5);
let config = ReasoningQecConfig {
num_steps: 3,
noise_rate: 0.0,
seed: Some(42),
};
let trace = ReasoningTrace::new(steps, config).unwrap();
// State should not be purely |000...0>
let probs = trace.state.probabilities();
assert!(probs[0] < 1.0);
}
}