Skip to main content

nabled_ml/
optimization.rs

1//! Optimization primitives over ndarray vectors.
2
3use std::fmt;
4
5use ndarray::{Array1, Array2};
6use num_complex::Complex64;
7
8const DEFAULT_TOLERANCE: f64 = 1.0e-12;
9
10/// Error type for optimization routines.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum OptimizationError {
13    /// Input vectors are empty.
14    EmptyInput,
15    /// Input dimensions are incompatible.
16    DimensionMismatch,
17    /// Non-finite values were observed.
18    NonFiniteInput,
19    /// Invalid optimizer configuration.
20    InvalidConfig,
21    /// Optimizer exceeded iteration budget.
22    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/// Configuration for backtracking line search.
40#[derive(Debug, Clone, Copy)]
41pub struct LineSearchConfig {
42    /// Initial step size.
43    pub initial_step:        f64,
44    /// Contraction factor in `(0, 1)`.
45    pub contraction:         f64,
46    /// Armijo sufficient decrease coefficient in `(0, 1)`.
47    pub sufficient_decrease: f64,
48    /// Maximum backtracking iterations.
49    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/// Configuration for SGD.
64#[derive(Debug, Clone, Copy)]
65pub struct SGDConfig {
66    /// Fixed learning rate.
67    pub learning_rate:  f64,
68    /// Maximum optimization iterations.
69    pub max_iterations: usize,
70    /// Gradient norm tolerance for convergence.
71    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/// Configuration for Adam optimizer.
79#[derive(Debug, Clone, Copy)]
80pub struct AdamConfig {
81    /// Base learning rate.
82    pub learning_rate:  f64,
83    /// Exponential decay for first moment.
84    pub beta1:          f64,
85    /// Exponential decay for second moment.
86    pub beta2:          f64,
87    /// Numerical epsilon.
88    pub epsilon:        f64,
89    /// Maximum optimization iterations.
90    pub max_iterations: usize,
91    /// Gradient norm tolerance for convergence.
92    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/// Configuration for momentum gradient descent.
109#[derive(Debug, Clone, Copy)]
110pub struct MomentumConfig {
111    /// Base learning rate.
112    pub learning_rate:  f64,
113    /// Momentum coefficient in `[0, 1)`.
114    pub momentum:       f64,
115    /// Maximum optimization iterations.
116    pub max_iterations: usize,
117    /// Gradient norm tolerance for convergence.
118    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/// Configuration for `RMSProp` optimizer.
133#[derive(Debug, Clone, Copy)]
134pub struct RMSPropConfig {
135    /// Base learning rate.
136    pub learning_rate:  f64,
137    /// Exponential decay factor for squared gradients in `[0, 1)`.
138    pub rho:            f64,
139    /// Numerical epsilon.
140    pub epsilon:        f64,
141    /// Maximum optimization iterations.
142    pub max_iterations: usize,
143    /// Gradient norm tolerance for convergence.
144    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/// Configuration for projected gradient descent with box constraints.
160#[derive(Debug, Clone, Copy)]
161pub struct ProjectedGradientConfig {
162    /// Base learning rate.
163    pub learning_rate:  f64,
164    /// Maximum optimization iterations.
165    pub max_iterations: usize,
166    /// Gradient norm tolerance for convergence.
167    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/// Configuration for `BFGS` quasi-Newton optimization.
175#[derive(Debug, Clone, Copy)]
176pub struct BFGSConfig {
177    /// Initial step size multiplier.
178    pub step_size:           f64,
179    /// Maximum optimization iterations.
180    pub max_iterations:      usize,
181    /// Gradient norm tolerance for convergence.
182    pub tolerance:           f64,
183    /// Minimum curvature `s^T y` required for Hessian updates.
184    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
393/// Perform Armijo backtracking line search.
394///
395/// # Errors
396/// Returns an error for invalid inputs/configuration or non-finite objective evaluations.
397pub 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
441/// Minimize objective with fixed-step gradient descent.
442///
443/// # Errors
444/// Returns an error for invalid inputs/configuration or non-finite gradients.
445pub 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
476/// Minimize objective with Adam.
477///
478/// # Errors
479/// Returns an error for invalid inputs/configuration or non-finite gradients.
480pub 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
526/// Minimize objective with momentum gradient descent.
527///
528/// # Errors
529/// Returns an error for invalid inputs/configuration or non-finite gradients.
530pub 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
566/// Minimize objective with `RMSProp`.
567///
568/// # Errors
569/// Returns an error for invalid inputs/configuration or non-finite gradients.
570pub 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
606/// Minimize objective with projected gradient descent under box constraints.
607///
608/// # Errors
609/// Returns an error for invalid inputs/configuration, invalid bounds, or non-finite gradients.
610pub 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
650/// Minimize objective with stochastic gradient descent.
651///
652/// `stochastic_gradient` receives `(current_point, iteration)` and returns a gradient sample.
653///
654/// # Errors
655/// Returns an error for invalid inputs/configuration or non-finite gradients.
656pub 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
682/// Minimize objective with `BFGS` quasi-Newton updates.
683///
684/// # Errors
685/// Returns an error for invalid inputs/configuration or non-finite gradients.
686pub 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
743/// Perform Armijo backtracking line search for complex vectors.
744///
745/// # Errors
746/// Returns an error for invalid inputs/configuration or non-finite objective evaluations.
747pub 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
793/// Minimize objective with fixed-step complex gradient descent.
794///
795/// # Errors
796/// Returns an error for invalid inputs/configuration or non-finite gradients.
797pub 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
830/// Minimize objective with complex Adam updates.
831///
832/// # Errors
833/// Returns an error for invalid inputs/configuration or non-finite gradients.
834pub 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
882/// Minimize objective with complex momentum gradient descent.
883///
884/// # Errors
885/// Returns an error for invalid inputs/configuration or non-finite gradients.
886pub 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
924/// Minimize objective with complex `RMSProp`.
925///
926/// # Errors
927/// Returns an error for invalid inputs/configuration or non-finite gradients.
928pub 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
966/// Minimize objective with complex projected gradient descent under box constraints.
967///
968/// Bounds clamp real and imaginary components independently.
969///
970/// # Errors
971/// Returns an error for invalid inputs/configuration, invalid bounds, or non-finite gradients.
972pub 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
1014/// Minimize objective with complex stochastic gradient descent.
1015///
1016/// `stochastic_gradient` receives `(current_point, iteration)` and returns a gradient sample.
1017///
1018/// # Errors
1019/// Returns an error for invalid inputs/configuration or non-finite gradients.
1020pub 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
1048/// Minimize objective with complex `BFGS` quasi-Newton updates.
1049///
1050/// # Errors
1051/// Returns an error for invalid inputs/configuration or non-finite gradients.
1052pub 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}