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

384 lines
12 KiB
Rust

//! # Syndrome Diagnosis -- QEC-Based System Fault Localization
//!
//! Treats AI system components as a graph. Injects artificial faults into
//! a quantum encoding of the system. Extracts syndrome patterns using QEC logic
//! (CNOT parity checks on ancilla qubits) to localize which component is fragile.
//!
//! This is **structural fault localization**, not log analysis. Multiple rounds
//! of fault injection build statistical fragility profiles and detect components
//! that propagate faults beyond their direct neighborhood.
use ruqu_core::error::QuantumError;
use ruqu_core::gate::Gate;
use ruqu_core::state::QuantumState;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
/// A system component (node in the diagnostic graph)
#[derive(Debug, Clone)]
pub struct Component {
pub id: String,
pub health: f64, // 1.0 = healthy, 0.0 = failed
}
/// An edge between two components (dependency/connection)
#[derive(Debug, Clone)]
pub struct Connection {
pub from: usize,
pub to: usize,
pub strength: f64, // coupling strength
}
/// Configuration for syndrome-based diagnosis
pub struct DiagnosisConfig {
pub fault_injection_rate: f64,
pub num_rounds: usize,
pub seed: u64,
}
/// Result of a single diagnostic round
#[derive(Debug)]
pub struct DiagnosticRound {
pub syndrome: Vec<bool>,
pub injected_faults: Vec<usize>,
}
/// Overall diagnosis result
#[derive(Debug)]
pub struct DiagnosisResult {
pub rounds: Vec<DiagnosticRound>,
/// Fragility score per component: how often it shows up in syndrome patterns
pub fragility_scores: Vec<(String, f64)>,
/// Most fragile component
pub weakest_component: Option<String>,
/// Components that propagate faults (appear in syndromes without being directly faulted)
pub fault_propagators: Vec<String>,
}
/// System graph for syndrome-based diagnosis.
///
/// Maps a system topology to a quantum circuit:
/// - Each component becomes a data qubit
/// - Each connection becomes an ancilla qubit for parity measurement
/// - Total qubits = num_components + num_connections (must be <= 25)
pub struct SystemDiagnostics {
components: Vec<Component>,
connections: Vec<Connection>,
}
impl SystemDiagnostics {
pub fn new(components: Vec<Component>, connections: Vec<Connection>) -> Self {
Self {
components,
connections,
}
}
/// Run a single diagnostic round:
/// 1. Encode components as qubits (healthy=|0>, degraded=Ry rotation)
/// 2. Inject random faults (X gates on selected qubits)
/// 3. Extract syndrome using ancilla qubits measuring parity of connected pairs
pub fn run_round(
&self,
config: &DiagnosisConfig,
round_seed: u64,
) -> Result<DiagnosticRound, QuantumError> {
let num_components = self.components.len();
let num_connections = self.connections.len();
let total_qubits = (num_components + num_connections) as u32;
// Limit check
if total_qubits > 25 {
return Err(QuantumError::QubitLimitExceeded {
requested: total_qubits,
maximum: 25,
});
}
let mut state = QuantumState::new_with_seed(total_qubits, round_seed)?;
let mut rng = StdRng::seed_from_u64(round_seed);
// 1. Encode component health as rotation from |0>
for (i, comp) in self.components.iter().enumerate() {
let angle = std::f64::consts::FRAC_PI_2 * (1.0 - comp.health);
if angle.abs() > 1e-15 {
state.apply_gate(&Gate::Ry(i as u32, angle))?;
}
}
// 2. Inject faults
let mut injected = Vec::new();
for i in 0..num_components {
if rng.gen::<f64>() < config.fault_injection_rate {
state.apply_gate(&Gate::X(i as u32))?;
injected.push(i);
}
}
// 3. For each connection, use an ancilla qubit to check parity
let mut syndrome = Vec::with_capacity(num_connections);
for (ci, conn) in self.connections.iter().enumerate() {
let ancilla = (num_components + ci) as u32;
state.reset_qubit(ancilla)?;
state.apply_gate(&Gate::CNOT(conn.from as u32, ancilla))?;
state.apply_gate(&Gate::CNOT(conn.to as u32, ancilla))?;
let outcome = state.measure(ancilla)?;
syndrome.push(outcome.result);
}
Ok(DiagnosticRound {
syndrome,
injected_faults: injected,
})
}
/// Run full diagnosis: multiple rounds of fault injection + syndrome extraction.
/// Accumulates statistics to identify fragile and fault-propagating components.
pub fn diagnose(&self, config: &DiagnosisConfig) -> Result<DiagnosisResult, QuantumError> {
let mut rounds = Vec::new();
let mut fault_counts = vec![0usize; self.components.len()];
let mut syndrome_counts = vec![0usize; self.components.len()];
for round in 0..config.num_rounds {
let round_seed = config.seed.wrapping_add(round as u64 * 1000);
let result = self.run_round(config, round_seed)?;
// Count which components were directly faulted
for &idx in &result.injected_faults {
fault_counts[idx] += 1;
}
// Count which components appear in triggered syndromes
for (ci, &fired) in result.syndrome.iter().enumerate() {
if fired {
let conn = &self.connections[ci];
syndrome_counts[conn.from] += 1;
syndrome_counts[conn.to] += 1;
}
}
rounds.push(result);
}
// Compute fragility scores (syndrome appearances / total rounds)
let fragility_scores: Vec<(String, f64)> = self
.components
.iter()
.enumerate()
.map(|(i, c)| {
(
c.id.clone(),
syndrome_counts[i] as f64 / config.num_rounds as f64,
)
})
.collect();
let weakest = fragility_scores
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(name, _)| name.clone());
// Fault propagators: components that appear in syndromes more often
// than they were directly faulted
let fault_propagators: Vec<String> = self
.components
.iter()
.enumerate()
.filter(|(i, _)| syndrome_counts[*i] > fault_counts[*i] + config.num_rounds / 4)
.map(|(_, c)| c.id.clone())
.collect();
Ok(DiagnosisResult {
rounds,
fragility_scores,
weakest_component: weakest,
fault_propagators,
})
}
/// Get the number of components
pub fn num_components(&self) -> usize {
self.components.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_linear_system(n: usize) -> (Vec<Component>, Vec<Connection>) {
let components: Vec<Component> = (0..n)
.map(|i| Component {
id: format!("comp_{}", i),
health: 1.0,
})
.collect();
let connections: Vec<Connection> = (0..n.saturating_sub(1))
.map(|i| Connection {
from: i,
to: i + 1,
strength: 1.0,
})
.collect();
(components, connections)
}
#[test]
fn test_new_system() {
let (comps, conns) = make_linear_system(5);
let diag = SystemDiagnostics::new(comps, conns);
assert_eq!(diag.num_components(), 5);
}
#[test]
fn test_qubit_limit_check() {
// 15 components + 14 connections = 29 > 25
let (comps, conns) = make_linear_system(15);
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.0,
num_rounds: 1,
seed: 42,
};
let result = diag.run_round(&config, 42);
assert!(result.is_err());
}
#[test]
fn test_single_round_no_faults() {
let (comps, conns) = make_linear_system(5);
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.0,
num_rounds: 1,
seed: 42,
};
let round = diag.run_round(&config, 42).unwrap();
// No faults injected -> no syndrome should fire (all healthy components
// are in |0>, parity checks should agree)
assert!(round.injected_faults.is_empty());
assert!(round.syndrome.iter().all(|&s| !s));
}
#[test]
fn test_diagnose_no_faults() {
let (comps, conns) = make_linear_system(5);
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.0,
num_rounds: 10,
seed: 42,
};
let result = diag.diagnose(&config).unwrap();
assert_eq!(result.rounds.len(), 10);
// No faults -> all fragility scores should be 0
for (_, score) in &result.fragility_scores {
assert!((*score - 0.0).abs() < 1e-10);
}
}
#[test]
fn test_diagnose_with_faults() {
let (comps, conns) = make_linear_system(5);
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.5,
num_rounds: 20,
seed: 123,
};
let result = diag.diagnose(&config).unwrap();
assert_eq!(result.rounds.len(), 20);
assert_eq!(result.fragility_scores.len(), 5);
// At least some syndromes should fire with 50% fault rate
let total_fired: usize = result
.rounds
.iter()
.map(|r| r.syndrome.iter().filter(|&&s| s).count())
.sum();
assert!(total_fired > 0);
}
#[test]
fn test_weakest_component_identified() {
let (comps, conns) = make_linear_system(5);
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.3,
num_rounds: 50,
seed: 999,
};
let result = diag.diagnose(&config).unwrap();
// Should identify a weakest component
assert!(result.weakest_component.is_some());
}
#[test]
fn test_degraded_components() {
// One component is already degraded
let mut comps: Vec<Component> = (0..5)
.map(|i| Component {
id: format!("comp_{}", i),
health: 1.0,
})
.collect();
comps[2].health = 0.3; // Component 2 is degraded
let conns: Vec<Connection> = (0..4)
.map(|i| Connection {
from: i,
to: i + 1,
strength: 1.0,
})
.collect();
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.0,
num_rounds: 1,
seed: 42,
};
// Should run without error
let round = diag.run_round(&config, 42).unwrap();
assert_eq!(round.syndrome.len(), 4);
}
#[test]
fn test_max_qubit_boundary() {
// 13 components + 12 connections = 25 qubits (exactly at limit)
let (comps, conns) = make_linear_system(13);
let diag = SystemDiagnostics::new(comps, conns);
let config = DiagnosisConfig {
fault_injection_rate: 0.0,
num_rounds: 1,
seed: 42,
};
let result = diag.run_round(&config, 42);
assert!(result.is_ok());
}
#[test]
fn test_empty_connections() {
// Components with no connections
let comps = vec![
Component {
id: "a".into(),
health: 1.0,
},
Component {
id: "b".into(),
health: 1.0,
},
];
let diag = SystemDiagnostics::new(comps, vec![]);
let config = DiagnosisConfig {
fault_injection_rate: 0.0,
num_rounds: 5,
seed: 42,
};
let result = diag.diagnose(&config).unwrap();
// No connections -> no syndrome bits
for round in &result.rounds {
assert!(round.syndrome.is_empty());
}
}
}