1use std::fmt;
4
5use ndarray::{Array1, Array2};
6use num_complex::Complex64;
7
8const DEFAULT_TOLERANCE: f64 = 1.0e-12;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum OptimizationError {
13 EmptyInput,
15 DimensionMismatch,
17 NonFiniteInput,
19 InvalidConfig,
21 MaxIterationsExceeded,
23}
24
25impl fmt::Display for OptimizationError {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 OptimizationError::EmptyInput => write!(f, "input cannot be empty"),
29 OptimizationError::DimensionMismatch => write!(f, "input dimensions are incompatible"),
30 OptimizationError::NonFiniteInput => write!(f, "input contains non-finite values"),
31 OptimizationError::InvalidConfig => write!(f, "invalid optimizer configuration"),
32 OptimizationError::MaxIterationsExceeded => write!(f, "maximum iterations exceeded"),
33 }
34 }
35}
36
37impl std::error::Error for OptimizationError {}
38
39#[derive(Debug, Clone, Copy)]
41pub struct LineSearchConfig {
42 pub initial_step: f64,
44 pub contraction: f64,
46 pub sufficient_decrease: f64,
48 pub max_iterations: usize,
50}
51
52impl Default for LineSearchConfig {
53 fn default() -> Self {
54 Self {
55 initial_step: 1.0,
56 contraction: 0.5,
57 sufficient_decrease: 1e-4,
58 max_iterations: 64,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy)]
65pub struct SGDConfig {
66 pub learning_rate: f64,
68 pub max_iterations: usize,
70 pub tolerance: f64,
72}
73
74impl Default for SGDConfig {
75 fn default() -> Self { Self { learning_rate: 1e-2, max_iterations: 10_000, tolerance: 1e-8 } }
76}
77
78#[derive(Debug, Clone, Copy)]
80pub struct AdamConfig {
81 pub learning_rate: f64,
83 pub beta1: f64,
85 pub beta2: f64,
87 pub epsilon: f64,
89 pub max_iterations: usize,
91 pub tolerance: f64,
93}
94
95impl Default for AdamConfig {
96 fn default() -> Self {
97 Self {
98 learning_rate: 1e-2,
99 beta1: 0.9,
100 beta2: 0.999,
101 epsilon: 1e-8,
102 max_iterations: 10_000,
103 tolerance: 1e-8,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy)]
110pub struct MomentumConfig {
111 pub learning_rate: f64,
113 pub momentum: f64,
115 pub max_iterations: usize,
117 pub tolerance: f64,
119}
120
121impl Default for MomentumConfig {
122 fn default() -> Self {
123 Self {
124 learning_rate: 1e-2,
125 momentum: 0.9,
126 max_iterations: 10_000,
127 tolerance: 1e-8,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy)]
134pub struct RMSPropConfig {
135 pub learning_rate: f64,
137 pub rho: f64,
139 pub epsilon: f64,
141 pub max_iterations: usize,
143 pub tolerance: f64,
145}
146
147impl Default for RMSPropConfig {
148 fn default() -> Self {
149 Self {
150 learning_rate: 1e-2,
151 rho: 0.9,
152 epsilon: 1e-8,
153 max_iterations: 10_000,
154 tolerance: 1e-8,
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy)]
161pub struct ProjectedGradientConfig {
162 pub learning_rate: f64,
164 pub max_iterations: usize,
166 pub tolerance: f64,
168}
169
170impl Default for ProjectedGradientConfig {
171 fn default() -> Self { Self { learning_rate: 1e-2, max_iterations: 10_000, tolerance: 1e-8 } }
172}
173
174#[derive(Debug, Clone, Copy)]
176pub struct BFGSConfig {
177 pub step_size: f64,
179 pub max_iterations: usize,
181 pub tolerance: f64,
183 pub curvature_tolerance: f64,
185}
186
187impl Default for BFGSConfig {
188 fn default() -> Self {
189 Self {
190 step_size: 1.0,
191 max_iterations: 2_000,
192 tolerance: 1e-8,
193 curvature_tolerance: 1e-12,
194 }
195 }
196}
197
198fn l2_norm(vector: &Array1<f64>) -> f64 {
199 vector.iter().map(|value| value * value).sum::<f64>().sqrt()
200}
201
202fn l2_norm_complex(vector: &Array1<Complex64>) -> f64 {
203 vector.iter().map(Complex64::norm_sqr).sum::<f64>().sqrt()
204}
205
206fn validate_vector(vector: &Array1<f64>) -> Result<(), OptimizationError> {
207 if vector.is_empty() {
208 return Err(OptimizationError::EmptyInput);
209 }
210 if vector.iter().any(|value| !value.is_finite()) {
211 return Err(OptimizationError::NonFiniteInput);
212 }
213 Ok(())
214}
215
216fn validate_vector_complex(vector: &Array1<Complex64>) -> Result<(), OptimizationError> {
217 if vector.is_empty() {
218 return Err(OptimizationError::EmptyInput);
219 }
220 if vector.iter().any(|value| !value.re.is_finite() || !value.im.is_finite()) {
221 return Err(OptimizationError::NonFiniteInput);
222 }
223 Ok(())
224}
225
226fn validate_line_search_config(config: &LineSearchConfig) -> Result<(), OptimizationError> {
227 if config.initial_step <= 0.0
228 || !(0.0..1.0).contains(&config.contraction)
229 || !(0.0..1.0).contains(&config.sufficient_decrease)
230 || config.max_iterations == 0
231 {
232 return Err(OptimizationError::InvalidConfig);
233 }
234 Ok(())
235}
236
237fn validate_sgd_config(config: &SGDConfig) -> Result<(), OptimizationError> {
238 if config.learning_rate <= 0.0 || config.max_iterations == 0 || config.tolerance < 0.0 {
239 return Err(OptimizationError::InvalidConfig);
240 }
241 Ok(())
242}
243
244fn validate_adam_config(config: &AdamConfig) -> Result<(), OptimizationError> {
245 if config.learning_rate <= 0.0
246 || !(0.0..1.0).contains(&config.beta1)
247 || !(0.0..1.0).contains(&config.beta2)
248 || config.epsilon <= 0.0
249 || config.max_iterations == 0
250 || config.tolerance < 0.0
251 {
252 return Err(OptimizationError::InvalidConfig);
253 }
254 Ok(())
255}
256
257fn validate_momentum_config(config: &MomentumConfig) -> Result<(), OptimizationError> {
258 if config.learning_rate <= 0.0
259 || !(0.0..1.0).contains(&config.momentum)
260 || config.max_iterations == 0
261 || config.tolerance < 0.0
262 {
263 return Err(OptimizationError::InvalidConfig);
264 }
265 Ok(())
266}
267
268fn validate_rmsprop_config(config: &RMSPropConfig) -> Result<(), OptimizationError> {
269 if config.learning_rate <= 0.0
270 || !(0.0..1.0).contains(&config.rho)
271 || config.epsilon <= 0.0
272 || config.max_iterations == 0
273 || config.tolerance < 0.0
274 {
275 return Err(OptimizationError::InvalidConfig);
276 }
277 Ok(())
278}
279
280fn validate_projected_gradient_config(
281 config: &ProjectedGradientConfig,
282) -> Result<(), OptimizationError> {
283 if config.learning_rate <= 0.0 || config.max_iterations == 0 || config.tolerance < 0.0 {
284 return Err(OptimizationError::InvalidConfig);
285 }
286 Ok(())
287}
288
289fn validate_bfgs_config(config: &BFGSConfig) -> Result<(), OptimizationError> {
290 if config.step_size <= 0.0
291 || config.max_iterations == 0
292 || config.tolerance < 0.0
293 || config.curvature_tolerance <= 0.0
294 {
295 return Err(OptimizationError::InvalidConfig);
296 }
297 Ok(())
298}
299
300fn validate_bounds(
301 initial: &Array1<f64>,
302 lower_bounds: &Array1<f64>,
303 upper_bounds: &Array1<f64>,
304) -> Result<(), OptimizationError> {
305 if initial.len() != lower_bounds.len() || initial.len() != upper_bounds.len() {
306 return Err(OptimizationError::DimensionMismatch);
307 }
308 for i in 0..initial.len() {
309 if !lower_bounds[i].is_finite()
310 || !upper_bounds[i].is_finite()
311 || lower_bounds[i] > upper_bounds[i]
312 {
313 return Err(OptimizationError::InvalidConfig);
314 }
315 }
316 Ok(())
317}
318
319fn validate_bounds_complex(
320 initial: &Array1<Complex64>,
321 lower_bounds: &Array1<Complex64>,
322 upper_bounds: &Array1<Complex64>,
323) -> Result<(), OptimizationError> {
324 if initial.len() != lower_bounds.len() || initial.len() != upper_bounds.len() {
325 return Err(OptimizationError::DimensionMismatch);
326 }
327 for i in 0..initial.len() {
328 let lower = lower_bounds[i];
329 let upper = upper_bounds[i];
330 if !lower.re.is_finite()
331 || !lower.im.is_finite()
332 || !upper.re.is_finite()
333 || !upper.im.is_finite()
334 || lower.re > upper.re
335 || lower.im > upper.im
336 {
337 return Err(OptimizationError::InvalidConfig);
338 }
339 }
340 Ok(())
341}
342
343fn project_to_bounds(
344 point: &mut Array1<f64>,
345 lower_bounds: &Array1<f64>,
346 upper_bounds: &Array1<f64>,
347) {
348 for i in 0..point.len() {
349 point[i] = point[i].clamp(lower_bounds[i], upper_bounds[i]);
350 }
351}
352
353fn project_to_bounds_complex(
354 point: &mut Array1<Complex64>,
355 lower_bounds: &Array1<Complex64>,
356 upper_bounds: &Array1<Complex64>,
357) {
358 for i in 0..point.len() {
359 let real = point[i].re.clamp(lower_bounds[i].re, upper_bounds[i].re);
360 let imag = point[i].im.clamp(lower_bounds[i].im, upper_bounds[i].im);
361 point[i] = Complex64::new(real, imag);
362 }
363}
364
365fn outer_product(left: &Array1<f64>, right: &Array1<f64>) -> Array2<f64> {
366 let mut output = Array2::<f64>::zeros((left.len(), right.len()));
367 for row in 0..left.len() {
368 for col in 0..right.len() {
369 output[[row, col]] = left[row] * right[col];
370 }
371 }
372 output
373}
374
375fn outer_product_complex(left: &Array1<Complex64>, right: &Array1<Complex64>) -> Array2<Complex64> {
376 let mut output = Array2::<Complex64>::zeros((left.len(), right.len()));
377 for row in 0..left.len() {
378 for col in 0..right.len() {
379 output[[row, col]] = left[row] * right[col].conj();
380 }
381 }
382 output
383}
384
385fn hermitian_transpose(matrix: &Array2<Complex64>) -> Array2<Complex64> {
386 matrix.t().mapv(|value| value.conj())
387}
388
389fn hermitian_dot(left: &Array1<Complex64>, right: &Array1<Complex64>) -> Complex64 {
390 left.iter().zip(right.iter()).map(|(lhs, rhs)| lhs.conj() * rhs).sum()
391}
392
393pub fn backtracking_line_search<F, G>(
398 point: &Array1<f64>,
399 direction: &Array1<f64>,
400 objective: F,
401 gradient: G,
402 config: &LineSearchConfig,
403) -> Result<f64, OptimizationError>
404where
405 F: Fn(&Array1<f64>) -> f64,
406 G: Fn(&Array1<f64>) -> Array1<f64>,
407{
408 validate_vector(point)?;
409 validate_vector(direction)?;
410 if point.len() != direction.len() {
411 return Err(OptimizationError::DimensionMismatch);
412 }
413 validate_line_search_config(config)?;
414
415 let grad = gradient(point);
416 if grad.len() != point.len() || grad.iter().any(|value| !value.is_finite()) {
417 return Err(OptimizationError::NonFiniteInput);
418 }
419
420 let fx = objective(point);
421 if !fx.is_finite() {
422 return Err(OptimizationError::NonFiniteInput);
423 }
424 let directional_derivative = grad.dot(direction);
425
426 let mut alpha = config.initial_step;
427 for _ in 0..config.max_iterations {
428 let candidate = point + &direction.mapv(|value| value * alpha);
429 let candidate_value = objective(&candidate);
430 if !candidate_value.is_finite() {
431 return Err(OptimizationError::NonFiniteInput);
432 }
433 if candidate_value <= fx + config.sufficient_decrease * alpha * directional_derivative {
434 return Ok(alpha);
435 }
436 alpha *= config.contraction;
437 }
438 Err(OptimizationError::MaxIterationsExceeded)
439}
440
441pub fn gradient_descent<F, G>(
446 initial: &Array1<f64>,
447 objective: F,
448 gradient: G,
449 config: &SGDConfig,
450) -> Result<Array1<f64>, OptimizationError>
451where
452 F: Fn(&Array1<f64>) -> f64,
453 G: Fn(&Array1<f64>) -> Array1<f64>,
454{
455 validate_vector(initial)?;
456 validate_sgd_config(config)?;
457
458 let mut x = initial.clone();
459 let _ = objective(&x);
460 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
461
462 for _ in 0..config.max_iterations {
463 let grad = gradient(&x);
464 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
465 return Err(OptimizationError::NonFiniteInput);
466 }
467 if l2_norm(&grad) <= tolerance {
468 return Ok(x);
469 }
470 x = &x - &grad.mapv(|value| value * config.learning_rate);
471 }
472
473 Err(OptimizationError::MaxIterationsExceeded)
474}
475
476pub fn adam<F, G>(
481 initial: &Array1<f64>,
482 objective: F,
483 gradient: G,
484 config: &AdamConfig,
485) -> Result<Array1<f64>, OptimizationError>
486where
487 F: Fn(&Array1<f64>) -> f64,
488 G: Fn(&Array1<f64>) -> Array1<f64>,
489{
490 validate_vector(initial)?;
491 validate_adam_config(config)?;
492
493 let mut x = initial.clone();
494 let mut m = Array1::<f64>::zeros(x.len());
495 let mut v = Array1::<f64>::zeros(x.len());
496 let mut beta1_power = 1.0_f64;
497 let mut beta2_power = 1.0_f64;
498 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
499
500 let _ = objective(&x);
501 for _ in 0..config.max_iterations {
502 let grad = gradient(&x);
503 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
504 return Err(OptimizationError::NonFiniteInput);
505 }
506 if l2_norm(&grad) <= tolerance {
507 return Ok(x);
508 }
509
510 beta1_power *= config.beta1;
511 beta2_power *= config.beta2;
512
513 for i in 0..x.len() {
514 m[i] = config.beta1 * m[i] + (1.0 - config.beta1) * grad[i];
515 v[i] = config.beta2 * v[i] + (1.0 - config.beta2) * grad[i] * grad[i];
516
517 let m_hat = m[i] / (1.0 - beta1_power);
518 let v_hat = v[i] / (1.0 - beta2_power);
519 x[i] -= config.learning_rate * m_hat / (v_hat.sqrt() + config.epsilon);
520 }
521 }
522
523 Err(OptimizationError::MaxIterationsExceeded)
524}
525
526pub fn momentum_descent<F, G>(
531 initial: &Array1<f64>,
532 objective: F,
533 gradient: G,
534 config: &MomentumConfig,
535) -> Result<Array1<f64>, OptimizationError>
536where
537 F: Fn(&Array1<f64>) -> f64,
538 G: Fn(&Array1<f64>) -> Array1<f64>,
539{
540 validate_vector(initial)?;
541 validate_momentum_config(config)?;
542
543 let mut x = initial.clone();
544 let mut velocity = Array1::<f64>::zeros(x.len());
545 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
546
547 let _ = objective(&x);
548 for _ in 0..config.max_iterations {
549 let grad = gradient(&x);
550 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
551 return Err(OptimizationError::NonFiniteInput);
552 }
553 if l2_norm(&grad) <= tolerance {
554 return Ok(x);
555 }
556
557 for i in 0..x.len() {
558 velocity[i] = config.momentum * velocity[i] + grad[i];
559 x[i] -= config.learning_rate * velocity[i];
560 }
561 }
562
563 Err(OptimizationError::MaxIterationsExceeded)
564}
565
566pub fn rmsprop<F, G>(
571 initial: &Array1<f64>,
572 objective: F,
573 gradient: G,
574 config: &RMSPropConfig,
575) -> Result<Array1<f64>, OptimizationError>
576where
577 F: Fn(&Array1<f64>) -> f64,
578 G: Fn(&Array1<f64>) -> Array1<f64>,
579{
580 validate_vector(initial)?;
581 validate_rmsprop_config(config)?;
582
583 let mut x = initial.clone();
584 let mut avg_sq = Array1::<f64>::zeros(x.len());
585 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
586
587 let _ = objective(&x);
588 for _ in 0..config.max_iterations {
589 let grad = gradient(&x);
590 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
591 return Err(OptimizationError::NonFiniteInput);
592 }
593 if l2_norm(&grad) <= tolerance {
594 return Ok(x);
595 }
596
597 for i in 0..x.len() {
598 avg_sq[i] = config.rho * avg_sq[i] + (1.0 - config.rho) * grad[i] * grad[i];
599 x[i] -= config.learning_rate * grad[i] / (avg_sq[i].sqrt() + config.epsilon);
600 }
601 }
602
603 Err(OptimizationError::MaxIterationsExceeded)
604}
605
606pub fn projected_gradient_descent_box<F, G>(
611 initial: &Array1<f64>,
612 objective: F,
613 gradient: G,
614 lower_bounds: &Array1<f64>,
615 upper_bounds: &Array1<f64>,
616 config: &ProjectedGradientConfig,
617) -> Result<Array1<f64>, OptimizationError>
618where
619 F: Fn(&Array1<f64>) -> f64,
620 G: Fn(&Array1<f64>) -> Array1<f64>,
621{
622 validate_vector(initial)?;
623 validate_vector(lower_bounds)?;
624 validate_vector(upper_bounds)?;
625 validate_projected_gradient_config(config)?;
626 validate_bounds(initial, lower_bounds, upper_bounds)?;
627
628 let mut x = initial.clone();
629 project_to_bounds(&mut x, lower_bounds, upper_bounds);
630 let _ = objective(&x);
631 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
632
633 for _ in 0..config.max_iterations {
634 let grad = gradient(&x);
635 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
636 return Err(OptimizationError::NonFiniteInput);
637 }
638 let previous = x.clone();
639 x = &x - &grad.mapv(|value| value * config.learning_rate);
640 project_to_bounds(&mut x, lower_bounds, upper_bounds);
641 let step_norm = l2_norm(&(&x - &previous));
642 if step_norm <= tolerance || l2_norm(&grad) <= tolerance {
643 return Ok(x);
644 }
645 }
646
647 Err(OptimizationError::MaxIterationsExceeded)
648}
649
650pub fn stochastic_gradient_descent<G>(
657 initial: &Array1<f64>,
658 stochastic_gradient: G,
659 config: &SGDConfig,
660) -> Result<Array1<f64>, OptimizationError>
661where
662 G: Fn(&Array1<f64>, usize) -> Array1<f64>,
663{
664 validate_vector(initial)?;
665 validate_sgd_config(config)?;
666
667 let mut x = initial.clone();
668 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
669 for iteration in 0..config.max_iterations {
670 let grad = stochastic_gradient(&x, iteration);
671 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
672 return Err(OptimizationError::NonFiniteInput);
673 }
674 if l2_norm(&grad) <= tolerance {
675 return Ok(x);
676 }
677 x = &x - &grad.mapv(|value| value * config.learning_rate);
678 }
679 Err(OptimizationError::MaxIterationsExceeded)
680}
681
682pub fn bfgs<F, G>(
687 initial: &Array1<f64>,
688 objective: F,
689 gradient: G,
690 config: &BFGSConfig,
691) -> Result<Array1<f64>, OptimizationError>
692where
693 F: Fn(&Array1<f64>) -> f64,
694 G: Fn(&Array1<f64>) -> Array1<f64>,
695{
696 validate_vector(initial)?;
697 validate_bfgs_config(config)?;
698
699 let dimension = initial.len();
700 let mut x = initial.clone();
701 let mut h_inv = Array2::<f64>::eye(dimension);
702 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
703
704 let _ = objective(&x);
705 for _ in 0..config.max_iterations {
706 let grad = gradient(&x);
707 if grad.len() != x.len() || grad.iter().any(|value| !value.is_finite()) {
708 return Err(OptimizationError::NonFiniteInput);
709 }
710 if l2_norm(&grad) <= tolerance {
711 return Ok(x);
712 }
713
714 let direction = -h_inv.dot(&grad);
715 let step = config.step_size;
716 let x_next = &x + &direction.mapv(|value| value * step);
717 let grad_next = gradient(&x_next);
718 if grad_next.len() != x.len() || grad_next.iter().any(|value| !value.is_finite()) {
719 return Err(OptimizationError::NonFiniteInput);
720 }
721
722 let s = &x_next - &x;
723 let y = &grad_next - &grad;
724 let ys = y.dot(&s);
725 if ys.abs() > config.curvature_tolerance {
726 let rho = 1.0 / ys;
727 let identity = Array2::<f64>::eye(dimension);
728 let sy = outer_product(&s, &y);
729 let ys_outer = outer_product(&y, &s);
730 let ss = outer_product(&s, &s);
731
732 let left = &identity - &(rho * sy);
733 let right = &identity - &(rho * ys_outer);
734 h_inv = left.dot(&h_inv).dot(&right) + rho * ss;
735 }
736
737 x = x_next;
738 }
739
740 Err(OptimizationError::MaxIterationsExceeded)
741}
742
743pub fn backtracking_line_search_complex<F, G>(
748 point: &Array1<Complex64>,
749 direction: &Array1<Complex64>,
750 objective: F,
751 gradient: G,
752 config: &LineSearchConfig,
753) -> Result<f64, OptimizationError>
754where
755 F: Fn(&Array1<Complex64>) -> f64,
756 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
757{
758 validate_vector_complex(point)?;
759 validate_vector_complex(direction)?;
760 if point.len() != direction.len() {
761 return Err(OptimizationError::DimensionMismatch);
762 }
763 validate_line_search_config(config)?;
764
765 let grad = gradient(point);
766 if grad.len() != point.len()
767 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
768 {
769 return Err(OptimizationError::NonFiniteInput);
770 }
771
772 let fx = objective(point);
773 if !fx.is_finite() {
774 return Err(OptimizationError::NonFiniteInput);
775 }
776 let directional_derivative = hermitian_dot(&grad, direction).re;
777
778 let mut alpha = config.initial_step;
779 for _ in 0..config.max_iterations {
780 let candidate = point + &direction.mapv(|value| value * alpha);
781 let candidate_value = objective(&candidate);
782 if !candidate_value.is_finite() {
783 return Err(OptimizationError::NonFiniteInput);
784 }
785 if candidate_value <= fx + config.sufficient_decrease * alpha * directional_derivative {
786 return Ok(alpha);
787 }
788 alpha *= config.contraction;
789 }
790 Err(OptimizationError::MaxIterationsExceeded)
791}
792
793pub fn gradient_descent_complex<F, G>(
798 initial: &Array1<Complex64>,
799 objective: F,
800 gradient: G,
801 config: &SGDConfig,
802) -> Result<Array1<Complex64>, OptimizationError>
803where
804 F: Fn(&Array1<Complex64>) -> f64,
805 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
806{
807 validate_vector_complex(initial)?;
808 validate_sgd_config(config)?;
809
810 let mut x = initial.clone();
811 let _ = objective(&x);
812 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
813
814 for _ in 0..config.max_iterations {
815 let grad = gradient(&x);
816 if grad.len() != x.len()
817 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
818 {
819 return Err(OptimizationError::NonFiniteInput);
820 }
821 if l2_norm_complex(&grad) <= tolerance {
822 return Ok(x);
823 }
824 x = &x - &grad.mapv(|value| value * config.learning_rate);
825 }
826
827 Err(OptimizationError::MaxIterationsExceeded)
828}
829
830pub fn adam_complex<F, G>(
835 initial: &Array1<Complex64>,
836 objective: F,
837 gradient: G,
838 config: &AdamConfig,
839) -> Result<Array1<Complex64>, OptimizationError>
840where
841 F: Fn(&Array1<Complex64>) -> f64,
842 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
843{
844 validate_vector_complex(initial)?;
845 validate_adam_config(config)?;
846
847 let mut x = initial.clone();
848 let mut m = Array1::<Complex64>::zeros(x.len());
849 let mut v = Array1::<f64>::zeros(x.len());
850 let mut beta1_power = 1.0_f64;
851 let mut beta2_power = 1.0_f64;
852 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
853
854 let _ = objective(&x);
855 for _ in 0..config.max_iterations {
856 let grad = gradient(&x);
857 if grad.len() != x.len()
858 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
859 {
860 return Err(OptimizationError::NonFiniteInput);
861 }
862 if l2_norm_complex(&grad) <= tolerance {
863 return Ok(x);
864 }
865
866 beta1_power *= config.beta1;
867 beta2_power *= config.beta2;
868
869 for i in 0..x.len() {
870 m[i] = config.beta1 * m[i] + (1.0 - config.beta1) * grad[i];
871 v[i] = config.beta2 * v[i] + (1.0 - config.beta2) * grad[i].norm_sqr();
872
873 let m_hat = m[i] / (1.0 - beta1_power);
874 let v_hat = v[i] / (1.0 - beta2_power);
875 x[i] -= config.learning_rate * m_hat / (v_hat.sqrt() + config.epsilon);
876 }
877 }
878
879 Err(OptimizationError::MaxIterationsExceeded)
880}
881
882pub fn momentum_descent_complex<F, G>(
887 initial: &Array1<Complex64>,
888 objective: F,
889 gradient: G,
890 config: &MomentumConfig,
891) -> Result<Array1<Complex64>, OptimizationError>
892where
893 F: Fn(&Array1<Complex64>) -> f64,
894 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
895{
896 validate_vector_complex(initial)?;
897 validate_momentum_config(config)?;
898
899 let mut x = initial.clone();
900 let mut velocity = Array1::<Complex64>::zeros(x.len());
901 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
902
903 let _ = objective(&x);
904 for _ in 0..config.max_iterations {
905 let grad = gradient(&x);
906 if grad.len() != x.len()
907 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
908 {
909 return Err(OptimizationError::NonFiniteInput);
910 }
911 if l2_norm_complex(&grad) <= tolerance {
912 return Ok(x);
913 }
914
915 for i in 0..x.len() {
916 velocity[i] = config.momentum * velocity[i] + grad[i];
917 x[i] -= config.learning_rate * velocity[i];
918 }
919 }
920
921 Err(OptimizationError::MaxIterationsExceeded)
922}
923
924pub fn rmsprop_complex<F, G>(
929 initial: &Array1<Complex64>,
930 objective: F,
931 gradient: G,
932 config: &RMSPropConfig,
933) -> Result<Array1<Complex64>, OptimizationError>
934where
935 F: Fn(&Array1<Complex64>) -> f64,
936 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
937{
938 validate_vector_complex(initial)?;
939 validate_rmsprop_config(config)?;
940
941 let mut x = initial.clone();
942 let mut avg_sq = Array1::<f64>::zeros(x.len());
943 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
944
945 let _ = objective(&x);
946 for _ in 0..config.max_iterations {
947 let grad = gradient(&x);
948 if grad.len() != x.len()
949 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
950 {
951 return Err(OptimizationError::NonFiniteInput);
952 }
953 if l2_norm_complex(&grad) <= tolerance {
954 return Ok(x);
955 }
956
957 for i in 0..x.len() {
958 avg_sq[i] = config.rho * avg_sq[i] + (1.0 - config.rho) * grad[i].norm_sqr();
959 x[i] -= config.learning_rate * grad[i] / (avg_sq[i].sqrt() + config.epsilon);
960 }
961 }
962
963 Err(OptimizationError::MaxIterationsExceeded)
964}
965
966pub fn projected_gradient_descent_box_complex<F, G>(
973 initial: &Array1<Complex64>,
974 objective: F,
975 gradient: G,
976 lower_bounds: &Array1<Complex64>,
977 upper_bounds: &Array1<Complex64>,
978 config: &ProjectedGradientConfig,
979) -> Result<Array1<Complex64>, OptimizationError>
980where
981 F: Fn(&Array1<Complex64>) -> f64,
982 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
983{
984 validate_vector_complex(initial)?;
985 validate_vector_complex(lower_bounds)?;
986 validate_vector_complex(upper_bounds)?;
987 validate_projected_gradient_config(config)?;
988 validate_bounds_complex(initial, lower_bounds, upper_bounds)?;
989
990 let mut x = initial.clone();
991 project_to_bounds_complex(&mut x, lower_bounds, upper_bounds);
992 let _ = objective(&x);
993 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
994
995 for _ in 0..config.max_iterations {
996 let grad = gradient(&x);
997 if grad.len() != x.len()
998 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
999 {
1000 return Err(OptimizationError::NonFiniteInput);
1001 }
1002 let previous = x.clone();
1003 x = &x - &grad.mapv(|value| value * config.learning_rate);
1004 project_to_bounds_complex(&mut x, lower_bounds, upper_bounds);
1005 let step_norm = l2_norm_complex(&(&x - &previous));
1006 if step_norm <= tolerance || l2_norm_complex(&grad) <= tolerance {
1007 return Ok(x);
1008 }
1009 }
1010
1011 Err(OptimizationError::MaxIterationsExceeded)
1012}
1013
1014pub fn stochastic_gradient_descent_complex<G>(
1021 initial: &Array1<Complex64>,
1022 stochastic_gradient: G,
1023 config: &SGDConfig,
1024) -> Result<Array1<Complex64>, OptimizationError>
1025where
1026 G: Fn(&Array1<Complex64>, usize) -> Array1<Complex64>,
1027{
1028 validate_vector_complex(initial)?;
1029 validate_sgd_config(config)?;
1030
1031 let mut x = initial.clone();
1032 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
1033 for iteration in 0..config.max_iterations {
1034 let grad = stochastic_gradient(&x, iteration);
1035 if grad.len() != x.len()
1036 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
1037 {
1038 return Err(OptimizationError::NonFiniteInput);
1039 }
1040 if l2_norm_complex(&grad) <= tolerance {
1041 return Ok(x);
1042 }
1043 x = &x - &grad.mapv(|value| value * config.learning_rate);
1044 }
1045 Err(OptimizationError::MaxIterationsExceeded)
1046}
1047
1048pub fn bfgs_complex<F, G>(
1053 initial: &Array1<Complex64>,
1054 objective: F,
1055 gradient: G,
1056 config: &BFGSConfig,
1057) -> Result<Array1<Complex64>, OptimizationError>
1058where
1059 F: Fn(&Array1<Complex64>) -> f64,
1060 G: Fn(&Array1<Complex64>) -> Array1<Complex64>,
1061{
1062 validate_vector_complex(initial)?;
1063 validate_bfgs_config(config)?;
1064
1065 let dimension = initial.len();
1066 let mut x = initial.clone();
1067 let mut h_inv = Array2::<Complex64>::eye(dimension);
1068 let tolerance = config.tolerance.max(DEFAULT_TOLERANCE);
1069
1070 let _ = objective(&x);
1071 for _ in 0..config.max_iterations {
1072 let grad = gradient(&x);
1073 if grad.len() != x.len()
1074 || grad.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
1075 {
1076 return Err(OptimizationError::NonFiniteInput);
1077 }
1078 if l2_norm_complex(&grad) <= tolerance {
1079 return Ok(x);
1080 }
1081
1082 let direction = -h_inv.dot(&grad);
1083 let step = config.step_size;
1084 let x_next = &x + &direction.mapv(|value| value * step);
1085 let grad_next = gradient(&x_next);
1086 if grad_next.len() != x.len()
1087 || grad_next.iter().any(|value| !value.re.is_finite() || !value.im.is_finite())
1088 {
1089 return Err(OptimizationError::NonFiniteInput);
1090 }
1091
1092 let s = &x_next - &x;
1093 let y = &grad_next - &grad;
1094 let curvature = hermitian_dot(&s, &y).re;
1095 if curvature > config.curvature_tolerance {
1096 let rho = 1.0 / curvature;
1097 let identity = Array2::<Complex64>::eye(dimension);
1098 let sy = outer_product_complex(&s, &y);
1099 let ys_outer = outer_product_complex(&y, &s);
1100 let ss = outer_product_complex(&s, &s);
1101
1102 let left = &identity - &sy.mapv(|value| value * rho);
1103 let right = &identity - &ys_outer.mapv(|value| value * rho);
1104 h_inv =
1105 left.dot(&h_inv).dot(&hermitian_transpose(&right)) + ss.mapv(|value| value * rho);
1106 }
1107
1108 x = x_next;
1109 }
1110
1111 Err(OptimizationError::MaxIterationsExceeded)
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use ndarray::arr1;
1117 use num_complex::Complex64;
1118
1119 use super::*;
1120
1121 fn objective(x: &Array1<f64>) -> f64 {
1122 let delta = x[0] - 3.0;
1123 delta * delta
1124 }
1125
1126 fn gradient(x: &Array1<f64>) -> Array1<f64> { arr1(&[2.0 * (x[0] - 3.0)]) }
1127
1128 fn objective_complex(x: &Array1<Complex64>) -> f64 {
1129 let delta = x[0] - Complex64::new(3.0, 2.0);
1130 delta.norm_sqr()
1131 }
1132
1133 fn gradient_complex(x: &Array1<Complex64>) -> Array1<Complex64> {
1134 arr1(&[Complex64::new(2.0, 0.0) * (x[0] - Complex64::new(3.0, 2.0))])
1135 }
1136
1137 #[test]
1138 fn backtracking_line_search_finds_descent_step() {
1139 let x = arr1(&[0.0_f64]);
1140 let direction = arr1(&[1.0_f64]);
1141 let alpha = backtracking_line_search(
1142 &x,
1143 &direction,
1144 objective,
1145 gradient,
1146 &LineSearchConfig::default(),
1147 )
1148 .unwrap();
1149 assert!(alpha > 0.0);
1150 }
1151
1152 #[test]
1153 fn gradient_descent_converges_on_quadratic() {
1154 let x0 = arr1(&[0.0_f64]);
1155 let solution = gradient_descent(&x0, objective, gradient, &SGDConfig::default()).unwrap();
1156 assert!((solution[0] - 3.0).abs() < 1e-4);
1157 }
1158
1159 #[test]
1160 fn adam_converges_on_quadratic() {
1161 let x0 = arr1(&[-5.0_f64]);
1162 let solution = adam(&x0, objective, gradient, &AdamConfig::default()).unwrap();
1163 assert!((solution[0] - 3.0).abs() < 1e-3);
1164 }
1165
1166 #[test]
1167 fn line_search_rejects_invalid_config_and_dimension_mismatch() {
1168 let x = arr1(&[0.0_f64]);
1169 let direction = arr1(&[1.0_f64, 2.0_f64]);
1170 let result = backtracking_line_search(
1171 &x,
1172 &direction,
1173 objective,
1174 gradient,
1175 &LineSearchConfig::default(),
1176 );
1177 assert!(matches!(result, Err(OptimizationError::DimensionMismatch)));
1178
1179 let bad_config = LineSearchConfig { contraction: 1.0, ..LineSearchConfig::default() };
1180 let result =
1181 backtracking_line_search(&x, &arr1(&[1.0_f64]), objective, gradient, &bad_config);
1182 assert!(matches!(result, Err(OptimizationError::InvalidConfig)));
1183 }
1184
1185 #[test]
1186 fn gradient_descent_and_adam_cover_error_paths() {
1187 let x0 = arr1(&[0.0_f64]);
1188
1189 let bad_gradient = |_x: &Array1<f64>| arr1(&[f64::NAN]);
1190 let gd_non_finite = gradient_descent(&x0, objective, bad_gradient, &SGDConfig::default());
1191 assert!(matches!(gd_non_finite, Err(OptimizationError::NonFiniteInput)));
1192
1193 let gd_stall = gradient_descent(&x0, objective, gradient, &SGDConfig {
1194 learning_rate: 1e-12,
1195 max_iterations: 1,
1196 tolerance: 0.0,
1197 });
1198 assert!(matches!(gd_stall, Err(OptimizationError::MaxIterationsExceeded)));
1199
1200 let bad_adam = AdamConfig { beta1: 1.0, ..AdamConfig::default() };
1201 let adam_invalid = adam(&x0, objective, gradient, &bad_adam);
1202 assert!(matches!(adam_invalid, Err(OptimizationError::InvalidConfig)));
1203 }
1204
1205 #[test]
1206 fn momentum_and_rmsprop_converge_on_quadratic() {
1207 let x0 = arr1(&[-4.0_f64]);
1208
1209 let momentum_solution =
1210 momentum_descent(&x0, objective, gradient, &MomentumConfig::default()).unwrap();
1211 assert!((momentum_solution[0] - 3.0).abs() < 1e-3);
1212
1213 let rmsprop_solution =
1214 rmsprop(&x0, objective, gradient, &RMSPropConfig::default()).unwrap();
1215 assert!((rmsprop_solution[0] - 3.0).abs() < 1e-3);
1216 }
1217
1218 #[test]
1219 fn momentum_and_rmsprop_reject_invalid_config() {
1220 let x0 = arr1(&[0.0_f64]);
1221
1222 let bad_momentum = MomentumConfig { momentum: 1.0, ..MomentumConfig::default() };
1223 let momentum_invalid = momentum_descent(&x0, objective, gradient, &bad_momentum);
1224 assert!(matches!(momentum_invalid, Err(OptimizationError::InvalidConfig)));
1225
1226 let bad_rmsprop = RMSPropConfig { rho: 1.0, ..RMSPropConfig::default() };
1227 let rmsprop_invalid = rmsprop(&x0, objective, gradient, &bad_rmsprop);
1228 assert!(matches!(rmsprop_invalid, Err(OptimizationError::InvalidConfig)));
1229 }
1230
1231 #[test]
1232 fn projected_gradient_descent_respects_bounds() {
1233 let x0 = arr1(&[-10.0_f64]);
1234 let lower = arr1(&[0.0_f64]);
1235 let upper = arr1(&[2.5_f64]);
1236 let solution = projected_gradient_descent_box(
1237 &x0,
1238 objective,
1239 gradient,
1240 &lower,
1241 &upper,
1242 &ProjectedGradientConfig::default(),
1243 )
1244 .unwrap();
1245 assert!((solution[0] - 2.5).abs() < 1e-8);
1246 }
1247
1248 #[test]
1249 fn stochastic_gradient_descent_converges_on_quadratic() {
1250 let x0 = arr1(&[-3.0_f64]);
1251 let solution = stochastic_gradient_descent(
1252 &x0,
1253 |x, _iteration| arr1(&[2.0 * (x[0] - 3.0)]),
1254 &SGDConfig { learning_rate: 5e-2, max_iterations: 2_000, tolerance: 1e-6 },
1255 )
1256 .unwrap();
1257 assert!((solution[0] - 3.0).abs() < 1e-3);
1258 }
1259
1260 #[test]
1261 fn bfgs_converges_on_quadratic() {
1262 let x0 = arr1(&[-8.0_f64]);
1263 let solution = bfgs(&x0, objective, gradient, &BFGSConfig::default()).unwrap();
1264 assert!((solution[0] - 3.0).abs() < 1e-6);
1265 }
1266
1267 #[test]
1268 fn advanced_optimizers_reject_invalid_inputs() {
1269 let x0 = arr1(&[0.0_f64]);
1270 let lower = arr1(&[2.0_f64]);
1271 let upper = arr1(&[1.0_f64]);
1272 let projected = projected_gradient_descent_box(
1273 &x0,
1274 objective,
1275 gradient,
1276 &lower,
1277 &upper,
1278 &ProjectedGradientConfig::default(),
1279 );
1280 assert!(matches!(projected, Err(OptimizationError::InvalidConfig)));
1281
1282 let sgd_non_finite =
1283 stochastic_gradient_descent(&x0, |_x, _| arr1(&[f64::NAN]), &SGDConfig::default());
1284 assert!(matches!(sgd_non_finite, Err(OptimizationError::NonFiniteInput)));
1285
1286 let bfgs_invalid =
1287 bfgs(&x0, objective, gradient, &BFGSConfig { step_size: 0.0, ..BFGSConfig::default() });
1288 assert!(matches!(bfgs_invalid, Err(OptimizationError::InvalidConfig)));
1289 }
1290
1291 #[test]
1292 fn complex_line_search_and_optimizers_converge() {
1293 let x = arr1(&[Complex64::new(0.0, 0.0)]);
1294 let direction = arr1(&[Complex64::new(1.0, 1.0)]);
1295 let alpha = backtracking_line_search_complex(
1296 &x,
1297 &direction,
1298 objective_complex,
1299 gradient_complex,
1300 &LineSearchConfig::default(),
1301 )
1302 .unwrap();
1303 assert!(alpha > 0.0);
1304
1305 let gd = gradient_descent_complex(
1306 &x,
1307 objective_complex,
1308 gradient_complex,
1309 &SGDConfig::default(),
1310 )
1311 .unwrap();
1312 assert!((gd[0] - Complex64::new(3.0, 2.0)).norm() < 1e-3);
1313
1314 let adam_solution =
1315 adam_complex(&x, objective_complex, gradient_complex, &AdamConfig::default()).unwrap();
1316 assert!((adam_solution[0] - Complex64::new(3.0, 2.0)).norm() < 1e-3);
1317
1318 let momentum_solution = momentum_descent_complex(
1319 &x,
1320 objective_complex,
1321 gradient_complex,
1322 &MomentumConfig::default(),
1323 )
1324 .unwrap();
1325 assert!((momentum_solution[0] - Complex64::new(3.0, 2.0)).norm() < 1e-3);
1326
1327 let rmsprop_solution =
1328 rmsprop_complex(&x, objective_complex, gradient_complex, &RMSPropConfig::default())
1329 .unwrap();
1330 assert!((rmsprop_solution[0] - Complex64::new(3.0, 2.0)).norm() < 1e-3);
1331
1332 let bfgs_solution =
1333 bfgs_complex(&x, objective_complex, gradient_complex, &BFGSConfig::default()).unwrap();
1334 assert!((bfgs_solution[0] - Complex64::new(3.0, 2.0)).norm() < 1e-6);
1335 }
1336
1337 #[test]
1338 fn complex_projected_and_stochastic_paths_work() {
1339 let x = arr1(&[Complex64::new(-5.0, -5.0)]);
1340 let lower = arr1(&[Complex64::new(0.0, 0.0)]);
1341 let upper = arr1(&[Complex64::new(2.5, 2.5)]);
1342 let projected = projected_gradient_descent_box_complex(
1343 &x,
1344 objective_complex,
1345 gradient_complex,
1346 &lower,
1347 &upper,
1348 &ProjectedGradientConfig::default(),
1349 )
1350 .unwrap();
1351 assert!((projected[0] - Complex64::new(2.5, 2.0)).norm() < 1e-3);
1352
1353 let stochastic = stochastic_gradient_descent_complex(
1354 &arr1(&[Complex64::new(-3.0, -1.0)]),
1355 |point, _iteration| {
1356 arr1(&[Complex64::new(2.0, 0.0) * (point[0] - Complex64::new(3.0, 2.0))])
1357 },
1358 &SGDConfig { learning_rate: 5e-2, max_iterations: 2_000, tolerance: 1e-6 },
1359 )
1360 .unwrap();
1361 assert!((stochastic[0] - Complex64::new(3.0, 2.0)).norm() < 1e-3);
1362 }
1363}