optirs_core/quantum_inspired/
vqe.rs1use scirs2_core::ndarray::{Array, Array1, 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::DEFAULT_SEED;
17
18pub(crate) const DEFAULT_SPSA_A: f64 = 0.1;
20pub(crate) const DEFAULT_SPSA_C: f64 = 0.1;
22pub(crate) const DEFAULT_SPSA_ALPHA: f64 = 0.602;
24pub(crate) const DEFAULT_SPSA_GAMMA: f64 = 0.101;
26pub(crate) const DEFAULT_SPSA_BIG_A: f64 = 10.0;
28
29#[derive(Debug)]
67pub struct VariationalQuantumOptimizer<A: Float + ScalarOperand + Debug> {
68 learning_rate: A,
70 c: A,
72 alpha: A,
74 gamma: A,
76 big_a: A,
78 step: usize,
80 last_loss: Option<A>,
82 seed: u64,
84 rng: Random<scirs2_core::random::rngs::StdRng>,
86 ansatz_params: Option<Array1<A>>,
88}
89
90impl<A> VariationalQuantumOptimizer<A>
91where
92 A: Float + ScalarOperand + Debug + Send + Sync,
93{
94 pub fn with_default_gain() -> Self {
107 Self::new(A::from(DEFAULT_SPSA_A).unwrap_or_else(A::one))
108 }
109
110 pub fn new(learning_rate: A) -> Self {
112 let c = A::from(DEFAULT_SPSA_C).unwrap_or_else(|| A::epsilon());
113 let alpha = A::from(DEFAULT_SPSA_ALPHA).unwrap_or_else(A::one);
114 let gamma = A::from(DEFAULT_SPSA_GAMMA).unwrap_or_else(A::one);
115 let big_a = A::from(DEFAULT_SPSA_BIG_A).unwrap_or_else(A::zero);
116 Self {
117 learning_rate,
118 c,
119 alpha,
120 gamma,
121 big_a,
122 step: 0,
123 last_loss: None,
124 seed: DEFAULT_SEED,
125 rng: Random::seed(DEFAULT_SEED),
126 ansatz_params: None,
127 }
128 }
129
130 pub fn with_perturbation(mut self, c: A) -> Self {
132 self.c = c;
133 self
134 }
135
136 pub fn with_gain_decay(mut self, alpha: A, gamma: A) -> Self {
138 self.alpha = alpha;
139 self.gamma = gamma;
140 self
141 }
142
143 pub fn with_stability(mut self, big_a: A) -> Self {
145 self.big_a = big_a;
146 self
147 }
148
149 pub fn with_seed(mut self, seed: u64) -> Self {
151 self.seed = seed;
152 self.rng = Random::seed(seed);
153 self
154 }
155
156 pub fn alpha(&self) -> A {
158 self.alpha
159 }
160
161 pub fn gamma(&self) -> A {
163 self.gamma
164 }
165
166 pub fn big_a(&self) -> A {
168 self.big_a
169 }
170
171 pub fn c(&self) -> A {
173 self.c
174 }
175
176 pub fn step_count(&self) -> usize {
178 self.step
179 }
180
181 pub fn last_loss(&self) -> Option<A> {
183 self.last_loss
184 }
185
186 pub fn seed(&self) -> u64 {
188 self.seed
189 }
190
191 pub fn learning_rate(&self) -> A {
195 self.learning_rate
196 }
197
198 pub fn set_lr(&mut self, learning_rate: A) {
200 self.learning_rate = learning_rate;
201 }
202
203 pub fn a_k(&self, k: usize) -> A {
205 let k_f = A::from(k).unwrap_or_else(A::zero);
206 let one = A::one();
207 let denom = (k_f + one + self.big_a).powf(self.alpha);
208 if denom <= A::zero() {
209 self.learning_rate
210 } else {
211 self.learning_rate / denom
212 }
213 }
214
215 pub fn c_k(&self, k: usize) -> A {
217 let k_f = A::from(k).unwrap_or_else(A::zero);
218 let one = A::one();
219 let denom = (k_f + one).powf(self.gamma);
220 if denom <= A::zero() {
221 self.c
222 } else {
223 self.c / denom
224 }
225 }
226
227 pub fn reset(&mut self) {
229 self.step = 0;
230 self.last_loss = None;
231 self.rng = Random::seed(self.seed);
232 self.ansatz_params = None;
233 }
234
235 pub fn ansatz_factor(theta: A) -> A {
237 let half = A::from(0.5).unwrap_or_else(A::one);
238 let c = (theta * half).cos();
239 c * c
240 }
241
242 pub(crate) fn sample_perturbation_vector(&mut self, dim: usize) -> Array1<A> {
244 let mut buf: Vec<A> = Vec::with_capacity(dim);
245 let one = A::one();
246 let neg_one = -A::one();
247 for _ in 0..dim {
248 let u: f64 = self.rng.gen_range(0.0..1.0);
249 buf.push(if u < 0.5 { neg_one } else { one });
250 }
251 Array1::from_vec(buf)
252 }
253
254 pub fn spsa_gradient<F>(
258 &mut self,
259 params: &Array1<A>,
260 loss_fn: F,
261 k: usize,
262 ) -> Result<(Array1<A>, A, Array1<A>)>
263 where
264 F: Fn(&Array1<A>) -> A,
265 {
266 let dim = params.len();
267 if dim == 0 {
268 return Err(OptimError::InvalidParameter(
269 "VariationalQuantumOptimizer: parameters must be non-empty".to_string(),
270 ));
271 }
272 let c_k = self.c_k(k);
273 if c_k <= A::zero() {
274 return Err(OptimError::InvalidConfig(
275 "VariationalQuantumOptimizer: c_k must be positive".to_string(),
276 ));
277 }
278 let delta = self.sample_perturbation_vector(dim);
279 let plus = params + &(&delta * c_k);
280 let minus = params - &(&delta * c_k);
281 let loss_plus = loss_fn(&plus);
282 let loss_minus = loss_fn(&minus);
283 let two = A::from(2.0).unwrap_or_else(A::one);
284 let numerator = loss_plus - loss_minus;
285 let denom = two * c_k;
286 let mut grad = Array1::<A>::zeros(dim);
287 for i in 0..dim {
288 let d = delta[i];
290 grad[i] = numerator / (denom * d);
291 }
292 Ok((grad, c_k, delta))
293 }
294
295 fn apply_ansatz(&self, params: &Array1<A>, grad: &Array1<A>, k: usize) -> Array1<A> {
299 let a_k = self.a_k(k);
300 let mut updated = params.clone();
301 for i in 0..updated.len() {
302 let factor = Self::ansatz_factor(updated[i]);
303 updated[i] = updated[i] - a_k * grad[i] * factor;
304 }
305 updated
306 }
307
308 pub fn step_from_loss<F>(&mut self, params: &Array1<A>, loss_fn: F) -> Result<Array1<A>>
313 where
314 F: Fn(&Array1<A>) -> A,
315 {
316 let k = self.step;
317 let (grad, _c_k, _delta) = self.spsa_gradient(params, &loss_fn, k)?;
318 let updated = self.apply_ansatz(params, &grad, k);
319 self.last_loss = Some(loss_fn(&updated));
320 self.step = self.step.saturating_add(1);
321 self.ansatz_params = Some(updated.clone());
322 Ok(updated)
323 }
324}
325
326impl<A, D> Optimizer<A, D> for VariationalQuantumOptimizer<A>
327where
328 A: Float + ScalarOperand + Debug + Send + Sync,
329 D: Dimension,
330{
331 fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
332 if params.shape() != gradients.shape() {
333 return Err(OptimError::DimensionMismatch(format!(
334 "VQE optimizer: parameters have shape {:?}, gradients have shape {:?}",
335 params.shape(),
336 gradients.shape()
337 )));
338 }
339
340 let params_dyn = params.to_owned().into_dyn();
341 let grads_dyn = gradients.to_owned().into_dyn();
342 let k = self.step;
343 let a_k = self.a_k(k);
344
345 let mut updated = params_dyn.clone();
349 for (out, (p, g)) in updated
350 .iter_mut()
351 .zip(params_dyn.iter().zip(grads_dyn.iter()))
352 {
353 let factor = Self::ansatz_factor(*p);
354 *out = *p - a_k * (*g) * factor;
355 }
356
357 self.step = self.step.saturating_add(1);
358
359 updated.into_dimensionality::<D>().map_err(|err| {
360 OptimError::ComputationError(format!(
361 "VQE optimizer: failed to restore dimension: {err}"
362 ))
363 })
364 }
365
366 fn get_learning_rate(&self) -> A {
367 self.learning_rate
368 }
369
370 fn set_learning_rate(&mut self, learning_rate: A) {
371 self.learning_rate = learning_rate;
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use approx::{assert_abs_diff_eq, assert_relative_eq};
379 use scirs2_core::ndarray::Array1;
380 use std::f64::consts::PI;
381
382 #[test]
383 fn test_default_config_values() {
384 let optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.1);
385 assert_abs_diff_eq!(optimizer.learning_rate(), 0.1);
386 assert_abs_diff_eq!(optimizer.c(), DEFAULT_SPSA_C);
387 assert_abs_diff_eq!(optimizer.alpha(), DEFAULT_SPSA_ALPHA);
388 assert_abs_diff_eq!(optimizer.gamma(), DEFAULT_SPSA_GAMMA);
389 assert_abs_diff_eq!(optimizer.big_a(), DEFAULT_SPSA_BIG_A);
390 assert_eq!(optimizer.step_count(), 0);
391 }
392
393 #[test]
394 fn test_builder_pattern() {
395 let optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.05)
396 .with_perturbation(0.2)
397 .with_gain_decay(0.5, 0.2)
398 .with_stability(20.0)
399 .with_seed(99);
400 assert_abs_diff_eq!(optimizer.c(), 0.2);
401 assert_abs_diff_eq!(optimizer.alpha(), 0.5);
402 assert_abs_diff_eq!(optimizer.gamma(), 0.2);
403 assert_abs_diff_eq!(optimizer.big_a(), 20.0);
404 assert_eq!(optimizer.seed(), 99);
405 }
406
407 #[test]
408 fn test_gain_sequences_endpoints() {
409 let optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.1)
410 .with_perturbation(0.1)
411 .with_gain_decay(0.602, 0.101)
412 .with_stability(10.0);
413 let expected_a0 = 0.1_f64 / 11.0_f64.powf(0.602);
415 assert_relative_eq!(optimizer.a_k(0), expected_a0, epsilon = 1e-12);
416 assert_abs_diff_eq!(optimizer.c_k(0), 0.1);
418 let expected_c10 = 0.1_f64 / 11.0_f64.powf(0.101);
420 assert_relative_eq!(optimizer.c_k(10), expected_c10, epsilon = 1e-12);
421 }
422
423 #[test]
424 fn test_gain_sequences_decay() {
425 let optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.1);
426 for k in 0..50 {
429 assert!(
430 optimizer.a_k(k + 1) < optimizer.a_k(k),
431 "a_k not decreasing at k={k}"
432 );
433 assert!(
434 optimizer.c_k(k + 1) < optimizer.c_k(k),
435 "c_k not decreasing at k={k}"
436 );
437 }
438 }
439
440 #[test]
441 fn test_perturbation_is_pm_one() {
442 let mut optimizer: VariationalQuantumOptimizer<f64> =
443 VariationalQuantumOptimizer::new(0.1).with_seed(13);
444 for _ in 0..20 {
445 let delta = optimizer.sample_perturbation_vector(32);
446 for v in delta.iter() {
447 assert!(
448 (*v == 1.0) || (*v == -1.0),
449 "perturbation entry {} was not in {{-1, 1}}",
450 *v
451 );
452 }
453 }
454 }
455
456 #[test]
457 fn test_spsa_gradient_unbiasedness() {
458 let theta = Array1::from_vec(vec![1.5, -0.5, 0.25]);
466 let mut accum = Array1::<f64>::zeros(theta.len());
467 let trials_per_seed = 2000;
468 let num_seeds = 5;
469 let mut total_trials = 0usize;
470 for seed in 0..num_seeds {
471 let mut optimizer: VariationalQuantumOptimizer<f64> =
472 VariationalQuantumOptimizer::new(0.01)
473 .with_perturbation(0.01)
474 .with_seed(2024 + seed as u64);
475 for _ in 0..trials_per_seed {
476 let (g, _c, _d) = optimizer
477 .spsa_gradient(&theta, |x| x.iter().map(|v| v * v).sum::<f64>(), 0)
478 .expect("spsa gradient failed");
479 accum = &accum + &g;
480 total_trials += 1;
481 }
482 }
483 let n = total_trials as f64;
484 for i in 0..theta.len() {
485 let mean = accum[i] / n;
486 let expected = 2.0 * theta[i];
487 assert!(
492 (mean - expected).abs() < 0.2,
493 "SPSA gradient bias too large at index {i}: mean={mean}, expected={expected}, n={n}"
494 );
495 }
496 }
497
498 #[test]
499 fn test_step_from_loss_decreases_loss() {
500 let mut optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.5)
501 .with_perturbation(0.05)
502 .with_gain_decay(0.602, 0.101)
503 .with_stability(2.0)
504 .with_seed(77);
505 let mut params = Array1::from_vec(vec![1.2, -0.8, 0.5]);
506 let loss_fn = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
507 let initial = loss_fn(¶ms);
508 for _ in 0..150 {
509 params = optimizer
510 .step_from_loss(¶ms, loss_fn)
511 .expect("step failed");
512 }
513 let final_loss = loss_fn(¶ms);
514 assert!(
515 final_loss < initial,
516 "Loss did not decrease: initial={initial}, final={final_loss}"
517 );
518 }
519
520 #[test]
521 fn test_ansatz_smoothness() {
522 let near_zero = VariationalQuantumOptimizer::<f64>::ansatz_factor(0.0);
524 assert_abs_diff_eq!(near_zero, 1.0, epsilon = 1e-12);
525 let at_pi = VariationalQuantumOptimizer::<f64>::ansatz_factor(PI);
527 assert_abs_diff_eq!(at_pi, 0.0, epsilon = 1e-12);
528 let mid = VariationalQuantumOptimizer::<f64>::ansatz_factor(PI / 2.0);
530 assert!(mid > 0.4 && mid < 0.6, "midpoint ansatz factor = {mid}");
531 }
532
533 #[test]
534 fn test_seed_reproducibility() {
535 let mut a: VariationalQuantumOptimizer<f64> =
536 VariationalQuantumOptimizer::new(0.1).with_seed(321);
537 let mut b: VariationalQuantumOptimizer<f64> =
538 VariationalQuantumOptimizer::new(0.1).with_seed(321);
539 let params = Array1::from_vec(vec![0.2, -0.4, 0.6, -0.8]);
540 let loss_fn = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
541 for _ in 0..25 {
542 let pa = a.step_from_loss(¶ms, loss_fn).expect("step failed");
543 let pb = b.step_from_loss(¶ms, loss_fn).expect("step failed");
544 for (x, y) in pa.iter().zip(pb.iter()) {
545 assert_abs_diff_eq!(*x, *y, epsilon = 1e-12);
546 }
547 }
548 }
549
550 #[test]
551 fn test_get_set_learning_rate() {
552 let mut optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.3);
553 assert_abs_diff_eq!(optimizer.learning_rate(), 0.3);
554 optimizer.set_lr(0.05);
555 assert_abs_diff_eq!(optimizer.learning_rate(), 0.05);
556 }
557
558 #[test]
559 fn test_step_returns_same_shape() {
560 let mut optimizer: VariationalQuantumOptimizer<f64> =
561 VariationalQuantumOptimizer::new(0.1).with_seed(5);
562 let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
563 let grads = Array1::from_vec(vec![0.5, -0.3, 0.2, 0.1]);
564 let updated = <VariationalQuantumOptimizer<f64> as Optimizer<f64, _>>::step(
565 &mut optimizer,
566 ¶ms,
567 &grads,
568 )
569 .expect("step failed");
570 assert_eq!(updated.shape(), params.shape());
571 }
572
573 #[test]
574 fn test_convergence_on_quadratic() {
575 let mut optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(1.0)
576 .with_perturbation(0.05)
577 .with_gain_decay(0.602, 0.101)
578 .with_stability(2.0)
579 .with_seed(31);
580 let mut params = Array1::from_vec(vec![1.0, -1.0]);
581 let loss_fn = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
582 for _ in 0..400 {
583 params = optimizer
584 .step_from_loss(¶ms, loss_fn)
585 .expect("step failed");
586 }
587 for v in params.iter() {
589 assert!(v.abs() < 0.5, "Parameter did not converge: |x|={}", v.abs());
590 }
591 }
592
593 #[test]
594 fn test_step_count_increments() {
595 let mut optimizer: VariationalQuantumOptimizer<f64> =
596 VariationalQuantumOptimizer::new(0.1).with_seed(1);
597 let params = Array1::from_vec(vec![0.5, -0.5]);
598 let loss_fn = |x: &Array1<f64>| x.iter().map(|v| v * v).sum::<f64>();
599 assert_eq!(optimizer.step_count(), 0);
600 for i in 1..=5 {
601 let _ = optimizer
602 .step_from_loss(¶ms, loss_fn)
603 .expect("step failed");
604 assert_eq!(optimizer.step_count(), i);
605 }
606 }
607
608 #[test]
609 fn test_dimension_mismatch_errors() {
610 let mut optimizer: VariationalQuantumOptimizer<f64> = VariationalQuantumOptimizer::new(0.1);
611 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
612 let grads = Array1::from_vec(vec![1.0, 2.0]);
613 let result = <VariationalQuantumOptimizer<f64> as Optimizer<f64, _>>::step(
614 &mut optimizer,
615 ¶ms,
616 &grads,
617 );
618 assert!(result.is_err(), "expected dimension mismatch error");
619 }
620}