Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View 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);
}
}

View 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};

View 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);
}
}

View 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);
}
}

View 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, &params)?;
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, &params_plus)?;
let e_minus = evaluate_energy(config, &params_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, &params);
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);
}
}