use ruqu_core::circuit::QuantumCircuit;
use ruqu_core::simulator::{SimConfig, Simulator};
use ruqu_core::types::{PauliOp, PauliString};
#[derive(Debug, Clone)]
pub struct Graph {
pub num_nodes: u32,
pub edges: Vec<(u32, u32, f64)>,
}
impl Graph {
pub fn new(num_nodes: u32) -> Self {
Self {
num_nodes,
edges: Vec::new(),
}
}
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));
}
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,
}
}
pub fn num_edges(&self) -> usize {
self.edges.len()
}
}
pub struct QaoaConfig {
pub graph: Graph,
pub p: u32,
pub max_iterations: u32,
pub learning_rate: f64,
pub seed: Option<u64>,
}
pub struct QaoaResult {
pub best_cut_value: f64,
pub best_bitstring: Vec<bool>,
pub optimal_gammas: Vec<f64>,
pub optimal_betas: Vec<f64>,
pub energy_history: Vec<f64>,
pub converged: bool,
}
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);
for q in 0..n {
circuit.h(q);
}
for layer in 0..p {
for &(i, j, w) in &graph.edges {
circuit.rzz(i, j, 2.0 * gammas[layer] * w);
}
for q in 0..n {
circuit.rx(q, 2.0 * betas[layer]);
}
}
circuit
}
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()
}
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)
}
pub fn run_qaoa(config: &QaoaConfig) -> ruqu_core::error::Result<QaoaResult> {
let p = config.p as usize;
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 {
let cost = evaluate_qaoa_cost(&config.graph, &gammas, &betas, config.seed)?;
energy_history.push(cost);
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();
}
if iter > 0 {
let prev = energy_history[iter as usize - 1];
if (cost - prev).abs() < 1e-6 {
converged = true;
break;
}
}
let shift = std::f64::consts::FRAC_PI_2;
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;
}
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,
})
}
pub fn triangle_graph() -> Graph {
Graph::unweighted(3, vec![(0, 1), (1, 2), (0, 2)])
}
pub fn ring4_graph() -> Graph {
Graph::unweighted(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)])
}
#[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();
assert_eq!(cut_value(&g, &[true, false, false]), 2.0);
assert_eq!(cut_value(&g, &[false, false, false]), 0.0);
}
#[test]
fn test_cut_value_ring4() {
let g = ring4_graph();
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);
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);
assert_eq!(cut_value(&g, &[true, false, true]), 5.0);
}
}