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