use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
use scirs2_core::numeric::Float;
use scirs2_core::random::Random;
use std::fmt::Debug;
use crate::error::{OptimError, Result};
use crate::optimizers::Optimizer;
use super::{
DEFAULT_FINAL_TEMP, DEFAULT_INITIAL_TEMP, DEFAULT_NUM_ITERATIONS, DEFAULT_SEED,
DEFAULT_TUNNELING_STRENGTH,
};
#[derive(Debug)]
pub struct QuantumAnnealing<A: Float + ScalarOperand + Debug> {
learning_rate: A,
current_temperature: A,
initial_temperature: A,
final_temperature: A,
num_iterations: usize,
current_step: usize,
tunneling_strength: A,
best_params: Option<Vec<A>>,
best_shape: Option<Vec<usize>>,
best_energy: A,
seed: u64,
rng: Random<scirs2_core::random::rngs::StdRng>,
}
impl<A> QuantumAnnealing<A>
where
A: Float + ScalarOperand + Debug + Send + Sync,
{
pub fn new(learning_rate: A) -> Self {
let initial = A::from(DEFAULT_INITIAL_TEMP).unwrap_or_else(A::one);
let final_t = A::from(DEFAULT_FINAL_TEMP).unwrap_or_else(|| A::epsilon());
let tunneling = A::from(DEFAULT_TUNNELING_STRENGTH).unwrap_or_else(A::zero);
Self {
learning_rate,
current_temperature: initial,
initial_temperature: initial,
final_temperature: final_t,
num_iterations: DEFAULT_NUM_ITERATIONS,
current_step: 0,
tunneling_strength: tunneling,
best_params: None,
best_shape: None,
best_energy: A::infinity(),
seed: DEFAULT_SEED,
rng: Random::seed(DEFAULT_SEED),
}
}
pub fn with_temperature_schedule(mut self, initial: A, final_t: A) -> Self {
self.initial_temperature = initial;
self.final_temperature = final_t;
self.current_temperature = initial;
self
}
pub fn with_tunneling(mut self, strength: A) -> Self {
self.tunneling_strength = strength;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self.rng = Random::seed(seed);
self
}
pub fn with_iterations(mut self, num_iterations: usize) -> Self {
self.num_iterations = num_iterations.max(1);
self
}
pub fn current_temperature(&self) -> A {
self.current_temperature
}
pub fn best_energy(&self) -> A {
self.best_energy
}
pub fn current_step(&self) -> usize {
self.current_step
}
pub fn initial_temperature(&self) -> A {
self.initial_temperature
}
pub fn final_temperature(&self) -> A {
self.final_temperature
}
pub fn tunneling_strength(&self) -> A {
self.tunneling_strength
}
pub fn num_iterations(&self) -> usize {
self.num_iterations
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn learning_rate(&self) -> A {
self.learning_rate
}
pub fn set_lr(&mut self, learning_rate: A) {
self.learning_rate = learning_rate;
}
pub fn best_params<D: Dimension>(&self) -> Option<Array<A, D>> {
let buf = self.best_params.as_ref()?;
let shape = self.best_shape.as_ref()?;
let arr = Array::from_shape_vec(scirs2_core::ndarray::IxDyn(shape), buf.clone()).ok()?;
arr.into_dimensionality::<D>().ok()
}
pub fn reset(&mut self) {
self.current_step = 0;
self.current_temperature = self.initial_temperature;
self.best_params = None;
self.best_shape = None;
self.best_energy = A::infinity();
self.rng = Random::seed(self.seed);
}
fn temperature_at(&self, step: usize) -> A {
if self.num_iterations == 0 {
return self.final_temperature;
}
let n = self.num_iterations;
let step_clamped = step.min(n);
let frac =
A::from(step_clamped).unwrap_or_else(A::zero) / A::from(n).unwrap_or_else(A::one);
if self.initial_temperature <= A::zero() || self.final_temperature <= A::zero() {
let span = self.initial_temperature - self.final_temperature;
return self.initial_temperature - span * frac;
}
let ratio = self.final_temperature / self.initial_temperature;
self.initial_temperature * ratio.powf(frac)
}
fn sample_perturbation(
&mut self,
shape: &[usize],
) -> Result<Array<A, scirs2_core::ndarray::IxDyn>> {
let scale = self.learning_rate * self.current_temperature;
let total: usize = shape.iter().product();
let mut buf: Vec<A> = Vec::with_capacity(total);
for _ in 0..total {
let u: f64 = self.rng.gen_range(-1.0..1.0);
let val = A::from(u).unwrap_or_else(A::zero) * scale;
buf.push(val);
}
Array::from_shape_vec(scirs2_core::ndarray::IxDyn(shape), buf).map_err(|err| {
OptimError::ComputationError(format!("Failed to build perturbation array: {err}"))
})
}
fn accept(&mut self, delta_e: A, perturbation_sq_norm: A) -> bool {
if delta_e <= A::zero() {
return true;
}
if self.current_temperature <= A::zero() {
return false;
}
let tunneling_kernel = (-perturbation_sq_norm).exp();
let exponent =
-delta_e / self.current_temperature + self.tunneling_strength * tunneling_kernel;
if exponent >= A::zero() {
return true;
}
let safety = A::from(-50.0).unwrap_or_else(|| -A::one());
let exponent_safe = if exponent < safety { safety } else { exponent };
let p = exponent_safe.exp();
let p_f64 = p.to_f64().unwrap_or(0.0);
let u: f64 = self.rng.gen_range(0.0..1.0);
u < p_f64
}
fn advance_temperature(&mut self) {
self.current_step = self.current_step.saturating_add(1);
self.current_temperature = self.temperature_at(self.current_step);
}
fn proxy_energy(
gradients: &Array<A, scirs2_core::ndarray::IxDyn>,
candidate_offset: &Array<A, scirs2_core::ndarray::IxDyn>,
) -> A {
gradients
.iter()
.zip(candidate_offset.iter())
.fold(A::zero(), |acc, (g, p)| acc + (*g) * (*p))
}
fn sq_norm(arr: &Array<A, scirs2_core::ndarray::IxDyn>) -> A {
arr.iter().fold(A::zero(), |acc, x| acc + (*x) * (*x))
}
fn track_best(&mut self, candidate: &Array<A, scirs2_core::ndarray::IxDyn>, energy: A) {
if energy < self.best_energy {
self.best_energy = energy;
self.best_params = Some(candidate.iter().copied().collect());
self.best_shape = Some(candidate.shape().to_vec());
}
}
}
impl<A, D> Optimizer<A, D> for QuantumAnnealing<A>
where
A: Float + ScalarOperand + Debug + Send + Sync,
D: Dimension,
{
fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
if params.shape() != gradients.shape() {
return Err(OptimError::DimensionMismatch(format!(
"Quantum annealing: parameters have shape {:?}, gradients have shape {:?}",
params.shape(),
gradients.shape()
)));
}
let params_dyn = params.to_owned().into_dyn();
let gradients_dyn = gradients.to_owned().into_dyn();
let shape: Vec<usize> = params_dyn.shape().to_vec();
if self.best_params.is_none() {
self.best_energy = A::zero();
self.best_params = Some(params_dyn.iter().copied().collect());
self.best_shape = Some(shape.clone());
}
let perturbation = self.sample_perturbation(&shape)?;
let delta_e = -Self::proxy_energy(&gradients_dyn, &perturbation);
let sq_norm = Self::sq_norm(&perturbation);
let updated_dyn = if self.accept(delta_e, sq_norm) {
¶ms_dyn - &perturbation
} else {
params_dyn.clone()
};
let accepted_offset = &updated_dyn - ¶ms_dyn;
let accepted_energy = -Self::proxy_energy(&gradients_dyn, &accepted_offset);
self.track_best(&updated_dyn, accepted_energy);
self.advance_temperature();
updated_dyn.into_dimensionality::<D>().map_err(|err| {
OptimError::ComputationError(format!(
"Quantum annealing: failed to restore dimension: {err}"
))
})
}
fn get_learning_rate(&self) -> A {
self.learning_rate
}
fn set_learning_rate(&mut self, learning_rate: A) {
self.learning_rate = learning_rate;
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::Array1;
fn quadratic_grad(params: &Array1<f64>) -> Array1<f64> {
params.mapv(|x| 2.0 * x)
}
#[test]
fn test_default_config() {
let optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1);
assert_abs_diff_eq!(optimizer.learning_rate(), 0.1);
assert_abs_diff_eq!(optimizer.initial_temperature(), DEFAULT_INITIAL_TEMP);
assert_abs_diff_eq!(optimizer.final_temperature(), DEFAULT_FINAL_TEMP);
assert_abs_diff_eq!(optimizer.tunneling_strength(), DEFAULT_TUNNELING_STRENGTH);
assert_eq!(optimizer.num_iterations(), DEFAULT_NUM_ITERATIONS);
assert_eq!(optimizer.current_step(), 0);
}
#[test]
fn test_builder_pattern() {
let optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05)
.with_temperature_schedule(2.0, 0.01)
.with_tunneling(0.7)
.with_iterations(250)
.with_seed(42);
assert_abs_diff_eq!(optimizer.initial_temperature(), 2.0);
assert_abs_diff_eq!(optimizer.final_temperature(), 0.01);
assert_abs_diff_eq!(optimizer.tunneling_strength(), 0.7);
assert_eq!(optimizer.num_iterations(), 250);
assert_eq!(optimizer.seed(), 42);
assert_abs_diff_eq!(optimizer.current_temperature(), 2.0);
}
#[test]
fn test_temperature_decay_monotonic() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
.with_temperature_schedule(1.0, 1e-3)
.with_iterations(50)
.with_seed(7);
let params = Array1::from_vec(vec![1.0, -1.0, 0.5]);
let mut prev = optimizer.current_temperature();
for _ in 0..30 {
let grads = quadratic_grad(¶ms);
let _ = optimizer.step(¶ms, &grads).expect("step failed");
let curr = optimizer.current_temperature();
assert!(
curr <= prev + 1e-12,
"Temperature did not decrease monotonically: prev={prev}, curr={curr}"
);
prev = curr;
}
}
#[test]
fn test_geometric_cooling_endpoints() {
let optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
.with_temperature_schedule(2.0, 1e-2)
.with_iterations(100);
let t0 = optimizer.temperature_at(0);
let tn = optimizer.temperature_at(100);
assert_abs_diff_eq!(t0, 2.0, epsilon = 1e-9);
assert_abs_diff_eq!(tn, 1e-2, epsilon = 1e-9);
let t_half = optimizer.temperature_at(50);
let expected_half = (2.0_f64 * 1e-2).sqrt();
assert_abs_diff_eq!(t_half, expected_half, epsilon = 1e-9);
}
#[test]
fn test_metropolis_accepts_lower_energy() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
.with_temperature_schedule(1.0, 1e-3)
.with_iterations(50)
.with_seed(11);
for _ in 0..30 {
let accept = optimizer.accept(-0.5, 0.25);
assert!(accept, "Metropolis rejected a strictly downhill move");
}
}
#[test]
fn test_metropolis_rejects_higher_energy_at_low_temp_probabilistically() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
.with_temperature_schedule(1e-6, 1e-6)
.with_tunneling(0.0)
.with_iterations(100)
.with_seed(101);
optimizer.current_temperature = 1e-6;
let mut accepted = 0;
let trials = 500;
for _ in 0..trials {
if optimizer.accept(1.0, 0.5) {
accepted += 1;
}
}
assert!(
accepted < (trials / 50).max(2),
"Too many uphill moves accepted at near-zero temperature: {accepted} / {trials}"
);
}
#[test]
fn test_tunneling_increases_acceptance() {
let mut low: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
.with_temperature_schedule(0.1, 0.1)
.with_tunneling(0.0)
.with_iterations(1000)
.with_seed(2024);
let mut high: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
.with_temperature_schedule(0.1, 0.1)
.with_tunneling(2.0)
.with_iterations(1000)
.with_seed(2024);
let mut accept_low = 0usize;
let mut accept_high = 0usize;
let trials = 4000;
for _ in 0..trials {
if low.accept(0.2, 0.1) {
accept_low += 1;
}
if high.accept(0.2, 0.1) {
accept_high += 1;
}
}
assert!(
accept_high > accept_low,
"Higher tunneling did not increase acceptance: low={accept_low}, high={accept_high}"
);
}
#[test]
fn test_convergence_on_quadratic_bowl() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(1.0)
.with_temperature_schedule(2.0, 1e-3)
.with_tunneling(0.05)
.with_iterations(200)
.with_seed(91);
let initial = 5.0_f64;
let mut params = Array1::from_vec(vec![initial]);
for _ in 0..200 {
let grads = quadratic_grad(¶ms);
params = optimizer.step(¶ms, &grads).expect("step failed");
}
assert!(
params[0].abs() < initial * 0.5,
"Optimizer did not converge: |x|={}, started at {initial}",
params[0].abs()
);
}
#[test]
fn test_seed_reproducibility() {
let mut a: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
.with_temperature_schedule(1.0, 1e-2)
.with_iterations(100)
.with_seed(123);
let mut b: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
.with_temperature_schedule(1.0, 1e-2)
.with_iterations(100)
.with_seed(123);
let params = Array1::from_vec(vec![1.0, 2.0, -1.0]);
let grads = Array1::from_vec(vec![0.1, -0.2, 0.05]);
for _ in 0..20 {
let p_a = a.step(¶ms, &grads).expect("step failed");
let p_b = b.step(¶ms, &grads).expect("step failed");
for (x, y) in p_a.iter().zip(p_b.iter()) {
assert_abs_diff_eq!(*x, *y, epsilon = 1e-12);
}
}
}
#[test]
fn test_set_learning_rate_changes_step_size() {
let params = Array1::from_vec(vec![1.0, 1.0, 1.0]);
let grads = Array1::from_vec(vec![0.0, 0.0, 0.0]);
let mut small: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
.with_temperature_schedule(1.0, 1.0)
.with_tunneling(0.0)
.with_iterations(10)
.with_seed(5);
let mut large: QuantumAnnealing<f64> = QuantumAnnealing::new(1.0)
.with_temperature_schedule(1.0, 1.0)
.with_tunneling(0.0)
.with_iterations(10)
.with_seed(5);
let mut max_small = 0.0_f64;
let mut max_large = 0.0_f64;
for _ in 0..30 {
let ps = small.step(¶ms, &grads).expect("step failed");
let pl = large.step(¶ms, &grads).expect("step failed");
for (s, l) in ps.iter().zip(pl.iter()) {
max_small = max_small.max((s - 1.0).abs());
max_large = max_large.max((l - 1.0).abs());
}
}
assert!(
max_large > max_small,
"large LR ({max_large}) did not move further than small LR ({max_small})"
);
let mut opt: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01);
opt.set_lr(0.5);
assert_abs_diff_eq!(opt.learning_rate(), 0.5);
}
#[test]
fn test_step_returns_same_shape_as_params() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05)
.with_temperature_schedule(0.5, 1e-3)
.with_iterations(20)
.with_seed(31);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let grads = Array1::from_vec(vec![0.1, -0.2, 0.3, -0.4, 0.5]);
let updated = optimizer.step(¶ms, &grads).expect("step failed");
assert_eq!(updated.shape(), params.shape());
}
#[test]
fn test_best_params_tracked() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05)
.with_temperature_schedule(0.5, 1e-3)
.with_iterations(100)
.with_seed(17);
let params = Array1::from_vec(vec![5.0]);
let mut current = params.clone();
for _ in 0..100 {
let grads = quadratic_grad(¤t);
current = optimizer.step(¤t, &grads).expect("step failed");
}
assert!(
optimizer.best_energy() <= 0.0,
"best_energy {} should be <= 0",
optimizer.best_energy()
);
let best: Option<Array1<f64>> = optimizer.best_params();
assert!(best.is_some(), "best_params should be tracked");
}
#[test]
fn test_dimension_mismatch_errors() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05).with_seed(42);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let grads = Array1::from_vec(vec![1.0, 2.0]);
let result = optimizer.step(¶ms, &grads);
assert!(result.is_err(), "expected dimension mismatch error");
}
#[test]
fn test_temperature_plateau_after_n_iterations() {
let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
.with_temperature_schedule(1.0, 1e-3)
.with_iterations(10)
.with_seed(3);
let params = Array1::from_vec(vec![0.0]);
let grads = Array1::from_vec(vec![0.0]);
for _ in 0..50 {
let _ = optimizer.step(¶ms, &grads).expect("step failed");
}
assert_abs_diff_eq!(optimizer.current_temperature(), 1e-3, epsilon = 1e-9);
}
}