mirror of
https://github.com/ruvnet/RuView
synced 2026-08-11 20:41:44 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
+701
@@ -0,0 +1,701 @@
|
||||
//! ruQu Demo Binary - Proof Artifact
|
||||
//!
|
||||
//! This is the runnable demonstration of ruQu's capabilities.
|
||||
//!
|
||||
//! ## What it does
|
||||
//!
|
||||
//! 1. Generates a streaming syndrome feed
|
||||
//! 2. Runs the coherence gate loop per round
|
||||
//! 3. Prints live status: round, cut value, risk, region mask
|
||||
//! 4. Writes metrics file: latency histogram, p50/p99/p999, false alarms
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Basic run with defaults
|
||||
//! cargo run --bin ruqu_demo --release
|
||||
//!
|
||||
//! # Custom parameters
|
||||
//! cargo run --bin ruqu_demo --release -- \
|
||||
//! --distance 7 \
|
||||
//! --error-rate 0.01 \
|
||||
//! --rounds 10000 \
|
||||
//! --output metrics.json
|
||||
//! ```
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
|
||||
use ruqu::stim::{StimSyndromeSource, SurfaceCodeConfig};
|
||||
use ruqu::syndrome::DetectorBitmap;
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DemoConfig {
|
||||
/// Code distance
|
||||
code_distance: usize,
|
||||
/// Physical error rate
|
||||
error_rate: f64,
|
||||
/// Number of rounds to run
|
||||
num_rounds: usize,
|
||||
/// Random seed
|
||||
seed: u64,
|
||||
/// Output metrics file
|
||||
output_file: Option<String>,
|
||||
/// Print interval (every N rounds)
|
||||
print_interval: usize,
|
||||
/// Gate threshold
|
||||
threshold: f64,
|
||||
}
|
||||
|
||||
impl Default for DemoConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code_distance: 5,
|
||||
error_rate: 0.01,
|
||||
num_rounds: 10000,
|
||||
seed: 42,
|
||||
output_file: Some("ruqu_metrics.json".to_string()),
|
||||
print_interval: 1000,
|
||||
threshold: 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args() -> DemoConfig {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut config = DemoConfig::default();
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--distance" | "-d" => {
|
||||
i += 1;
|
||||
config.code_distance = args[i].parse().expect("Invalid distance");
|
||||
}
|
||||
"--error-rate" | "-e" => {
|
||||
i += 1;
|
||||
config.error_rate = args[i].parse().expect("Invalid error rate");
|
||||
}
|
||||
"--rounds" | "-r" => {
|
||||
i += 1;
|
||||
config.num_rounds = args[i].parse().expect("Invalid rounds");
|
||||
}
|
||||
"--seed" | "-s" => {
|
||||
i += 1;
|
||||
config.seed = args[i].parse().expect("Invalid seed");
|
||||
}
|
||||
"--output" | "-o" => {
|
||||
i += 1;
|
||||
config.output_file = Some(args[i].clone());
|
||||
}
|
||||
"--threshold" | "-t" => {
|
||||
i += 1;
|
||||
config.threshold = args[i].parse().expect("Invalid threshold");
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_help();
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown argument: {}", args[i]);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
r#"
|
||||
ruQu Demo - Coherence Gate Demonstration
|
||||
|
||||
USAGE:
|
||||
ruqu_demo [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
-d, --distance <N> Code distance (default: 5)
|
||||
-e, --error-rate <P> Physical error rate (default: 0.01)
|
||||
-r, --rounds <N> Number of rounds (default: 10000)
|
||||
-s, --seed <N> Random seed (default: 42)
|
||||
-o, --output <FILE> Output metrics file (default: ruqu_metrics.json)
|
||||
-t, --threshold <T> Gate threshold (default: 5.0)
|
||||
-h, --help Print this help message
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LATENCY TRACKING
|
||||
// ============================================================================
|
||||
|
||||
struct LatencyTracker {
|
||||
latencies: Vec<u64>,
|
||||
recent: VecDeque<u64>,
|
||||
max_recent: usize,
|
||||
}
|
||||
|
||||
impl LatencyTracker {
|
||||
fn new(max_recent: usize) -> Self {
|
||||
Self {
|
||||
latencies: Vec::new(),
|
||||
recent: VecDeque::with_capacity(max_recent),
|
||||
max_recent,
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, latency_ns: u64) {
|
||||
self.latencies.push(latency_ns);
|
||||
if self.recent.len() >= self.max_recent {
|
||||
self.recent.pop_front();
|
||||
}
|
||||
self.recent.push_back(latency_ns);
|
||||
}
|
||||
|
||||
fn percentile(&self, p: f64) -> u64 {
|
||||
if self.latencies.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut sorted = self.latencies.clone();
|
||||
sorted.sort_unstable();
|
||||
let idx = ((p / 100.0) * (sorted.len() - 1) as f64) as usize;
|
||||
sorted[idx]
|
||||
}
|
||||
|
||||
fn p50(&self) -> u64 {
|
||||
self.percentile(50.0)
|
||||
}
|
||||
|
||||
fn p99(&self) -> u64 {
|
||||
self.percentile(99.0)
|
||||
}
|
||||
|
||||
fn p999(&self) -> u64 {
|
||||
self.percentile(99.9)
|
||||
}
|
||||
|
||||
fn max(&self) -> u64 {
|
||||
self.latencies.iter().copied().max().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn mean(&self) -> f64 {
|
||||
if self.latencies.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let sum: u64 = self.latencies.iter().sum();
|
||||
sum as f64 / self.latencies.len() as f64
|
||||
}
|
||||
|
||||
fn count(&self) -> usize {
|
||||
self.latencies.len()
|
||||
}
|
||||
|
||||
fn histogram(&self, num_buckets: usize) -> Vec<(u64, u64, usize)> {
|
||||
if self.latencies.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let min = *self.latencies.iter().min().unwrap();
|
||||
let max = self.max();
|
||||
let range = max - min + 1;
|
||||
let bucket_size = (range / num_buckets as u64).max(1);
|
||||
|
||||
let mut buckets = vec![0usize; num_buckets];
|
||||
for &lat in &self.latencies {
|
||||
let bucket = ((lat - min) / bucket_size).min(num_buckets as u64 - 1) as usize;
|
||||
buckets[bucket] += 1;
|
||||
}
|
||||
|
||||
buckets
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, count)| {
|
||||
let start = min + i as u64 * bucket_size;
|
||||
let end = start + bucket_size;
|
||||
(start, end, count)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SIMPLE MIN-CUT GATE
|
||||
// ============================================================================
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
struct MinCutGate {
|
||||
threshold: f64,
|
||||
grid_size: usize,
|
||||
base_weight: f64,
|
||||
}
|
||||
|
||||
impl MinCutGate {
|
||||
fn new(code_distance: usize, error_rate: f64, threshold: f64) -> Self {
|
||||
Self {
|
||||
threshold,
|
||||
grid_size: code_distance - 1,
|
||||
base_weight: (-error_rate.ln()).max(0.1),
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&self, syndrome: &DetectorBitmap) -> GateResult {
|
||||
let start = Instant::now();
|
||||
|
||||
// Compute min-cut
|
||||
let fired_set: HashSet<usize> = syndrome.iter_fired().collect();
|
||||
let min_cut = self.compute_min_cut(&fired_set);
|
||||
|
||||
// Compute risk
|
||||
let risk = if min_cut < self.threshold {
|
||||
1.0 - (min_cut / self.threshold)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Compute region mask (simplified: which quadrants have errors)
|
||||
let region_mask = self.compute_region_mask(&fired_set);
|
||||
|
||||
let latency_ns = start.elapsed().as_nanos() as u64;
|
||||
|
||||
GateResult {
|
||||
min_cut,
|
||||
risk,
|
||||
region_mask,
|
||||
decision: if min_cut >= self.threshold {
|
||||
Decision::Permit
|
||||
} else if min_cut >= self.threshold * 0.5 {
|
||||
Decision::Defer
|
||||
} else {
|
||||
Decision::Deny
|
||||
},
|
||||
latency_ns,
|
||||
fired_count: fired_set.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_min_cut(&self, fired_set: &HashSet<usize>) -> f64 {
|
||||
// Simple s-t min-cut using Edmonds-Karp
|
||||
let mut adj: HashMap<u32, Vec<(u32, f64)>> = HashMap::new();
|
||||
let fired_weight = 0.01;
|
||||
|
||||
// Build grid
|
||||
for row in 0..self.grid_size {
|
||||
for col in 0..self.grid_size {
|
||||
let node = (row * self.grid_size + col) as u32;
|
||||
let is_fired = fired_set.contains(&(node as usize));
|
||||
|
||||
if col + 1 < self.grid_size {
|
||||
let right = (row * self.grid_size + col + 1) as u32;
|
||||
let right_fired = fired_set.contains(&(right as usize));
|
||||
let weight = if is_fired || right_fired {
|
||||
fired_weight
|
||||
} else {
|
||||
self.base_weight
|
||||
};
|
||||
adj.entry(node).or_default().push((right, weight));
|
||||
adj.entry(right).or_default().push((node, weight));
|
||||
}
|
||||
|
||||
if row + 1 < self.grid_size {
|
||||
let bottom = ((row + 1) * self.grid_size + col) as u32;
|
||||
let bottom_fired = fired_set.contains(&(bottom as usize));
|
||||
let weight = if is_fired || bottom_fired {
|
||||
fired_weight
|
||||
} else {
|
||||
self.base_weight
|
||||
};
|
||||
adj.entry(node).or_default().push((bottom, weight));
|
||||
adj.entry(bottom).or_default().push((node, weight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let source = (self.grid_size * self.grid_size) as u32;
|
||||
let sink = source + 1;
|
||||
|
||||
// Connect boundaries
|
||||
let boundary_weight = self.base_weight * 2.0;
|
||||
for row in 0..self.grid_size {
|
||||
let left = (row * self.grid_size) as u32;
|
||||
let right = (row * self.grid_size + self.grid_size - 1) as u32;
|
||||
adj.entry(source).or_default().push((left, boundary_weight));
|
||||
adj.entry(left).or_default().push((source, boundary_weight));
|
||||
adj.entry(right).or_default().push((sink, boundary_weight));
|
||||
adj.entry(sink).or_default().push((right, boundary_weight));
|
||||
}
|
||||
|
||||
// Max-flow = min-cut
|
||||
let mut capacity: HashMap<(u32, u32), f64> = HashMap::new();
|
||||
for (&u, neighbors) in &adj {
|
||||
for &(v, w) in neighbors {
|
||||
*capacity.entry((u, v)).or_default() += w;
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_flow = 0.0;
|
||||
loop {
|
||||
// BFS for augmenting path
|
||||
let mut parent: HashMap<u32, u32> = HashMap::new();
|
||||
let mut visited = HashSet::new();
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
|
||||
queue.push_back(source);
|
||||
visited.insert(source);
|
||||
|
||||
while let Some(u) = queue.pop_front() {
|
||||
if u == sink {
|
||||
break;
|
||||
}
|
||||
if let Some(neighbors) = adj.get(&u) {
|
||||
for &(v, _) in neighbors {
|
||||
let cap = capacity.get(&(u, v)).copied().unwrap_or(0.0);
|
||||
if !visited.contains(&v) && cap > 1e-10 {
|
||||
visited.insert(v);
|
||||
parent.insert(v, u);
|
||||
queue.push_back(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !parent.contains_key(&sink) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Find bottleneck
|
||||
let mut path_flow = f64::INFINITY;
|
||||
let mut v = sink;
|
||||
while v != source {
|
||||
let u = parent[&v];
|
||||
path_flow = path_flow.min(capacity.get(&(u, v)).copied().unwrap_or(0.0));
|
||||
v = u;
|
||||
}
|
||||
|
||||
// Update capacities
|
||||
v = sink;
|
||||
while v != source {
|
||||
let u = parent[&v];
|
||||
*capacity.entry((u, v)).or_default() -= path_flow;
|
||||
*capacity.entry((v, u)).or_default() += path_flow;
|
||||
v = u;
|
||||
}
|
||||
|
||||
max_flow += path_flow;
|
||||
}
|
||||
|
||||
max_flow
|
||||
}
|
||||
|
||||
fn compute_region_mask(&self, fired_set: &HashSet<usize>) -> u64 {
|
||||
// Split into 4 quadrants
|
||||
let half = self.grid_size / 2;
|
||||
let mut mask = 0u64;
|
||||
|
||||
for &det in fired_set {
|
||||
let row = det / self.grid_size;
|
||||
let col = det % self.grid_size;
|
||||
let quadrant = match (row < half, col < half) {
|
||||
(true, true) => 0, // Top-left
|
||||
(true, false) => 1, // Top-right
|
||||
(false, true) => 2, // Bottom-left
|
||||
(false, false) => 3, // Bottom-right
|
||||
};
|
||||
mask |= 1 << quadrant;
|
||||
}
|
||||
|
||||
mask
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Decision {
|
||||
Permit,
|
||||
Defer,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GateResult {
|
||||
min_cut: f64,
|
||||
risk: f64,
|
||||
region_mask: u64,
|
||||
decision: Decision,
|
||||
latency_ns: u64,
|
||||
fired_count: usize,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// METRICS OUTPUT
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct DemoMetrics {
|
||||
config: MetricsConfig,
|
||||
summary: MetricsSummary,
|
||||
latency: LatencyMetrics,
|
||||
decisions: DecisionMetrics,
|
||||
histogram: Vec<HistogramBucket>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct MetricsConfig {
|
||||
code_distance: usize,
|
||||
error_rate: f64,
|
||||
num_rounds: usize,
|
||||
seed: u64,
|
||||
threshold: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct MetricsSummary {
|
||||
total_rounds: usize,
|
||||
total_time_ms: f64,
|
||||
throughput_per_sec: f64,
|
||||
total_fired: usize,
|
||||
avg_fired_per_round: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct LatencyMetrics {
|
||||
mean_ns: f64,
|
||||
p50_ns: u64,
|
||||
p99_ns: u64,
|
||||
p999_ns: u64,
|
||||
max_ns: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct DecisionMetrics {
|
||||
permits: usize,
|
||||
defers: usize,
|
||||
denies: usize,
|
||||
permit_rate: f64,
|
||||
deny_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct HistogramBucket {
|
||||
start_ns: u64,
|
||||
end_ns: u64,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN
|
||||
// ============================================================================
|
||||
|
||||
fn main() {
|
||||
let config = parse_args();
|
||||
|
||||
println!("╔═══════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ ruQu Demo - Proof Artifact ║");
|
||||
println!("╠═══════════════════════════════════════════════════════════════════╣");
|
||||
println!(
|
||||
"║ Code Distance: d={} | Error Rate: {:.4} | Rounds: {:>6} ║",
|
||||
config.code_distance, config.error_rate, config.num_rounds
|
||||
);
|
||||
println!(
|
||||
"║ Threshold: {:.2} | Seed: {:>10} ║",
|
||||
config.threshold, config.seed
|
||||
);
|
||||
println!("╚═══════════════════════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
|
||||
// Initialize components
|
||||
let surface_config =
|
||||
SurfaceCodeConfig::new(config.code_distance, config.error_rate).with_seed(config.seed);
|
||||
let mut syndrome_source = match StimSyndromeSource::new(surface_config) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create syndrome source: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let gate = MinCutGate::new(config.code_distance, config.error_rate, config.threshold);
|
||||
let mut latency_tracker = LatencyTracker::new(1000);
|
||||
|
||||
// Counters
|
||||
let mut permits = 0usize;
|
||||
let mut defers = 0usize;
|
||||
let mut denies = 0usize;
|
||||
let mut total_fired = 0usize;
|
||||
|
||||
// Run demo
|
||||
println!("Round │ Cut │ Risk │ Decision │ Regions │ Latency │ Fired");
|
||||
println!("──────┼───────┼───────┼──────────┼─────────┼─────────┼──────");
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
for round in 0..config.num_rounds {
|
||||
// Get syndrome
|
||||
let syndrome: DetectorBitmap = match syndrome_source.sample() {
|
||||
Ok(s) => s,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// Process through gate
|
||||
let result = gate.process(&syndrome);
|
||||
latency_tracker.record(result.latency_ns);
|
||||
total_fired += result.fired_count;
|
||||
|
||||
// Update counters
|
||||
match result.decision {
|
||||
Decision::Permit => permits += 1,
|
||||
Decision::Defer => defers += 1,
|
||||
Decision::Deny => denies += 1,
|
||||
}
|
||||
|
||||
// Print live status
|
||||
if round % config.print_interval == 0 || result.decision == Decision::Deny {
|
||||
let decision_str = match result.decision {
|
||||
Decision::Permit => "\x1b[32mPERMIT\x1b[0m ",
|
||||
Decision::Defer => "\x1b[33mDEFER\x1b[0m ",
|
||||
Decision::Deny => "\x1b[31mDENY\x1b[0m ",
|
||||
};
|
||||
println!(
|
||||
"{:>5} │ {:>5.2} │ {:>5.2} │ {} │ {:>07b} │ {:>5}ns │ {:>3}",
|
||||
round,
|
||||
result.min_cut,
|
||||
result.risk,
|
||||
decision_str,
|
||||
result.region_mask,
|
||||
result.latency_ns,
|
||||
result.fired_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let total_time = start_time.elapsed();
|
||||
|
||||
// Summary
|
||||
println!();
|
||||
println!("╔═══════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ RESULTS SUMMARY ║");
|
||||
println!("╠═══════════════════════════════════════════════════════════════════╣");
|
||||
println!(
|
||||
"║ Total Time: {:>10.2} ms ║",
|
||||
total_time.as_secs_f64() * 1000.0
|
||||
);
|
||||
println!(
|
||||
"║ Throughput: {:>10.0} rounds/sec ║",
|
||||
config.num_rounds as f64 / total_time.as_secs_f64()
|
||||
);
|
||||
println!(
|
||||
"║ Avg Fired/Round: {:>10.2} ║",
|
||||
total_fired as f64 / config.num_rounds as f64
|
||||
);
|
||||
println!("╠═══════════════════════════════════════════════════════════════════╣");
|
||||
println!("║ Latency: ║");
|
||||
println!(
|
||||
"║ Mean: {:>8.0} ns ║",
|
||||
latency_tracker.mean()
|
||||
);
|
||||
println!(
|
||||
"║ P50: {:>8} ns ║",
|
||||
latency_tracker.p50()
|
||||
);
|
||||
println!(
|
||||
"║ P99: {:>8} ns ║",
|
||||
latency_tracker.p99()
|
||||
);
|
||||
println!(
|
||||
"║ P999: {:>8} ns ║",
|
||||
latency_tracker.p999()
|
||||
);
|
||||
println!(
|
||||
"║ Max: {:>8} ns ║",
|
||||
latency_tracker.max()
|
||||
);
|
||||
println!("╠═══════════════════════════════════════════════════════════════════╣");
|
||||
println!("║ Decisions: ║");
|
||||
println!(
|
||||
"║ Permits: {:>6} ({:>5.1}%) ║",
|
||||
permits,
|
||||
permits as f64 / config.num_rounds as f64 * 100.0
|
||||
);
|
||||
println!(
|
||||
"║ Defers: {:>6} ({:>5.1}%) ║",
|
||||
defers,
|
||||
defers as f64 / config.num_rounds as f64 * 100.0
|
||||
);
|
||||
println!(
|
||||
"║ Denies: {:>6} ({:>5.1}%) ║",
|
||||
denies,
|
||||
denies as f64 / config.num_rounds as f64 * 100.0
|
||||
);
|
||||
println!("╚═══════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
// Write metrics file
|
||||
if let Some(output_file) = &config.output_file {
|
||||
let metrics = DemoMetrics {
|
||||
config: MetricsConfig {
|
||||
code_distance: config.code_distance,
|
||||
error_rate: config.error_rate,
|
||||
num_rounds: config.num_rounds,
|
||||
seed: config.seed,
|
||||
threshold: config.threshold,
|
||||
},
|
||||
summary: MetricsSummary {
|
||||
total_rounds: config.num_rounds,
|
||||
total_time_ms: total_time.as_secs_f64() * 1000.0,
|
||||
throughput_per_sec: config.num_rounds as f64 / total_time.as_secs_f64(),
|
||||
total_fired,
|
||||
avg_fired_per_round: total_fired as f64 / config.num_rounds as f64,
|
||||
},
|
||||
latency: LatencyMetrics {
|
||||
mean_ns: latency_tracker.mean(),
|
||||
p50_ns: latency_tracker.p50(),
|
||||
p99_ns: latency_tracker.p99(),
|
||||
p999_ns: latency_tracker.p999(),
|
||||
max_ns: latency_tracker.max(),
|
||||
},
|
||||
decisions: DecisionMetrics {
|
||||
permits,
|
||||
defers,
|
||||
denies,
|
||||
permit_rate: permits as f64 / config.num_rounds as f64,
|
||||
deny_rate: denies as f64 / config.num_rounds as f64,
|
||||
},
|
||||
histogram: latency_tracker
|
||||
.histogram(20)
|
||||
.into_iter()
|
||||
.map(|(start, end, count)| HistogramBucket {
|
||||
start_ns: start,
|
||||
end_ns: end,
|
||||
count,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
match File::create(output_file) {
|
||||
Ok(mut file) => {
|
||||
let json = serde_json::to_string_pretty(&metrics).unwrap();
|
||||
file.write_all(json.as_bytes()).unwrap();
|
||||
println!("\nMetrics written to: {}", output_file);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to write metrics file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latency histogram
|
||||
println!("\nLatency Histogram:");
|
||||
let histogram = latency_tracker.histogram(10);
|
||||
let max_count = histogram.iter().map(|(_, _, c)| *c).max().unwrap_or(1);
|
||||
for (start, end, count) in histogram {
|
||||
let bar_len = (count as f64 / max_count as f64 * 40.0) as usize;
|
||||
let bar = "█".repeat(bar_len);
|
||||
println!("{:>8}-{:<8} │{:<40} {:>5}", start, end, bar, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,856 @@
|
||||
//! ruQu Predictive Evaluation Binary
|
||||
//!
|
||||
//! This binary produces formal evaluation metrics for ruQu's predictive capabilities.
|
||||
//! It demonstrates that ruQu can detect logical failure risk BEFORE it manifests.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run --bin ruqu_predictive_eval --release -- \
|
||||
//! --distance 5 \
|
||||
//! --error-rate 0.001 \
|
||||
//! --runs 100
|
||||
//! ```
|
||||
//!
|
||||
//! ## Output
|
||||
//!
|
||||
//! Produces DARPA-style evaluation metrics including:
|
||||
//! - Lead time distribution (median, p10, p90)
|
||||
//! - Precision and recall
|
||||
//! - False alarm rate per 100k cycles
|
||||
//! - Actionability for different mitigation windows
|
||||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::time::Instant;
|
||||
|
||||
use ruqu::stim::{StimSyndromeSource, SurfaceCodeConfig};
|
||||
use ruqu::syndrome::DetectorBitmap;
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EvalConfig {
|
||||
code_distance: usize,
|
||||
error_rate: f64,
|
||||
num_runs: usize,
|
||||
cycles_per_run: usize,
|
||||
seed: u64,
|
||||
inject_mode: InjectMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum InjectMode {
|
||||
/// Independent noise only (baseline)
|
||||
Independent,
|
||||
/// Correlated burst injection
|
||||
CorrelatedBurst,
|
||||
/// Both modes for comparison
|
||||
Both,
|
||||
}
|
||||
|
||||
impl Default for EvalConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code_distance: 5,
|
||||
error_rate: 0.001,
|
||||
num_runs: 100,
|
||||
cycles_per_run: 500,
|
||||
seed: 42,
|
||||
inject_mode: InjectMode::CorrelatedBurst,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args() -> EvalConfig {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut config = EvalConfig::default();
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--distance" | "-d" => {
|
||||
i += 1;
|
||||
config.code_distance = args[i].parse().expect("Invalid distance");
|
||||
}
|
||||
"--error-rate" | "-e" => {
|
||||
i += 1;
|
||||
config.error_rate = args[i].parse().expect("Invalid error rate");
|
||||
}
|
||||
"--runs" | "-r" => {
|
||||
i += 1;
|
||||
config.num_runs = args[i].parse().expect("Invalid runs");
|
||||
}
|
||||
"--cycles" | "-c" => {
|
||||
i += 1;
|
||||
config.cycles_per_run = args[i].parse().expect("Invalid cycles");
|
||||
}
|
||||
"--seed" | "-s" => {
|
||||
i += 1;
|
||||
config.seed = args[i].parse().expect("Invalid seed");
|
||||
}
|
||||
"--inject" => {
|
||||
i += 1;
|
||||
config.inject_mode = match args[i].as_str() {
|
||||
"independent" => InjectMode::Independent,
|
||||
"burst" | "correlated" | "correlated_burst" => InjectMode::CorrelatedBurst,
|
||||
"both" => InjectMode::Both,
|
||||
_ => panic!("Invalid inject mode: {}", args[i]),
|
||||
};
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_help();
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown argument: {}", args[i]);
|
||||
print_help();
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("ruQu Predictive Evaluation");
|
||||
println!();
|
||||
println!("USAGE:");
|
||||
println!(" ruqu_predictive_eval [OPTIONS]");
|
||||
println!();
|
||||
println!("OPTIONS:");
|
||||
println!(" -d, --distance <N> Code distance (default: 5)");
|
||||
println!(" -e, --error-rate <F> Physical error rate (default: 0.001)");
|
||||
println!(" -r, --runs <N> Number of evaluation runs (default: 100)");
|
||||
println!(" -c, --cycles <N> Cycles per run (default: 500)");
|
||||
println!(" -s, --seed <N> Random seed (default: 42)");
|
||||
println!(" --inject <MODE> Injection mode: independent, burst, both");
|
||||
println!(" -h, --help Print this help");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STRUCTURAL SIGNAL WITH DYNAMICS
|
||||
// ============================================================================
|
||||
|
||||
/// Structural signal with cut dynamics (velocity and curvature)
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StructuralSignal {
|
||||
/// Current min-cut value
|
||||
pub cut: f64,
|
||||
/// Rate of change (Δλ)
|
||||
pub velocity: f64,
|
||||
/// Acceleration of change (Δ²λ)
|
||||
pub curvature: f64,
|
||||
/// Baseline mean for adaptive thresholding
|
||||
pub baseline_mean: f64,
|
||||
/// Baseline standard deviation
|
||||
pub baseline_std: f64,
|
||||
}
|
||||
|
||||
/// Warning detector with velocity and curvature tracking
|
||||
struct WarningDetector {
|
||||
history: VecDeque<f64>,
|
||||
velocity_history: VecDeque<f64>,
|
||||
max_history: usize,
|
||||
warmup_samples: usize,
|
||||
baseline_mean: f64,
|
||||
baseline_std: f64,
|
||||
theta_sigma: f64,
|
||||
theta_absolute: f64,
|
||||
delta: f64,
|
||||
lookback: usize,
|
||||
min_event_count: usize,
|
||||
}
|
||||
|
||||
impl WarningDetector {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
history: VecDeque::new(),
|
||||
velocity_history: VecDeque::new(),
|
||||
max_history: 100,
|
||||
warmup_samples: 20,
|
||||
baseline_mean: 0.0,
|
||||
baseline_std: 0.0,
|
||||
theta_sigma: 2.5,
|
||||
theta_absolute: 2.0,
|
||||
delta: 1.2,
|
||||
lookback: 5,
|
||||
min_event_count: 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, cut: f64) {
|
||||
// Track velocity
|
||||
if let Some(&prev) = self.history.back() {
|
||||
let velocity = cut - prev;
|
||||
self.velocity_history.push_back(velocity);
|
||||
if self.velocity_history.len() > self.max_history {
|
||||
self.velocity_history.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
self.history.push_back(cut);
|
||||
if self.history.len() > self.max_history {
|
||||
self.history.pop_front();
|
||||
}
|
||||
|
||||
// Update baseline during warmup
|
||||
if self.history.len() <= self.warmup_samples {
|
||||
let sum: f64 = self.history.iter().sum();
|
||||
self.baseline_mean = sum / self.history.len() as f64;
|
||||
|
||||
if self.history.len() > 1 {
|
||||
let variance: f64 = self
|
||||
.history
|
||||
.iter()
|
||||
.map(|x| (x - self.baseline_mean).powi(2))
|
||||
.sum::<f64>()
|
||||
/ (self.history.len() - 1) as f64;
|
||||
self.baseline_std = variance.sqrt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current(&self) -> f64 {
|
||||
self.history.back().copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn velocity(&self) -> f64 {
|
||||
self.velocity_history.back().copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn curvature(&self) -> f64 {
|
||||
if self.velocity_history.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let n = self.velocity_history.len();
|
||||
self.velocity_history[n - 1] - self.velocity_history[n - 2]
|
||||
}
|
||||
|
||||
fn signal(&self) -> StructuralSignal {
|
||||
StructuralSignal {
|
||||
cut: self.current(),
|
||||
velocity: self.velocity(),
|
||||
curvature: self.curvature(),
|
||||
baseline_mean: self.baseline_mean,
|
||||
baseline_std: self.baseline_std,
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_from_lookback(&self) -> f64 {
|
||||
if self.history.len() <= self.lookback {
|
||||
return 0.0;
|
||||
}
|
||||
let n = self.history.len();
|
||||
self.history[n - 1] - self.history[n - 1 - self.lookback]
|
||||
}
|
||||
|
||||
fn is_warning(&self, event_count: usize) -> bool {
|
||||
if self.history.len() < self.warmup_samples {
|
||||
return false;
|
||||
}
|
||||
if self.baseline_mean == 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let adaptive_threshold =
|
||||
(self.baseline_mean - self.theta_sigma * self.baseline_std).max(0.5);
|
||||
|
||||
let below_adaptive = self.current() <= adaptive_threshold;
|
||||
let below_absolute = self.current() <= self.theta_absolute;
|
||||
let rapid_drop = self.drop_from_lookback() <= -self.delta;
|
||||
let high_events = event_count >= self.min_event_count;
|
||||
|
||||
// AND mode: structural + drop + intensity
|
||||
(below_adaptive || below_absolute) && rapid_drop && high_events
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QEC GRAPH CONSTRUCTION
|
||||
// ============================================================================
|
||||
|
||||
struct STMinCutGraph {
|
||||
num_nodes: u32,
|
||||
edges: Vec<(u32, u32, f64)>,
|
||||
source_edges: Vec<(u32, f64)>,
|
||||
sink_edges: Vec<(u32, f64)>,
|
||||
}
|
||||
|
||||
impl STMinCutGraph {
|
||||
fn new(num_nodes: u32) -> Self {
|
||||
Self {
|
||||
num_nodes,
|
||||
edges: Vec::new(),
|
||||
source_edges: Vec::new(),
|
||||
sink_edges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_edge(&mut self, u: u32, v: u32, weight: f64) {
|
||||
self.edges.push((u, v, weight));
|
||||
}
|
||||
|
||||
fn connect_source(&mut self, node: u32, weight: f64) {
|
||||
self.source_edges.push((node, weight));
|
||||
}
|
||||
|
||||
fn connect_sink(&mut self, node: u32, weight: f64) {
|
||||
self.sink_edges.push((node, weight));
|
||||
}
|
||||
|
||||
fn compute_min_cut(&self) -> f64 {
|
||||
// BFS-based approximation
|
||||
let mut visited = vec![false; self.num_nodes as usize];
|
||||
let mut queue = VecDeque::new();
|
||||
let mut total_flow = 0.0;
|
||||
|
||||
// Build adjacency with capacities
|
||||
let mut adj: HashMap<u32, Vec<(u32, f64)>> = HashMap::new();
|
||||
for &(u, v, w) in &self.edges {
|
||||
adj.entry(u).or_default().push((v, w));
|
||||
adj.entry(v).or_default().push((u, w));
|
||||
}
|
||||
|
||||
// Start from source-connected nodes
|
||||
for &(node, cap) in &self.source_edges {
|
||||
if !visited[node as usize] {
|
||||
queue.push_back((node, cap));
|
||||
visited[node as usize] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// BFS to sink
|
||||
let sink_set: HashSet<u32> = self.sink_edges.iter().map(|(n, _)| *n).collect();
|
||||
|
||||
while let Some((current, flow)) = queue.pop_front() {
|
||||
if sink_set.contains(¤t) {
|
||||
total_flow += flow;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(neighbors) = adj.get(¤t) {
|
||||
for &(next, cap) in neighbors {
|
||||
if !visited[next as usize] {
|
||||
visited[next as usize] = true;
|
||||
let next_flow = flow.min(cap);
|
||||
queue.push_back((next, next_flow));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return cut value (inverse of flow for this approximation)
|
||||
let source_capacity: f64 = self.source_edges.iter().map(|(_, c)| c).sum();
|
||||
(source_capacity - total_flow).max(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_qec_graph(
|
||||
code_distance: usize,
|
||||
error_rate: f64,
|
||||
syndrome: &DetectorBitmap,
|
||||
) -> STMinCutGraph {
|
||||
let grid_size = code_distance - 1;
|
||||
let num_detectors = 2 * grid_size * grid_size;
|
||||
|
||||
let mut graph = STMinCutGraph::new(num_detectors as u32);
|
||||
let fired_set: HashSet<usize> = syndrome.iter_fired().collect();
|
||||
|
||||
let base_weight = (-error_rate.ln()).max(0.1);
|
||||
let fired_weight = 0.01;
|
||||
|
||||
// Build X-stabilizer grid
|
||||
for row in 0..grid_size {
|
||||
for col in 0..grid_size {
|
||||
let node = (row * grid_size + col) as u32;
|
||||
let is_fired = fired_set.contains(&(node as usize));
|
||||
|
||||
if col + 1 < grid_size {
|
||||
let right = (row * grid_size + col + 1) as u32;
|
||||
let right_fired = fired_set.contains(&(right as usize));
|
||||
let weight = if is_fired || right_fired {
|
||||
fired_weight
|
||||
} else {
|
||||
base_weight
|
||||
};
|
||||
graph.add_edge(node, right, weight);
|
||||
}
|
||||
|
||||
if row + 1 < grid_size {
|
||||
let bottom = ((row + 1) * grid_size + col) as u32;
|
||||
let bottom_fired = fired_set.contains(&(bottom as usize));
|
||||
let weight = if is_fired || bottom_fired {
|
||||
fired_weight
|
||||
} else {
|
||||
base_weight
|
||||
};
|
||||
graph.add_edge(node, bottom, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let boundary_weight = base_weight * 2.0;
|
||||
for row in 0..grid_size {
|
||||
let left = (row * grid_size) as u32;
|
||||
let right = (row * grid_size + grid_size - 1) as u32;
|
||||
graph.connect_source(left, boundary_weight);
|
||||
graph.connect_sink(right, boundary_weight);
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GROUND TRUTH
|
||||
// ============================================================================
|
||||
|
||||
fn is_logical_failure(syndrome: &DetectorBitmap, code_distance: usize) -> bool {
|
||||
let grid_size = code_distance - 1;
|
||||
let fired: HashSet<usize> = syndrome.iter_fired().collect();
|
||||
|
||||
if fired.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let left_boundary: Vec<usize> = (0..grid_size)
|
||||
.map(|row| row * grid_size)
|
||||
.filter(|&d| fired.contains(&d))
|
||||
.collect();
|
||||
|
||||
if left_boundary.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut visited: HashSet<usize> = HashSet::new();
|
||||
let mut queue: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for &start in &left_boundary {
|
||||
queue.push_back(start);
|
||||
visited.insert(start);
|
||||
}
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
let row = current / grid_size;
|
||||
let col = current % grid_size;
|
||||
|
||||
if col == grid_size - 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let neighbors = [
|
||||
if col > 0 {
|
||||
Some(row * grid_size + col - 1)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if col + 1 < grid_size {
|
||||
Some(row * grid_size + col + 1)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if row > 0 {
|
||||
Some((row - 1) * grid_size + col)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if row + 1 < grid_size {
|
||||
Some((row + 1) * grid_size + col)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
];
|
||||
|
||||
for neighbor_opt in neighbors.iter().flatten() {
|
||||
let neighbor = *neighbor_opt;
|
||||
if fired.contains(&neighbor) && !visited.contains(&neighbor) {
|
||||
visited.insert(neighbor);
|
||||
queue.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVALUATION RESULTS
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Default)]
|
||||
struct EvalResults {
|
||||
total_cycles: u64,
|
||||
failures_observed: u64,
|
||||
warnings_issued: u64,
|
||||
true_warnings: u64,
|
||||
false_warnings: u64,
|
||||
lead_times: Vec<u64>,
|
||||
}
|
||||
|
||||
impl EvalResults {
|
||||
fn precision(&self) -> f64 {
|
||||
if self.warnings_issued == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.true_warnings as f64 / self.warnings_issued as f64
|
||||
}
|
||||
|
||||
fn recall(&self) -> f64 {
|
||||
if self.failures_observed == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.true_warnings as f64 / self.failures_observed as f64
|
||||
}
|
||||
|
||||
fn false_alarms_per_100k(&self) -> f64 {
|
||||
if self.total_cycles == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.false_warnings as f64 / self.total_cycles as f64 * 100_000.0
|
||||
}
|
||||
|
||||
fn median_lead_time(&self) -> f64 {
|
||||
if self.lead_times.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut sorted = self.lead_times.clone();
|
||||
sorted.sort();
|
||||
sorted[sorted.len() / 2] as f64
|
||||
}
|
||||
|
||||
fn p10_lead_time(&self) -> f64 {
|
||||
if self.lead_times.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut sorted = self.lead_times.clone();
|
||||
sorted.sort();
|
||||
let idx = (sorted.len() as f64 * 0.10) as usize;
|
||||
sorted[idx.min(sorted.len() - 1)] as f64
|
||||
}
|
||||
|
||||
fn p90_lead_time(&self) -> f64 {
|
||||
if self.lead_times.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut sorted = self.lead_times.clone();
|
||||
sorted.sort();
|
||||
let idx = (sorted.len() as f64 * 0.90) as usize;
|
||||
sorted[idx.min(sorted.len() - 1)] as f64
|
||||
}
|
||||
|
||||
fn actionable_rate(&self, min_cycles: u64) -> f64 {
|
||||
if self.lead_times.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let actionable = self.lead_times.iter().filter(|&&t| t >= min_cycles).count();
|
||||
actionable as f64 / self.lead_times.len() as f64
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SYNDROME GENERATOR WITH BURST INJECTION
|
||||
// ============================================================================
|
||||
|
||||
struct SyndromeGenerator {
|
||||
source: StimSyndromeSource,
|
||||
burst_active: bool,
|
||||
burst_remaining: usize,
|
||||
burst_center: usize,
|
||||
burst_radius: usize,
|
||||
code_distance: usize,
|
||||
}
|
||||
|
||||
impl SyndromeGenerator {
|
||||
fn new(code_distance: usize, error_rate: f64, seed: u64) -> Self {
|
||||
let config = SurfaceCodeConfig {
|
||||
distance: code_distance,
|
||||
error_rate,
|
||||
seed: Some(seed),
|
||||
rounds: 1,
|
||||
rotated: false,
|
||||
measure_errors: true,
|
||||
};
|
||||
Self {
|
||||
source: StimSyndromeSource::new(config).expect("Failed to create source"),
|
||||
burst_active: false,
|
||||
burst_remaining: 0,
|
||||
burst_center: 0,
|
||||
burst_radius: 2,
|
||||
code_distance,
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_burst(&mut self, duration: usize, center: usize) {
|
||||
self.burst_active = true;
|
||||
self.burst_remaining = duration;
|
||||
self.burst_center = center;
|
||||
}
|
||||
|
||||
fn sample(&mut self) -> DetectorBitmap {
|
||||
let mut syndrome = self.source.sample().unwrap_or_else(|_| {
|
||||
DetectorBitmap::new(2 * (self.code_distance - 1) * (self.code_distance - 1))
|
||||
});
|
||||
|
||||
if self.burst_active && self.burst_remaining > 0 {
|
||||
let grid_size = self.code_distance - 1;
|
||||
let center_row = self.burst_center / grid_size;
|
||||
let center_col = self.burst_center % grid_size;
|
||||
|
||||
for dr in 0..=self.burst_radius {
|
||||
for dc in 0..=self.burst_radius {
|
||||
if dr == 0 && dc == 0 {
|
||||
continue;
|
||||
}
|
||||
for &(sr, sc) in &[(1i32, 1i32), (1, -1), (-1, 1), (-1, -1)] {
|
||||
let row = center_row as i32 + dr as i32 * sr;
|
||||
let col = center_col as i32 + dc as i32 * sc;
|
||||
if row >= 0 && row < grid_size as i32 && col >= 0 && col < grid_size as i32
|
||||
{
|
||||
let detector = (row as usize) * grid_size + (col as usize);
|
||||
if detector < syndrome.detector_count() {
|
||||
syndrome.set(detector, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.burst_center < syndrome.detector_count() {
|
||||
syndrome.set(self.burst_center, true);
|
||||
}
|
||||
|
||||
self.burst_remaining -= 1;
|
||||
if self.burst_remaining == 0 {
|
||||
self.burst_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
syndrome
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN EVALUATION
|
||||
// ============================================================================
|
||||
|
||||
fn run_evaluation(config: &EvalConfig, with_bursts: bool) -> EvalResults {
|
||||
let mut results = EvalResults::default();
|
||||
let grid_size = config.code_distance - 1;
|
||||
let num_detectors = 2 * grid_size * grid_size;
|
||||
|
||||
for run in 0..config.num_runs {
|
||||
let seed = config.seed + run as u64;
|
||||
let mut generator = SyndromeGenerator::new(config.code_distance, config.error_rate, seed);
|
||||
let mut detector = WarningDetector::new();
|
||||
|
||||
let mut warning_active = false;
|
||||
let mut warning_start = 0u64;
|
||||
let mut cycles_since_warning = 0u64;
|
||||
|
||||
// Schedule burst injection at random point
|
||||
let burst_cycle = if with_bursts {
|
||||
(seed % (config.cycles_per_run as u64 / 2)) as usize + config.cycles_per_run / 4
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
let burst_duration = 8;
|
||||
let burst_center = ((seed * 7) % num_detectors as u64) as usize;
|
||||
|
||||
for cycle in 0..config.cycles_per_run {
|
||||
// Inject burst at scheduled time
|
||||
if cycle == burst_cycle && with_bursts {
|
||||
generator.inject_burst(burst_duration, burst_center);
|
||||
}
|
||||
|
||||
let syndrome = generator.sample();
|
||||
let graph = build_qec_graph(config.code_distance, config.error_rate, &syndrome);
|
||||
let cut = graph.compute_min_cut();
|
||||
let event_count = syndrome.fired_count();
|
||||
|
||||
detector.push(cut);
|
||||
|
||||
let is_failure = is_logical_failure(&syndrome, config.code_distance);
|
||||
let is_warning = detector.is_warning(event_count);
|
||||
|
||||
// Track warning onset
|
||||
if is_warning && !warning_active {
|
||||
warning_active = true;
|
||||
warning_start = cycle as u64;
|
||||
cycles_since_warning = 0;
|
||||
results.warnings_issued += 1;
|
||||
}
|
||||
|
||||
if warning_active {
|
||||
cycles_since_warning += 1;
|
||||
}
|
||||
|
||||
// Track failures
|
||||
if is_failure {
|
||||
results.failures_observed += 1;
|
||||
|
||||
if warning_active && cycles_since_warning > 0 {
|
||||
results.true_warnings += 1;
|
||||
results.lead_times.push(cycles_since_warning);
|
||||
}
|
||||
|
||||
// Reset warning state after failure
|
||||
warning_active = false;
|
||||
}
|
||||
|
||||
// Timeout warnings without failure (false alarm)
|
||||
if warning_active && cycles_since_warning > 20 {
|
||||
results.false_warnings += 1;
|
||||
warning_active = false;
|
||||
}
|
||||
|
||||
results.total_cycles += 1;
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let config = parse_args();
|
||||
let start_time = Instant::now();
|
||||
|
||||
println!();
|
||||
println!("╔═══════════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ ruQu PREDICTIVE EVALUATION ║");
|
||||
println!("║ Formal Metrics for Early Warning ║");
|
||||
println!("╚═══════════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
println!();
|
||||
println!("Configuration:");
|
||||
println!(" Code Distance: d={}", config.code_distance);
|
||||
println!(" Error Rate: {:.4}", config.error_rate);
|
||||
println!(" Runs: {}", config.num_runs);
|
||||
println!(" Cycles/Run: {}", config.cycles_per_run);
|
||||
println!(" Seed: {}", config.seed);
|
||||
println!(" Inject Mode: {:?}", config.inject_mode);
|
||||
|
||||
// Run with correlated bursts
|
||||
let results = match config.inject_mode {
|
||||
InjectMode::Independent => run_evaluation(&config, false),
|
||||
InjectMode::CorrelatedBurst => run_evaluation(&config, true),
|
||||
InjectMode::Both => {
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
println!(" REGIME A: Independent Noise");
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
let independent = run_evaluation(&config, false);
|
||||
print_results(&independent);
|
||||
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
println!(" REGIME B: Correlated Bursts");
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
let bursts = run_evaluation(&config, true);
|
||||
print_results(&bursts);
|
||||
|
||||
bursts
|
||||
}
|
||||
};
|
||||
|
||||
if !matches!(config.inject_mode, InjectMode::Both) {
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
println!(" EVALUATION RESULTS");
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
print_results(&results);
|
||||
}
|
||||
|
||||
// Actionability breakdown
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
println!(" ACTIONABILITY");
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
println!();
|
||||
println!(
|
||||
" Decoder switch (1 cycle): {:>5.1}%",
|
||||
results.actionable_rate(1) * 100.0
|
||||
);
|
||||
println!(
|
||||
" Extra syndrome round (2 cycles): {:>5.1}%",
|
||||
results.actionable_rate(2) * 100.0
|
||||
);
|
||||
println!(
|
||||
" Region quarantine (5 cycles): {:>5.1}%",
|
||||
results.actionable_rate(5) * 100.0
|
||||
);
|
||||
println!(
|
||||
" Full recalibration (10 cycles): {:>5.1}%",
|
||||
results.actionable_rate(10) * 100.0
|
||||
);
|
||||
|
||||
// Summary
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
println!(" SUMMARY");
|
||||
println!("═══════════════════════════════════════════════════════════════════════");
|
||||
|
||||
let predictive = results.recall() >= 0.80
|
||||
&& results.false_alarms_per_100k() < 50.0
|
||||
&& results.median_lead_time() >= 2.0;
|
||||
|
||||
if predictive {
|
||||
println!();
|
||||
println!(" ✓ PREDICTIVE: ruQu satisfies all criteria");
|
||||
println!(" - Recall >= 80%: {:.1}%", results.recall() * 100.0);
|
||||
println!(
|
||||
" - False alarms < 50/100k: {:.1}/100k",
|
||||
results.false_alarms_per_100k()
|
||||
);
|
||||
println!(
|
||||
" - Median lead >= 2 cycles: {:.1} cycles",
|
||||
results.median_lead_time()
|
||||
);
|
||||
} else {
|
||||
println!();
|
||||
println!(" ~ PARTIAL: Some criteria not met");
|
||||
println!(
|
||||
" - Recall: {:.1}% (target: >=80%)",
|
||||
results.recall() * 100.0
|
||||
);
|
||||
println!(
|
||||
" - False alarms: {:.1}/100k (target: <50)",
|
||||
results.false_alarms_per_100k()
|
||||
);
|
||||
println!(
|
||||
" - Median lead: {:.1} cycles (target: >=2)",
|
||||
results.median_lead_time()
|
||||
);
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
println!();
|
||||
println!(" Total time: {:.2}s", elapsed.as_secs_f64());
|
||||
println!(
|
||||
" Throughput: {:.0} cycles/sec",
|
||||
results.total_cycles as f64 / elapsed.as_secs_f64()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_results(results: &EvalResults) {
|
||||
println!();
|
||||
println!("Failures observed: {}", results.failures_observed);
|
||||
println!("Warnings issued: {}", results.warnings_issued);
|
||||
println!("True warnings: {}", results.true_warnings);
|
||||
println!("False warnings: {}", results.false_warnings);
|
||||
println!();
|
||||
println!("Lead time (cycles):");
|
||||
println!(" median: {:.1}", results.median_lead_time());
|
||||
println!(" p10: {:.1}", results.p10_lead_time());
|
||||
println!(" p90: {:.1}", results.p90_lead_time());
|
||||
println!();
|
||||
println!("Precision: {:.2}", results.precision());
|
||||
println!("Recall: {:.2}", results.recall());
|
||||
println!(
|
||||
"False alarms: {:.1} / 100k cycles",
|
||||
results.false_alarms_per_100k()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user