1use crate::error::{OptimizeError, OptimizeResult};
11use crate::result::OptimizeResults;
12use scirs2_core::ndarray::{Array1, ArrayView1};
13use scirs2_core::random::rngs::StdRng;
14use scirs2_core::random::{rng, Rng, SeedableRng};
15use scirs2_core::RngExt;
16
17#[derive(Debug, Clone)]
23pub enum CoolingSchedule {
24 Linear,
26 Exponential {
28 alpha: f64,
30 },
31 Logarithmic {
33 c: f64,
35 },
36 Adaptive {
38 target_acceptance: f64,
40 adjustment_factor: f64,
42 },
43}
44
45impl Default for CoolingSchedule {
46 fn default() -> Self {
47 CoolingSchedule::Exponential { alpha: 0.95 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct AdaptiveCoolingState {
54 pub accepted: usize,
56 pub total: usize,
58 pub effective_alpha: f64,
60 pub window_size: usize,
62}
63
64impl Default for AdaptiveCoolingState {
65 fn default() -> Self {
66 Self {
67 accepted: 0,
68 total: 0,
69 effective_alpha: 0.95,
70 window_size: 100,
71 }
72 }
73}
74
75impl AdaptiveCoolingState {
76 fn update(&mut self, accepted: bool, target: f64, adjustment: f64) -> f64 {
78 if accepted {
79 self.accepted += 1;
80 }
81 self.total += 1;
82
83 if self.total >= self.window_size {
84 let ratio = self.accepted as f64 / self.total as f64;
85 if ratio > target {
87 self.effective_alpha *= adjustment; } else {
89 self.effective_alpha /= adjustment; }
91 self.effective_alpha = self.effective_alpha.clamp(0.5, 0.999);
93 self.accepted = 0;
94 self.total = 0;
95 }
96 self.effective_alpha
97 }
98}
99
100#[derive(Debug, Clone)]
106pub enum ReheatingStrategy {
107 None,
109 Periodic {
111 interval: usize,
113 fraction: f64,
115 },
116 Stagnation {
118 patience: usize,
120 fraction: f64,
122 },
123}
124
125impl Default for ReheatingStrategy {
126 fn default() -> Self {
127 ReheatingStrategy::None
128 }
129}
130
131#[derive(Debug, Clone)]
137pub struct PenaltyConstraint {
138 pub penalty_coeff: f64,
140 pub adaptive: bool,
142 pub max_penalty: f64,
144}
145
146impl Default for PenaltyConstraint {
147 fn default() -> Self {
148 Self {
149 penalty_coeff: 1000.0,
150 adaptive: false,
151 max_penalty: 1e8,
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct ConstraintHandler {
159 pub penalty: PenaltyConstraint,
163}
164
165impl Default for ConstraintHandler {
166 fn default() -> Self {
167 Self {
168 penalty: PenaltyConstraint::default(),
169 }
170 }
171}
172
173impl ConstraintHandler {
174 pub fn compute_penalty(&self, violations: &[f64], iteration: usize, max_iter: usize) -> f64 {
176 let coeff = if self.penalty.adaptive {
177 let progress = iteration as f64 / max_iter.max(1) as f64;
178 let scaled = self.penalty.penalty_coeff * (1.0 + progress * 10.0);
179 scaled.min(self.penalty.max_penalty)
180 } else {
181 self.penalty.penalty_coeff
182 };
183
184 violations
185 .iter()
186 .map(|v| {
187 if *v > 0.0 {
188 coeff * v * v } else {
190 0.0
191 }
192 })
193 .sum()
194 }
195}
196
197#[derive(Debug, Clone)]
203pub struct MetaheuristicSaOptions {
204 pub max_iter: usize,
206 pub initial_temp: f64,
208 pub final_temp: f64,
210 pub cooling: CoolingSchedule,
212 pub steps_per_temp: usize,
214 pub step_size: f64,
216 pub reheating: ReheatingStrategy,
218 pub seed: Option<u64>,
220 pub bounds: Option<Vec<(f64, f64)>>,
222 pub tol: f64,
224}
225
226impl Default for MetaheuristicSaOptions {
227 fn default() -> Self {
228 Self {
229 max_iter: 10_000,
230 initial_temp: 100.0,
231 final_temp: 1e-10,
232 cooling: CoolingSchedule::default(),
233 steps_per_temp: 50,
234 step_size: 1.0,
235 reheating: ReheatingStrategy::None,
236 seed: None,
237 bounds: None,
238 tol: 1e-12,
239 }
240 }
241}
242
243#[derive(Debug, Clone)]
245pub struct MetaheuristicSaResult {
246 pub x: Array1<f64>,
248 pub fun: f64,
250 pub nfev: usize,
252 pub nit: usize,
254 pub success: bool,
256 pub message: String,
258 pub final_temperature: f64,
260 pub acceptance_ratio: f64,
262}
263
264impl MetaheuristicSaResult {
265 pub fn to_optimize_results(&self) -> OptimizeResults<f64> {
267 OptimizeResults {
268 x: self.x.clone(),
269 fun: self.fun,
270 jac: None,
271 hess: None,
272 constr: None,
273 nit: self.nit,
274 nfev: self.nfev,
275 njev: 0,
276 nhev: 0,
277 maxcv: 0,
278 message: self.message.clone(),
279 success: self.success,
280 status: if self.success { 0 } else { 1 },
281 }
282 }
283}
284
285pub struct SimulatedAnnealingOptimizer {
291 options: MetaheuristicSaOptions,
292 rng: StdRng,
293}
294
295impl SimulatedAnnealingOptimizer {
296 pub fn new(options: MetaheuristicSaOptions) -> Self {
298 let seed = options.seed.unwrap_or_else(|| rng().random());
299 Self {
300 options,
301 rng: StdRng::seed_from_u64(seed),
302 }
303 }
304
305 fn next_temperature(
307 &self,
308 current_temp: f64,
309 iteration: usize,
310 adaptive_state: &mut AdaptiveCoolingState,
311 accepted: bool,
312 ) -> f64 {
313 match &self.options.cooling {
314 CoolingSchedule::Linear => {
315 let t0 = self.options.initial_temp;
316 let tf = self.options.final_temp;
317 let max_iter = self.options.max_iter;
318 let step = (t0 - tf) / max_iter.max(1) as f64;
319 (current_temp - step).max(tf)
320 }
321 CoolingSchedule::Exponential { alpha } => {
322 (current_temp * alpha).max(self.options.final_temp)
323 }
324 CoolingSchedule::Logarithmic { c } => {
325 let t0 = self.options.initial_temp;
326 let denom = 1.0 + c * ((1.0 + iteration as f64).ln());
327 (t0 / denom).max(self.options.final_temp)
328 }
329 CoolingSchedule::Adaptive {
330 target_acceptance,
331 adjustment_factor,
332 } => {
333 let alpha = adaptive_state.update(accepted, *target_acceptance, *adjustment_factor);
334 (current_temp * alpha).max(self.options.final_temp)
335 }
336 }
337 }
338
339 fn generate_neighbor(&mut self, x: &Array1<f64>) -> Array1<f64> {
341 let ndim = x.len();
342 let mut neighbor = x.clone();
343 let dim_idx = self.rng.random_range(0..ndim);
345 let perturbation = (self.rng.random::<f64>() * 2.0 - 1.0) * self.options.step_size;
346 neighbor[dim_idx] += perturbation;
347
348 if let Some(ref bounds) = self.options.bounds {
350 for (i, (lo, hi)) in bounds.iter().enumerate() {
351 neighbor[i] = neighbor[i].clamp(*lo, *hi);
352 }
353 }
354 neighbor
355 }
356
357 fn accept(&mut self, delta_e: f64, temperature: f64) -> bool {
359 if delta_e < 0.0 {
360 true } else if temperature <= 0.0 {
362 false
363 } else {
364 let prob = (-delta_e / temperature).exp();
365 self.rng.random::<f64>() < prob
366 }
367 }
368
369 fn maybe_reheat(
371 &self,
372 current_temp: f64,
373 iteration: usize,
374 stagnation_count: usize,
375 ) -> Option<f64> {
376 match &self.options.reheating {
377 ReheatingStrategy::None => None,
378 ReheatingStrategy::Periodic { interval, fraction } => {
379 if *interval > 0 && iteration > 0 && iteration % interval == 0 {
380 Some(self.options.initial_temp * fraction)
381 } else {
382 None
383 }
384 }
385 ReheatingStrategy::Stagnation { patience, fraction } => {
386 if stagnation_count >= *patience {
387 if current_temp < self.options.initial_temp * 0.1 {
389 Some(self.options.initial_temp * fraction)
390 } else {
391 None
392 }
393 } else {
394 None
395 }
396 }
397 }
398 }
399
400 pub fn optimize<F>(
402 &mut self,
403 func: F,
404 x0: &Array1<f64>,
405 ) -> OptimizeResult<MetaheuristicSaResult>
406 where
407 F: Fn(&ArrayView1<f64>) -> f64,
408 {
409 self.optimize_constrained(func, x0, None::<fn(&ArrayView1<f64>) -> Vec<f64>>)
410 }
411
412 pub fn optimize_constrained<F, G>(
416 &mut self,
417 func: F,
418 x0: &Array1<f64>,
419 constraints_fn: Option<G>,
420 ) -> OptimizeResult<MetaheuristicSaResult>
421 where
422 F: Fn(&ArrayView1<f64>) -> f64,
423 G: Fn(&ArrayView1<f64>) -> Vec<f64>,
424 {
425 let constraint_handler = ConstraintHandler::default();
426 self.optimize_with_handler(func, x0, constraints_fn, &constraint_handler)
427 }
428
429 pub fn optimize_with_handler<F, G>(
431 &mut self,
432 func: F,
433 x0: &Array1<f64>,
434 constraints_fn: Option<G>,
435 constraint_handler: &ConstraintHandler,
436 ) -> OptimizeResult<MetaheuristicSaResult>
437 where
438 F: Fn(&ArrayView1<f64>) -> f64,
439 G: Fn(&ArrayView1<f64>) -> Vec<f64>,
440 {
441 if x0.is_empty() {
442 return Err(OptimizeError::InvalidInput(
443 "Initial point must not be empty".to_string(),
444 ));
445 }
446 if self.options.initial_temp <= 0.0 {
447 return Err(OptimizeError::InvalidParameter(
448 "Initial temperature must be positive".to_string(),
449 ));
450 }
451
452 let max_iter = self.options.max_iter;
453
454 let evaluate = |x: &Array1<f64>, iter: usize| -> f64 {
455 let obj = func(&x.view());
456 if let Some(ref cf) = constraints_fn {
457 let violations = cf(&x.view());
458 let penalty = constraint_handler.compute_penalty(&violations, iter, max_iter);
459 obj + penalty
460 } else {
461 obj
462 }
463 };
464
465 let mut current_x = x0.clone();
466 if let Some(ref bounds) = self.options.bounds {
468 for (i, (lo, hi)) in bounds.iter().enumerate() {
469 if i < current_x.len() {
470 current_x[i] = current_x[i].clamp(*lo, *hi);
471 }
472 }
473 }
474
475 let mut current_val = evaluate(¤t_x, 0);
476 let mut best_x = current_x.clone();
477 let mut best_val = current_val;
478 let mut temperature = self.options.initial_temp;
479 let mut nfev: usize = 1;
480 let mut nit: usize = 0;
481 let mut total_accepted: usize = 0;
482 let mut total_tried: usize = 0;
483 let mut stagnation_count: usize = 0;
484 let mut adaptive_state = AdaptiveCoolingState::default();
485
486 for iteration in 0..self.options.max_iter {
487 nit = iteration + 1;
488 let mut step_accepted = false;
489
490 for _step in 0..self.options.steps_per_temp {
491 let neighbor = self.generate_neighbor(¤t_x);
492 let neighbor_val = evaluate(&neighbor, iteration);
493 nfev += 1;
494
495 let delta_e = neighbor_val - current_val;
496 total_tried += 1;
497
498 if self.accept(delta_e, temperature) {
499 current_x = neighbor;
500 current_val = neighbor_val;
501 total_accepted += 1;
502 step_accepted = true;
503
504 if current_val < best_val {
505 best_x = current_x.clone();
506 best_val = current_val;
507 stagnation_count = 0;
508 }
509 }
510 }
511
512 if !step_accepted {
513 stagnation_count += 1;
514 }
515
516 temperature =
518 self.next_temperature(temperature, iteration, &mut adaptive_state, step_accepted);
519
520 if let Some(new_temp) = self.maybe_reheat(temperature, iteration, stagnation_count) {
522 temperature = new_temp;
523 stagnation_count = 0;
524 }
525
526 if temperature <= self.options.final_temp {
528 break;
529 }
530 }
531
532 let acceptance_ratio = if total_tried > 0 {
533 total_accepted as f64 / total_tried as f64
534 } else {
535 0.0
536 };
537
538 Ok(MetaheuristicSaResult {
539 x: best_x,
540 fun: best_val,
541 nfev,
542 nit,
543 success: true,
544 message: format!(
545 "SA completed: {} iterations, {} evaluations, acceptance ratio {:.4}",
546 nit, nfev, acceptance_ratio
547 ),
548 final_temperature: temperature,
549 acceptance_ratio,
550 })
551 }
552}
553
554#[derive(Debug, Clone)]
560pub struct MultiStartSaOptions {
561 pub n_starts: usize,
563 pub sa_options: MetaheuristicSaOptions,
565 pub seed: Option<u64>,
567}
568
569impl Default for MultiStartSaOptions {
570 fn default() -> Self {
571 Self {
572 n_starts: 5,
573 sa_options: MetaheuristicSaOptions::default(),
574 seed: None,
575 }
576 }
577}
578
579pub fn multi_start_sa<F>(
584 func: F,
585 bounds: &[(f64, f64)],
586 options: MultiStartSaOptions,
587) -> OptimizeResult<MetaheuristicSaResult>
588where
589 F: Fn(&ArrayView1<f64>) -> f64,
590{
591 if bounds.is_empty() {
592 return Err(OptimizeError::InvalidInput(
593 "Bounds must not be empty for multi-start SA".to_string(),
594 ));
595 }
596 if options.n_starts == 0 {
597 return Err(OptimizeError::InvalidParameter(
598 "n_starts must be at least 1".to_string(),
599 ));
600 }
601
602 let ndim = bounds.len();
603 let outer_seed = options.seed.unwrap_or_else(|| rng().random());
604 let mut outer_rng = StdRng::seed_from_u64(outer_seed);
605
606 let mut best_result: Option<MetaheuristicSaResult> = None;
607 let mut total_nfev: usize = 0;
608
609 for start_idx in 0..options.n_starts {
610 let x0 = Array1::from_vec(
612 bounds
613 .iter()
614 .map(|(lo, hi)| lo + outer_rng.random::<f64>() * (hi - lo))
615 .collect::<Vec<_>>(),
616 );
617
618 let mut sa_opts = options.sa_options.clone();
619 sa_opts.bounds = Some(bounds.to_vec());
620 sa_opts.seed = Some(outer_rng.random::<u64>());
622
623 let mut optimizer = SimulatedAnnealingOptimizer::new(sa_opts);
624 match optimizer.optimize(&func, &x0) {
625 Ok(result) => {
626 total_nfev += result.nfev;
627 let is_better = best_result
628 .as_ref()
629 .map_or(true, |best| result.fun < best.fun);
630 if is_better {
631 best_result = Some(result);
632 }
633 }
634 Err(_) => {
635 continue;
637 }
638 }
639 }
640
641 match best_result {
642 Some(mut result) => {
643 result.nfev = total_nfev;
644 result.message = format!(
645 "Multi-start SA: best of {} starts, {} total evaluations",
646 options.n_starts, total_nfev
647 );
648 Ok(result)
649 }
650 None => Err(OptimizeError::ComputationError(
651 "All multi-start SA runs failed".to_string(),
652 )),
653 }
654}
655
656#[cfg(test)]
661mod tests {
662 use super::*;
663 use scirs2_core::ndarray::array;
664
665 fn sphere(x: &ArrayView1<f64>) -> f64 {
666 x.iter().map(|xi| xi * xi).sum()
667 }
668
669 fn rosenbrock(x: &ArrayView1<f64>) -> f64 {
670 let mut sum = 0.0;
671 for i in 0..x.len() - 1 {
672 sum += 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2);
673 }
674 sum
675 }
676
677 fn rastrigin(x: &ArrayView1<f64>) -> f64 {
678 let n = x.len() as f64;
679 10.0 * n
680 + x.iter()
681 .map(|xi| xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos())
682 .sum::<f64>()
683 }
684
685 #[test]
688 fn test_sa_sphere_exponential() {
689 let opts = MetaheuristicSaOptions {
690 max_iter: 5000,
691 initial_temp: 100.0,
692 final_temp: 1e-10,
693 cooling: CoolingSchedule::Exponential { alpha: 0.99 },
694 steps_per_temp: 20,
695 step_size: 0.5,
696 seed: Some(42),
697 bounds: Some(vec![(-5.0, 5.0); 2]),
698 ..Default::default()
699 };
700
701 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
702 let x0 = array![3.0, -2.0];
703 let result = optimizer.optimize(sphere, &x0).expect("SA should succeed");
704
705 assert!(
706 result.fun < 1.0,
707 "Sphere minimum should be near 0, got {}",
708 result.fun
709 );
710 assert!(result.success);
711 assert!(result.nfev > 0);
712 }
713
714 #[test]
715 fn test_sa_sphere_linear_cooling() {
716 let opts = MetaheuristicSaOptions {
717 max_iter: 5000,
718 initial_temp: 50.0,
719 final_temp: 1e-8,
720 cooling: CoolingSchedule::Linear,
721 steps_per_temp: 20,
722 step_size: 0.3,
723 seed: Some(123),
724 bounds: Some(vec![(-5.0, 5.0); 2]),
725 ..Default::default()
726 };
727
728 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
729 let x0 = array![2.0, 2.0];
730 let result = optimizer.optimize(sphere, &x0).expect("SA should succeed");
731
732 assert!(
733 result.fun < 2.0,
734 "Sphere should converge near 0, got {}",
735 result.fun
736 );
737 }
738
739 #[test]
740 fn test_sa_sphere_logarithmic_cooling() {
741 let opts = MetaheuristicSaOptions {
742 max_iter: 5000,
743 initial_temp: 100.0,
744 final_temp: 1e-10,
745 cooling: CoolingSchedule::Logarithmic { c: 1.0 },
746 steps_per_temp: 20,
747 step_size: 0.5,
748 seed: Some(7),
749 bounds: Some(vec![(-5.0, 5.0); 2]),
750 ..Default::default()
751 };
752
753 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
754 let x0 = array![4.0, -3.0];
755 let result = optimizer.optimize(sphere, &x0).expect("SA should succeed");
756
757 assert!(result.fun < 5.0, "Log cooling on sphere got {}", result.fun);
759 }
760
761 #[test]
762 fn test_sa_adaptive_cooling() {
763 let opts = MetaheuristicSaOptions {
764 max_iter: 5000,
765 initial_temp: 100.0,
766 final_temp: 1e-10,
767 cooling: CoolingSchedule::Adaptive {
768 target_acceptance: 0.44,
769 adjustment_factor: 1.02,
770 },
771 steps_per_temp: 20,
772 step_size: 0.5,
773 seed: Some(99),
774 bounds: Some(vec![(-5.0, 5.0); 2]),
775 ..Default::default()
776 };
777
778 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
779 let x0 = array![3.0, 3.0];
780 let result = optimizer.optimize(sphere, &x0).expect("SA should succeed");
781
782 assert!(result.fun < 5.0, "Adaptive SA on sphere got {}", result.fun);
783 }
784
785 #[test]
788 fn test_sa_periodic_reheating() {
789 let opts = MetaheuristicSaOptions {
790 max_iter: 3000,
791 initial_temp: 50.0,
792 final_temp: 1e-8,
793 cooling: CoolingSchedule::Exponential { alpha: 0.99 },
794 steps_per_temp: 10,
795 step_size: 0.3,
796 reheating: ReheatingStrategy::Periodic {
797 interval: 500,
798 fraction: 0.3,
799 },
800 seed: Some(55),
801 bounds: Some(vec![(-5.0, 5.0); 2]),
802 ..Default::default()
803 };
804
805 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
806 let x0 = array![4.0, -4.0];
807 let result = optimizer
808 .optimize(sphere, &x0)
809 .expect("SA with reheating should succeed");
810
811 assert!(result.success);
812 assert!(result.fun < 5.0, "Periodic reheating got {}", result.fun);
813 }
814
815 #[test]
816 fn test_sa_stagnation_reheating() {
817 let opts = MetaheuristicSaOptions {
818 max_iter: 3000,
819 initial_temp: 50.0,
820 final_temp: 1e-8,
821 cooling: CoolingSchedule::Exponential { alpha: 0.99 },
822 steps_per_temp: 10,
823 step_size: 0.3,
824 reheating: ReheatingStrategy::Stagnation {
825 patience: 200,
826 fraction: 0.5,
827 },
828 seed: Some(77),
829 bounds: Some(vec![(-5.0, 5.0); 2]),
830 ..Default::default()
831 };
832
833 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
834 let x0 = array![3.0, 3.0];
835 let result = optimizer
836 .optimize(sphere, &x0)
837 .expect("SA with stagnation reheating should succeed");
838 assert!(result.success);
839 }
840
841 #[test]
844 fn test_sa_with_constraints() {
845 let constraints = |x: &ArrayView1<f64>| -> Vec<f64> {
847 let sum = x[0] + x[1];
848 vec![2.0 - sum] };
850
851 let opts = MetaheuristicSaOptions {
852 max_iter: 5000,
853 initial_temp: 50.0,
854 final_temp: 1e-10,
855 cooling: CoolingSchedule::Exponential { alpha: 0.995 },
856 steps_per_temp: 20,
857 step_size: 0.2,
858 seed: Some(42),
859 bounds: Some(vec![(-5.0, 5.0); 2]),
860 ..Default::default()
861 };
862
863 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
864 let x0 = array![1.5, 1.5];
865 let result = optimizer
866 .optimize_constrained(sphere, &x0, Some(constraints))
867 .expect("Constrained SA should succeed");
868
869 let sum = result.x[0] + result.x[1];
871 assert!(
872 sum >= 1.5,
873 "Constraint should be approximately satisfied: sum = {}",
874 sum
875 );
876 }
877
878 #[test]
879 fn test_sa_adaptive_penalty() {
880 let constraints = |x: &ArrayView1<f64>| -> Vec<f64> {
881 vec![1.0 - x[0], 1.0 - x[1]] };
883
884 let handler = ConstraintHandler {
885 penalty: PenaltyConstraint {
886 penalty_coeff: 100.0,
887 adaptive: true,
888 max_penalty: 1e6,
889 },
890 };
891
892 let opts = MetaheuristicSaOptions {
893 max_iter: 3000,
894 initial_temp: 50.0,
895 final_temp: 1e-8,
896 cooling: CoolingSchedule::Exponential { alpha: 0.995 },
897 steps_per_temp: 20,
898 step_size: 0.3,
899 seed: Some(42),
900 bounds: Some(vec![(-5.0, 5.0); 2]),
901 ..Default::default()
902 };
903
904 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
905 let x0 = array![2.0, 2.0];
906 let result = optimizer
907 .optimize_with_handler(sphere, &x0, Some(constraints), &handler)
908 .expect("Adaptive penalty SA should succeed");
909
910 assert!(
912 result.x[0] > 0.5 && result.x[1] > 0.5,
913 "Constrained result: x={:.4}, y={:.4}",
914 result.x[0],
915 result.x[1]
916 );
917 }
918
919 #[test]
922 fn test_multi_start_sa() {
923 let bounds = vec![(-5.0, 5.0); 2];
924 let ms_opts = MultiStartSaOptions {
925 n_starts: 5,
926 sa_options: MetaheuristicSaOptions {
927 max_iter: 2000,
928 initial_temp: 50.0,
929 final_temp: 1e-8,
930 cooling: CoolingSchedule::Exponential { alpha: 0.99 },
931 steps_per_temp: 10,
932 step_size: 0.3,
933 seed: None,
934 ..Default::default()
935 },
936 seed: Some(42),
937 };
938
939 let result =
940 multi_start_sa(sphere, &bounds, ms_opts).expect("Multi-start SA should succeed");
941
942 assert!(result.fun < 2.0, "Multi-start on sphere got {}", result.fun);
943 assert!(result.success);
944 }
945
946 #[test]
947 fn test_multi_start_sa_rastrigin() {
948 let bounds = vec![(-5.12, 5.12); 3];
949 let ms_opts = MultiStartSaOptions {
950 n_starts: 10,
951 sa_options: MetaheuristicSaOptions {
952 max_iter: 3000,
953 initial_temp: 100.0,
954 final_temp: 1e-8,
955 cooling: CoolingSchedule::Exponential { alpha: 0.995 },
956 steps_per_temp: 20,
957 step_size: 0.5,
958 seed: None,
959 ..Default::default()
960 },
961 seed: Some(123),
962 };
963
964 let result =
965 multi_start_sa(rastrigin, &bounds, ms_opts).expect("Multi-start SA on rastrigin");
966
967 assert!(
969 result.fun < 20.0,
970 "Multi-start rastrigin got {}",
971 result.fun
972 );
973 }
974
975 #[test]
978 fn test_sa_empty_input_error() {
979 let opts = MetaheuristicSaOptions {
980 seed: Some(1),
981 ..Default::default()
982 };
983 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
984 let x0 = Array1::<f64>::zeros(0);
985 let result = optimizer.optimize(sphere, &x0);
986 assert!(result.is_err());
987 }
988
989 #[test]
990 fn test_sa_invalid_temp_error() {
991 let opts = MetaheuristicSaOptions {
992 initial_temp: -1.0,
993 seed: Some(1),
994 ..Default::default()
995 };
996 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
997 let x0 = array![1.0, 1.0];
998 let result = optimizer.optimize(sphere, &x0);
999 assert!(result.is_err());
1000 }
1001
1002 #[test]
1003 fn test_sa_single_dimension() {
1004 let opts = MetaheuristicSaOptions {
1005 max_iter: 2000,
1006 initial_temp: 50.0,
1007 final_temp: 1e-8,
1008 cooling: CoolingSchedule::Exponential { alpha: 0.99 },
1009 steps_per_temp: 10,
1010 step_size: 0.5,
1011 seed: Some(42),
1012 bounds: Some(vec![(-10.0, 10.0)]),
1013 ..Default::default()
1014 };
1015
1016 let mut optimizer = SimulatedAnnealingOptimizer::new(opts);
1017 let x0 = array![5.0];
1018 let result = optimizer
1019 .optimize(|x: &ArrayView1<f64>| x[0] * x[0], &x0)
1020 .expect("1D SA");
1021 assert!(result.fun < 1.0, "1D SA got {}", result.fun);
1022 }
1023
1024 #[test]
1025 fn test_sa_to_optimize_results() {
1026 let sa_result = MetaheuristicSaResult {
1027 x: array![1.0, 2.0],
1028 fun: 5.0,
1029 nfev: 1000,
1030 nit: 500,
1031 success: true,
1032 message: "test".to_string(),
1033 final_temperature: 0.001,
1034 acceptance_ratio: 0.4,
1035 };
1036 let opt_results = sa_result.to_optimize_results();
1037 assert_eq!(opt_results.nfev, 1000);
1038 assert_eq!(opt_results.nit, 500);
1039 assert!(opt_results.success);
1040 }
1041
1042 #[test]
1043 fn test_multi_start_sa_empty_bounds_error() {
1044 let ms_opts = MultiStartSaOptions {
1045 n_starts: 3,
1046 seed: Some(1),
1047 ..Default::default()
1048 };
1049 let result = multi_start_sa(sphere, &[], ms_opts);
1050 assert!(result.is_err());
1051 }
1052
1053 #[test]
1054 fn test_multi_start_sa_zero_starts_error() {
1055 let ms_opts = MultiStartSaOptions {
1056 n_starts: 0,
1057 seed: Some(1),
1058 ..Default::default()
1059 };
1060 let result = multi_start_sa(sphere, &[(-1.0, 1.0)], ms_opts);
1061 assert!(result.is_err());
1062 }
1063
1064 #[test]
1065 fn test_constraint_handler_no_violation() {
1066 let handler = ConstraintHandler::default();
1067 let violations = vec![-1.0, -0.5]; let penalty = handler.compute_penalty(&violations, 0, 100);
1069 assert!((penalty - 0.0).abs() < 1e-15);
1070 }
1071
1072 #[test]
1073 fn test_constraint_handler_with_violation() {
1074 let handler = ConstraintHandler {
1075 penalty: PenaltyConstraint {
1076 penalty_coeff: 100.0,
1077 adaptive: false,
1078 max_penalty: 1e8,
1079 },
1080 };
1081 let violations = vec![2.0]; let penalty = handler.compute_penalty(&violations, 0, 100);
1083 assert!((penalty - 400.0).abs() < 1e-10); }
1085
1086 #[test]
1087 fn test_adaptive_cooling_state_update() {
1088 let mut state = AdaptiveCoolingState {
1089 window_size: 5,
1090 ..Default::default()
1091 };
1092 for _ in 0..5 {
1094 state.update(true, 0.44, 1.05);
1095 }
1096 assert!(state.effective_alpha > 0.95); }
1098}