1use crate::error::{OptimizeError, OptimizeResult};
21use crate::unconstrained::{
22 minimize, Bounds as UnconstrainedBounds, Method as UnconstrainedMethod,
23 OptimizeResult as LocalOptResult, Options,
24};
25use scirs2_core::ndarray::{Array1, ArrayView1};
26use scirs2_core::random::rngs::StdRng;
27use scirs2_core::random::{Rng, RngExt, SeedableRng};
28
29pub type Bounds = Vec<(f64, f64)>;
31
32#[derive(Debug, Clone)]
38pub struct AdvancedMultiStartOptions {
39 pub n_starts: usize,
41 pub local_method: UnconstrainedMethod,
43 pub max_local_fevals: usize,
45 pub merge_tol: f64,
47 pub seed: Option<u64>,
49}
50
51impl Default for AdvancedMultiStartOptions {
52 fn default() -> Self {
53 Self {
54 n_starts: 20,
55 local_method: UnconstrainedMethod::BFGS,
56 max_local_fevals: 5_000,
57 merge_tol: 1e-4,
58 seed: None,
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct AdvancedMultiStartResult {
66 pub x: Array1<f64>,
68 pub fun: f64,
70 pub local_optima: Vec<(Array1<f64>, f64)>,
72 pub nfev: usize,
74 pub n_successful: usize,
76 pub success: bool,
78 pub message: String,
80}
81
82pub fn advanced_multi_start<F>(
84 func: F,
85 bounds: &Bounds,
86 options: Option<AdvancedMultiStartOptions>,
87) -> OptimizeResult<AdvancedMultiStartResult>
88where
89 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
90{
91 let options = options.unwrap_or_default();
92 let ndim = bounds.len();
93 if ndim == 0 {
94 return Err(OptimizeError::InvalidInput(
95 "Bounds must have at least one dimension".to_string(),
96 ));
97 }
98
99 let seed = options
100 .seed
101 .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
102 let mut rng = StdRng::seed_from_u64(seed);
103
104 let starting_points = generate_lhs_points(ndim, options.n_starts, bounds, &mut rng);
106
107 let unconstrained_bounds = UnconstrainedBounds::from_vecs(
108 bounds.iter().map(|&(lb, _)| Some(lb)).collect(),
109 bounds.iter().map(|&(_, ub)| Some(ub)).collect(),
110 )
111 .ok();
112
113 let mut all_results: Vec<(Array1<f64>, f64)> = Vec::new();
114 let mut total_fevals = 0_usize;
115 let mut n_successful = 0_usize;
116
117 for x0 in starting_points {
118 let f = func.clone();
119 let opts = Options {
120 bounds: unconstrained_bounds.clone(),
121 max_iter: options.max_local_fevals,
122 ..Default::default()
123 };
124
125 let result = minimize(
126 move |x: &ArrayView1<f64>| f(x),
127 &x0.to_vec(),
128 options.local_method,
129 Some(opts),
130 );
131
132 match result {
133 Ok(res) => {
134 total_fevals += res.nfev;
135 if res.success {
136 n_successful += 1;
137 all_results.push((res.x, res.fun));
138 }
139 }
140 Err(_) => {
141 }
143 }
144 }
145
146 let merged = merge_optima(&all_results, options.merge_tol);
148
149 let mut sorted = merged;
151 sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
152
153 if sorted.is_empty() {
154 return Ok(AdvancedMultiStartResult {
155 x: Array1::zeros(ndim),
156 fun: f64::INFINITY,
157 local_optima: Vec::new(),
158 nfev: total_fevals,
159 n_successful: 0,
160 success: false,
161 message: "All local optimizations failed".to_string(),
162 });
163 }
164
165 let best = sorted[0].clone();
166
167 Ok(AdvancedMultiStartResult {
168 x: best.0,
169 fun: best.1,
170 local_optima: sorted,
171 nfev: total_fevals,
172 n_successful,
173 success: true,
174 message: format!(
175 "Found {} distinct optima from {} starts",
176 n_successful, options.n_starts
177 ),
178 })
179}
180
181#[derive(Debug, Clone)]
187pub struct MonotonicBasinHoppingOptions {
188 pub n_iterations: usize,
190 pub step_size: f64,
192 pub local_method: UnconstrainedMethod,
194 pub seed: Option<u64>,
196 pub step_increase: f64,
198 pub step_decrease: f64,
200 pub step_min: f64,
202 pub step_max: f64,
204 pub target_accept_ratio: f64,
206}
207
208impl Default for MonotonicBasinHoppingOptions {
209 fn default() -> Self {
210 Self {
211 n_iterations: 100,
212 step_size: 0.5,
213 local_method: UnconstrainedMethod::BFGS,
214 seed: None,
215 step_increase: 1.1,
216 step_decrease: 0.9,
217 step_min: 1e-6,
218 step_max: 10.0,
219 target_accept_ratio: 0.5,
220 }
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct MonotonicBasinHoppingResult {
227 pub x: Array1<f64>,
229 pub fun: f64,
231 pub nfev: usize,
233 pub n_accepted: usize,
235 pub nit: usize,
237 pub final_step_size: f64,
239 pub success: bool,
241 pub message: String,
243}
244
245pub fn monotonic_basin_hopping<F>(
251 func: F,
252 x0: &[f64],
253 bounds: &Bounds,
254 options: Option<MonotonicBasinHoppingOptions>,
255) -> OptimizeResult<MonotonicBasinHoppingResult>
256where
257 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
258{
259 let options = options.unwrap_or_default();
260 let ndim = x0.len();
261
262 let seed = options
263 .seed
264 .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
265 let mut rng = StdRng::seed_from_u64(seed);
266
267 let unconstrained_bounds = UnconstrainedBounds::from_vecs(
269 bounds.iter().map(|&(lb, _)| Some(lb)).collect(),
270 bounds.iter().map(|&(_, ub)| Some(ub)).collect(),
271 )
272 .ok();
273
274 let initial_opts = Options {
275 bounds: unconstrained_bounds.clone(),
276 ..Default::default()
277 };
278
279 let initial_result = minimize(func.clone(), x0, options.local_method, Some(initial_opts))
280 .map_err(|e| {
281 OptimizeError::ComputationError(format!("Initial minimization failed: {}", e))
282 })?;
283
284 let mut current_x = initial_result.x;
285 let mut current_f = initial_result.fun;
286 let mut best_x = current_x.clone();
287 let mut best_f = current_f;
288 let mut total_fevals = initial_result.nfev;
289 let mut step_size = options.step_size;
290 let mut n_accepted = 0_usize;
291
292 for iteration in 0..options.n_iterations {
293 let mut x_new = current_x.clone();
295 for i in 0..ndim {
296 x_new[i] += rng.random_range(-step_size..step_size);
297 if i < bounds.len() {
299 x_new[i] = x_new[i].clamp(bounds[i].0, bounds[i].1);
300 }
301 }
302
303 let local_opts = Options {
305 bounds: unconstrained_bounds.clone(),
306 ..Default::default()
307 };
308
309 let result = minimize(
310 func.clone(),
311 &x_new.to_vec(),
312 options.local_method,
313 Some(local_opts),
314 );
315
316 if let Ok(res) = result {
317 total_fevals += res.nfev;
318
319 if res.fun < current_f {
321 current_x = res.x;
322 current_f = res.fun;
323 n_accepted += 1;
324
325 if current_f < best_f {
326 best_f = current_f;
327 best_x = current_x.clone();
328 }
329
330 step_size = (step_size * options.step_increase).min(options.step_max);
332 } else {
333 step_size = (step_size * options.step_decrease).max(options.step_min);
335 }
336 }
337
338 let accept_ratio = if iteration > 0 {
340 n_accepted as f64 / (iteration + 1) as f64
341 } else {
342 0.5
343 };
344
345 if accept_ratio < options.target_accept_ratio * 0.5 {
346 step_size = (step_size * 0.8).max(options.step_min);
347 } else if accept_ratio > options.target_accept_ratio * 1.5 {
348 step_size = (step_size * 1.2).min(options.step_max);
349 }
350 }
351
352 Ok(MonotonicBasinHoppingResult {
353 x: best_x,
354 fun: best_f,
355 nfev: total_fevals,
356 n_accepted,
357 nit: options.n_iterations,
358 final_step_size: step_size,
359 success: true,
360 message: format!(
361 "Monotonic basin-hopping: {} accepted of {} iterations",
362 n_accepted, options.n_iterations
363 ),
364 })
365}
366
367#[derive(Debug, Clone)]
373pub struct StochasticTunnelingOptions {
374 pub n_iterations: usize,
376 pub gamma: f64,
378 pub step_size: f64,
380 pub local_method: UnconstrainedMethod,
382 pub local_every: usize,
384 pub seed: Option<u64>,
386}
387
388impl Default for StochasticTunnelingOptions {
389 fn default() -> Self {
390 Self {
391 n_iterations: 1_000,
392 gamma: 1.0,
393 step_size: 0.1,
394 local_method: UnconstrainedMethod::BFGS,
395 local_every: 50,
396 seed: None,
397 }
398 }
399}
400
401#[derive(Debug, Clone)]
403pub struct StochasticTunnelingResult {
404 pub x: Array1<f64>,
406 pub fun: f64,
408 pub nfev: usize,
410 pub nit: usize,
412 pub n_local_opts: usize,
414 pub success: bool,
416 pub message: String,
418}
419
420pub fn stochastic_tunneling<F>(
428 func: F,
429 x0: &[f64],
430 bounds: &Bounds,
431 options: Option<StochasticTunnelingOptions>,
432) -> OptimizeResult<StochasticTunnelingResult>
433where
434 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
435{
436 let options = options.unwrap_or_default();
437 let ndim = x0.len();
438
439 let seed = options
440 .seed
441 .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
442 let mut rng = StdRng::seed_from_u64(seed);
443
444 let mut current_x = Array1::from_vec(x0.to_vec());
445 for i in 0..ndim.min(bounds.len()) {
447 current_x[i] = current_x[i].clamp(bounds[i].0, bounds[i].1);
448 }
449
450 let mut current_f = func(¤t_x.view());
451 let mut best_x = current_x.clone();
452 let mut best_f = current_f;
453 let mut f_ref = current_f; let mut fevals = 1_usize;
455 let mut n_local_opts = 0_usize;
456
457 let unconstrained_bounds = UnconstrainedBounds::from_vecs(
458 bounds.iter().map(|&(lb, _)| Some(lb)).collect(),
459 bounds.iter().map(|&(_, ub)| Some(ub)).collect(),
460 )
461 .ok();
462
463 for iteration in 0..options.n_iterations {
464 let stun_current = 1.0 - (-options.gamma * (current_f - f_ref)).exp();
466
467 let mut x_new = current_x.clone();
469 for i in 0..ndim {
470 x_new[i] += rng.random_range(-options.step_size..options.step_size);
471 if i < bounds.len() {
472 x_new[i] = x_new[i].clamp(bounds[i].0, bounds[i].1);
473 }
474 }
475
476 let f_new = func(&x_new.view());
477 fevals += 1;
478
479 let stun_new = 1.0 - (-options.gamma * (f_new - f_ref)).exp();
480
481 let delta_stun = stun_new - stun_current;
483 let accept = if delta_stun <= 0.0 {
484 true
485 } else {
486 let prob = (-delta_stun).exp();
487 rng.random_range(0.0..1.0) < prob
488 };
489
490 if accept {
491 current_x = x_new;
492 current_f = f_new;
493
494 if current_f < best_f {
495 best_f = current_f;
496 best_x = current_x.clone();
497 f_ref = best_f; }
499 }
500
501 if (iteration + 1) % options.local_every == 0 {
503 let local_opts = Options {
504 bounds: unconstrained_bounds.clone(),
505 ..Default::default()
506 };
507
508 if let Ok(res) = minimize(
509 func.clone(),
510 ¤t_x.to_vec(),
511 options.local_method,
512 Some(local_opts),
513 ) {
514 fevals += res.nfev;
515 n_local_opts += 1;
516
517 if res.fun < best_f {
518 best_f = res.fun;
519 best_x = res.x.clone();
520 f_ref = best_f;
521 }
522 if res.success {
523 current_x = res.x;
524 current_f = res.fun;
525 }
526 }
527 }
528 }
529
530 Ok(StochasticTunnelingResult {
531 x: best_x,
532 fun: best_f,
533 nfev: fevals,
534 nit: options.n_iterations,
535 n_local_opts,
536 success: true,
537 message: format!(
538 "Stochastic tunneling: {} iterations, {} local optimizations",
539 options.n_iterations, n_local_opts
540 ),
541 })
542}
543
544#[derive(Debug, Clone)]
550pub struct DeflationOptions {
551 pub max_optima: usize,
553 pub deflation_radius: f64,
555 pub deflation_power: f64,
557 pub n_starts: usize,
559 pub local_method: UnconstrainedMethod,
561 pub f_threshold: f64,
563 pub seed: Option<u64>,
565}
566
567impl Default for DeflationOptions {
568 fn default() -> Self {
569 Self {
570 max_optima: 10,
571 deflation_radius: 0.1,
572 deflation_power: 2.0,
573 n_starts: 10,
574 local_method: UnconstrainedMethod::BFGS,
575 f_threshold: f64::INFINITY,
576 seed: None,
577 }
578 }
579}
580
581#[derive(Debug, Clone)]
583pub struct DeflationResult {
584 pub optima: Vec<(Array1<f64>, f64)>,
586 pub nfev: usize,
588 pub success: bool,
590 pub message: String,
592}
593
594pub fn deflation_search<F>(
602 func: F,
603 bounds: &Bounds,
604 options: Option<DeflationOptions>,
605) -> OptimizeResult<DeflationResult>
606where
607 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
608{
609 let options = options.unwrap_or_default();
610 let ndim = bounds.len();
611 if ndim == 0 {
612 return Err(OptimizeError::InvalidInput(
613 "Bounds must have at least one dimension".to_string(),
614 ));
615 }
616
617 let seed = options
618 .seed
619 .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
620 let mut rng = StdRng::seed_from_u64(seed);
621
622 let mut found_optima: Vec<(Array1<f64>, f64)> = Vec::new();
623 let mut total_fevals = 0_usize;
624
625 let unconstrained_bounds = UnconstrainedBounds::from_vecs(
626 bounds.iter().map(|&(lb, _)| Some(lb)).collect(),
627 bounds.iter().map(|&(_, ub)| Some(ub)).collect(),
628 )
629 .ok();
630
631 for search_round in 0..options.max_optima {
632 let mut best_result: Option<LocalOptResult<f64>> = None;
633 let known = found_optima.clone();
634 let deflation_radius = options.deflation_radius;
635 let deflation_power = options.deflation_power;
636
637 let deflated_func = {
639 let f = func.clone();
640 let known_optima = known.clone();
641 move |x: &ArrayView1<f64>| -> f64 {
642 let f_val = f(x);
643
644 let mut deflation = 1.0;
646 for (opt_x, _) in &known_optima {
647 let mut sq_dist = 0.0;
648 for i in 0..x.len() {
649 let diff = x[i] - opt_x[i];
650 sq_dist += diff * diff;
651 }
652 let dist = sq_dist.sqrt();
653 if dist < deflation_radius {
654 let ratio = dist / deflation_radius;
656 deflation *= ratio.powf(deflation_power);
657 }
658 }
659
660 if deflation < 1e-30 {
661 f64::MAX / 2.0 } else {
663 f_val / deflation }
665 }
666 };
667
668 for _ in 0..options.n_starts {
670 let mut x0 = vec![0.0; ndim];
671 for i in 0..ndim {
672 x0[i] = rng.random_range(bounds[i].0..bounds[i].1);
673 }
674
675 let local_opts = Options {
676 bounds: unconstrained_bounds.clone(),
677 ..Default::default()
678 };
679
680 let df = deflated_func.clone();
681 if let Ok(res) = minimize(
682 move |x: &ArrayView1<f64>| df(x),
683 &x0,
684 options.local_method,
685 Some(local_opts),
686 ) {
687 total_fevals += res.nfev;
688
689 let f_original = func(&res.x.view());
691 total_fevals += 1;
692
693 let is_new = !found_optima.iter().any(|(opt_x, _)| {
694 let mut sq_dist = 0.0;
695 for i in 0..ndim {
696 let diff = res.x[i] - opt_x[i];
697 sq_dist += diff * diff;
698 }
699 sq_dist.sqrt() < options.deflation_radius
700 });
701
702 if is_new && f_original < options.f_threshold {
703 let update = match &best_result {
704 None => true,
705 Some(prev) => f_original < prev.fun,
706 };
707 if update {
708 best_result = Some(LocalOptResult {
709 x: res.x,
710 fun: f_original,
711 success: true,
712 message: format!("Deflation round {}", search_round),
713 ..Default::default()
714 });
715 }
716 }
717 }
718 }
719
720 match best_result {
721 Some(res) => {
722 found_optima.push((res.x, res.fun));
723 }
724 None => {
725 break;
727 }
728 }
729 }
730
731 found_optima.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
733
734 let success = !found_optima.is_empty();
735 let message = format!("Found {} distinct optima", found_optima.len());
736
737 Ok(DeflationResult {
738 optima: found_optima,
739 nfev: total_fevals,
740 success,
741 message,
742 })
743}
744
745fn generate_lhs_points(
751 ndim: usize,
752 n: usize,
753 bounds: &Bounds,
754 rng: &mut StdRng,
755) -> Vec<Array1<f64>> {
756 let mut points = Vec::with_capacity(n);
757
758 let mut perms: Vec<Vec<usize>> = (0..ndim)
760 .map(|_| {
761 let mut perm: Vec<usize> = (0..n).collect();
762 for i in (1..n).rev() {
764 let j = rng.random_range(0..=i);
765 perm.swap(i, j);
766 }
767 perm
768 })
769 .collect();
770
771 for i in 0..n {
772 let mut point = Array1::zeros(ndim);
773 for j in 0..ndim {
774 let cell = perms[j][i];
775 let u = rng.random_range(0.0..1.0);
776 let t = (cell as f64 + u) / n as f64;
777 let (lb, ub) = bounds[j];
778 point[j] = lb + t * (ub - lb);
779 }
780 points.push(point);
781 }
782
783 let _ = perms; points
785}
786
787fn merge_optima(optima: &[(Array1<f64>, f64)], tol: f64) -> Vec<(Array1<f64>, f64)> {
789 let mut merged: Vec<(Array1<f64>, f64)> = Vec::new();
790
791 for (x, f) in optima {
792 let mut is_duplicate = false;
793 for (mx, mf) in &mut merged {
794 let mut sq_dist = 0.0;
795 for i in 0..x.len() {
796 let diff = x[i] - mx[i];
797 sq_dist += diff * diff;
798 }
799 if sq_dist.sqrt() < tol {
800 if *f < *mf {
802 *mx = x.clone();
803 *mf = *f;
804 }
805 is_duplicate = true;
806 break;
807 }
808 }
809 if !is_duplicate {
810 merged.push((x.clone(), *f));
811 }
812 }
813
814 merged
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820
821 fn sphere(x: &ArrayView1<f64>) -> f64 {
822 x.iter().map(|xi| xi * xi).sum()
823 }
824
825 fn rosenbrock(x: &ArrayView1<f64>) -> f64 {
826 let mut sum = 0.0;
827 for i in 0..x.len() - 1 {
828 sum += 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2);
829 }
830 sum
831 }
832
833 fn rastrigin(x: &ArrayView1<f64>) -> f64 {
834 let n = x.len() as f64;
835 let mut sum = 10.0 * n;
836 for &xi in x.iter() {
837 sum += xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos();
838 }
839 sum
840 }
841
842 fn two_minima(x: &ArrayView1<f64>) -> f64 {
844 let x0 = x[0];
845 let well1 = (x0 + 2.0).powi(2);
847 let well2 = 0.5 * (x0 - 2.0).powi(2) + 0.5;
848 well1.min(well2)
849 }
850
851 #[test]
852 fn test_advanced_multi_start_sphere() {
853 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
854 let result = advanced_multi_start(
855 sphere,
856 &bounds,
857 Some(AdvancedMultiStartOptions {
858 n_starts: 10,
859 seed: Some(42),
860 ..Default::default()
861 }),
862 );
863 assert!(result.is_ok());
864 let res = result.expect("Multi-start sphere failed");
865 assert!(res.fun < 0.1, "Multi-start sphere value: {}", res.fun);
866 assert!(res.n_successful > 0);
867 }
868
869 #[test]
870 fn test_advanced_multi_start_rosenbrock() {
871 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
872 let result = advanced_multi_start(
873 rosenbrock,
874 &bounds,
875 Some(AdvancedMultiStartOptions {
876 n_starts: 15,
877 seed: Some(123),
878 ..Default::default()
879 }),
880 );
881 assert!(result.is_ok());
882 let res = result.expect("Multi-start Rosenbrock failed");
883 assert!(res.fun < 1.0, "Multi-start Rosenbrock: {}", res.fun);
884 }
885
886 #[test]
887 fn test_monotonic_basin_hopping_sphere() {
888 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
889 let result = monotonic_basin_hopping(
890 sphere,
891 &[3.0, -2.0],
892 &bounds,
893 Some(MonotonicBasinHoppingOptions {
894 n_iterations: 30,
895 seed: Some(42),
896 ..Default::default()
897 }),
898 );
899 assert!(result.is_ok());
900 let res = result.expect("Monotonic BH sphere failed");
901 assert!(res.fun < 0.1, "Monotonic BH sphere: {}", res.fun);
902 }
903
904 #[test]
905 fn test_monotonic_basin_hopping_rastrigin() {
906 let bounds = vec![(-5.12, 5.12), (-5.12, 5.12)];
907 let result = monotonic_basin_hopping(
908 rastrigin,
909 &[2.0, -3.0],
910 &bounds,
911 Some(MonotonicBasinHoppingOptions {
912 n_iterations: 50,
913 step_size: 1.0,
914 seed: Some(99),
915 ..Default::default()
916 }),
917 );
918 assert!(result.is_ok());
919 let res = result.expect("Monotonic BH Rastrigin failed");
920 assert!(res.fun < 20.0, "Monotonic BH Rastrigin: {}", res.fun);
921 }
922
923 #[test]
924 fn test_stochastic_tunneling_sphere() {
925 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
926 let result = stochastic_tunneling(
927 sphere,
928 &[3.0, -2.0],
929 &bounds,
930 Some(StochasticTunnelingOptions {
931 n_iterations: 200,
932 gamma: 1.0,
933 step_size: 0.5,
934 local_every: 50,
935 seed: Some(42),
936 ..Default::default()
937 }),
938 );
939 assert!(result.is_ok());
940 let res = result.expect("Stochastic tunneling sphere failed");
941 assert!(res.fun < 1.0, "Stochastic tunneling sphere: {}", res.fun);
942 }
943
944 #[test]
945 fn test_stochastic_tunneling_rastrigin() {
946 let bounds = vec![(-5.12, 5.12), (-5.12, 5.12)];
947 let result = stochastic_tunneling(
948 rastrigin,
949 &[2.0, -3.0],
950 &bounds,
951 Some(StochasticTunnelingOptions {
952 n_iterations: 500,
953 gamma: 0.5,
954 step_size: 0.5,
955 local_every: 50,
956 seed: Some(42),
957 ..Default::default()
958 }),
959 );
960 assert!(result.is_ok());
961 let res = result.expect("Stochastic tunneling Rastrigin failed");
962 assert!(
963 res.fun < 20.0,
964 "Stochastic tunneling Rastrigin: {}",
965 res.fun
966 );
967 }
968
969 #[test]
970 fn test_deflation_two_minima() {
971 let bounds = vec![(-5.0, 5.0)];
972 let result = deflation_search(
973 two_minima,
974 &bounds,
975 Some(DeflationOptions {
976 max_optima: 3,
977 deflation_radius: 0.5,
978 n_starts: 10,
979 seed: Some(42),
980 ..Default::default()
981 }),
982 );
983 assert!(result.is_ok());
984 let res = result.expect("Deflation two_minima failed");
985 assert!(!res.optima.is_empty(), "Should find at least one optimum");
986 }
987
988 #[test]
989 fn test_deflation_sphere() {
990 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
991 let result = deflation_search(
992 sphere,
993 &bounds,
994 Some(DeflationOptions {
995 max_optima: 3,
996 deflation_radius: 1.0,
997 n_starts: 5,
998 seed: Some(42),
999 ..Default::default()
1000 }),
1001 );
1002 assert!(result.is_ok());
1003 let res = result.expect("Deflation sphere failed");
1004 assert!(res.success);
1005 assert!(
1007 res.optima[0].1 < 1.0,
1008 "Best optimum value: {}",
1009 res.optima[0].1
1010 );
1011 }
1012
1013 #[test]
1014 fn test_lhs_generation() {
1015 let bounds = vec![(-1.0, 1.0), (0.0, 10.0)];
1016 let mut rng = StdRng::seed_from_u64(42);
1017 let points = generate_lhs_points(2, 10, &bounds, &mut rng);
1018 assert_eq!(points.len(), 10);
1019 for p in &points {
1020 assert!(p[0] >= -1.0 && p[0] <= 1.0);
1021 assert!(p[1] >= 0.0 && p[1] <= 10.0);
1022 }
1023 }
1024
1025 #[test]
1026 fn test_merge_optima() {
1027 let optima = vec![
1028 (Array1::from_vec(vec![1.0, 1.0]), 0.5),
1029 (Array1::from_vec(vec![1.001, 1.001]), 0.4), (Array1::from_vec(vec![5.0, 5.0]), 1.0), ];
1032 let merged = merge_optima(&optima, 0.01);
1033 assert_eq!(merged.len(), 2);
1034 assert!((merged[0].1 - 0.4).abs() < 1e-10);
1036 }
1037
1038 #[test]
1039 fn test_advanced_multi_start_empty_bounds() {
1040 let result = advanced_multi_start(sphere, &vec![], None);
1041 assert!(result.is_err());
1042 }
1043
1044 #[test]
1045 fn test_monotonic_bh_adaptive_step() {
1046 let bounds = vec![(-5.0, 5.0)];
1047 let result = monotonic_basin_hopping(
1048 |x: &ArrayView1<f64>| x[0] * x[0],
1049 &[4.0],
1050 &bounds,
1051 Some(MonotonicBasinHoppingOptions {
1052 n_iterations: 20,
1053 step_size: 0.5,
1054 step_increase: 1.2,
1055 step_decrease: 0.8,
1056 seed: Some(42),
1057 ..Default::default()
1058 }),
1059 );
1060 assert!(result.is_ok());
1061 let res = result.expect("Adaptive step MBH failed");
1062 assert!(res.fun < 1.0, "Adaptive step MBH value: {}", res.fun);
1063 }
1064}