1use std::fmt::Debug;
40
41use scirs2_core::ndarray::{Array1, Ix1, ScalarOperand};
42use scirs2_core::numeric::Float;
43
44use optirs_core::optimizers::Optimizer;
45use optirs_core::quantum_inspired::{
46 HybridQuantumClassical, QuantumAnnealing, VariationalQuantumOptimizer,
47};
48
49use crate::domain_optimizers::{l2_norm, AdvancedOptimizer, OptimizerStateInfo};
50use crate::error::{OptimError, Result};
51
52const DEFAULT_NORM_EMA_DECAY: f64 = 0.9;
54
55#[derive(Debug)]
62pub enum QuantumBackend<T>
63where
64 T: Float + ScalarOperand + Debug + Send + Sync + 'static,
65{
66 Annealing(QuantumAnnealing<T>),
68 Variational(VariationalQuantumOptimizer<T>),
70 Hybrid(HybridQuantumClassical<T>),
72}
73
74impl<T> QuantumBackend<T>
75where
76 T: Float + ScalarOperand + Debug + Send + Sync + 'static,
77{
78 fn step_array1(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
84 let outcome = match self {
85 QuantumBackend::Annealing(inner) => {
86 <QuantumAnnealing<T> as Optimizer<T, Ix1>>::step(inner, params, gradients)
87 }
88 QuantumBackend::Variational(inner) => <VariationalQuantumOptimizer<T> as Optimizer<
89 T,
90 Ix1,
91 >>::step(inner, params, gradients),
92 QuantumBackend::Hybrid(inner) => {
93 <HybridQuantumClassical<T> as Optimizer<T, Ix1>>::step(inner, params, gradients)
94 }
95 };
96 outcome.map_err(|err| OptimError::ComputationError(err.to_string()))
97 }
98
99 fn learning_rate(&self) -> T {
101 match self {
102 QuantumBackend::Annealing(inner) => {
103 <QuantumAnnealing<T> as Optimizer<T, Ix1>>::get_learning_rate(inner)
104 }
105 QuantumBackend::Variational(inner) => {
106 <VariationalQuantumOptimizer<T> as Optimizer<T, Ix1>>::get_learning_rate(inner)
107 }
108 QuantumBackend::Hybrid(inner) => {
109 <HybridQuantumClassical<T> as Optimizer<T, Ix1>>::get_learning_rate(inner)
110 }
111 }
112 }
113
114 fn set_learning_rate(&mut self, learning_rate: T) {
116 match self {
117 QuantumBackend::Annealing(inner) => {
118 <QuantumAnnealing<T> as Optimizer<T, Ix1>>::set_learning_rate(inner, learning_rate)
119 }
120 QuantumBackend::Variational(inner) => {
121 <VariationalQuantumOptimizer<T> as Optimizer<T, Ix1>>::set_learning_rate(
122 inner,
123 learning_rate,
124 )
125 }
126 QuantumBackend::Hybrid(inner) => {
127 <HybridQuantumClassical<T> as Optimizer<T, Ix1>>::set_learning_rate(
128 inner,
129 learning_rate,
130 )
131 }
132 }
133 }
134
135 fn name(&self) -> &'static str {
137 match self {
138 QuantumBackend::Annealing(_) => "QuantumAnnealingLearned",
139 QuantumBackend::Variational(_) => "VariationalQuantumLearned",
140 QuantumBackend::Hybrid(_) => "HybridQuantumLearned",
141 }
142 }
143}
144
145#[derive(Debug)]
152pub struct QuantumLearnedOptimizer<T>
153where
154 T: Float + ScalarOperand + Debug + Send + Sync + 'static,
155{
156 backend: QuantumBackend<T>,
158 step_count: usize,
160 grad_norm_ema: T,
162 norm_ema_decay: T,
164}
165
166impl<T> QuantumLearnedOptimizer<T>
167where
168 T: Float + ScalarOperand + Debug + Send + Sync + 'static,
169{
170 fn default_norm_ema_decay() -> T {
172 T::from(DEFAULT_NORM_EMA_DECAY).unwrap_or_else(|| {
173 let nine = T::from(9.0).unwrap_or_else(T::one);
176 let ten = T::from(10.0).unwrap_or_else(|| T::one() + T::one());
177 nine / ten
178 })
179 }
180
181 fn from_backend(backend: QuantumBackend<T>) -> Self {
183 Self {
184 backend,
185 step_count: 0,
186 grad_norm_ema: T::zero(),
187 norm_ema_decay: Self::default_norm_ema_decay(),
188 }
189 }
190
191 pub fn annealing(learning_rate: T) -> Self {
196 Self::from_backend(QuantumBackend::Annealing(QuantumAnnealing::new(
197 learning_rate,
198 )))
199 }
200
201 pub fn variational(learning_rate: T) -> Self {
206 Self::from_backend(QuantumBackend::Variational(
207 VariationalQuantumOptimizer::new(learning_rate),
208 ))
209 }
210
211 pub fn hybrid(learning_rate: T, switch_step: usize) -> Self {
217 Self::from_backend(QuantumBackend::Hybrid(HybridQuantumClassical::new(
218 learning_rate,
219 switch_step,
220 )))
221 }
222
223 pub fn norm_ema_decay(&self) -> T {
225 self.norm_ema_decay
226 }
227
228 pub fn set_norm_ema_decay(&mut self, decay: T) -> Result<()> {
236 if !(decay > T::zero() && decay < T::one()) {
237 return Err(OptimError::InvalidConfig(format!(
238 "norm_ema_decay must lie in (0, 1), got {decay:?}"
239 )));
240 }
241 self.norm_ema_decay = decay;
242 Ok(())
243 }
244
245 pub fn backend(&self) -> &QuantumBackend<T> {
247 &self.backend
248 }
249
250 pub fn step_count(&self) -> usize {
252 self.step_count
253 }
254}
255
256impl<T> AdvancedOptimizer<T> for QuantumLearnedOptimizer<T>
257where
258 T: Float + ScalarOperand + Debug + Send + Sync + 'static,
259{
260 fn step(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
261 let updated = self.backend.step_array1(params, gradients)?;
265
266 let grad_norm = l2_norm(gradients);
267 self.grad_norm_ema =
268 self.norm_ema_decay * self.grad_norm_ema + (T::one() - self.norm_ema_decay) * grad_norm;
269 self.step_count = self.step_count.saturating_add(1);
270
271 Ok(updated)
272 }
273
274 fn get_learning_rate(&self) -> T {
275 self.backend.learning_rate()
276 }
277
278 fn set_learning_rate(&mut self, lr: T) {
279 self.backend.set_learning_rate(lr);
280 }
281
282 fn name(&self) -> &str {
283 self.backend.name()
284 }
285
286 fn get_state(&self) -> OptimizerStateInfo<T> {
287 OptimizerStateInfo {
288 step_count: self.step_count,
289 current_lr: self.get_learning_rate(),
290 grad_norm_ema: self.grad_norm_ema,
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use scirs2_core::ndarray::Array1;
299
300 fn quadratic_grad(params: &Array1<f64>) -> Array1<f64> {
302 params.mapv(|x| 2.0 * x)
303 }
304
305 fn all_finite(arr: &Array1<f64>) -> bool {
306 arr.iter().all(|x| x.is_finite())
307 }
308
309 #[test]
310 fn test_annealing_steps_preserve_shape_and_track_state() {
311 let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
312 let mut params = Array1::from_vec(vec![1.0, -2.0, 0.5, 3.0]);
313 assert_eq!(opt.step_count(), 0);
314 for i in 1..=10 {
315 let grads = quadratic_grad(¶ms);
316 params = opt.step(¶ms, &grads).expect("annealing step succeeds");
317 assert_eq!(params.len(), 4, "shape must be preserved");
318 assert!(all_finite(¶ms), "outputs must remain finite");
319 assert_eq!(opt.step_count(), i, "step count must increment");
320 }
321 assert!(
322 opt.get_state().grad_norm_ema > 0.0,
323 "grad_norm_ema must be positive after nonzero-grad steps"
324 );
325 }
326
327 #[test]
328 fn test_variational_steps_preserve_shape_and_track_state() {
329 let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.1);
330 let mut params = Array1::from_vec(vec![2.0, -1.0, 0.25]);
331 for i in 1..=10 {
332 let grads = quadratic_grad(¶ms);
333 params = opt
334 .step(¶ms, &grads)
335 .expect("variational step succeeds");
336 assert_eq!(params.len(), 3);
337 assert!(all_finite(¶ms));
338 assert_eq!(opt.step_count(), i);
339 }
340 assert!(opt.get_state().grad_norm_ema > 0.0);
341 }
342
343 #[test]
344 fn test_hybrid_steps_preserve_shape_and_track_state() {
345 let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 4);
346 let mut params = Array1::from_vec(vec![1.5, -0.75]);
347 for i in 1..=8 {
348 let grads = quadratic_grad(¶ms);
349 params = opt.step(¶ms, &grads).expect("hybrid step succeeds");
350 assert_eq!(params.len(), 2);
351 assert!(all_finite(¶ms));
352 assert_eq!(opt.step_count(), i);
353 }
354 assert!(opt.get_state().grad_norm_ema > 0.0);
355 }
356
357 #[test]
358 fn test_annealing_learning_rate_round_trip() {
359 let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
360 assert!((opt.get_learning_rate() - 0.05).abs() < 1e-12);
361 opt.set_learning_rate(0.123);
362 assert!((opt.get_learning_rate() - 0.123).abs() < 1e-12);
363 }
364
365 #[test]
366 fn test_variational_learning_rate_round_trip() {
367 let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.2);
368 assert!((opt.get_learning_rate() - 0.2).abs() < 1e-12);
369 opt.set_learning_rate(0.01);
370 assert!((opt.get_learning_rate() - 0.01).abs() < 1e-12);
371 }
372
373 #[test]
374 fn test_hybrid_learning_rate_round_trip() {
375 let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 10);
378 assert!((opt.get_learning_rate() - 0.05).abs() < 1e-12);
379 opt.set_learning_rate(0.33);
380 assert!((opt.get_learning_rate() - 0.33).abs() < 1e-12);
381 }
382
383 #[test]
384 fn test_names_per_backend() {
385 let annealing = QuantumLearnedOptimizer::<f64>::annealing(0.05);
386 let variational = QuantumLearnedOptimizer::<f64>::variational(0.05);
387 let hybrid = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 5);
388 assert_eq!(annealing.name(), "QuantumAnnealingLearned");
389 assert_eq!(variational.name(), "VariationalQuantumLearned");
390 assert_eq!(hybrid.name(), "HybridQuantumLearned");
391 }
392
393 #[test]
394 fn test_annealing_length_mismatch_errors() {
395 let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
396 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
397 let grads = Array1::from_vec(vec![1.0, 2.0]);
398 let result = opt.step(¶ms, &grads);
399 assert!(result.is_err(), "mismatched lengths must error");
400 assert_eq!(opt.step_count(), 0);
402 }
403
404 #[test]
405 fn test_variational_length_mismatch_errors() {
406 let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.1);
407 let params = Array1::from_vec(vec![1.0, 2.0]);
408 let grads = Array1::from_vec(vec![1.0, 2.0, 3.0]);
409 let result = opt.step(¶ms, &grads);
410 assert!(result.is_err());
411 assert_eq!(opt.step_count(), 0);
412 }
413
414 #[test]
415 fn test_hybrid_length_mismatch_errors() {
416 let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 5);
417 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
418 let grads = Array1::from_vec(vec![1.0]);
419 let result = opt.step(¶ms, &grads);
420 assert!(result.is_err());
421 assert_eq!(opt.step_count(), 0);
422 }
423
424 #[test]
425 fn test_hybrid_crosses_switch_step_without_error() {
426 let switch_step = 6usize;
429 let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, switch_step);
430 let mut params = Array1::from_vec(vec![2.0, -1.0, 0.5]);
431 for _ in 0..(switch_step + 2) {
432 let grads = quadratic_grad(¶ms);
433 params = opt
434 .step(¶ms, &grads)
435 .expect("hybrid step across switch succeeds");
436 assert!(all_finite(¶ms));
437 }
438 assert_eq!(opt.step_count(), switch_step + 2);
439 }
440
441 #[test]
442 fn test_get_state_reflects_learning_rate_and_steps() {
443 let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.07);
444 let params = Array1::from_vec(vec![0.5, -0.5, 0.5]);
445 let grads = quadratic_grad(¶ms);
446 let _ = opt.step(¶ms, &grads).expect("step succeeds");
447 let state = opt.get_state();
448 assert_eq!(state.step_count, 1);
449 assert!((state.current_lr - 0.07).abs() < 1e-12);
450 assert!(state.grad_norm_ema >= 0.0);
451 }
452
453 #[test]
454 fn test_norm_ema_decay_validation() {
455 let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
456 assert!(opt.norm_ema_decay() > 0.0 && opt.norm_ema_decay() < 1.0);
458 assert!(opt.set_norm_ema_decay(0.5).is_ok());
459 assert!((opt.norm_ema_decay() - 0.5).abs() < 1e-12);
460 assert!(opt.set_norm_ema_decay(0.0).is_err());
462 assert!(opt.set_norm_ema_decay(1.0).is_err());
463 assert!(opt.set_norm_ema_decay(-0.1).is_err());
464 assert!(opt.set_norm_ema_decay(1.5).is_err());
465 }
466
467 #[test]
468 fn test_zero_gradient_keeps_norm_ema_zero() {
469 let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 3);
471 let params = Array1::from_vec(vec![1.0, 1.0, 1.0]);
472 let grads = Array1::from_vec(vec![0.0, 0.0, 0.0]);
473 for _ in 0..5 {
474 let _ = opt.step(¶ms, &grads).expect("zero-grad step succeeds");
475 }
476 assert!(opt.get_state().grad_norm_ema.abs() < 1e-15);
477 }
478
479 #[test]
480 fn test_backend_accessor_matches_name() {
481 let opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
482 match opt.backend() {
483 QuantumBackend::Annealing(_) => {}
484 _ => panic!("expected annealing backend"),
485 }
486 }
487
488 #[test]
490 fn test_is_send_sync() {
491 fn assert_send_sync<U: Send + Sync>() {}
492 assert_send_sync::<QuantumLearnedOptimizer<f64>>();
493 }
494}