1use 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#[derive(Debug, Clone, Copy)]
21pub enum Method {
22 BFGS,
23 LBFGS,
24 ConjugateGradient,
25 NewtonCG,
26 TrustRegion,
27 NelderMead,
28 Powell,
29}
30
31#[derive(Debug, Clone)]
33pub struct Options {
34 pub max_iter: usize,
35 pub max_iterations: usize, pub tolerance: f64,
37 pub ftol: f64, pub gtol: f64, pub xtol: f64, 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#[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, 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(¶ms.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 let mut centroid = Array1::zeros(dim);
326 for point in &simplex[..worst_index] {
327 centroid = ¢roid + point;
328 }
329 centroid.mapv_inplace(|v| v / dim as f64);
330
331 let mut xr = ¢roid + &(centroid.clone() - &worst) * REFLECTION;
333 let fr = safe_fun_eval(fun, &xr);
334 evals += 1;
335
336 if fr < values[0] {
337 let mut xe = ¢roid + &(xr.clone() - ¢roid) * 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 let mut xc = ¢roid + &(worst - ¢roid) * 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 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
397pub 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, ¶ms);
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, ¶ms, 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 for value in gradient.iter_mut() {
455 if !value.is_finite() {
456 *value = 0.0;
457 }
458 }
459
460 let updated_params = backend.step(¶ms, &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 ¶ms,
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#[derive(Debug, Clone)]
519pub struct DifferentialEvolutionOptions {
520 pub population_size: usize,
521 pub popsize: usize, pub max_generations: usize,
523 pub maxiter: usize, pub tolerance: f64,
525 pub tol: f64, }
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
541pub 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}