Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
230
vendor/ruvector/crates/ruqu-algorithms/src/grover.rs
vendored
Normal file
230
vendor/ruvector/crates/ruqu-algorithms/src/grover.rs
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
//! Grover's Search Algorithm
|
||||
//!
|
||||
//! Provides a quadratic speedup for **unstructured search**: given an oracle
|
||||
//! that marks M target states out of N = 2^n total states, Grover's algorithm
|
||||
//! finds a marked state with high probability in O(sqrt(N/M)) queries.
|
||||
//!
|
||||
//! # Implementation strategy
|
||||
//!
|
||||
//! Because this is a *simulation* library (not a hardware backend), the oracle
|
||||
//! and diffusion operator are implemented via **direct state-vector
|
||||
//! manipulation** through [`QuantumState::amplitudes_mut`]. This gives O(M)
|
||||
//! oracle cost and O(N) diffuser cost per iteration -- far cheaper than
|
||||
//! decomposing a general multi-controlled-Z into elementary gates.
|
||||
//!
|
||||
//! Single-qubit Hadamard gates are still applied through the normal gate
|
||||
//! pipeline so that the simulator's bookkeeping (metrics, noise, etc.)
|
||||
//! remains consistent.
|
||||
|
||||
use ruqu_core::gate::Gate;
|
||||
use ruqu_core::state::QuantumState;
|
||||
use ruqu_core::types::{Complex, QubitIndex};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration and result types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Configuration for Grover's search.
|
||||
pub struct GroverConfig {
|
||||
/// Number of qubits (search space has 2^num_qubits states).
|
||||
pub num_qubits: u32,
|
||||
/// Indices of the marked (target) basis states. Each index must be in
|
||||
/// `0 .. 2^num_qubits`.
|
||||
pub target_states: Vec<usize>,
|
||||
/// Number of Grover iterations. When `None`, the theoretically optimal
|
||||
/// count is computed from [`optimal_iterations`].
|
||||
pub num_iterations: Option<u32>,
|
||||
/// Optional RNG seed forwarded to [`QuantumState::new_with_seed`].
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
/// Result of a Grover search run.
|
||||
pub struct GroverResult {
|
||||
/// The basis-state index obtained by measuring all qubits.
|
||||
pub measured_state: usize,
|
||||
/// Whether `measured_state` is one of the target states.
|
||||
pub target_found: bool,
|
||||
/// Pre-measurement probability of observing *any* target state.
|
||||
pub success_probability: f64,
|
||||
/// Number of Grover iterations that were executed.
|
||||
pub num_iterations: u32,
|
||||
/// Post-measurement quantum state (collapsed).
|
||||
pub state: QuantumState,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Optimal iteration count
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute the theoretically optimal number of Grover iterations.
|
||||
///
|
||||
/// For N = 2^n states and M marked targets the optimal count is:
|
||||
///
|
||||
/// ```text
|
||||
/// k = round( (pi / 4) * sqrt(N / M) - 0.5 )
|
||||
/// ```
|
||||
///
|
||||
/// which maximizes the success probability (close to 1 when M << N).
|
||||
/// Returns at least 1.
|
||||
pub fn optimal_iterations(num_qubits: u32, num_targets: usize) -> u32 {
|
||||
let n = 1usize << num_qubits;
|
||||
let theta = (num_targets as f64 / n as f64).sqrt().asin();
|
||||
let k = (std::f64::consts::FRAC_PI_4 / theta - 0.5).round().max(1.0);
|
||||
k as u32
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core algorithm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Run Grover's search algorithm.
|
||||
///
|
||||
/// # Algorithm outline
|
||||
///
|
||||
/// 1. Prepare equal superposition |s> = H^n |0>.
|
||||
/// 2. Repeat for `num_iterations`:
|
||||
/// a. **Oracle** -- negate the amplitude of every target state.
|
||||
/// b. **Diffuser** -- reflect about |s>:
|
||||
/// i. Apply H on all qubits.
|
||||
/// ii. Negate all amplitudes except the |0...0> component.
|
||||
/// iii.Apply H on all qubits.
|
||||
/// 3. Compute success probability from the final state.
|
||||
/// 4. Measure all qubits to obtain a classical bitstring.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`ruqu_core::error::QuantumError`] if the qubit count exceeds
|
||||
/// simulator limits or any gate application fails.
|
||||
pub fn run_grover(config: &GroverConfig) -> ruqu_core::error::Result<GroverResult> {
|
||||
let n = config.num_qubits;
|
||||
let dim = 1usize << n;
|
||||
|
||||
// Validate target indices.
|
||||
for &t in &config.target_states {
|
||||
assert!(
|
||||
t < dim,
|
||||
"target state index {} out of range for {} qubits (max {})",
|
||||
t,
|
||||
n,
|
||||
dim - 1,
|
||||
);
|
||||
}
|
||||
|
||||
let iterations = config
|
||||
.num_iterations
|
||||
.unwrap_or_else(|| optimal_iterations(n, config.target_states.len()));
|
||||
|
||||
// ----- Step 1: Initialize to equal superposition -----
|
||||
let mut state = match config.seed {
|
||||
Some(s) => QuantumState::new_with_seed(n, s)?,
|
||||
None => QuantumState::new(n)?,
|
||||
};
|
||||
for q in 0..n {
|
||||
state.apply_gate(&Gate::H(q))?;
|
||||
}
|
||||
|
||||
// ----- Step 2: Grover iterations -----
|
||||
for _ in 0..iterations {
|
||||
// (a) Oracle: negate amplitudes of target states.
|
||||
{
|
||||
let amps = state.amplitudes_mut();
|
||||
for &target in &config.target_states {
|
||||
let a = amps[target];
|
||||
amps[target] = Complex {
|
||||
re: -a.re,
|
||||
im: -a.im,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// (b) Diffuser: 2|s><s| - I = H^n (2|0><0| - I) H^n
|
||||
// (2|0><0| - I) keeps |0> unchanged and negates everything else.
|
||||
for q in 0..n {
|
||||
state.apply_gate(&Gate::H(q))?;
|
||||
}
|
||||
{
|
||||
let amps = state.amplitudes_mut();
|
||||
for i in 1..amps.len() {
|
||||
let a = amps[i];
|
||||
amps[i] = Complex {
|
||||
re: -a.re,
|
||||
im: -a.im,
|
||||
};
|
||||
}
|
||||
}
|
||||
for q in 0..n {
|
||||
state.apply_gate(&Gate::H(q))?;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Step 3: Compute success probability before measurement -----
|
||||
let probs = state.probabilities();
|
||||
let success_probability: f64 = config.target_states.iter().map(|&t| probs[t]).sum();
|
||||
|
||||
// ----- Step 4: Measure all qubits -----
|
||||
let measured = measure_all_qubits(&mut state, n)?;
|
||||
let target_found = config.target_states.contains(&measured);
|
||||
|
||||
Ok(GroverResult {
|
||||
measured_state: measured,
|
||||
target_found,
|
||||
success_probability,
|
||||
num_iterations: iterations,
|
||||
state,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Measure every qubit (0 through `num_qubits - 1`) and assemble the results
|
||||
/// into a single `usize` where bit `q` is 1 when qubit `q` measured |1>.
|
||||
///
|
||||
/// Measurements are performed in ascending qubit order. Each measurement
|
||||
/// collapses the state, so subsequent outcomes are conditioned on earlier
|
||||
/// ones. The joint distribution over all qubits matches `probabilities()`.
|
||||
fn measure_all_qubits(
|
||||
state: &mut QuantumState,
|
||||
num_qubits: u32,
|
||||
) -> ruqu_core::error::Result<usize> {
|
||||
let mut result: usize = 0;
|
||||
for q in 0..num_qubits {
|
||||
let outcome = state.measure(q as QubitIndex)?;
|
||||
if outcome.result {
|
||||
result |= 1 << q;
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_optimal_iterations_single_target() {
|
||||
// N=8, M=1 -> k = round(pi/4 * sqrt(8) - 0.5) = round(1.72) = 2
|
||||
let k = optimal_iterations(3, 1);
|
||||
assert_eq!(k, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optimal_iterations_half_marked() {
|
||||
// N=4, M=2 -> theta = asin(sqrt(0.5)) = pi/4
|
||||
// k = round(pi/4 / (pi/4) - 0.5) = round(0.5) = 1
|
||||
let k = optimal_iterations(2, 2);
|
||||
assert!(k >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optimal_iterations_minimum_one() {
|
||||
// Even pathological inputs should produce at least 1.
|
||||
let k = optimal_iterations(1, 1);
|
||||
assert!(k >= 1);
|
||||
}
|
||||
}
|
||||
45
vendor/ruvector/crates/ruqu-algorithms/src/lib.rs
vendored
Normal file
45
vendor/ruvector/crates/ruqu-algorithms/src/lib.rs
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
//! # ruqu-algorithms -- Quantum Algorithm Implementations
|
||||
//!
|
||||
//! High-level quantum algorithms built on the `ruqu-core` simulation engine:
|
||||
//!
|
||||
//! - **VQE** (Variational Quantum Eigensolver): Find ground-state energies of
|
||||
//! molecular Hamiltonians using a classical-quantum hybrid loop with
|
||||
//! hardware-efficient ansatz and parameter-shift gradient descent.
|
||||
//!
|
||||
//! - **Grover's Search**: Quadratic speedup for unstructured search over N items,
|
||||
//! using amplitude amplification with direct state-vector oracle access.
|
||||
//!
|
||||
//! - **QAOA** (Quantum Approximate Optimization Algorithm): Approximate solutions
|
||||
//! to combinatorial optimization problems (MaxCut) via parameterized
|
||||
//! phase-separation and mixing layers.
|
||||
//!
|
||||
//! - **Surface Code**: Distance-3 surface code error correction simulation with
|
||||
//! stabilizer measurement cycles, noise injection, and syndrome decoding.
|
||||
//!
|
||||
//! # Quick Start
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ruqu_algorithms::{VqeConfig, run_vqe, vqe::h2_hamiltonian};
|
||||
//!
|
||||
//! let config = VqeConfig {
|
||||
//! hamiltonian: h2_hamiltonian(),
|
||||
//! num_qubits: 2,
|
||||
//! ansatz_depth: 2,
|
||||
//! max_iterations: 100,
|
||||
//! convergence_threshold: 1e-6,
|
||||
//! learning_rate: 0.1,
|
||||
//! seed: Some(42),
|
||||
//! };
|
||||
//! let result = run_vqe(&config).expect("VQE failed");
|
||||
//! println!("Ground state energy: {:.6}", result.optimal_energy);
|
||||
//! ```
|
||||
|
||||
pub mod grover;
|
||||
pub mod qaoa;
|
||||
pub mod surface_code;
|
||||
pub mod vqe;
|
||||
|
||||
pub use grover::{run_grover, GroverConfig, GroverResult};
|
||||
pub use qaoa::{run_qaoa, Graph, QaoaConfig, QaoaResult};
|
||||
pub use surface_code::{run_surface_code, SurfaceCodeConfig, SurfaceCodeResult};
|
||||
pub use vqe::{run_vqe, VqeConfig, VqeResult};
|
||||
394
vendor/ruvector/crates/ruqu-algorithms/src/qaoa.rs
vendored
Normal file
394
vendor/ruvector/crates/ruqu-algorithms/src/qaoa.rs
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
//! Quantum Approximate Optimization Algorithm (QAOA) for MaxCut
|
||||
//!
|
||||
//! QAOA is a hybrid classical-quantum algorithm for combinatorial optimization.
|
||||
//! This module implements the **MaxCut** variant: given an undirected weighted
|
||||
//! graph, find a partition of vertices into two sets that maximizes the total
|
||||
//! weight of edges crossing the partition.
|
||||
//!
|
||||
//! # Circuit structure
|
||||
//!
|
||||
//! A depth-p QAOA circuit has the form:
|
||||
//!
|
||||
//! ```text
|
||||
//! |+>^n --[C(gamma_1)][B(beta_1)]--...--[C(gamma_p)][B(beta_p)]-- measure
|
||||
//! ```
|
||||
//!
|
||||
//! where:
|
||||
//! - **Phase separator** C(gamma) = prod_{(i,j) in E} exp(-i * gamma * w_ij * Z_i Z_j)
|
||||
//! is implemented with Rzz gates.
|
||||
//! - **Mixer** B(beta) = prod_i exp(-i * beta * X_i) is implemented with Rx gates.
|
||||
//!
|
||||
//! The 2p parameters (gamma_1..gamma_p, beta_1..beta_p) are optimized
|
||||
//! classically to maximize the expected cut value.
|
||||
|
||||
use ruqu_core::circuit::QuantumCircuit;
|
||||
use ruqu_core::simulator::{SimConfig, Simulator};
|
||||
use ruqu_core::types::{PauliOp, PauliString};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Graph representation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Simple undirected weighted graph for MaxCut problems.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Graph {
|
||||
/// Number of vertices (each mapped to one qubit).
|
||||
pub num_nodes: u32,
|
||||
/// Edges as `(node_i, node_j, weight)` triples. Both directions are
|
||||
/// represented by a single entry (undirected).
|
||||
pub edges: Vec<(u32, u32, f64)>,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
/// Create an empty graph with the given number of nodes.
|
||||
pub fn new(num_nodes: u32) -> Self {
|
||||
Self {
|
||||
num_nodes,
|
||||
edges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an undirected weighted edge between nodes `i` and `j`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `i` or `j` is out of range.
|
||||
pub fn add_edge(&mut self, i: u32, j: u32, weight: f64) {
|
||||
assert!(i < self.num_nodes, "node index {} out of range", i);
|
||||
assert!(j < self.num_nodes, "node index {} out of range", j);
|
||||
self.edges.push((i, j, weight));
|
||||
}
|
||||
|
||||
/// Convenience constructor for an unweighted graph (all weights = 1.0).
|
||||
pub fn unweighted(num_nodes: u32, edges: Vec<(u32, u32)>) -> Self {
|
||||
let weighted: Vec<(u32, u32, f64)> = edges.into_iter().map(|(i, j)| (i, j, 1.0)).collect();
|
||||
Self {
|
||||
num_nodes,
|
||||
edges: weighted,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the total number of edges.
|
||||
pub fn num_edges(&self) -> usize {
|
||||
self.edges.len()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration and result types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Configuration for a QAOA MaxCut run.
|
||||
pub struct QaoaConfig {
|
||||
/// The graph instance to solve MaxCut on.
|
||||
pub graph: Graph,
|
||||
/// QAOA depth (number of alternating phase-separation / mixing layers).
|
||||
pub p: u32,
|
||||
/// Maximum number of classical optimizer iterations.
|
||||
pub max_iterations: u32,
|
||||
/// Step size for gradient ascent.
|
||||
pub learning_rate: f64,
|
||||
/// Optional RNG seed for reproducible simulation.
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
/// Result of a QAOA MaxCut run.
|
||||
pub struct QaoaResult {
|
||||
/// Highest expected cut value found.
|
||||
pub best_cut_value: f64,
|
||||
/// Bitstring that achieves (or approximates) `best_cut_value`.
|
||||
/// `best_bitstring[v]` is `true` when vertex `v` belongs to partition S1.
|
||||
pub best_bitstring: Vec<bool>,
|
||||
/// Optimized gamma parameters (phase-separation angles).
|
||||
pub optimal_gammas: Vec<f64>,
|
||||
/// Optimized beta parameters (mixer angles).
|
||||
pub optimal_betas: Vec<f64>,
|
||||
/// Expected cut value at each iteration.
|
||||
pub energy_history: Vec<f64>,
|
||||
/// Whether the optimizer converged.
|
||||
pub converged: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Circuit construction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Build a QAOA circuit for the MaxCut problem on `graph`.
|
||||
///
|
||||
/// The circuit starts with Hadamard on every qubit (equal superposition),
|
||||
/// then applies `p` alternating layers:
|
||||
///
|
||||
/// 1. **Phase separation**: `Rzz(2 * gamma * w)` on each edge `(i, j, w)`.
|
||||
/// 2. **Mixing**: `Rx(2 * beta)` on each qubit.
|
||||
///
|
||||
/// `gammas` and `betas` must each have length `p`.
|
||||
pub fn build_qaoa_circuit(graph: &Graph, gammas: &[f64], betas: &[f64]) -> QuantumCircuit {
|
||||
assert_eq!(
|
||||
gammas.len(),
|
||||
betas.len(),
|
||||
"gammas and betas must have equal length"
|
||||
);
|
||||
let n = graph.num_nodes;
|
||||
let p = gammas.len();
|
||||
let mut circuit = QuantumCircuit::new(n);
|
||||
|
||||
// Initial equal superposition
|
||||
for q in 0..n {
|
||||
circuit.h(q);
|
||||
}
|
||||
|
||||
// QAOA layers
|
||||
for layer in 0..p {
|
||||
// Phase separator: Rzz for each edge
|
||||
for &(i, j, w) in &graph.edges {
|
||||
circuit.rzz(i, j, 2.0 * gammas[layer] * w);
|
||||
}
|
||||
// Mixer: Rx on each qubit
|
||||
for q in 0..n {
|
||||
circuit.rx(q, 2.0 * betas[layer]);
|
||||
}
|
||||
}
|
||||
|
||||
circuit
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cost evaluation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute the classical MaxCut value for a given bitstring.
|
||||
///
|
||||
/// An edge (i, j, w) contributes `w` to the cut if `bitstring[i] != bitstring[j]`.
|
||||
pub fn cut_value(graph: &Graph, bitstring: &[bool]) -> f64 {
|
||||
graph
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|(i, j, _)| bitstring[*i as usize] != bitstring[*j as usize])
|
||||
.map(|(_, _, w)| w)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Evaluate the expected MaxCut cost from a QAOA state.
|
||||
///
|
||||
/// For each edge (i, j) with weight w:
|
||||
/// ```text
|
||||
/// C_{ij} = w * 0.5 * (1 - <Z_i Z_j>)
|
||||
/// ```
|
||||
///
|
||||
/// The total expected cost is the sum over all edges.
|
||||
pub fn evaluate_qaoa_cost(
|
||||
graph: &Graph,
|
||||
gammas: &[f64],
|
||||
betas: &[f64],
|
||||
seed: Option<u64>,
|
||||
) -> ruqu_core::error::Result<f64> {
|
||||
let circuit = build_qaoa_circuit(graph, gammas, betas);
|
||||
let sim_config = SimConfig {
|
||||
seed,
|
||||
noise: None,
|
||||
shots: None,
|
||||
};
|
||||
let result = Simulator::run_with_config(&circuit, &sim_config)?;
|
||||
|
||||
let mut cost = 0.0;
|
||||
for &(i, j, w) in &graph.edges {
|
||||
let zz = result.state.expectation_value(&PauliString {
|
||||
ops: vec![(i, PauliOp::Z), (j, PauliOp::Z)],
|
||||
});
|
||||
cost += w * 0.5 * (1.0 - zz);
|
||||
}
|
||||
Ok(cost)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// QAOA optimizer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Run QAOA optimization for MaxCut using gradient ascent with the
|
||||
/// parameter-shift rule.
|
||||
///
|
||||
/// The optimizer maximizes the expected cut value by adjusting gamma and beta
|
||||
/// parameters. Convergence is declared when the absolute change in cost
|
||||
/// between successive iterations drops below 1e-6.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`ruqu_core::error::QuantumError`] on simulator failures.
|
||||
pub fn run_qaoa(config: &QaoaConfig) -> ruqu_core::error::Result<QaoaResult> {
|
||||
let p = config.p as usize;
|
||||
|
||||
// Initialize parameters at reasonable starting values.
|
||||
let mut gammas = vec![0.5_f64; p];
|
||||
let mut betas = vec![0.5_f64; p];
|
||||
let mut energy_history: Vec<f64> = Vec::with_capacity(config.max_iterations as usize);
|
||||
let mut best_cost = f64::NEG_INFINITY;
|
||||
let mut best_bitstring = vec![false; config.graph.num_nodes as usize];
|
||||
let mut converged = false;
|
||||
|
||||
for iter in 0..config.max_iterations {
|
||||
// ------------------------------------------------------------------
|
||||
// Evaluate current expected cost
|
||||
// ------------------------------------------------------------------
|
||||
let cost = evaluate_qaoa_cost(&config.graph, &gammas, &betas, config.seed)?;
|
||||
energy_history.push(cost);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Track best solution: sample the most probable bitstring
|
||||
// ------------------------------------------------------------------
|
||||
if cost > best_cost {
|
||||
best_cost = cost;
|
||||
let circuit = build_qaoa_circuit(&config.graph, &gammas, &betas);
|
||||
let sim_result = Simulator::run_with_config(
|
||||
&circuit,
|
||||
&SimConfig {
|
||||
seed: config.seed,
|
||||
noise: None,
|
||||
shots: None,
|
||||
},
|
||||
)?;
|
||||
let probs = sim_result.state.probabilities();
|
||||
let best_idx = probs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0);
|
||||
best_bitstring = (0..config.graph.num_nodes)
|
||||
.map(|q| (best_idx >> q) & 1 == 1)
|
||||
.collect();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Convergence check
|
||||
// ------------------------------------------------------------------
|
||||
if iter > 0 {
|
||||
let prev = energy_history[iter as usize - 1];
|
||||
if (cost - prev).abs() < 1e-6 {
|
||||
converged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Gradient ascent via parameter-shift rule
|
||||
// ------------------------------------------------------------------
|
||||
let shift = std::f64::consts::FRAC_PI_2;
|
||||
|
||||
// Update gamma parameters
|
||||
for i in 0..p {
|
||||
let mut gp = gammas.clone();
|
||||
gp[i] += shift;
|
||||
let mut gm = gammas.clone();
|
||||
gm[i] -= shift;
|
||||
let cp = evaluate_qaoa_cost(&config.graph, &gp, &betas, config.seed)?;
|
||||
let cm = evaluate_qaoa_cost(&config.graph, &gm, &betas, config.seed)?;
|
||||
gammas[i] += config.learning_rate * (cp - cm) / 2.0;
|
||||
}
|
||||
|
||||
// Update beta parameters
|
||||
for i in 0..p {
|
||||
let mut bp = betas.clone();
|
||||
bp[i] += shift;
|
||||
let mut bm = betas.clone();
|
||||
bm[i] -= shift;
|
||||
let cp = evaluate_qaoa_cost(&config.graph, &gammas, &bp, config.seed)?;
|
||||
let cm = evaluate_qaoa_cost(&config.graph, &gammas, &bm, config.seed)?;
|
||||
betas[i] += config.learning_rate * (cp - cm) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(QaoaResult {
|
||||
best_cut_value: best_cost,
|
||||
best_bitstring,
|
||||
optimal_gammas: gammas,
|
||||
optimal_betas: betas,
|
||||
energy_history,
|
||||
converged,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Graph construction helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create a triangle graph (3 nodes, 3 edges, all weight 1).
|
||||
///
|
||||
/// The optimal MaxCut is 2 (any partition has exactly one edge within a
|
||||
/// group and two edges crossing).
|
||||
pub fn triangle_graph() -> Graph {
|
||||
Graph::unweighted(3, vec![(0, 1), (1, 2), (0, 2)])
|
||||
}
|
||||
|
||||
/// Create a 4-node ring graph (cycle C4, all weight 1).
|
||||
///
|
||||
/// The optimal MaxCut is 4 (bipartition {0,2} vs {1,3} cuts all edges).
|
||||
pub fn ring4_graph() -> Graph {
|
||||
Graph::unweighted(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_graph_construction() {
|
||||
let g = triangle_graph();
|
||||
assert_eq!(g.num_nodes, 3);
|
||||
assert_eq!(g.num_edges(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_graph_add_edge() {
|
||||
let mut g = Graph::new(4);
|
||||
g.add_edge(0, 1, 2.5);
|
||||
g.add_edge(2, 3, 1.0);
|
||||
assert_eq!(g.num_edges(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "node index 5 out of range")]
|
||||
fn test_graph_add_edge_out_of_range() {
|
||||
let mut g = Graph::new(4);
|
||||
g.add_edge(0, 5, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cut_value_triangle() {
|
||||
let g = triangle_graph();
|
||||
// Partition {0} vs {1,2}: edges (0,1) and (0,2) are cut, (1,2) is not.
|
||||
assert_eq!(cut_value(&g, &[true, false, false]), 2.0);
|
||||
// All same partition: no cut.
|
||||
assert_eq!(cut_value(&g, &[false, false, false]), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cut_value_ring4() {
|
||||
let g = ring4_graph();
|
||||
// Optimal: alternate partitions {0,2} vs {1,3} -> cut all 4 edges.
|
||||
assert_eq!(cut_value(&g, &[true, false, true, false]), 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_qaoa_circuit_gate_count() {
|
||||
let g = triangle_graph();
|
||||
let gammas = vec![0.5];
|
||||
let betas = vec![0.3];
|
||||
let circuit = build_qaoa_circuit(&g, &gammas, &betas);
|
||||
assert_eq!(circuit.num_qubits(), 3);
|
||||
// 3 H + 3 Rzz + 3 Rx = 9 gates
|
||||
assert_eq!(circuit.gates().len(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cut_value_weighted() {
|
||||
let mut g = Graph::new(3);
|
||||
g.add_edge(0, 1, 2.0);
|
||||
g.add_edge(1, 2, 3.0);
|
||||
// Partition {0,2} vs {1}: cuts both edges -> 2.0 + 3.0 = 5.0
|
||||
assert_eq!(cut_value(&g, &[true, false, true]), 5.0);
|
||||
}
|
||||
}
|
||||
494
vendor/ruvector/crates/ruqu-algorithms/src/surface_code.rs
vendored
Normal file
494
vendor/ruvector/crates/ruqu-algorithms/src/surface_code.rs
vendored
Normal file
@@ -0,0 +1,494 @@
|
||||
//! Surface Code Error Correction Simulation
|
||||
//!
|
||||
//! Simulates a **distance-3 rotated surface code** with:
|
||||
//!
|
||||
//! - 9 data qubits (3 x 3 grid)
|
||||
//! - 4 X-type stabilizers (plaquettes, detect Z errors)
|
||||
//! - 4 Z-type stabilizers (vertices, detect X errors)
|
||||
//! - 8 ancilla qubits (one per stabilizer)
|
||||
//!
|
||||
//! Each QEC cycle performs:
|
||||
//! 1. **Noise injection** -- random Pauli errors on data qubits.
|
||||
//! 2. **Stabilizer measurement** -- entangle ancillas with data qubits and
|
||||
//! measure the ancillas to extract the error syndrome.
|
||||
//! 3. **Decoding** -- a simple lookup decoder maps the syndrome to a
|
||||
//! correction (placeholder; production systems would use MWPM).
|
||||
//! 4. **Correction** -- apply compensating Pauli gates.
|
||||
//!
|
||||
//! # Qubit layout (distance 3)
|
||||
//!
|
||||
//! ```text
|
||||
//! Data qubits: Ancilla assignment:
|
||||
//! d0 d1 d2 X-anc: 9, 10, 11, 12
|
||||
//! d3 d4 d5 Z-anc: 13, 14, 15, 16
|
||||
//! d6 d7 d8
|
||||
//! ```
|
||||
//!
|
||||
//! X stabilizers (plaquettes):
|
||||
//! - X0 (anc 9): {d0, d1, d3, d4}
|
||||
//! - X1 (anc 10): {d1, d2, d4, d5}
|
||||
//! - X2 (anc 11): {d3, d4, d6, d7}
|
||||
//! - X3 (anc 12): {d4, d5, d7, d8}
|
||||
//!
|
||||
//! Z stabilizers (boundary vertices):
|
||||
//! - Z0 (anc 13): {d0, d1}
|
||||
//! - Z1 (anc 14): {d2, d5}
|
||||
//! - Z2 (anc 15): {d3, d6}
|
||||
//! - Z3 (anc 16): {d7, d8}
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use ruqu_core::gate::Gate;
|
||||
use ruqu_core::state::QuantumState;
|
||||
use ruqu_core::types::QubitIndex;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration and result types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Configuration for a surface code error correction simulation.
|
||||
pub struct SurfaceCodeConfig {
|
||||
/// Code distance (currently only 3 is supported).
|
||||
pub distance: u32,
|
||||
/// Number of QEC syndrome-extraction cycles to run.
|
||||
pub num_cycles: u32,
|
||||
/// Physical error rate per data qubit per cycle. Each data qubit
|
||||
/// independently suffers a Pauli-X with probability `noise_rate` and a
|
||||
/// Pauli-Z with probability `noise_rate` (simplified depolarizing model).
|
||||
pub noise_rate: f64,
|
||||
/// Optional RNG seed for reproducibility.
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
/// Result of a surface code simulation.
|
||||
pub struct SurfaceCodeResult {
|
||||
/// Number of detected logical errors (simplified check).
|
||||
pub logical_errors: u32,
|
||||
/// Total QEC cycles executed.
|
||||
pub total_cycles: u32,
|
||||
/// Logical error rate = `logical_errors / total_cycles`.
|
||||
pub logical_error_rate: f64,
|
||||
/// Syndrome bit-vector for each cycle. Each inner `Vec<bool>` has
|
||||
/// `num_x_stabilizers + num_z_stabilizers` entries.
|
||||
pub syndrome_history: Vec<Vec<bool>>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Surface code layout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Physical layout of a surface code: which data qubits participate in each
|
||||
/// stabilizer, and the ancilla qubit assigned to each stabilizer.
|
||||
pub struct SurfaceCodeLayout {
|
||||
/// Indices of data qubits.
|
||||
pub data_qubits: Vec<QubitIndex>,
|
||||
/// Indices of X-type (plaquette) ancilla qubits.
|
||||
pub x_ancillas: Vec<QubitIndex>,
|
||||
/// Indices of Z-type (vertex) ancilla qubits.
|
||||
pub z_ancillas: Vec<QubitIndex>,
|
||||
/// For each X stabilizer, the data qubits it acts on.
|
||||
pub x_stabilizers: Vec<Vec<QubitIndex>>,
|
||||
/// For each Z stabilizer, the data qubits it acts on.
|
||||
pub z_stabilizers: Vec<Vec<QubitIndex>>,
|
||||
}
|
||||
|
||||
impl SurfaceCodeLayout {
|
||||
/// Create the layout for a distance-3 rotated surface code.
|
||||
///
|
||||
/// Total qubits: 9 data (indices 0..8) + 4 X-ancillas (9..12) +
|
||||
/// 4 Z-ancillas (13..16) = 17.
|
||||
pub fn distance_3() -> Self {
|
||||
Self {
|
||||
data_qubits: (0..9).collect(),
|
||||
x_ancillas: vec![9, 10, 11, 12],
|
||||
z_ancillas: vec![13, 14, 15, 16],
|
||||
x_stabilizers: vec![
|
||||
vec![0, 1, 3, 4], // X0: top-left plaquette
|
||||
vec![1, 2, 4, 5], // X1: top-right plaquette
|
||||
vec![3, 4, 6, 7], // X2: bottom-left plaquette
|
||||
vec![4, 5, 7, 8], // X3: bottom-right plaquette
|
||||
],
|
||||
z_stabilizers: vec![
|
||||
vec![0, 1], // Z0: top boundary
|
||||
vec![2, 5], // Z1: right boundary
|
||||
vec![3, 6], // Z2: left boundary
|
||||
vec![7, 8], // Z3: bottom boundary
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Total number of physical qubits (data + ancilla).
|
||||
pub fn total_qubits(&self) -> u32 {
|
||||
(self.data_qubits.len() + self.x_ancillas.len() + self.z_ancillas.len()) as u32
|
||||
}
|
||||
|
||||
/// Total number of stabilizers (X + Z).
|
||||
pub fn num_stabilizers(&self) -> usize {
|
||||
self.x_stabilizers.len() + self.z_stabilizers.len()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Noise injection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Inject simplified depolarizing noise on each data qubit.
|
||||
///
|
||||
/// For each data qubit, independently:
|
||||
/// - With probability `noise_rate`: apply X (bit flip)
|
||||
/// - With probability `noise_rate`: apply Z (phase flip)
|
||||
/// - Otherwise: no error
|
||||
///
|
||||
/// The two error channels are independent (a qubit can get both X and Z = Y).
|
||||
fn inject_noise(
|
||||
state: &mut QuantumState,
|
||||
data_qubits: &[QubitIndex],
|
||||
noise_rate: f64,
|
||||
rng: &mut StdRng,
|
||||
) -> ruqu_core::error::Result<()> {
|
||||
for &q in data_qubits {
|
||||
let r: f64 = rng.gen();
|
||||
if r < noise_rate {
|
||||
state.apply_gate(&Gate::X(q))?;
|
||||
} else if r < 2.0 * noise_rate {
|
||||
state.apply_gate(&Gate::Z(q))?;
|
||||
}
|
||||
// else: no error on this qubit in this channel
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stabilizer measurement (one QEC cycle)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Execute one QEC cycle: reset ancillas, entangle with data qubits via
|
||||
/// stabilizer circuits, and measure ancillas.
|
||||
///
|
||||
/// Returns the syndrome vector (one bool per stabilizer, X-stabilizers first,
|
||||
/// then Z-stabilizers). A `true` entry means the stabilizer measured -1
|
||||
/// (error detected).
|
||||
fn run_cycle(
|
||||
state: &mut QuantumState,
|
||||
layout: &SurfaceCodeLayout,
|
||||
) -> ruqu_core::error::Result<Vec<bool>> {
|
||||
// Reset all ancilla qubits to |0>.
|
||||
for &a in layout.x_ancillas.iter().chain(layout.z_ancillas.iter()) {
|
||||
state.reset_qubit(a)?;
|
||||
}
|
||||
|
||||
// ---- X-stabilizer measurement circuits ----
|
||||
// To measure the product X_a X_b X_c X_d:
|
||||
// 1. H(ancilla)
|
||||
// 2. CNOT(ancilla, data_a), ..., CNOT(ancilla, data_d)
|
||||
// 3. H(ancilla)
|
||||
// 4. Measure ancilla
|
||||
for (i, stabilizer) in layout.x_stabilizers.iter().enumerate() {
|
||||
let ancilla = layout.x_ancillas[i];
|
||||
state.apply_gate(&Gate::H(ancilla))?;
|
||||
for &data in stabilizer {
|
||||
state.apply_gate(&Gate::CNOT(ancilla, data))?;
|
||||
}
|
||||
state.apply_gate(&Gate::H(ancilla))?;
|
||||
}
|
||||
|
||||
// ---- Z-stabilizer measurement circuits ----
|
||||
// To measure the product Z_a Z_b Z_c Z_d:
|
||||
// 1. CNOT(data_a, ancilla), ..., CNOT(data_d, ancilla)
|
||||
// 2. Measure ancilla
|
||||
for (i, stabilizer) in layout.z_stabilizers.iter().enumerate() {
|
||||
let ancilla = layout.z_ancillas[i];
|
||||
for &data in stabilizer {
|
||||
state.apply_gate(&Gate::CNOT(data, ancilla))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Measure all ancillas and collect syndrome bits.
|
||||
let mut syndrome = Vec::with_capacity(layout.num_stabilizers());
|
||||
for &a in layout.x_ancillas.iter().chain(layout.z_ancillas.iter()) {
|
||||
let outcome = state.measure(a)?;
|
||||
syndrome.push(outcome.result);
|
||||
}
|
||||
|
||||
Ok(syndrome)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Syndrome decoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Simple lookup decoder for the distance-3 surface code.
|
||||
///
|
||||
/// This is a **placeholder** decoder that applies a single-qubit X correction
|
||||
/// on the data qubit most likely responsible for the detected syndrome
|
||||
/// pattern. A production implementation would use Minimum Weight Perfect
|
||||
/// Matching (MWPM) via e.g. `fusion-blossom`.
|
||||
///
|
||||
/// # Decoding strategy
|
||||
///
|
||||
/// The syndrome has 8 bits (4 X-stabilizer + 4 Z-stabilizer). The decoder
|
||||
/// only looks at the X-stabilizer syndrome (bits 0..3) to correct Z errors
|
||||
/// and the Z-stabilizer syndrome (bits 4..7) to correct X errors.
|
||||
///
|
||||
/// For each stabilizer group, if exactly one stabilizer fires, apply a
|
||||
/// correction on the first data qubit of that stabilizer. If multiple fire,
|
||||
/// correct the data qubit shared by the most triggered stabilizers (heuristic).
|
||||
fn decode_syndrome(syndrome: &[bool], layout: &SurfaceCodeLayout) -> Vec<Gate> {
|
||||
let mut corrections = Vec::new();
|
||||
let n_x = layout.x_stabilizers.len();
|
||||
|
||||
// ---- Correct Z errors using X-stabilizer syndrome (bits 0..n_x) ----
|
||||
let x_syndrome = &syndrome[..n_x];
|
||||
let x_triggered: Vec<usize> = x_syndrome
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &s)| s)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if x_triggered.len() == 1 {
|
||||
// Single stabilizer fired: correct its first data qubit with Z.
|
||||
let data_q = layout.x_stabilizers[x_triggered[0]][0];
|
||||
corrections.push(Gate::Z(data_q));
|
||||
} else if x_triggered.len() >= 2 {
|
||||
// Multiple stabilizers fired: find the data qubit that appears in
|
||||
// the most triggered stabilizers and correct it.
|
||||
if let Some(q) = most_common_data_qubit(&layout.x_stabilizers, &x_triggered) {
|
||||
corrections.push(Gate::Z(q));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Correct X errors using Z-stabilizer syndrome (bits n_x..) ----
|
||||
let z_syndrome = &syndrome[n_x..];
|
||||
let z_triggered: Vec<usize> = z_syndrome
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &s)| s)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if z_triggered.len() == 1 {
|
||||
let data_q = layout.z_stabilizers[z_triggered[0]][0];
|
||||
corrections.push(Gate::X(data_q));
|
||||
} else if z_triggered.len() >= 2 {
|
||||
if let Some(q) = most_common_data_qubit(&layout.z_stabilizers, &z_triggered) {
|
||||
corrections.push(Gate::X(q));
|
||||
}
|
||||
}
|
||||
|
||||
corrections
|
||||
}
|
||||
|
||||
/// Find the data qubit that appears in the most stabilizers among the
|
||||
/// triggered set. Returns `None` if the triggered list is empty.
|
||||
fn most_common_data_qubit(
|
||||
stabilizers: &[Vec<QubitIndex>],
|
||||
triggered_indices: &[usize],
|
||||
) -> Option<QubitIndex> {
|
||||
// Count how many triggered stabilizers each data qubit participates in.
|
||||
let mut counts: std::collections::HashMap<QubitIndex, usize> = std::collections::HashMap::new();
|
||||
for &idx in triggered_indices {
|
||||
for &dq in &stabilizers[idx] {
|
||||
*counts.entry(dq).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
counts
|
||||
.into_iter()
|
||||
.max_by_key(|&(_, count)| count)
|
||||
.map(|(qubit, _)| qubit)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Run a surface code error correction simulation.
|
||||
///
|
||||
/// Currently only **distance 3** is supported. The simulation:
|
||||
/// 1. Initializes all qubits in |0> (the logical |0_L> state).
|
||||
/// 2. For each cycle: injects noise, extracts the syndrome, decodes, and
|
||||
/// applies corrections.
|
||||
/// 3. After all cycles, returns the syndrome history and error statistics.
|
||||
///
|
||||
/// # Logical error detection (simplified)
|
||||
///
|
||||
/// A logical Z error is detected by checking the parity of a representative
|
||||
/// row of data qubits. If the initial logical state was |0_L>, a flipped
|
||||
/// parity indicates a logical error. This is a coarse approximation; a full
|
||||
/// implementation would track the Pauli frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`ruqu_core::error::QuantumError`] on simulator failures.
|
||||
pub fn run_surface_code(config: &SurfaceCodeConfig) -> ruqu_core::error::Result<SurfaceCodeResult> {
|
||||
assert_eq!(
|
||||
config.distance, 3,
|
||||
"Only distance-3 surface codes are currently supported"
|
||||
);
|
||||
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
let total_qubits = layout.total_qubits();
|
||||
|
||||
let mut state = match config.seed {
|
||||
Some(s) => QuantumState::new_with_seed(total_qubits, s)?,
|
||||
None => QuantumState::new(total_qubits)?,
|
||||
};
|
||||
|
||||
// Seeded RNG for noise injection.
|
||||
let mut rng = match config.seed {
|
||||
Some(s) => StdRng::seed_from_u64(s),
|
||||
None => StdRng::from_entropy(),
|
||||
};
|
||||
|
||||
let mut logical_errors = 0u32;
|
||||
let mut syndrome_history = Vec::with_capacity(config.num_cycles as usize);
|
||||
|
||||
// Record the initial parity of the top row (d0, d1, d2) for logical
|
||||
// error detection. For |0_L>, this parity should be even (all |0>).
|
||||
// After each cycle we compare against this baseline.
|
||||
let logical_row: [QubitIndex; 3] = [0, 1, 2];
|
||||
|
||||
for _cycle in 0..config.num_cycles {
|
||||
// 1. Inject noise on data qubits.
|
||||
inject_noise(&mut state, &layout.data_qubits, config.noise_rate, &mut rng)?;
|
||||
|
||||
// 2. Syndrome extraction.
|
||||
let syndrome = run_cycle(&mut state, &layout)?;
|
||||
syndrome_history.push(syndrome.clone());
|
||||
|
||||
// 3. Decode and apply corrections.
|
||||
let corrections = decode_syndrome(&syndrome, &layout);
|
||||
for gate in &corrections {
|
||||
state.apply_gate(gate)?;
|
||||
}
|
||||
|
||||
// 4. Simplified logical error check.
|
||||
// Measure Z-parity of the top-row data qubits non-destructively
|
||||
// by reading expectation values. If <Z_0 Z_1 Z_2> < 0, the
|
||||
// row has odd parity -> logical error.
|
||||
let mut row_parity = 1.0_f64;
|
||||
for &q in &logical_row {
|
||||
let z_exp = state.expectation_value(&ruqu_core::types::PauliString {
|
||||
ops: vec![(q, ruqu_core::types::PauliOp::Z)],
|
||||
});
|
||||
// Each Z expectation is in [-1, 1]. For a computational basis
|
||||
// state, it is exactly +1 (|0>) or -1 (|1>). For superpositions
|
||||
// we approximate: sign of the product captures parity.
|
||||
row_parity *= z_exp;
|
||||
}
|
||||
if row_parity < 0.0 {
|
||||
logical_errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let logical_error_rate = if config.num_cycles > 0 {
|
||||
logical_errors as f64 / config.num_cycles as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Ok(SurfaceCodeResult {
|
||||
logical_errors,
|
||||
total_cycles: config.num_cycles,
|
||||
logical_error_rate,
|
||||
syndrome_history,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_layout_distance_3() {
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
assert_eq!(layout.data_qubits.len(), 9);
|
||||
assert_eq!(layout.x_ancillas.len(), 4);
|
||||
assert_eq!(layout.z_ancillas.len(), 4);
|
||||
assert_eq!(layout.total_qubits(), 17);
|
||||
assert_eq!(layout.num_stabilizers(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_x_stabilizers_cover_all_data() {
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
let mut covered: std::collections::HashSet<QubitIndex> = std::collections::HashSet::new();
|
||||
for stab in &layout.x_stabilizers {
|
||||
for &q in stab {
|
||||
covered.insert(q);
|
||||
}
|
||||
}
|
||||
// All 9 data qubits should be covered by X stabilizers.
|
||||
for q in 0..9u32 {
|
||||
assert!(
|
||||
covered.contains(&q),
|
||||
"data qubit {} not covered by X stabilizers",
|
||||
q
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_z_stabilizers_boundary() {
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
// Z stabilizers are weight-2 boundary stabilizers for d=3.
|
||||
for stab in &layout.z_stabilizers {
|
||||
assert_eq!(stab.len(), 2, "Z stabilizer should have weight 2");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_syndrome_no_error() {
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
let syndrome = vec![false; 8];
|
||||
let corrections = decode_syndrome(&syndrome, &layout);
|
||||
assert!(
|
||||
corrections.is_empty(),
|
||||
"no corrections when syndrome is trivial"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_syndrome_single_x_stabilizer() {
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
// Only X0 fires -> correct data qubit 0 with Z.
|
||||
let mut syndrome = vec![false; 8];
|
||||
syndrome[0] = true;
|
||||
let corrections = decode_syndrome(&syndrome, &layout);
|
||||
assert_eq!(corrections.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_syndrome_single_z_stabilizer() {
|
||||
let layout = SurfaceCodeLayout::distance_3();
|
||||
// Only Z0 fires (index 4 in syndrome vector).
|
||||
let mut syndrome = vec![false; 8];
|
||||
syndrome[4] = true;
|
||||
let corrections = decode_syndrome(&syndrome, &layout);
|
||||
assert_eq!(corrections.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_most_common_data_qubit() {
|
||||
let stabilizers = vec![vec![0, 1, 3, 4], vec![1, 2, 4, 5]];
|
||||
// Both stabilizers 0 and 1 triggered: qubit 1 and 4 appear in both.
|
||||
let result = most_common_data_qubit(&stabilizers, &[0, 1]);
|
||||
assert!(result == Some(1) || result == Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Only distance-3")]
|
||||
fn test_unsupported_distance() {
|
||||
let config = SurfaceCodeConfig {
|
||||
distance: 5,
|
||||
num_cycles: 1,
|
||||
noise_rate: 0.01,
|
||||
seed: Some(42),
|
||||
};
|
||||
let _ = run_surface_code(&config);
|
||||
}
|
||||
}
|
||||
329
vendor/ruvector/crates/ruqu-algorithms/src/vqe.rs
vendored
Normal file
329
vendor/ruvector/crates/ruqu-algorithms/src/vqe.rs
vendored
Normal file
@@ -0,0 +1,329 @@
|
||||
//! Variational Quantum Eigensolver (VQE)
|
||||
//!
|
||||
//! Finds the ground-state energy of a Hamiltonian using a classical-quantum
|
||||
//! hybrid optimization loop:
|
||||
//!
|
||||
//! 1. A parameterized **ansatz** circuit prepares a trial state on the quantum
|
||||
//! processor (or simulator).
|
||||
//! 2. The **expectation value** of the Hamiltonian is measured for that state.
|
||||
//! 3. A **classical optimizer** (gradient descent with parameter-shift rule)
|
||||
//! updates the circuit parameters to minimize the energy.
|
||||
//! 4. Steps 1-3 repeat until convergence or the iteration budget is exhausted.
|
||||
//!
|
||||
//! The ansatz used here is "hardware-efficient": each layer applies Ry and Rz
|
||||
//! rotations on every qubit, followed by a linear CNOT entangling chain.
|
||||
|
||||
use ruqu_core::circuit::QuantumCircuit;
|
||||
use ruqu_core::simulator::{SimConfig, Simulator};
|
||||
use ruqu_core::types::{Hamiltonian, PauliOp, PauliString};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration and result types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Configuration for a VQE run.
|
||||
pub struct VqeConfig {
|
||||
/// The Hamiltonian whose ground-state energy we seek.
|
||||
pub hamiltonian: Hamiltonian,
|
||||
/// Number of qubits in the ansatz circuit.
|
||||
pub num_qubits: u32,
|
||||
/// Number of ansatz layers (depth). Each layer contributes
|
||||
/// `2 * num_qubits` parameters (Ry + Rz per qubit).
|
||||
pub ansatz_depth: u32,
|
||||
/// Maximum number of classical optimizer iterations.
|
||||
pub max_iterations: u32,
|
||||
/// Stop early when the absolute energy change between successive
|
||||
/// iterations falls below this threshold.
|
||||
pub convergence_threshold: f64,
|
||||
/// Step size for gradient descent.
|
||||
pub learning_rate: f64,
|
||||
/// Optional RNG seed for reproducible simulation.
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
/// Result returned by [`run_vqe`].
|
||||
pub struct VqeResult {
|
||||
/// Lowest energy found during the optimization.
|
||||
pub optimal_energy: f64,
|
||||
/// Parameter vector that produced `optimal_energy`.
|
||||
pub optimal_parameters: Vec<f64>,
|
||||
/// Energy at each iteration (length = `num_iterations`).
|
||||
pub energy_history: Vec<f64>,
|
||||
/// Total number of iterations executed.
|
||||
pub num_iterations: u32,
|
||||
/// Whether the optimizer converged before exhausting `max_iterations`.
|
||||
pub converged: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ansatz construction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Return the total number of variational parameters for the given ansatz
|
||||
/// dimensions. Each layer uses `2 * num_qubits` parameters (one Ry and one
|
||||
/// Rz rotation per qubit).
|
||||
pub fn num_parameters(num_qubits: u32, depth: u32) -> usize {
|
||||
(2 * num_qubits as usize) * (depth as usize)
|
||||
}
|
||||
|
||||
/// Build a hardware-efficient ansatz circuit.
|
||||
///
|
||||
/// Each layer consists of:
|
||||
/// 1. **Rotation sub-layer**: Ry(theta) on every qubit.
|
||||
/// 2. **Rotation sub-layer**: Rz(theta) on every qubit.
|
||||
/// 3. **Entangling sub-layer**: Linear CNOT chain (0->1, 1->2, ..., n-2->n-1).
|
||||
///
|
||||
/// `params` must have exactly [`num_parameters`]`(num_qubits, depth)` entries.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `params.len()` does not equal the expected parameter count.
|
||||
pub fn build_ansatz(num_qubits: u32, depth: u32, params: &[f64]) -> QuantumCircuit {
|
||||
let expected = num_parameters(num_qubits, depth);
|
||||
assert_eq!(
|
||||
params.len(),
|
||||
expected,
|
||||
"build_ansatz: expected {} parameters, got {}",
|
||||
expected,
|
||||
params.len()
|
||||
);
|
||||
|
||||
let mut circuit = QuantumCircuit::new(num_qubits);
|
||||
let mut idx = 0;
|
||||
|
||||
for _layer in 0..depth {
|
||||
// Ry rotations
|
||||
for q in 0..num_qubits {
|
||||
circuit.ry(q, params[idx]);
|
||||
idx += 1;
|
||||
}
|
||||
// Rz rotations
|
||||
for q in 0..num_qubits {
|
||||
circuit.rz(q, params[idx]);
|
||||
idx += 1;
|
||||
}
|
||||
// Linear CNOT entangling chain
|
||||
for q in 0..num_qubits.saturating_sub(1) {
|
||||
circuit.cnot(q, q + 1);
|
||||
}
|
||||
}
|
||||
|
||||
circuit
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Energy evaluation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Evaluate the expectation value of the Hamiltonian for a given set of
|
||||
/// ansatz parameters.
|
||||
///
|
||||
/// Builds the ansatz, simulates it, and returns `<psi|H|psi>`.
|
||||
pub fn evaluate_energy(config: &VqeConfig, params: &[f64]) -> ruqu_core::error::Result<f64> {
|
||||
let circuit = build_ansatz(config.num_qubits, config.ansatz_depth, params);
|
||||
let sim_config = SimConfig {
|
||||
seed: config.seed,
|
||||
noise: None,
|
||||
shots: None,
|
||||
};
|
||||
let result = Simulator::run_with_config(&circuit, &sim_config)?;
|
||||
Ok(result.state.expectation_hamiltonian(&config.hamiltonian))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VQE optimizer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Run the VQE optimization loop.
|
||||
///
|
||||
/// Uses gradient descent with the **parameter-shift rule** to compute
|
||||
/// analytical gradients. For each parameter theta_i the gradient is:
|
||||
///
|
||||
/// ```text
|
||||
/// dE/d(theta_i) = [ E(theta_i + pi/2) - E(theta_i - pi/2) ] / 2
|
||||
/// ```
|
||||
///
|
||||
/// This requires 2 circuit evaluations per parameter per iteration, so the
|
||||
/// total cost is `O(max_iterations * 2 * num_parameters)` circuit runs.
|
||||
pub fn run_vqe(config: &VqeConfig) -> ruqu_core::error::Result<VqeResult> {
|
||||
let n_params = num_parameters(config.num_qubits, config.ansatz_depth);
|
||||
|
||||
// Initialize parameters with small values to break symmetry.
|
||||
let mut params = vec![0.1_f64; n_params];
|
||||
|
||||
let mut energy_history: Vec<f64> = Vec::with_capacity(config.max_iterations as usize);
|
||||
let mut converged = false;
|
||||
|
||||
let mut best_energy = f64::MAX;
|
||||
let mut best_params = params.clone();
|
||||
|
||||
for iteration in 0..config.max_iterations {
|
||||
// ------------------------------------------------------------------
|
||||
// Forward pass: compute current energy
|
||||
// ------------------------------------------------------------------
|
||||
let energy = evaluate_energy(config, ¶ms)?;
|
||||
energy_history.push(energy);
|
||||
|
||||
if energy < best_energy {
|
||||
best_energy = energy;
|
||||
best_params = params.clone();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Convergence check (skip first iteration since we need a delta)
|
||||
// ------------------------------------------------------------------
|
||||
if iteration > 0 {
|
||||
let prev = energy_history[iteration as usize - 1];
|
||||
if (prev - energy).abs() < config.convergence_threshold {
|
||||
converged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Backward pass: compute gradient via parameter-shift rule
|
||||
// ------------------------------------------------------------------
|
||||
let shift = std::f64::consts::FRAC_PI_2;
|
||||
let mut gradient = vec![0.0_f64; n_params];
|
||||
|
||||
for i in 0..n_params {
|
||||
let mut params_plus = params.clone();
|
||||
let mut params_minus = params.clone();
|
||||
params_plus[i] += shift;
|
||||
params_minus[i] -= shift;
|
||||
|
||||
let e_plus = evaluate_energy(config, ¶ms_plus)?;
|
||||
let e_minus = evaluate_energy(config, ¶ms_minus)?;
|
||||
gradient[i] = (e_plus - e_minus) / 2.0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Parameter update (gradient descent -- minimize energy)
|
||||
// ------------------------------------------------------------------
|
||||
for i in 0..n_params {
|
||||
params[i] -= config.learning_rate * gradient[i];
|
||||
}
|
||||
}
|
||||
|
||||
let num_iterations = energy_history.len() as u32;
|
||||
Ok(VqeResult {
|
||||
optimal_energy: best_energy,
|
||||
optimal_parameters: best_params,
|
||||
energy_history,
|
||||
num_iterations,
|
||||
converged,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hamiltonian helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create an approximate H2 (molecular hydrogen) Hamiltonian in the STO-3G
|
||||
/// basis mapped to 2 qubits via the Bravyi-Kitaev transformation.
|
||||
///
|
||||
/// ```text
|
||||
/// H = -1.0523 II + 0.3979 IZ + -0.3979 ZI + -0.0112 ZZ + 0.1809 XX
|
||||
/// ```
|
||||
///
|
||||
/// The exact ground-state energy of this Hamiltonian is approximately -1.137
|
||||
/// Hartree (at equilibrium bond length ~0.735 angstrom).
|
||||
pub fn h2_hamiltonian() -> Hamiltonian {
|
||||
Hamiltonian {
|
||||
terms: vec![
|
||||
// Identity term (constant offset)
|
||||
(-1.0523, PauliString { ops: vec![] }),
|
||||
// IZ: Pauli-Z on qubit 1
|
||||
(
|
||||
0.3979,
|
||||
PauliString {
|
||||
ops: vec![(1, PauliOp::Z)],
|
||||
},
|
||||
),
|
||||
// ZI: Pauli-Z on qubit 0
|
||||
(
|
||||
-0.3979,
|
||||
PauliString {
|
||||
ops: vec![(0, PauliOp::Z)],
|
||||
},
|
||||
),
|
||||
// ZZ: Pauli-Z on both qubits
|
||||
(
|
||||
-0.0112,
|
||||
PauliString {
|
||||
ops: vec![(0, PauliOp::Z), (1, PauliOp::Z)],
|
||||
},
|
||||
),
|
||||
// XX: Pauli-X on both qubits
|
||||
(
|
||||
0.1809,
|
||||
PauliString {
|
||||
ops: vec![(0, PauliOp::X), (1, PauliOp::X)],
|
||||
},
|
||||
),
|
||||
],
|
||||
num_qubits: 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple single-qubit Z Hamiltonian: `H = -1.0 Z`.
|
||||
///
|
||||
/// The ground state is |0> with energy -1.0. Useful for smoke-testing VQE.
|
||||
pub fn single_z_hamiltonian() -> Hamiltonian {
|
||||
Hamiltonian {
|
||||
terms: vec![(
|
||||
-1.0,
|
||||
PauliString {
|
||||
ops: vec![(0, PauliOp::Z)],
|
||||
},
|
||||
)],
|
||||
num_qubits: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_num_parameters() {
|
||||
assert_eq!(num_parameters(2, 1), 4);
|
||||
assert_eq!(num_parameters(4, 3), 24);
|
||||
assert_eq!(num_parameters(1, 5), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_ansatz_gate_count() {
|
||||
let n = 3;
|
||||
let depth = 2;
|
||||
let params = vec![0.0; num_parameters(n, depth)];
|
||||
let circuit = build_ansatz(n, depth, ¶ms);
|
||||
assert_eq!(circuit.num_qubits(), n);
|
||||
// Each layer: 3 Ry + 3 Rz + 2 CNOT = 8 gates, times 2 layers = 16
|
||||
assert_eq!(circuit.gates().len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "expected 4 parameters")]
|
||||
fn test_build_ansatz_wrong_param_count() {
|
||||
build_ansatz(2, 1, &[0.0; 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_h2_hamiltonian_structure() {
|
||||
let h = h2_hamiltonian();
|
||||
assert_eq!(h.num_qubits, 2);
|
||||
assert_eq!(h.terms.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_z_hamiltonian() {
|
||||
let h = single_z_hamiltonian();
|
||||
assert_eq!(h.num_qubits, 1);
|
||||
assert_eq!(h.terms.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user