quantrs2_core/
optimization_stubs.rs

1//! Optimization utilities backed by OptiRS optimizers.
2//!\n//! This module preserves the historical `Method`/`Options` interface that the
3//! rest of the QuantRS2 codebase depends on, while delegating the actual work to
4//! OptiRS implementations. Gradient-based methods rely on finite-difference
5//! estimates so that existing objective functions can remain unchanged.
6
7use crate::error::{QuantRS2Error, QuantRS2Result};
8use optirs_core::error::OptimError;
9use optirs_core::optimizers::{Adam, Optimizer, LBFGS};
10use scirs2_core::ndarray::{Array1, ArrayView1, Ix1};
11use scirs2_core::random::{rngs::StdRng, seq::SliceRandom, thread_rng, Rng, SeedableRng};
12use std::cmp::Ordering;
13
14const DEFAULT_GRADIENT_STEP: f64 = 1e-5;
15const DEFAULT_MAX_FUNCTION_EVALS: usize = 50_000;
16const DEFAULT_HISTORY_SIZE: usize = 20;
17const DEFAULT_LEARNING_RATE: f64 = 0.1;
18
19/// Optimization method enum
20#[derive(Debug, Clone, Copy)]
21pub enum Method {
22    BFGS,
23    LBFGS,
24    ConjugateGradient,
25    NewtonCG,
26    TrustRegion,
27    NelderMead,
28    Powell,
29}
30
31/// Optimization options
32#[derive(Debug, Clone)]
33pub struct Options {
34    pub max_iter: usize,
35    pub max_iterations: usize, // Alias for compatibility
36    pub tolerance: f64,
37    pub ftol: f64, // Function tolerance
38    pub gtol: f64, // Gradient tolerance
39    pub xtol: f64, // Parameter tolerance
40    pub method: Method,
41}
42
43impl Default for Options {
44    fn default() -> Self {
45        Self {
46            max_iter: 1000,
47            max_iterations: 1000,
48            tolerance: 1e-6,
49            ftol: 1e-6,
50            gtol: 1e-6,
51            xtol: 1e-6,
52            method: Method::LBFGS,
53        }
54    }
55}
56
57impl Options {
58    fn resolved_max_iter(&self) -> usize {
59        let base = if self.max_iter == 0 {
60            self.max_iterations
61        } else {
62            self.max_iter
63        };
64        if base == 0 {
65            1000
66        } else {
67            base
68        }
69    }
70
71    fn resolved_ftol(&self) -> f64 {
72        if self.ftol > 0.0 {
73            self.ftol
74        } else if self.tolerance > 0.0 {
75            self.tolerance
76        } else {
77            1e-9
78        }
79    }
80
81    fn resolved_gtol(&self) -> f64 {
82        if self.gtol > 0.0 {
83            self.gtol
84        } else if self.tolerance > 0.0 {
85            self.tolerance
86        } else {
87            1e-9
88        }
89    }
90
91    fn resolved_xtol(&self) -> f64 {
92        if self.xtol > 0.0 {
93            self.xtol
94        } else if self.tolerance > 0.0 {
95            self.tolerance
96        } else {
97            1e-9
98        }
99    }
100}
101
102fn default_learning_rate(options: &Options) -> f64 {
103    let tol = options.resolved_gtol().abs();
104    if tol.is_finite() && tol > 0.0 {
105        (tol * 10.0).clamp(1e-3, 0.5)
106    } else {
107        DEFAULT_LEARNING_RATE
108    }
109}
110
111/// Optimization result
112#[derive(Debug, Clone)]
113pub struct OptimizeResult<T = f64> {
114    pub x: Array1<T>,
115    pub fun: T,
116    pub nit: usize,
117    pub iterations: usize, // Alias for nit
118    pub success: bool,
119    pub message: String,
120}
121
122enum OptiRSBackend {
123    LBFGS(LBFGS<f64>),
124    Adam(Adam<f64>),
125}
126
127impl OptiRSBackend {
128    fn name(&self) -> &'static str {
129        match self {
130            OptiRSBackend::LBFGS(_) => "L-BFGS",
131            OptiRSBackend::Adam(_) => "Adam",
132        }
133    }
134
135    fn set_learning_rate(&mut self, learning_rate: f64) {
136        match self {
137            OptiRSBackend::LBFGS(optimizer) => optimizer.set_lr(learning_rate),
138            OptiRSBackend::Adam(optimizer) => {
139                <Adam<f64> as Optimizer<f64, Ix1>>::set_learning_rate(optimizer, learning_rate)
140            }
141        }
142    }
143
144    fn step(
145        &mut self,
146        params: &Array1<f64>,
147        gradients: &Array1<f64>,
148    ) -> Result<Array1<f64>, OptimError> {
149        match self {
150            OptiRSBackend::LBFGS(optimizer) => optimizer.step(params, gradients),
151            OptiRSBackend::Adam(optimizer) => optimizer.step(params, gradients),
152        }
153    }
154}
155
156fn map_optirs_error(err: OptimError) -> QuantRS2Error {
157    QuantRS2Error::OptimizationFailed(err.to_string())
158}
159
160fn safe_fun_eval<F>(fun: &F, params: &Array1<f64>) -> f64
161where
162    F: Fn(&ArrayView1<f64>) -> f64,
163{
164    let value = fun(&params.view());
165    if value.is_finite() {
166        value
167    } else {
168        f64::INFINITY
169    }
170}
171
172fn numerical_gradient<F>(
173    fun: &F,
174    params: &Array1<f64>,
175    step: f64,
176    eval_counter: &mut usize,
177) -> QuantRS2Result<Array1<f64>>
178where
179    F: Fn(&ArrayView1<f64>) -> f64,
180{
181    if params.is_empty() {
182        return Ok(Array1::zeros(0));
183    }
184
185    let step = if step.is_finite() && step > 0.0 {
186        step
187    } else {
188        DEFAULT_GRADIENT_STEP
189    };
190
191    let mut gradient = Array1::zeros(params.len());
192    let mut forward = params.clone();
193    let mut backward = params.clone();
194
195    for i in 0..params.len() {
196        forward[i] = params[i] + step;
197        backward[i] = params[i] - step;
198
199        let f_plus = safe_fun_eval(fun, &forward);
200        let f_minus = safe_fun_eval(fun, &backward);
201        *eval_counter += 2;
202
203        if !(f_plus.is_finite() && f_minus.is_finite()) {
204            return Err(QuantRS2Error::OptimizationFailed(
205                "Encountered non-finite objective value during gradient estimation".to_string(),
206            ));
207        }
208
209        gradient[i] = (f_plus - f_minus) / (2.0 * step);
210
211        forward[i] = params[i];
212        backward[i] = params[i];
213    }
214
215    Ok(gradient)
216}
217
218fn select_backend(method: Method, options: &Options) -> OptiRSBackend {
219    let learning_rate = default_learning_rate(options);
220    match method {
221        Method::BFGS
222        | Method::LBFGS
223        | Method::ConjugateGradient
224        | Method::NewtonCG
225        | Method::TrustRegion => {
226            let optimizer = LBFGS::new_with_config(
227                learning_rate,
228                DEFAULT_HISTORY_SIZE,
229                options.resolved_gtol(),
230                1e-4,
231                0.9,
232                20,
233            );
234            OptiRSBackend::LBFGS(optimizer)
235        }
236        Method::Powell | Method::NelderMead => {
237            let lr = learning_rate.max(0.01);
238            let mut optimizer = Adam::new(lr);
239            <Adam<f64> as Optimizer<f64, Ix1>>::set_learning_rate(&mut optimizer, lr);
240            OptiRSBackend::Adam(optimizer)
241        }
242    }
243}
244
245fn check_convergence(
246    params: &Array1<f64>,
247    new_params: &Array1<f64>,
248    current_fun: f64,
249    new_fun: f64,
250    grad_norm: f64,
251    options: &Options,
252) -> Option<String> {
253    if grad_norm <= options.resolved_gtol() {
254        return Some("Gradient tolerance reached".to_string());
255    }
256
257    let step_norm = (&*new_params - params).mapv(|v| v * v).sum().sqrt();
258    if step_norm <= options.resolved_xtol() {
259        return Some("Parameter tolerance reached".to_string());
260    }
261
262    if (current_fun - new_fun).abs() <= options.resolved_ftol() {
263        return Some("Function tolerance reached".to_string());
264    }
265
266    None
267}
268
269fn nelder_mead_minimize<F>(
270    fun: &F,
271    x0: &Array1<f64>,
272    options: &Options,
273) -> QuantRS2Result<OptimizeResult<f64>>
274where
275    F: Fn(&ArrayView1<f64>) -> f64,
276{
277    let dim = x0.len();
278    if dim == 0 {
279        let value = safe_fun_eval(fun, x0);
280        return Ok(OptimizeResult {
281            x: x0.clone(),
282            fun: value,
283            nit: 0,
284            iterations: 0,
285            success: true,
286            message: "Trivial optimization (zero dimension)".to_string(),
287        });
288    }
289
290    let mut simplex = Vec::with_capacity(dim + 1);
291    simplex.push(x0.clone());
292    for i in 0..dim {
293        let mut point = x0.clone();
294        point[i] += DEFAULT_GRADIENT_STEP.max(1e-3);
295        simplex.push(point);
296    }
297
298    let mut values: Vec<f64> = simplex.iter().map(|p| safe_fun_eval(fun, p)).collect();
299    let mut evals = values.len();
300    let mut iterations = 0usize;
301
302    const REFLECTION: f64 = 1.0;
303    const EXPANSION: f64 = 2.0;
304    const CONTRACTION: f64 = 0.5;
305    const SHRINK: f64 = 0.5;
306
307    while iterations < options.resolved_max_iter() && evals < DEFAULT_MAX_FUNCTION_EVALS {
308        let mut order: Vec<usize> = (0..simplex.len()).collect();
309        order.sort_by(|&a, &b| values[a].partial_cmp(&values[b]).unwrap_or(Ordering::Equal));
310
311        let mut ordered_simplex = Vec::with_capacity(simplex.len());
312        let mut ordered_values = Vec::with_capacity(values.len());
313        for idx in order {
314            ordered_simplex.push(simplex[idx].clone());
315            ordered_values.push(values[idx]);
316        }
317        simplex = ordered_simplex;
318        values = ordered_values;
319
320        let best = simplex[0].clone();
321        let worst_index = simplex.len() - 1;
322        let worst = simplex[worst_index].clone();
323
324        // Compute centroid excluding worst point
325        let mut centroid = Array1::zeros(dim);
326        for point in &simplex[..worst_index] {
327            centroid = &centroid + point;
328        }
329        centroid.mapv_inplace(|v| v / dim as f64);
330
331        // Reflection
332        let mut xr = &centroid + &(centroid.clone() - &worst) * REFLECTION;
333        let fr = safe_fun_eval(fun, &xr);
334        evals += 1;
335
336        if fr < values[0] {
337            // Expansion
338            let mut xe = &centroid + &(xr.clone() - &centroid) * EXPANSION;
339            let fe = safe_fun_eval(fun, &xe);
340            evals += 1;
341            if fe < fr {
342                simplex[worst_index] = xe;
343                values[worst_index] = fe;
344            } else {
345                simplex[worst_index] = xr;
346                values[worst_index] = fr;
347            }
348        } else if fr < values[values.len() - 2] {
349            simplex[worst_index] = xr;
350            values[worst_index] = fr;
351        } else {
352            // Contraction
353            let mut xc = &centroid + &(worst - &centroid) * CONTRACTION;
354            let fc = safe_fun_eval(fun, &xc);
355            evals += 1;
356            if fc < values[worst_index] {
357                simplex[worst_index] = xc;
358                values[worst_index] = fc;
359            } else {
360                // Shrink towards best
361                for i in 1..simplex.len() {
362                    simplex[i] = &best + &(simplex[i].clone() - &best) * SHRINK;
363                    values[i] = safe_fun_eval(fun, &simplex[i]);
364                }
365                evals += dim;
366            }
367        }
368
369        iterations += 1;
370
371        let (min_value, max_value) = values
372            .iter()
373            .fold((f64::INFINITY, f64::NEG_INFINITY), |acc, &v| {
374                (acc.0.min(v), acc.1.max(v))
375            });
376        if (max_value - min_value).abs() <= options.resolved_ftol() {
377            break;
378        }
379    }
380
381    let (best_idx, best_val) = values
382        .iter()
383        .enumerate()
384        .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(Ordering::Equal))
385        .unwrap();
386
387    Ok(OptimizeResult {
388        x: simplex[best_idx].clone(),
389        fun: *best_val,
390        nit: iterations,
391        iterations,
392        success: true,
393        message: format!("Nelder-Mead completed in {} iterations", iterations),
394    })
395}
396
397/// Minimize function using OptiRS optimizers.
398pub fn minimize<F>(
399    fun: F,
400    x0: &Array1<f64>,
401    method: Method,
402    options: Option<Options>,
403) -> QuantRS2Result<OptimizeResult<f64>>
404where
405    F: Fn(&ArrayView1<f64>) -> f64,
406{
407    let options = options.unwrap_or_default();
408
409    match method {
410        Method::NelderMead | Method::Powell => {
411            let mut result = nelder_mead_minimize(&fun, x0, &options)?;
412            if matches!(method, Method::Powell) {
413                result.message = format!(
414                    "Powell (approximated via Nelder-Mead) in {} iterations",
415                    result.iterations
416                );
417            }
418            return Ok(result);
419        }
420        _ => {}
421    }
422
423    let mut backend = select_backend(method, &options);
424    backend.set_learning_rate(default_learning_rate(&options));
425
426    let mut params = x0.clone();
427    let mut current_fun = safe_fun_eval(&fun, &params);
428    let mut best_params = params.clone();
429    let mut best_fun = current_fun;
430
431    let mut iterations = 0usize;
432    let mut evals = 1usize;
433
434    while iterations < options.resolved_max_iter() && evals < DEFAULT_MAX_FUNCTION_EVALS {
435        let mut gradient = numerical_gradient(&fun, &params, DEFAULT_GRADIENT_STEP, &mut evals)?;
436        let grad_norm = gradient.dot(&gradient).sqrt();
437
438        if grad_norm <= options.resolved_gtol() {
439            return Ok(OptimizeResult {
440                x: params,
441                fun: current_fun,
442                nit: iterations,
443                iterations,
444                success: true,
445                message: format!(
446                    "Optimization converged in {} iterations ({} backend)",
447                    iterations,
448                    backend.name()
449                ),
450            });
451        }
452
453        // Ensure gradients are finite
454        for value in gradient.iter_mut() {
455            if !value.is_finite() {
456                *value = 0.0;
457            }
458        }
459
460        let updated_params = backend.step(&params, &gradient).map_err(map_optirs_error)?;
461
462        let new_fun = safe_fun_eval(&fun, &updated_params);
463        evals += 1;
464
465        if new_fun < best_fun {
466            best_fun = new_fun;
467            best_params = updated_params.clone();
468        }
469
470        if let Some(reason) = check_convergence(
471            &params,
472            &updated_params,
473            current_fun,
474            new_fun,
475            grad_norm,
476            &options,
477        ) {
478            return Ok(OptimizeResult {
479                x: updated_params,
480                fun: new_fun,
481                nit: iterations + 1,
482                iterations: iterations + 1,
483                success: true,
484                message: format!("{} ({} backend)", reason, backend.name()),
485            });
486        }
487
488        params = updated_params;
489        current_fun = new_fun;
490        iterations += 1;
491    }
492
493    let success = iterations < options.resolved_max_iter() && evals < DEFAULT_MAX_FUNCTION_EVALS;
494    Ok(OptimizeResult {
495        x: if success { params } else { best_params },
496        fun: if success { current_fun } else { best_fun },
497        nit: iterations,
498        iterations,
499        success,
500        message: if success {
501            format!(
502                "Optimization converged in {} iterations using {}",
503                iterations,
504                backend.name()
505            )
506        } else {
507            format!(
508                "Reached iteration/function evaluation limit ({} iterations, {} evals) using {}",
509                iterations,
510                evals,
511                backend.name()
512            )
513        },
514    })
515}
516
517/// Differential evolution options
518#[derive(Debug, Clone)]
519pub struct DifferentialEvolutionOptions {
520    pub population_size: usize,
521    pub popsize: usize, // Alias for population_size
522    pub max_generations: usize,
523    pub maxiter: usize, // Alias for max_generations
524    pub tolerance: f64,
525    pub tol: f64, // Alias for tolerance
526}
527
528impl Default for DifferentialEvolutionOptions {
529    fn default() -> Self {
530        Self {
531            population_size: 15,
532            popsize: 15,
533            max_generations: 1000,
534            maxiter: 1000,
535            tolerance: 1e-6,
536            tol: 1e-6,
537        }
538    }
539}
540
541/// Differential evolution optimization using SciRS2 random utilities
542pub fn differential_evolution<F>(
543    fun: F,
544    bounds: &[(f64, f64)],
545    options: Option<DifferentialEvolutionOptions>,
546    random_state: Option<u64>,
547) -> QuantRS2Result<OptimizeResult<f64>>
548where
549    F: Fn(&ArrayView1<f64>) -> f64,
550{
551    let options = options.unwrap_or_default();
552    let dim = bounds.len();
553    if dim == 0 {
554        return Err(QuantRS2Error::InvalidParameter(
555            "Differential evolution requires at least one bounded variable".to_string(),
556        ));
557    }
558
559    let pop_size = options
560        .population_size
561        .max(options.popsize)
562        .max(4 * dim)
563        .max(10);
564    let max_generations = if options.max_generations == 0 {
565        options.maxiter.max(1)
566    } else {
567        options.max_generations
568    };
569    let tolerance = if options.tolerance > 0.0 {
570        options.tolerance
571    } else if options.tol > 0.0 {
572        options.tol
573    } else {
574        1e-9
575    };
576
577    let mut rng = match random_state {
578        Some(seed) => StdRng::seed_from_u64(seed),
579        None => {
580            let mut seed_rng = thread_rng();
581            let seed: u64 = seed_rng.gen();
582            StdRng::seed_from_u64(seed)
583        }
584    };
585
586    let mut population = Vec::with_capacity(pop_size);
587    let mut scores = Vec::with_capacity(pop_size);
588
589    for _ in 0..pop_size {
590        let mut candidate = Array1::zeros(dim);
591        for (idx, &(lower, upper)) in bounds.iter().enumerate() {
592            if !(lower.is_finite() && upper.is_finite() && upper > lower) {
593                return Err(QuantRS2Error::InvalidParameter(format!(
594                    "Invalid bounds for dimension {}: [{}, {}]",
595                    idx, lower, upper
596                )));
597            }
598            let span = upper - lower;
599            candidate[idx] = lower + rng.gen::<f64>() * span;
600        }
601        let score = safe_fun_eval(&fun, &candidate);
602        population.push(candidate);
603        scores.push(score);
604    }
605
606    let mut best_index = 0usize;
607    for (idx, score) in scores.iter().enumerate() {
608        if score < &scores[best_index] {
609            best_index = idx;
610        }
611    }
612    let mut best_candidate = population[best_index].clone();
613    let mut best_score = scores[best_index];
614
615    let mut iterations = 0usize;
616    let mut evals = pop_size;
617
618    while iterations < max_generations {
619        for i in 0..pop_size {
620            let mut indices: Vec<usize> = (0..pop_size).filter(|&idx| idx != i).collect();
621            indices.shuffle(&mut rng);
622            let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
623
624            let mut trial = population[r1].clone();
625            let j_rand = rng.gen_range(0..dim);
626
627            for j in 0..dim {
628                if rng.gen::<f64>() < 0.7 || j == j_rand {
629                    trial[j] = population[r1][j] + 0.8 * (population[r2][j] - population[r3][j]);
630                }
631                let (lower, upper) = bounds[j];
632                if trial[j] < lower {
633                    trial[j] = lower;
634                }
635                if trial[j] > upper {
636                    trial[j] = upper;
637                }
638            }
639
640            let trial_score = safe_fun_eval(&fun, &trial);
641            evals += 1;
642
643            if trial_score < scores[i] {
644                population[i] = trial;
645                scores[i] = trial_score;
646                if trial_score < best_score {
647                    best_score = trial_score;
648                    best_candidate = population[i].clone();
649                }
650            }
651        }
652
653        iterations += 1;
654
655        let (min_score, max_score) = scores
656            .iter()
657            .fold((f64::INFINITY, f64::NEG_INFINITY), |acc, &val| {
658                (acc.0.min(val), acc.1.max(val))
659            });
660        if (max_score - min_score).abs() <= tolerance {
661            break;
662        }
663    }
664
665    Ok(OptimizeResult {
666        x: best_candidate,
667        fun: best_score,
668        nit: iterations,
669        iterations,
670        success: true,
671        message: format!(
672            "Differential evolution converged in {} generations ({} evaluations)",
673            iterations, evals
674        ),
675    })
676}