1use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
9use scirs2_core::numeric::Float;
10use scirs2_core::random::Random;
11use std::fmt::Debug;
12
13use crate::error::{OptimError, Result};
14use crate::optimizers::Optimizer;
15
16use super::{
17 DEFAULT_FINAL_TEMP, DEFAULT_INITIAL_TEMP, DEFAULT_NUM_ITERATIONS, DEFAULT_SEED,
18 DEFAULT_TUNNELING_STRENGTH,
19};
20
21#[derive(Debug)]
63pub struct QuantumAnnealing<A: Float + ScalarOperand + Debug> {
64 learning_rate: A,
66 current_temperature: A,
68 initial_temperature: A,
70 final_temperature: A,
72 num_iterations: usize,
74 current_step: usize,
76 tunneling_strength: A,
78 best_params: Option<Vec<A>>,
80 best_shape: Option<Vec<usize>>,
82 best_energy: A,
84 seed: u64,
86 rng: Random<scirs2_core::random::rngs::StdRng>,
88}
89
90impl<A> QuantumAnnealing<A>
91where
92 A: Float + ScalarOperand + Debug + Send + Sync,
93{
94 pub fn new(learning_rate: A) -> Self {
97 let initial = A::from(DEFAULT_INITIAL_TEMP).unwrap_or_else(A::one);
98 let final_t = A::from(DEFAULT_FINAL_TEMP).unwrap_or_else(|| A::epsilon());
99 let tunneling = A::from(DEFAULT_TUNNELING_STRENGTH).unwrap_or_else(A::zero);
100 Self {
101 learning_rate,
102 current_temperature: initial,
103 initial_temperature: initial,
104 final_temperature: final_t,
105 num_iterations: DEFAULT_NUM_ITERATIONS,
106 current_step: 0,
107 tunneling_strength: tunneling,
108 best_params: None,
109 best_shape: None,
110 best_energy: A::infinity(),
111 seed: DEFAULT_SEED,
112 rng: Random::seed(DEFAULT_SEED),
113 }
114 }
115
116 pub fn with_temperature_schedule(mut self, initial: A, final_t: A) -> Self {
122 self.initial_temperature = initial;
123 self.final_temperature = final_t;
124 self.current_temperature = initial;
125 self
126 }
127
128 pub fn with_tunneling(mut self, strength: A) -> Self {
133 self.tunneling_strength = strength;
134 self
135 }
136
137 pub fn with_seed(mut self, seed: u64) -> Self {
139 self.seed = seed;
140 self.rng = Random::seed(seed);
141 self
142 }
143
144 pub fn with_iterations(mut self, num_iterations: usize) -> Self {
146 self.num_iterations = num_iterations.max(1);
147 self
148 }
149
150 pub fn current_temperature(&self) -> A {
152 self.current_temperature
153 }
154
155 pub fn best_energy(&self) -> A {
157 self.best_energy
158 }
159
160 pub fn current_step(&self) -> usize {
162 self.current_step
163 }
164
165 pub fn initial_temperature(&self) -> A {
167 self.initial_temperature
168 }
169
170 pub fn final_temperature(&self) -> A {
172 self.final_temperature
173 }
174
175 pub fn tunneling_strength(&self) -> A {
177 self.tunneling_strength
178 }
179
180 pub fn num_iterations(&self) -> usize {
182 self.num_iterations
183 }
184
185 pub fn seed(&self) -> u64 {
187 self.seed
188 }
189
190 pub fn learning_rate(&self) -> A {
194 self.learning_rate
195 }
196
197 pub fn set_lr(&mut self, learning_rate: A) {
199 self.learning_rate = learning_rate;
200 }
201
202 pub fn best_params<D: Dimension>(&self) -> Option<Array<A, D>> {
205 let buf = self.best_params.as_ref()?;
206 let shape = self.best_shape.as_ref()?;
207 let arr = Array::from_shape_vec(scirs2_core::ndarray::IxDyn(shape), buf.clone()).ok()?;
208 arr.into_dimensionality::<D>().ok()
209 }
210
211 pub fn reset(&mut self) {
214 self.current_step = 0;
215 self.current_temperature = self.initial_temperature;
216 self.best_params = None;
217 self.best_shape = None;
218 self.best_energy = A::infinity();
219 self.rng = Random::seed(self.seed);
220 }
221
222 fn temperature_at(&self, step: usize) -> A {
224 if self.num_iterations == 0 {
225 return self.final_temperature;
226 }
227 let n = self.num_iterations;
228 let step_clamped = step.min(n);
229 let frac =
230 A::from(step_clamped).unwrap_or_else(A::zero) / A::from(n).unwrap_or_else(A::one);
231 if self.initial_temperature <= A::zero() || self.final_temperature <= A::zero() {
235 let span = self.initial_temperature - self.final_temperature;
236 return self.initial_temperature - span * frac;
237 }
238 let ratio = self.final_temperature / self.initial_temperature;
239 self.initial_temperature * ratio.powf(frac)
240 }
241
242 fn sample_perturbation(
245 &mut self,
246 shape: &[usize],
247 ) -> Result<Array<A, scirs2_core::ndarray::IxDyn>> {
248 let scale = self.learning_rate * self.current_temperature;
249 let total: usize = shape.iter().product();
253 let mut buf: Vec<A> = Vec::with_capacity(total);
254 for _ in 0..total {
255 let u: f64 = self.rng.gen_range(-1.0..1.0);
256 let val = A::from(u).unwrap_or_else(A::zero) * scale;
257 buf.push(val);
258 }
259 Array::from_shape_vec(scirs2_core::ndarray::IxDyn(shape), buf).map_err(|err| {
260 OptimError::ComputationError(format!("Failed to build perturbation array: {err}"))
261 })
262 }
263
264 fn accept(&mut self, delta_e: A, perturbation_sq_norm: A) -> bool {
266 if delta_e <= A::zero() {
269 return true;
270 }
271 if self.current_temperature <= A::zero() {
273 return false;
274 }
275 let tunneling_kernel = (-perturbation_sq_norm).exp();
276 let exponent =
277 -delta_e / self.current_temperature + self.tunneling_strength * tunneling_kernel;
278 if exponent >= A::zero() {
281 return true;
282 }
283 let safety = A::from(-50.0).unwrap_or_else(|| -A::one());
284 let exponent_safe = if exponent < safety { safety } else { exponent };
285 let p = exponent_safe.exp();
286 let p_f64 = p.to_f64().unwrap_or(0.0);
287 let u: f64 = self.rng.gen_range(0.0..1.0);
288 u < p_f64
289 }
290
291 fn advance_temperature(&mut self) {
293 self.current_step = self.current_step.saturating_add(1);
296 self.current_temperature = self.temperature_at(self.current_step);
297 }
298
299 fn proxy_energy(
301 gradients: &Array<A, scirs2_core::ndarray::IxDyn>,
302 candidate_offset: &Array<A, scirs2_core::ndarray::IxDyn>,
303 ) -> A {
304 gradients
305 .iter()
306 .zip(candidate_offset.iter())
307 .fold(A::zero(), |acc, (g, p)| acc + (*g) * (*p))
308 }
309
310 fn sq_norm(arr: &Array<A, scirs2_core::ndarray::IxDyn>) -> A {
312 arr.iter().fold(A::zero(), |acc, x| acc + (*x) * (*x))
313 }
314
315 fn track_best(&mut self, candidate: &Array<A, scirs2_core::ndarray::IxDyn>, energy: A) {
317 if energy < self.best_energy {
318 self.best_energy = energy;
319 self.best_params = Some(candidate.iter().copied().collect());
320 self.best_shape = Some(candidate.shape().to_vec());
321 }
322 }
323}
324
325impl<A, D> Optimizer<A, D> for QuantumAnnealing<A>
326where
327 A: Float + ScalarOperand + Debug + Send + Sync,
328 D: Dimension,
329{
330 fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
331 if params.shape() != gradients.shape() {
332 return Err(OptimError::DimensionMismatch(format!(
333 "Quantum annealing: parameters have shape {:?}, gradients have shape {:?}",
334 params.shape(),
335 gradients.shape()
336 )));
337 }
338
339 let params_dyn = params.to_owned().into_dyn();
340 let gradients_dyn = gradients.to_owned().into_dyn();
341 let shape: Vec<usize> = params_dyn.shape().to_vec();
342
343 if self.best_params.is_none() {
346 self.best_energy = A::zero();
347 self.best_params = Some(params_dyn.iter().copied().collect());
348 self.best_shape = Some(shape.clone());
349 }
350
351 let perturbation = self.sample_perturbation(&shape)?;
352 let delta_e = -Self::proxy_energy(&gradients_dyn, &perturbation);
360 let sq_norm = Self::sq_norm(&perturbation);
361
362 let updated_dyn = if self.accept(delta_e, sq_norm) {
363 ¶ms_dyn - &perturbation
364 } else {
365 params_dyn.clone()
366 };
367
368 let accepted_offset = &updated_dyn - ¶ms_dyn;
371 let accepted_energy = -Self::proxy_energy(&gradients_dyn, &accepted_offset);
372 self.track_best(&updated_dyn, accepted_energy);
373
374 self.advance_temperature();
375
376 updated_dyn.into_dimensionality::<D>().map_err(|err| {
377 OptimError::ComputationError(format!(
378 "Quantum annealing: failed to restore dimension: {err}"
379 ))
380 })
381 }
382
383 fn get_learning_rate(&self) -> A {
384 self.learning_rate
385 }
386
387 fn set_learning_rate(&mut self, learning_rate: A) {
388 self.learning_rate = learning_rate;
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use approx::assert_abs_diff_eq;
396 use scirs2_core::ndarray::Array1;
397
398 fn quadratic_grad(params: &Array1<f64>) -> Array1<f64> {
399 params.mapv(|x| 2.0 * x)
400 }
401
402 #[test]
403 fn test_default_config() {
404 let optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1);
405 assert_abs_diff_eq!(optimizer.learning_rate(), 0.1);
406 assert_abs_diff_eq!(optimizer.initial_temperature(), DEFAULT_INITIAL_TEMP);
407 assert_abs_diff_eq!(optimizer.final_temperature(), DEFAULT_FINAL_TEMP);
408 assert_abs_diff_eq!(optimizer.tunneling_strength(), DEFAULT_TUNNELING_STRENGTH);
409 assert_eq!(optimizer.num_iterations(), DEFAULT_NUM_ITERATIONS);
410 assert_eq!(optimizer.current_step(), 0);
411 }
412
413 #[test]
414 fn test_builder_pattern() {
415 let optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05)
416 .with_temperature_schedule(2.0, 0.01)
417 .with_tunneling(0.7)
418 .with_iterations(250)
419 .with_seed(42);
420 assert_abs_diff_eq!(optimizer.initial_temperature(), 2.0);
421 assert_abs_diff_eq!(optimizer.final_temperature(), 0.01);
422 assert_abs_diff_eq!(optimizer.tunneling_strength(), 0.7);
423 assert_eq!(optimizer.num_iterations(), 250);
424 assert_eq!(optimizer.seed(), 42);
425 assert_abs_diff_eq!(optimizer.current_temperature(), 2.0);
427 }
428
429 #[test]
430 fn test_temperature_decay_monotonic() {
431 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
432 .with_temperature_schedule(1.0, 1e-3)
433 .with_iterations(50)
434 .with_seed(7);
435 let params = Array1::from_vec(vec![1.0, -1.0, 0.5]);
436 let mut prev = optimizer.current_temperature();
437 for _ in 0..30 {
438 let grads = quadratic_grad(¶ms);
439 let _ = optimizer.step(¶ms, &grads).expect("step failed");
440 let curr = optimizer.current_temperature();
441 assert!(
442 curr <= prev + 1e-12,
443 "Temperature did not decrease monotonically: prev={prev}, curr={curr}"
444 );
445 prev = curr;
446 }
447 }
448
449 #[test]
450 fn test_geometric_cooling_endpoints() {
451 let optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
452 .with_temperature_schedule(2.0, 1e-2)
453 .with_iterations(100);
454 let t0 = optimizer.temperature_at(0);
456 let tn = optimizer.temperature_at(100);
457 assert_abs_diff_eq!(t0, 2.0, epsilon = 1e-9);
458 assert_abs_diff_eq!(tn, 1e-2, epsilon = 1e-9);
459 let t_half = optimizer.temperature_at(50);
461 let expected_half = (2.0_f64 * 1e-2).sqrt();
462 assert_abs_diff_eq!(t_half, expected_half, epsilon = 1e-9);
463 }
464
465 #[test]
466 fn test_metropolis_accepts_lower_energy() {
467 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
469 .with_temperature_schedule(1.0, 1e-3)
470 .with_iterations(50)
471 .with_seed(11);
472 for _ in 0..30 {
474 let accept = optimizer.accept(-0.5, 0.25);
475 assert!(accept, "Metropolis rejected a strictly downhill move");
476 }
477 }
478
479 #[test]
480 fn test_metropolis_rejects_higher_energy_at_low_temp_probabilistically() {
481 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
484 .with_temperature_schedule(1e-6, 1e-6)
485 .with_tunneling(0.0)
486 .with_iterations(100)
487 .with_seed(101);
488 optimizer.current_temperature = 1e-6;
490 let mut accepted = 0;
491 let trials = 500;
492 for _ in 0..trials {
493 if optimizer.accept(1.0, 0.5) {
494 accepted += 1;
495 }
496 }
497 assert!(
498 accepted < (trials / 50).max(2),
499 "Too many uphill moves accepted at near-zero temperature: {accepted} / {trials}"
500 );
501 }
502
503 #[test]
504 fn test_tunneling_increases_acceptance() {
505 let mut low: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
509 .with_temperature_schedule(0.1, 0.1)
510 .with_tunneling(0.0)
511 .with_iterations(1000)
512 .with_seed(2024);
513 let mut high: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
514 .with_temperature_schedule(0.1, 0.1)
515 .with_tunneling(2.0)
516 .with_iterations(1000)
517 .with_seed(2024);
518
519 let mut accept_low = 0usize;
520 let mut accept_high = 0usize;
521 let trials = 4000;
522 for _ in 0..trials {
523 if low.accept(0.2, 0.1) {
524 accept_low += 1;
525 }
526 if high.accept(0.2, 0.1) {
527 accept_high += 1;
528 }
529 }
530 assert!(
531 accept_high > accept_low,
532 "Higher tunneling did not increase acceptance: low={accept_low}, high={accept_high}"
533 );
534 }
535
536 #[test]
537 fn test_convergence_on_quadratic_bowl() {
538 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(1.0)
545 .with_temperature_schedule(2.0, 1e-3)
546 .with_tunneling(0.05)
547 .with_iterations(200)
548 .with_seed(91);
549 let initial = 5.0_f64;
550 let mut params = Array1::from_vec(vec![initial]);
551 for _ in 0..200 {
552 let grads = quadratic_grad(¶ms);
553 params = optimizer.step(¶ms, &grads).expect("step failed");
554 }
555 assert!(
559 params[0].abs() < initial * 0.5,
560 "Optimizer did not converge: |x|={}, started at {initial}",
561 params[0].abs()
562 );
563 }
564
565 #[test]
566 fn test_seed_reproducibility() {
567 let mut a: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
568 .with_temperature_schedule(1.0, 1e-2)
569 .with_iterations(100)
570 .with_seed(123);
571 let mut b: QuantumAnnealing<f64> = QuantumAnnealing::new(0.1)
572 .with_temperature_schedule(1.0, 1e-2)
573 .with_iterations(100)
574 .with_seed(123);
575 let params = Array1::from_vec(vec![1.0, 2.0, -1.0]);
576 let grads = Array1::from_vec(vec![0.1, -0.2, 0.05]);
577 for _ in 0..20 {
578 let p_a = a.step(¶ms, &grads).expect("step failed");
579 let p_b = b.step(¶ms, &grads).expect("step failed");
580 for (x, y) in p_a.iter().zip(p_b.iter()) {
581 assert_abs_diff_eq!(*x, *y, epsilon = 1e-12);
582 }
583 }
584 }
585
586 #[test]
587 fn test_set_learning_rate_changes_step_size() {
588 let params = Array1::from_vec(vec![1.0, 1.0, 1.0]);
589 let grads = Array1::from_vec(vec![0.0, 0.0, 0.0]);
590
591 let mut small: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
592 .with_temperature_schedule(1.0, 1.0)
593 .with_tunneling(0.0)
594 .with_iterations(10)
595 .with_seed(5);
596 let mut large: QuantumAnnealing<f64> = QuantumAnnealing::new(1.0)
597 .with_temperature_schedule(1.0, 1.0)
598 .with_tunneling(0.0)
599 .with_iterations(10)
600 .with_seed(5);
601
602 let mut max_small = 0.0_f64;
603 let mut max_large = 0.0_f64;
604 for _ in 0..30 {
605 let ps = small.step(¶ms, &grads).expect("step failed");
606 let pl = large.step(¶ms, &grads).expect("step failed");
607 for (s, l) in ps.iter().zip(pl.iter()) {
608 max_small = max_small.max((s - 1.0).abs());
609 max_large = max_large.max((l - 1.0).abs());
610 }
611 }
612 assert!(
614 max_large > max_small,
615 "large LR ({max_large}) did not move further than small LR ({max_small})"
616 );
617
618 let mut opt: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01);
620 opt.set_lr(0.5);
621 assert_abs_diff_eq!(opt.learning_rate(), 0.5);
622 }
623
624 #[test]
625 fn test_step_returns_same_shape_as_params() {
626 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05)
627 .with_temperature_schedule(0.5, 1e-3)
628 .with_iterations(20)
629 .with_seed(31);
630 let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
631 let grads = Array1::from_vec(vec![0.1, -0.2, 0.3, -0.4, 0.5]);
632 let updated = optimizer.step(¶ms, &grads).expect("step failed");
633 assert_eq!(updated.shape(), params.shape());
634 }
635
636 #[test]
637 fn test_best_params_tracked() {
638 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05)
639 .with_temperature_schedule(0.5, 1e-3)
640 .with_iterations(100)
641 .with_seed(17);
642 let params = Array1::from_vec(vec![5.0]);
643 let mut current = params.clone();
644 for _ in 0..100 {
645 let grads = quadratic_grad(¤t);
646 current = optimizer.step(¤t, &grads).expect("step failed");
647 }
648 assert!(
649 optimizer.best_energy() <= 0.0,
650 "best_energy {} should be <= 0",
651 optimizer.best_energy()
652 );
653 let best: Option<Array1<f64>> = optimizer.best_params();
654 assert!(best.is_some(), "best_params should be tracked");
655 }
656
657 #[test]
658 fn test_dimension_mismatch_errors() {
659 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.05).with_seed(42);
660 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
661 let grads = Array1::from_vec(vec![1.0, 2.0]);
662 let result = optimizer.step(¶ms, &grads);
663 assert!(result.is_err(), "expected dimension mismatch error");
664 }
665
666 #[test]
667 fn test_temperature_plateau_after_n_iterations() {
668 let mut optimizer: QuantumAnnealing<f64> = QuantumAnnealing::new(0.01)
669 .with_temperature_schedule(1.0, 1e-3)
670 .with_iterations(10)
671 .with_seed(3);
672 let params = Array1::from_vec(vec![0.0]);
673 let grads = Array1::from_vec(vec![0.0]);
674 for _ in 0..50 {
675 let _ = optimizer.step(¶ms, &grads).expect("step failed");
676 }
677 assert_abs_diff_eq!(optimizer.current_temperature(), 1e-3, epsilon = 1e-9);
680 }
681}