use std::fmt::Debug;
use scirs2_core::ndarray::{Array1, Ix1, ScalarOperand};
use scirs2_core::numeric::Float;
use optirs_core::optimizers::Optimizer;
use optirs_core::quantum_inspired::{
HybridQuantumClassical, QuantumAnnealing, VariationalQuantumOptimizer,
};
use crate::domain_optimizers::{l2_norm, AdvancedOptimizer, OptimizerStateInfo};
use crate::error::{OptimError, Result};
const DEFAULT_NORM_EMA_DECAY: f64 = 0.9;
#[derive(Debug)]
pub enum QuantumBackend<T>
where
T: Float + ScalarOperand + Debug + Send + Sync + 'static,
{
Annealing(QuantumAnnealing<T>),
Variational(VariationalQuantumOptimizer<T>),
Hybrid(HybridQuantumClassical<T>),
}
impl<T> QuantumBackend<T>
where
T: Float + ScalarOperand + Debug + Send + Sync + 'static,
{
fn step_array1(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
let outcome = match self {
QuantumBackend::Annealing(inner) => {
<QuantumAnnealing<T> as Optimizer<T, Ix1>>::step(inner, params, gradients)
}
QuantumBackend::Variational(inner) => <VariationalQuantumOptimizer<T> as Optimizer<
T,
Ix1,
>>::step(inner, params, gradients),
QuantumBackend::Hybrid(inner) => {
<HybridQuantumClassical<T> as Optimizer<T, Ix1>>::step(inner, params, gradients)
}
};
outcome.map_err(|err| OptimError::ComputationError(err.to_string()))
}
fn learning_rate(&self) -> T {
match self {
QuantumBackend::Annealing(inner) => {
<QuantumAnnealing<T> as Optimizer<T, Ix1>>::get_learning_rate(inner)
}
QuantumBackend::Variational(inner) => {
<VariationalQuantumOptimizer<T> as Optimizer<T, Ix1>>::get_learning_rate(inner)
}
QuantumBackend::Hybrid(inner) => {
<HybridQuantumClassical<T> as Optimizer<T, Ix1>>::get_learning_rate(inner)
}
}
}
fn set_learning_rate(&mut self, learning_rate: T) {
match self {
QuantumBackend::Annealing(inner) => {
<QuantumAnnealing<T> as Optimizer<T, Ix1>>::set_learning_rate(inner, learning_rate)
}
QuantumBackend::Variational(inner) => {
<VariationalQuantumOptimizer<T> as Optimizer<T, Ix1>>::set_learning_rate(
inner,
learning_rate,
)
}
QuantumBackend::Hybrid(inner) => {
<HybridQuantumClassical<T> as Optimizer<T, Ix1>>::set_learning_rate(
inner,
learning_rate,
)
}
}
}
fn name(&self) -> &'static str {
match self {
QuantumBackend::Annealing(_) => "QuantumAnnealingLearned",
QuantumBackend::Variational(_) => "VariationalQuantumLearned",
QuantumBackend::Hybrid(_) => "HybridQuantumLearned",
}
}
}
#[derive(Debug)]
pub struct QuantumLearnedOptimizer<T>
where
T: Float + ScalarOperand + Debug + Send + Sync + 'static,
{
backend: QuantumBackend<T>,
step_count: usize,
grad_norm_ema: T,
norm_ema_decay: T,
}
impl<T> QuantumLearnedOptimizer<T>
where
T: Float + ScalarOperand + Debug + Send + Sync + 'static,
{
fn default_norm_ema_decay() -> T {
T::from(DEFAULT_NORM_EMA_DECAY).unwrap_or_else(|| {
let nine = T::from(9.0).unwrap_or_else(T::one);
let ten = T::from(10.0).unwrap_or_else(|| T::one() + T::one());
nine / ten
})
}
fn from_backend(backend: QuantumBackend<T>) -> Self {
Self {
backend,
step_count: 0,
grad_norm_ema: T::zero(),
norm_ema_decay: Self::default_norm_ema_decay(),
}
}
pub fn annealing(learning_rate: T) -> Self {
Self::from_backend(QuantumBackend::Annealing(QuantumAnnealing::new(
learning_rate,
)))
}
pub fn variational(learning_rate: T) -> Self {
Self::from_backend(QuantumBackend::Variational(
VariationalQuantumOptimizer::new(learning_rate),
))
}
pub fn hybrid(learning_rate: T, switch_step: usize) -> Self {
Self::from_backend(QuantumBackend::Hybrid(HybridQuantumClassical::new(
learning_rate,
switch_step,
)))
}
pub fn norm_ema_decay(&self) -> T {
self.norm_ema_decay
}
pub fn set_norm_ema_decay(&mut self, decay: T) -> Result<()> {
if !(decay > T::zero() && decay < T::one()) {
return Err(OptimError::InvalidConfig(format!(
"norm_ema_decay must lie in (0, 1), got {decay:?}"
)));
}
self.norm_ema_decay = decay;
Ok(())
}
pub fn backend(&self) -> &QuantumBackend<T> {
&self.backend
}
pub fn step_count(&self) -> usize {
self.step_count
}
}
impl<T> AdvancedOptimizer<T> for QuantumLearnedOptimizer<T>
where
T: Float + ScalarOperand + Debug + Send + Sync + 'static,
{
fn step(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
let updated = self.backend.step_array1(params, gradients)?;
let grad_norm = l2_norm(gradients);
self.grad_norm_ema =
self.norm_ema_decay * self.grad_norm_ema + (T::one() - self.norm_ema_decay) * grad_norm;
self.step_count = self.step_count.saturating_add(1);
Ok(updated)
}
fn get_learning_rate(&self) -> T {
self.backend.learning_rate()
}
fn set_learning_rate(&mut self, lr: T) {
self.backend.set_learning_rate(lr);
}
fn name(&self) -> &str {
self.backend.name()
}
fn get_state(&self) -> OptimizerStateInfo<T> {
OptimizerStateInfo {
step_count: self.step_count,
current_lr: self.get_learning_rate(),
grad_norm_ema: self.grad_norm_ema,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
fn quadratic_grad(params: &Array1<f64>) -> Array1<f64> {
params.mapv(|x| 2.0 * x)
}
fn all_finite(arr: &Array1<f64>) -> bool {
arr.iter().all(|x| x.is_finite())
}
#[test]
fn test_annealing_steps_preserve_shape_and_track_state() {
let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
let mut params = Array1::from_vec(vec![1.0, -2.0, 0.5, 3.0]);
assert_eq!(opt.step_count(), 0);
for i in 1..=10 {
let grads = quadratic_grad(¶ms);
params = opt.step(¶ms, &grads).expect("annealing step succeeds");
assert_eq!(params.len(), 4, "shape must be preserved");
assert!(all_finite(¶ms), "outputs must remain finite");
assert_eq!(opt.step_count(), i, "step count must increment");
}
assert!(
opt.get_state().grad_norm_ema > 0.0,
"grad_norm_ema must be positive after nonzero-grad steps"
);
}
#[test]
fn test_variational_steps_preserve_shape_and_track_state() {
let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.1);
let mut params = Array1::from_vec(vec![2.0, -1.0, 0.25]);
for i in 1..=10 {
let grads = quadratic_grad(¶ms);
params = opt
.step(¶ms, &grads)
.expect("variational step succeeds");
assert_eq!(params.len(), 3);
assert!(all_finite(¶ms));
assert_eq!(opt.step_count(), i);
}
assert!(opt.get_state().grad_norm_ema > 0.0);
}
#[test]
fn test_hybrid_steps_preserve_shape_and_track_state() {
let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 4);
let mut params = Array1::from_vec(vec![1.5, -0.75]);
for i in 1..=8 {
let grads = quadratic_grad(¶ms);
params = opt.step(¶ms, &grads).expect("hybrid step succeeds");
assert_eq!(params.len(), 2);
assert!(all_finite(¶ms));
assert_eq!(opt.step_count(), i);
}
assert!(opt.get_state().grad_norm_ema > 0.0);
}
#[test]
fn test_annealing_learning_rate_round_trip() {
let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
assert!((opt.get_learning_rate() - 0.05).abs() < 1e-12);
opt.set_learning_rate(0.123);
assert!((opt.get_learning_rate() - 0.123).abs() < 1e-12);
}
#[test]
fn test_variational_learning_rate_round_trip() {
let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.2);
assert!((opt.get_learning_rate() - 0.2).abs() < 1e-12);
opt.set_learning_rate(0.01);
assert!((opt.get_learning_rate() - 0.01).abs() < 1e-12);
}
#[test]
fn test_hybrid_learning_rate_round_trip() {
let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 10);
assert!((opt.get_learning_rate() - 0.05).abs() < 1e-12);
opt.set_learning_rate(0.33);
assert!((opt.get_learning_rate() - 0.33).abs() < 1e-12);
}
#[test]
fn test_names_per_backend() {
let annealing = QuantumLearnedOptimizer::<f64>::annealing(0.05);
let variational = QuantumLearnedOptimizer::<f64>::variational(0.05);
let hybrid = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 5);
assert_eq!(annealing.name(), "QuantumAnnealingLearned");
assert_eq!(variational.name(), "VariationalQuantumLearned");
assert_eq!(hybrid.name(), "HybridQuantumLearned");
}
#[test]
fn test_annealing_length_mismatch_errors() {
let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let grads = Array1::from_vec(vec![1.0, 2.0]);
let result = opt.step(¶ms, &grads);
assert!(result.is_err(), "mismatched lengths must error");
assert_eq!(opt.step_count(), 0);
}
#[test]
fn test_variational_length_mismatch_errors() {
let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.1);
let params = Array1::from_vec(vec![1.0, 2.0]);
let grads = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = opt.step(¶ms, &grads);
assert!(result.is_err());
assert_eq!(opt.step_count(), 0);
}
#[test]
fn test_hybrid_length_mismatch_errors() {
let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 5);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let grads = Array1::from_vec(vec![1.0]);
let result = opt.step(¶ms, &grads);
assert!(result.is_err());
assert_eq!(opt.step_count(), 0);
}
#[test]
fn test_hybrid_crosses_switch_step_without_error() {
let switch_step = 6usize;
let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, switch_step);
let mut params = Array1::from_vec(vec![2.0, -1.0, 0.5]);
for _ in 0..(switch_step + 2) {
let grads = quadratic_grad(¶ms);
params = opt
.step(¶ms, &grads)
.expect("hybrid step across switch succeeds");
assert!(all_finite(¶ms));
}
assert_eq!(opt.step_count(), switch_step + 2);
}
#[test]
fn test_get_state_reflects_learning_rate_and_steps() {
let mut opt = QuantumLearnedOptimizer::<f64>::variational(0.07);
let params = Array1::from_vec(vec![0.5, -0.5, 0.5]);
let grads = quadratic_grad(¶ms);
let _ = opt.step(¶ms, &grads).expect("step succeeds");
let state = opt.get_state();
assert_eq!(state.step_count, 1);
assert!((state.current_lr - 0.07).abs() < 1e-12);
assert!(state.grad_norm_ema >= 0.0);
}
#[test]
fn test_norm_ema_decay_validation() {
let mut opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
assert!(opt.norm_ema_decay() > 0.0 && opt.norm_ema_decay() < 1.0);
assert!(opt.set_norm_ema_decay(0.5).is_ok());
assert!((opt.norm_ema_decay() - 0.5).abs() < 1e-12);
assert!(opt.set_norm_ema_decay(0.0).is_err());
assert!(opt.set_norm_ema_decay(1.0).is_err());
assert!(opt.set_norm_ema_decay(-0.1).is_err());
assert!(opt.set_norm_ema_decay(1.5).is_err());
}
#[test]
fn test_zero_gradient_keeps_norm_ema_zero() {
let mut opt = QuantumLearnedOptimizer::<f64>::hybrid(0.05, 3);
let params = Array1::from_vec(vec![1.0, 1.0, 1.0]);
let grads = Array1::from_vec(vec![0.0, 0.0, 0.0]);
for _ in 0..5 {
let _ = opt.step(¶ms, &grads).expect("zero-grad step succeeds");
}
assert!(opt.get_state().grad_norm_ema.abs() < 1e-15);
}
#[test]
fn test_backend_accessor_matches_name() {
let opt = QuantumLearnedOptimizer::<f64>::annealing(0.05);
match opt.backend() {
QuantumBackend::Annealing(_) => {}
_ => panic!("expected annealing backend"),
}
}
#[test]
fn test_is_send_sync() {
fn assert_send_sync<U: Send + Sync>() {}
assert_send_sync::<QuantumLearnedOptimizer<f64>>();
}
}