use crate::error::{QuantumError, QuantumResult};
use crate::handle::LcgRng;
use crate::pauli::expval::expectation_value;
use crate::pauli::hamiltonian::Hamiltonian;
use crate::statevec::state::StateVector;
use crate::vqe::ansatz::HardwareEfficientAnsatz;
#[derive(Debug, Clone)]
pub struct SpsaConfig {
pub max_iter: usize,
pub a: f32,
pub c: f32,
pub alpha: f32,
pub gamma: f32,
pub a_offset: usize,
pub n_warmup: usize,
pub tol: f32,
pub patience: usize,
}
impl Default for SpsaConfig {
fn default() -> Self {
Self {
max_iter: 200,
a: 0.1,
c: 0.1,
alpha: 0.602,
gamma: 0.101,
a_offset: 10,
n_warmup: 0,
tol: 1e-5,
patience: 10,
}
}
}
#[derive(Debug, Clone)]
pub struct SpsaResult {
pub final_energy: f32,
pub final_params: Vec<f32>,
pub energy_history: Vec<f32>,
pub n_iter: usize,
pub converged: bool,
}
#[derive(Debug, Clone)]
pub struct SpsaVqeOptimizer {
pub ansatz: HardwareEfficientAnsatz,
pub ham: Hamiltonian,
pub params: Vec<f32>,
pub cfg: SpsaConfig,
}
impl SpsaVqeOptimizer {
pub fn new(
ansatz: HardwareEfficientAnsatz,
ham: Hamiltonian,
cfg: SpsaConfig,
rng: &mut LcgRng,
) -> Self {
let n = ansatz.n_params();
let params = (0..n).map(|_| rng.next_normal() * 0.1).collect();
Self {
ansatz,
ham,
params,
cfg,
}
}
pub fn energy(&self, params: &[f32]) -> QuantumResult<f32> {
let circ = self.ansatz.build_circuit(params)?;
let mut rng = LcgRng::new(0);
let sv = circ.exec_on_state(
&StateVector::new_zero_state(self.ansatz.n_qubits)?,
&mut rng,
)?;
expectation_value(&sv, &self.ham)
}
#[must_use]
pub fn bernoulli_perturbation(n: usize, rng: &mut LcgRng) -> Vec<f32> {
(0..n)
.map(|_| {
if rng.next_u32() & 1 == 0 {
-1.0_f32
} else {
1.0_f32
}
})
.collect()
}
#[must_use]
pub fn step_size(&self, k: usize) -> f32 {
let denom = (k + 1 + self.cfg.a_offset) as f32;
self.cfg.a / denom.powf(self.cfg.alpha)
}
#[must_use]
pub fn perturbation_size(&self, k: usize) -> f32 {
let denom = (k + 1) as f32;
self.cfg.c / denom.powf(self.cfg.gamma)
}
pub fn spsa_gradient(
&self,
params: &[f32],
c_k: f32,
rng: &mut LcgRng,
) -> QuantumResult<Vec<f32>> {
let n = params.len();
let delta = Self::bernoulli_perturbation(n, rng);
let mut p_plus = params.to_vec();
let mut p_minus = params.to_vec();
for i in 0..n {
p_plus[i] += c_k * delta[i];
p_minus[i] -= c_k * delta[i];
}
let f_plus = self.energy(&p_plus)?;
let f_minus = self.energy(&p_minus)?;
let diff = f_plus - f_minus;
let two_ck = 2.0 * c_k;
let grad = delta.iter().map(|&d_i| diff / (two_ck * d_i)).collect();
Ok(grad)
}
pub fn optimize(&mut self, rng: &mut LcgRng) -> QuantumResult<SpsaResult> {
let max_iter = self.cfg.max_iter;
let patience = self.cfg.patience;
let tol = self.cfg.tol;
let n_warmup = self.cfg.n_warmup;
let mut params = self.params.clone();
let mut energy_history = Vec::with_capacity(max_iter);
let mut n_iter = 0_usize;
let mut converged = false;
for k in 0..max_iter {
let a_k = self.step_size(k);
let c_k = self.perturbation_size(k);
let grad = self.spsa_gradient(¶ms, c_k, rng)?;
if k >= n_warmup {
for (p, g) in params.iter_mut().zip(grad.iter()) {
*p -= a_k * g;
}
}
let e = self.energy(¶ms)?;
if !e.is_finite() {
return Err(QuantumError::OptimizationDiverged { iter: k });
}
energy_history.push(e);
n_iter = k + 1;
if patience > 0 && energy_history.len() >= patience {
let window = &energy_history[energy_history.len() - patience..];
let e_max = window.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let e_min = window.iter().cloned().fold(f32::INFINITY, f32::min);
if e_max - e_min < tol {
converged = true;
break;
}
}
}
let final_energy = energy_history.last().copied().unwrap_or(f32::NAN);
self.params = params.clone();
Ok(SpsaResult {
final_energy,
final_params: params,
energy_history,
n_iter,
converged,
})
}
pub fn hessian_diagonal_estimate(
&self,
params: &[f32],
c_k: f32,
rng: &mut LcgRng,
) -> QuantumResult<Vec<f32>> {
let n = params.len();
let delta1 = Self::bernoulli_perturbation(n, rng);
let delta2 = Self::bernoulli_perturbation(n, rng);
let mut pp = params.to_vec();
let mut pm = params.to_vec();
let mut mp = params.to_vec();
let mut mm = params.to_vec();
for i in 0..n {
pp[i] += c_k * delta1[i] + c_k * delta2[i];
pm[i] += c_k * delta1[i] - c_k * delta2[i];
mp[i] += -c_k * delta1[i] + c_k * delta2[i];
mm[i] += -c_k * delta1[i] - c_k * delta2[i];
}
let f_pp = self.energy(&pp)?;
let f_pm = self.energy(&pm)?;
let f_mp = self.energy(&mp)?;
let f_mm = self.energy(&mm)?;
let numerator = f_pp - f_pm - f_mp + f_mm;
let four_c2 = 4.0 * c_k * c_k;
let hdiag = delta1
.iter()
.zip(delta2.iter())
.map(|(&d1, &d2)| numerator / (four_c2 * d1 * d2))
.collect();
Ok(hdiag)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pauli::pauli_string::PauliOp;
fn make_optimizer(n_qubits: usize, depth: usize, seed: u64) -> (SpsaVqeOptimizer, LcgRng) {
let ans = HardwareEfficientAnsatz::new(n_qubits, depth);
let mut ham = Hamiltonian::new();
ham.add_term(1.0, vec![PauliOp::Z, PauliOp::Z]);
let cfg = SpsaConfig::default();
let mut rng = LcgRng::new(seed);
let opt = SpsaVqeOptimizer::new(ans, ham, cfg, &mut rng);
(opt, rng)
}
#[test]
fn spsa_energy_is_finite() {
let (opt, _) = make_optimizer(2, 1, 1);
let e = opt.energy(&opt.params.clone()).unwrap();
assert!(e.is_finite(), "energy={e}");
}
#[test]
fn bernoulli_perturbation_length() {
let mut rng = LcgRng::new(2);
let v = SpsaVqeOptimizer::bernoulli_perturbation(7, &mut rng);
assert_eq!(v.len(), 7);
}
#[test]
fn bernoulli_perturbation_values_pm1() {
let mut rng = LcgRng::new(3);
let v = SpsaVqeOptimizer::bernoulli_perturbation(200, &mut rng);
for &x in &v {
assert!(x == 1.0 || x == -1.0, "unexpected value {x}");
}
}
#[test]
fn step_size_decreasing() {
let (opt, _) = make_optimizer(2, 1, 4);
for k in 0..10 {
assert!(
opt.step_size(k + 1) < opt.step_size(k),
"step_size not decreasing at k={k}"
);
}
}
#[test]
fn perturbation_size_decreasing() {
let (opt, _) = make_optimizer(2, 1, 5);
for k in 0..10 {
assert!(
opt.perturbation_size(k + 1) < opt.perturbation_size(k),
"perturbation_size not decreasing at k={k}"
);
}
}
#[test]
fn step_size_positive() {
let (opt, _) = make_optimizer(2, 1, 6);
for k in 0..50 {
assert!(opt.step_size(k) > 0.0, "step_size nonpositive at k={k}");
}
}
#[test]
fn perturbation_size_positive() {
let (opt, _) = make_optimizer(2, 1, 7);
for k in 0..50 {
assert!(
opt.perturbation_size(k) > 0.0,
"perturbation_size nonpositive at k={k}"
);
}
}
#[test]
fn spsa_gradient_length() {
let (opt, mut rng) = make_optimizer(2, 1, 8);
let params = opt.params.clone();
let n = params.len();
let grad = opt.spsa_gradient(¶ms, 0.1, &mut rng).unwrap();
assert_eq!(grad.len(), n);
}
#[test]
fn spsa_gradient_finite() {
let (opt, mut rng) = make_optimizer(2, 1, 9);
let params = opt.params.clone();
let grad = opt.spsa_gradient(¶ms, 0.1, &mut rng).unwrap();
for (i, &g) in grad.iter().enumerate() {
assert!(g.is_finite(), "gradient[{i}] is not finite: {g}");
}
}
#[test]
fn spsa_optimize_returns_finite_energy() {
let (mut opt, mut rng) = make_optimizer(2, 1, 10);
opt.cfg = SpsaConfig {
max_iter: 5,
..SpsaConfig::default()
};
let result = opt.optimize(&mut rng).unwrap();
assert!(
result.final_energy.is_finite(),
"final_energy={}",
result.final_energy
);
}
#[test]
fn spsa_optimize_energy_history_length() {
let (mut opt, mut rng) = make_optimizer(2, 1, 11);
let max_iter = 8;
opt.cfg.max_iter = max_iter;
opt.cfg.tol = 0.0;
opt.cfg.patience = 0;
let result = opt.optimize(&mut rng).unwrap();
assert_eq!(
result.energy_history.len(),
result.n_iter,
"history length mismatch"
);
}
#[test]
fn spsa_optimize_uses_max_iter() {
let (mut opt, mut rng) = make_optimizer(2, 1, 12);
let max_iter = 15;
opt.cfg.max_iter = max_iter;
opt.cfg.tol = 0.0;
opt.cfg.patience = 0;
let result = opt.optimize(&mut rng).unwrap();
assert_eq!(result.n_iter, max_iter, "should use all iterations");
}
#[test]
fn spsa_optimize_reduces_energy() {
let ans = HardwareEfficientAnsatz::new(2, 1);
let mut ham = Hamiltonian::new();
ham.add_term(-1.0, vec![PauliOp::Z, PauliOp::I]);
ham.add_term(-1.0, vec![PauliOp::I, PauliOp::Z]);
let cfg = SpsaConfig {
max_iter: 50,
a: 0.2,
c: 0.15,
tol: 0.0,
patience: 0,
..SpsaConfig::default()
};
let mut rng = LcgRng::new(13);
let mut opt = SpsaVqeOptimizer::new(ans, ham, cfg, &mut rng);
let e_init = opt.energy(&opt.params.clone()).unwrap();
let result = opt.optimize(&mut rng).unwrap();
assert!(
result.final_energy <= e_init + 0.5,
"energy did not improve: init={e_init} final={}",
result.final_energy
);
}
#[test]
fn spsa_converged_flag() {
let (mut opt, mut rng) = make_optimizer(2, 1, 14);
opt.cfg.max_iter = 200;
opt.cfg.tol = 100.0;
opt.cfg.patience = 5;
let result = opt.optimize(&mut rng).unwrap();
assert!(
result.converged,
"expected converged=true with very large tol"
);
}
#[test]
fn hessian_diagonal_estimate_length() {
let (opt, mut rng) = make_optimizer(2, 1, 15);
let params = opt.params.clone();
let n = params.len();
let hdiag = opt
.hessian_diagonal_estimate(¶ms, 0.1, &mut rng)
.unwrap();
assert_eq!(hdiag.len(), n);
}
#[test]
fn hessian_diagonal_estimate_finite() {
let (opt, mut rng) = make_optimizer(2, 1, 16);
let params = opt.params.clone();
let hdiag = opt
.hessian_diagonal_estimate(¶ms, 0.1, &mut rng)
.unwrap();
for (i, &h) in hdiag.iter().enumerate() {
assert!(h.is_finite(), "hessian_diag[{i}] is not finite: {h}");
}
}
#[test]
fn new_initializes_params() {
let ans = HardwareEfficientAnsatz::new(3, 2);
let n_expected = ans.n_params();
let mut ham = Hamiltonian::new();
ham.add_term(1.0, vec![PauliOp::Z, PauliOp::I, PauliOp::Z]);
let mut rng = LcgRng::new(17);
let opt = SpsaVqeOptimizer::new(ans, ham, SpsaConfig::default(), &mut rng);
assert_eq!(opt.params.len(), n_expected);
}
#[test]
fn spsa_vs_gradient_descent_same_landscape() {
let make_ham = || {
let mut ham = Hamiltonian::new();
ham.add_term(1.0, vec![PauliOp::Z, PauliOp::Z]);
ham
};
let ans1 = HardwareEfficientAnsatz::new(2, 1);
let cfg = SpsaConfig {
max_iter: 30,
a: 0.3,
c: 0.2,
tol: 0.0,
patience: 0,
..SpsaConfig::default()
};
let mut rng1 = LcgRng::new(18);
let mut spsa_opt = SpsaVqeOptimizer::new(ans1, make_ham(), cfg, &mut rng1);
let spsa_result = spsa_opt.optimize(&mut rng1).unwrap();
let ans2 = HardwareEfficientAnsatz::new(2, 1);
let mut rng2 = LcgRng::new(18);
let mut vqe_opt = crate::vqe::vqe::VqeOptimizer::new(ans2, make_ham(), &mut rng2);
let (gd_energy, _) = vqe_opt.optimize(30, 0.1).unwrap();
assert!(
spsa_result.final_energy.is_finite() && gd_energy.is_finite(),
"both methods should give finite energies; spsa={} gd={}",
spsa_result.final_energy,
gd_energy
);
}
}