1use crate::error::{MLError, Result};
2use scirs2_core::ndarray::{Array1, ArrayView1};
3use std::collections::HashMap;
4use std::fmt;
5
6const SPSA_ALPHA: f64 = 0.602;
9const SPSA_GAMMA: f64 = 0.101;
12
13#[derive(Debug, Clone, Copy)]
15pub enum OptimizationMethod {
16 GradientDescent,
18
19 Adam,
21
22 SPSA,
24
25 LBFGS,
27
28 QuantumNaturalGradient,
30
31 SciRS2Adam,
33
34 SciRS2LBFGS,
36
37 SciRS2CG,
39}
40
41#[derive(Debug, Clone)]
43pub enum Optimizer {
44 GradientDescent {
46 learning_rate: f64,
48 },
49
50 Adam {
52 learning_rate: f64,
54
55 beta1: f64,
57
58 beta2: f64,
60
61 epsilon: f64,
63
64 m: Array1<f64>,
67
68 v: Array1<f64>,
72 },
73
74 SPSA {
76 learning_rate: f64,
78
79 perturbation: f64,
81 },
82
83 QuantumNaturalGradient {
91 learning_rate: f64,
93 regularization: f64,
95 },
96
97 SciRS2 {
102 method: String,
104 config: HashMap<String, f64>,
106 adam_m: Array1<f64>,
108 adam_v: Array1<f64>,
110 lbfgs_history: Vec<(Array1<f64>, Array1<f64>)>,
114 prev_params: Option<Array1<f64>>,
117 prev_gradient: Option<Array1<f64>>,
119 cg_direction: Option<Array1<f64>>,
121 },
122}
123
124impl Optimizer {
125 pub fn new(method: OptimizationMethod) -> Self {
127 match method {
128 OptimizationMethod::GradientDescent => Optimizer::GradientDescent {
129 learning_rate: 0.01,
130 },
131 OptimizationMethod::Adam => Optimizer::Adam {
132 learning_rate: 0.01,
133 beta1: 0.9,
134 beta2: 0.999,
135 epsilon: 1e-8,
136 m: Array1::zeros(0),
137 v: Array1::zeros(0),
138 },
139 OptimizationMethod::SPSA => Optimizer::SPSA {
140 learning_rate: 0.01,
141 perturbation: 0.01,
142 },
143 OptimizationMethod::LBFGS => {
144 Optimizer::Adam {
146 learning_rate: 0.01,
147 beta1: 0.9,
148 beta2: 0.999,
149 epsilon: 1e-8,
150 m: Array1::zeros(0),
151 v: Array1::zeros(0),
152 }
153 }
154 OptimizationMethod::QuantumNaturalGradient => Optimizer::QuantumNaturalGradient {
155 learning_rate: 0.01,
156 regularization: 1e-3,
157 },
158 OptimizationMethod::SciRS2Adam => {
159 let mut config = HashMap::new();
160 config.insert("learning_rate".to_string(), 0.001);
161 config.insert("beta1".to_string(), 0.9);
162 config.insert("beta2".to_string(), 0.999);
163 config.insert("epsilon".to_string(), 1e-8);
164 Optimizer::SciRS2 {
165 method: "adam".to_string(),
166 config,
167 adam_m: Array1::zeros(0),
168 adam_v: Array1::zeros(0),
169 lbfgs_history: Vec::new(),
170 prev_params: None,
171 prev_gradient: None,
172 cg_direction: None,
173 }
174 }
175 OptimizationMethod::SciRS2LBFGS => {
176 let mut config = HashMap::new();
177 config.insert("m".to_string(), 10.0); config.insert("c1".to_string(), 1e-4);
179 config.insert("c2".to_string(), 0.9);
180 config.insert("learning_rate".to_string(), 0.1);
181 Optimizer::SciRS2 {
182 method: "lbfgs".to_string(),
183 config,
184 adam_m: Array1::zeros(0),
185 adam_v: Array1::zeros(0),
186 lbfgs_history: Vec::new(),
187 prev_params: None,
188 prev_gradient: None,
189 cg_direction: None,
190 }
191 }
192 OptimizationMethod::SciRS2CG => {
193 let mut config = HashMap::new();
194 config.insert("beta_method".to_string(), 0.0); config.insert("restart_threshold".to_string(), 100.0);
196 config.insert("learning_rate".to_string(), 0.01);
197 Optimizer::SciRS2 {
198 method: "cg".to_string(),
199 config,
200 adam_m: Array1::zeros(0),
201 adam_v: Array1::zeros(0),
202 lbfgs_history: Vec::new(),
203 prev_params: None,
204 prev_gradient: None,
205 cg_direction: None,
206 }
207 }
208 }
209 }
210
211 pub fn update_parameters(
217 &mut self,
218 parameters: &mut Array1<f64>,
219 gradients: &ArrayView1<f64>,
220 iteration: usize,
221 ) -> Result<()> {
222 match self {
223 Optimizer::GradientDescent { learning_rate } => {
224 for i in 0..parameters.len() {
226 parameters[i] -= *learning_rate * gradients[i];
227 }
228 Ok(())
229 }
230 Optimizer::Adam {
231 learning_rate,
232 beta1,
233 beta2,
234 epsilon,
235 m,
236 v,
237 } => {
238 Self::adam_update(
239 parameters,
240 gradients,
241 iteration,
242 *learning_rate,
243 *beta1,
244 *beta2,
245 *epsilon,
246 m,
247 v,
248 );
249 Ok(())
250 }
251 Optimizer::SPSA {
252 learning_rate,
253 perturbation,
254 } => {
255 let k = iteration as f64 + 1.0;
263 let a_k = *learning_rate / k.powf(SPSA_ALPHA);
264 let c_k = *perturbation / k.powf(SPSA_GAMMA);
265 let damping = 1.0 + c_k;
266 let step = a_k / damping;
267 for i in 0..parameters.len() {
268 parameters[i] -= step * gradients[i];
269 }
270 Ok(())
271 }
272 Optimizer::QuantumNaturalGradient {
273 learning_rate,
274 regularization,
275 } => {
276 let damp = 1.0 + *regularization;
280 for i in 0..parameters.len() {
281 parameters[i] -= *learning_rate * gradients[i] / damp;
282 }
283 Ok(())
284 }
285 Optimizer::SciRS2 {
286 method,
287 config,
288 adam_m,
289 adam_v,
290 lbfgs_history,
291 prev_params,
292 prev_gradient,
293 cg_direction,
294 } => match method.as_str() {
295 "adam" => {
296 let learning_rate = config.get("learning_rate").copied().unwrap_or(0.001);
297 let beta1 = config.get("beta1").copied().unwrap_or(0.9);
298 let beta2 = config.get("beta2").copied().unwrap_or(0.999);
299 let epsilon = config.get("epsilon").copied().unwrap_or(1e-8);
300 Self::adam_update(
301 parameters,
302 gradients,
303 iteration,
304 learning_rate,
305 beta1,
306 beta2,
307 epsilon,
308 adam_m,
309 adam_v,
310 );
311 Ok(())
312 }
313 "lbfgs" => {
314 Self::lbfgs_update(
315 parameters,
316 gradients,
317 config,
318 lbfgs_history,
319 prev_params,
320 prev_gradient,
321 );
322 Ok(())
323 }
324 "cg" => {
325 Self::cg_update(
326 parameters,
327 gradients,
328 iteration,
329 config,
330 prev_params,
331 prev_gradient,
332 cg_direction,
333 );
334 Ok(())
335 }
336 _ => Err(MLError::InvalidConfiguration(format!(
337 "Unknown SciRS2 optimizer method: {}",
338 method
339 ))),
340 },
341 }
342 }
343
344 #[allow(clippy::too_many_arguments)]
347 fn adam_update(
348 parameters: &mut Array1<f64>,
349 gradients: &ArrayView1<f64>,
350 iteration: usize,
351 learning_rate: f64,
352 beta1: f64,
353 beta2: f64,
354 epsilon: f64,
355 m: &mut Array1<f64>,
356 v: &mut Array1<f64>,
357 ) {
358 let n = parameters.len();
359 if m.len() != n {
360 *m = Array1::zeros(n);
361 *v = Array1::zeros(n);
362 }
363 let t = iteration as f64 + 1.0;
364 let bias_correction1 = 1.0 - beta1.powf(t);
365 let bias_correction2 = 1.0 - beta2.powf(t);
366 for i in 0..n {
367 m[i] = beta1 * m[i] + (1.0 - beta1) * gradients[i];
368 v[i] = beta2 * v[i] + (1.0 - beta2) * gradients[i] * gradients[i];
369 let m_hat = m[i] / bias_correction1;
370 let v_hat = v[i] / bias_correction2;
371 parameters[i] -= learning_rate * m_hat / (v_hat.sqrt() + epsilon);
372 }
373 }
374
375 #[allow(clippy::too_many_arguments)]
380 fn cg_update(
381 parameters: &mut Array1<f64>,
382 gradients: &ArrayView1<f64>,
383 iteration: usize,
384 config: &HashMap<String, f64>,
385 prev_params: &mut Option<Array1<f64>>,
386 prev_gradient: &mut Option<Array1<f64>>,
387 cg_direction: &mut Option<Array1<f64>>,
388 ) {
389 let n = parameters.len();
390 let learning_rate = config.get("learning_rate").copied().unwrap_or(0.01);
391 let beta_method = config.get("beta_method").copied().unwrap_or(0.0);
392 let restart_threshold = config
393 .get("restart_threshold")
394 .copied()
395 .unwrap_or(100.0)
396 .max(1.0) as usize;
397
398 let theta_t = parameters.clone();
403 let restart =
404 prev_gradient.is_none() || cg_direction.is_none() || iteration % restart_threshold == 0;
405
406 let direction = if !restart {
407 let prev_g = prev_gradient.as_ref().expect("checked Some above");
408 let prev_d = cg_direction.as_ref().expect("checked Some above");
409 let denom: f64 = prev_g.iter().map(|g| g * g).sum();
410 let beta = if denom.abs() > 1e-15 {
411 if beta_method < 0.5 {
412 let numer: f64 = gradients.iter().map(|g| g * g).sum();
414 (numer / denom).max(0.0)
415 } else {
416 let numer: f64 = gradients
419 .iter()
420 .zip(prev_g.iter())
421 .map(|(g, gp)| g * (g - gp))
422 .sum();
423 (numer / denom).max(0.0)
424 }
425 } else {
426 0.0
427 };
428 let mut d = Array1::zeros(n);
429 for i in 0..n {
430 d[i] = -gradients[i] + beta * prev_d[i];
431 }
432 d
433 } else {
434 let mut d = Array1::zeros(n);
435 for i in 0..n {
436 d[i] = -gradients[i];
437 }
438 d
439 };
440
441 for i in 0..n {
442 parameters[i] += learning_rate * direction[i];
443 }
444
445 *prev_params = Some(theta_t);
446 *prev_gradient = Some(gradients.to_owned());
447 *cg_direction = Some(direction);
448 }
449
450 fn lbfgs_update(
455 parameters: &mut Array1<f64>,
456 gradients: &ArrayView1<f64>,
457 config: &HashMap<String, f64>,
458 history: &mut Vec<(Array1<f64>, Array1<f64>)>,
459 prev_params: &mut Option<Array1<f64>>,
460 prev_gradient: &mut Option<Array1<f64>>,
461 ) {
462 let n = parameters.len();
463 let memory = config.get("m").copied().unwrap_or(10.0).max(1.0) as usize;
464 let learning_rate = config.get("learning_rate").copied().unwrap_or(0.1);
465
466 let theta_t = parameters.clone();
470
471 if let (Some(prev_p), Some(prev_g)) = (prev_params.as_ref(), prev_gradient.as_ref()) {
473 let s = &theta_t - prev_p;
474 let y = gradients.to_owned() - prev_g;
475 let sy: f64 = s.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
476 if sy > 1e-10 {
479 history.push((s, y));
480 while history.len() > memory {
481 history.remove(0);
482 }
483 }
484 }
485
486 let mut q = gradients.to_owned();
488 let mut alphas = Vec::with_capacity(history.len());
489 for (s, y) in history.iter().rev() {
490 let sy: f64 = s.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
491 let rho = 1.0 / sy;
492 let alpha = rho * s.iter().zip(q.iter()).map(|(a, b)| a * b).sum::<f64>();
493 for i in 0..n {
494 q[i] -= alpha * y[i];
495 }
496 alphas.push((rho, alpha));
497 }
498
499 let gamma = match history.last() {
500 Some((s, y)) => {
501 let sy: f64 = s.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
502 let yy: f64 = y.iter().map(|v| v * v).sum();
503 if yy.abs() > 1e-15 {
504 sy / yy
505 } else {
506 1.0
507 }
508 }
509 None => 1.0,
510 };
511 let mut r = q.mapv(|x| x * gamma);
512
513 for (idx, (s, y)) in history.iter().enumerate() {
514 let (rho, alpha) = alphas[history.len() - 1 - idx];
515 let beta = rho * y.iter().zip(r.iter()).map(|(a, b)| a * b).sum::<f64>();
516 for i in 0..n {
517 r[i] += s[i] * (alpha - beta);
518 }
519 }
520
521 for i in 0..n {
523 parameters[i] -= learning_rate * r[i];
524 }
525
526 *prev_params = Some(theta_t);
527 *prev_gradient = Some(gradients.to_owned());
528 }
529}
530
531pub trait ObjectiveFunction {
533 fn evaluate(&self, parameters: &ArrayView1<f64>) -> Result<f64>;
535
536 fn gradient(&self, parameters: &ArrayView1<f64>) -> Result<Array1<f64>> {
538 let epsilon = 1e-6;
540 let n = parameters.len();
541 let mut gradient = Array1::zeros(n);
542
543 let f0 = self.evaluate(parameters)?;
544
545 for i in 0..n {
546 let mut params_plus = parameters.to_owned();
547 params_plus[i] += epsilon;
548
549 let f_plus = self.evaluate(¶ms_plus.view())?;
550
551 gradient[i] = (f_plus - f0) / epsilon;
552 }
553
554 Ok(gradient)
555 }
556}
557
558impl fmt::Display for OptimizationMethod {
559 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560 match self {
561 OptimizationMethod::GradientDescent => write!(f, "Gradient Descent"),
562 OptimizationMethod::Adam => write!(f, "Adam"),
563 OptimizationMethod::SPSA => write!(f, "SPSA"),
564 OptimizationMethod::LBFGS => write!(f, "L-BFGS"),
565 OptimizationMethod::QuantumNaturalGradient => write!(f, "Quantum Natural Gradient"),
566 OptimizationMethod::SciRS2Adam => write!(f, "SciRS2 Adam"),
567 OptimizationMethod::SciRS2LBFGS => write!(f, "SciRS2 L-BFGS"),
568 OptimizationMethod::SciRS2CG => write!(f, "SciRS2 Conjugate Gradient"),
569 }
570 }
571}
572
573impl fmt::Display for Optimizer {
574 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575 match self {
576 Optimizer::GradientDescent { learning_rate } => {
577 write!(f, "Gradient Descent (learning_rate: {})", learning_rate)
578 }
579 Optimizer::Adam {
580 learning_rate,
581 beta1,
582 beta2,
583 epsilon,
584 ..
585 } => {
586 write!(
587 f,
588 "Adam (learning_rate: {}, beta1: {}, beta2: {}, epsilon: {})",
589 learning_rate, beta1, beta2, epsilon
590 )
591 }
592 Optimizer::SPSA {
593 learning_rate,
594 perturbation,
595 } => {
596 write!(
597 f,
598 "SPSA (learning_rate: {}, perturbation: {})",
599 learning_rate, perturbation
600 )
601 }
602 Optimizer::QuantumNaturalGradient {
603 learning_rate,
604 regularization,
605 } => {
606 write!(
607 f,
608 "Quantum Natural Gradient (learning_rate: {}, regularization: {})",
609 learning_rate, regularization
610 )
611 }
612 Optimizer::SciRS2 { method, config, .. } => {
613 write!(f, "SciRS2 {} with config: {:?}", method, config)
614 }
615 }
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622
623 #[test]
630 fn test_adam_is_not_plain_gradient_descent() {
631 let mut optimizer = Optimizer::new(OptimizationMethod::Adam);
632 let mut params = Array1::from_vec(vec![1.0, 1.0]);
633 let gradients = Array1::from_vec(vec![10.0, 10.0]);
634
635 optimizer
636 .update_parameters(&mut params, &gradients.view(), 0)
637 .expect("Adam update should succeed");
638
639 let plain_gd_step = 0.01 * 10.0;
642 let actual_step = 1.0 - params[0];
643 assert!(
644 (actual_step - plain_gd_step).abs() > 1e-3,
645 "Adam step ({actual_step}) should differ from plain GD step ({plain_gd_step})"
646 );
647 assert!((actual_step - 0.01).abs() < 1e-3);
650 }
651
652 #[test]
653 fn test_adam_moments_persist_across_calls() {
654 let mut warmed = Optimizer::new(OptimizationMethod::Adam);
663 let mut params_warm = Array1::from_vec(vec![0.0]);
664 let g1 = Array1::from_vec(vec![1.0]);
665 warmed
666 .update_parameters(&mut params_warm, &g1.view(), 0)
667 .expect("update 1");
668 let g2 = Array1::from_vec(vec![-1.0]);
669 let before_second = params_warm[0];
670 warmed
671 .update_parameters(&mut params_warm, &g2.view(), 1)
672 .expect("update 2");
673 let warmed_step = params_warm[0] - before_second;
674
675 let mut fresh = Optimizer::new(OptimizationMethod::Adam);
676 let mut params_fresh = Array1::from_vec(vec![0.0]);
677 fresh
678 .update_parameters(&mut params_fresh, &g2.view(), 1)
679 .expect("fresh update");
680 let fresh_step = params_fresh[0];
681
682 assert!(
683 (warmed_step - fresh_step).abs() > 1e-3,
684 "step with accumulated momentum ({warmed_step}) should differ substantially from a \
685 fresh optimizer's step ({fresh_step}) given the same gradient/iteration"
686 );
687 }
688
689 #[test]
690 fn test_spsa_uses_decaying_gain_sequence() {
691 let mut optimizer = Optimizer::new(OptimizationMethod::SPSA);
695 let gradients = Array1::from_vec(vec![1.0]);
696
697 let mut params_early = Array1::from_vec(vec![0.0]);
698 optimizer
699 .update_parameters(&mut params_early, &gradients.view(), 0)
700 .expect("update at iteration 0");
701
702 let mut optimizer2 = Optimizer::new(OptimizationMethod::SPSA);
703 let mut params_late = Array1::from_vec(vec![0.0]);
704 optimizer2
705 .update_parameters(&mut params_late, &gradients.view(), 1000)
706 .expect("update at iteration 1000");
707
708 assert!(
709 params_early[0].abs() > params_late[0].abs(),
710 "SPSA step at iteration 0 ({}) should be larger than at iteration 1000 ({})",
711 params_early[0].abs(),
712 params_late[0].abs()
713 );
714 }
715
716 #[test]
717 fn test_scirs2_cg_direction_differs_from_gradient_descent() {
718 let mut optimizer = Optimizer::new(OptimizationMethod::SciRS2CG);
722 let mut params = Array1::from_vec(vec![1.0, 1.0]);
723
724 let g1 = Array1::from_vec(vec![1.0, 0.0]);
726 optimizer
727 .update_parameters(&mut params, &g1.view(), 0)
728 .expect("cg update 1");
729 let after_first = params.clone();
730
731 let g2 = Array1::from_vec(vec![0.5, 0.5]);
734 optimizer
735 .update_parameters(&mut params, &g2.view(), 1)
736 .expect("cg update 2");
737
738 let learning_rate = 0.01;
739 let plain_steepest_descent_step = -learning_rate * g2[0];
740 let actual_step = params[0] - after_first[0];
741 assert!(
742 (actual_step - plain_steepest_descent_step).abs() > 1e-8,
743 "CG step ({actual_step}) should differ from plain steepest descent ({plain_steepest_descent_step})"
744 );
745 }
746
747 #[test]
748 fn test_scirs2_lbfgs_uses_curvature_history() {
749 let mut optimizer = Optimizer::new(OptimizationMethod::SciRS2LBFGS);
753 let mut params = Array1::from_vec(vec![2.0, -1.0]);
754
755 let g1 = Array1::from_vec(vec![2.0, -1.0]);
756 optimizer
757 .update_parameters(&mut params, &g1.view(), 0)
758 .expect("lbfgs update 1");
759
760 let g2 = Array1::from_vec(vec![1.0, -0.5]);
761 let before_second = params.clone();
762 optimizer
763 .update_parameters(&mut params, &g2.view(), 1)
764 .expect("lbfgs update 2");
765
766 let learning_rate = 0.1;
770 let naive_step = -learning_rate * g2[0];
771 let actual_step = params[0] - before_second[0];
772 assert!(
773 (actual_step - naive_step).abs() > 1e-8,
774 "L-BFGS step ({actual_step}) should differ from a naive scaled-gradient step ({naive_step})"
775 );
776 }
777
778 #[test]
779 fn test_quantum_natural_gradient_damping_unchanged() {
780 let mut optimizer = Optimizer::QuantumNaturalGradient {
783 learning_rate: 0.1,
784 regularization: 1.0,
785 };
786 let mut params = Array1::from_vec(vec![1.0]);
787 let gradients = Array1::from_vec(vec![2.0]);
788 optimizer
789 .update_parameters(&mut params, &gradients.view(), 0)
790 .expect("QNG update");
791 assert!((params[0] - 0.9).abs() < 1e-9);
793 }
794}