1use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering};
44use std::sync::{Arc, Mutex, MutexGuard};
45use std::time::Instant;
46
47use rayon::prelude::*;
48
49use crate::rng::Rng;
50
51#[derive(Clone, Debug, PartialEq)]
53pub struct RetryBounds {
54 lower: Arc<[f64]>,
55 upper: Arc<[f64]>,
56}
57
58impl RetryBounds {
59 pub fn new(lower: Vec<f64>, upper: Vec<f64>) -> Result<Self, &'static str> {
67 if lower.is_empty() || lower.len() != upper.len() {
68 return Err("bounds must be non-empty and have equal lengths");
69 }
70 if lower
71 .iter()
72 .zip(&upper)
73 .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
74 {
75 return Err("bounds must contain finite intervals with lower < upper");
76 }
77 Ok(Self {
78 lower: lower.into(),
79 upper: upper.into(),
80 })
81 }
82
83 #[inline]
84 pub fn dim(&self) -> usize {
86 self.lower.len()
87 }
88
89 #[inline]
90 pub fn lower(&self) -> &[f64] {
92 &self.lower
93 }
94
95 #[inline]
96 pub fn upper(&self) -> &[f64] {
98 &self.upper
99 }
100}
101
102#[derive(Clone, Debug)]
104#[non_exhaustive]
105pub struct RetryContext {
106 pub run_id: usize,
108 pub run_seed: u64,
114 pub seed: u64,
119 pub bounds: RetryBounds,
121 pub guess: Option<Vec<f64>>,
123 pub sdev: Vec<f64>,
125 pub max_evaluations: u64,
127 pub value_limit: f64,
129 pub crossover: bool,
131}
132
133#[derive(Clone, Debug, PartialEq)]
135pub struct RetryRunResult {
136 pub x: Vec<f64>,
138 pub y: f64,
140 pub evaluations: u64,
142}
143
144#[derive(Clone, Debug, PartialEq)]
146pub struct RetryEntry {
147 pub x: Vec<f64>,
149 pub y: f64,
151}
152
153#[derive(Clone, Debug, PartialEq)]
156pub struct RetryImprovement {
157 pub elapsed_seconds: f64,
159 pub evaluations: u64,
161 pub value: f64,
163}
164
165#[derive(Clone, Debug)]
167pub struct RetryResult {
168 pub x: Vec<f64>,
170 pub y: f64,
172 pub evaluations: u64,
174 pub runs: usize,
176 pub success: bool,
178 pub entries: Vec<RetryEntry>,
180 pub improvements: Vec<RetryImprovement>,
182}
183
184#[derive(Clone, Debug)]
186pub struct RetryConfig {
187 pub num_retries: usize,
189 pub workers: usize,
191 pub capacity: usize,
193 pub value_limit: f64,
195 pub stop_fitness: f64,
197 pub max_evaluations: u64,
199 pub seed: u64,
201 pub statistic_num: usize,
203}
204
205impl Default for RetryConfig {
206 fn default() -> Self {
207 Self {
208 num_retries: 1_024,
209 workers: 0,
210 capacity: 500,
211 value_limit: f64::INFINITY,
212 stop_fitness: f64::NEG_INFINITY,
213 max_evaluations: 50_000,
214 seed: 0,
215 statistic_num: 0,
216 }
217 }
218}
219
220#[derive(Clone, Debug)]
222pub struct AdvancedRetryConfig {
223 pub retry: RetryConfig,
225 pub check_interval: usize,
227 pub max_eval_fac: f64,
229 pub crossover_probability: f64,
231 pub diversity_threshold: f64,
233}
234
235impl Default for AdvancedRetryConfig {
236 fn default() -> Self {
237 Self {
238 retry: RetryConfig {
239 num_retries: 5_000,
240 max_evaluations: 1_500,
241 ..Default::default()
242 },
243 check_interval: 100,
244 max_eval_fac: 50.0,
245 crossover_probability: 0.5,
246 diversity_threshold: 0.15,
247 }
248 }
249}
250
251#[derive(Debug)]
252struct RetryStore {
253 dim: usize,
254 capacity: usize,
255 entries: Vec<RetryEntry>,
256 best_x: Vec<f64>,
257 best_y: f64,
258 evaluations: u64,
259 completed_runs: usize,
260 improvements: Vec<RetryImprovement>,
261 statistic_num: usize,
262 started: Instant,
263}
264
265impl RetryStore {
266 fn new(dim: usize, capacity: usize, statistic_num: usize) -> Self {
267 Self {
268 dim,
269 capacity: capacity.max(1),
270 entries: Vec::with_capacity(capacity.max(1)),
271 best_x: vec![0.0; dim],
272 best_y: f64::INFINITY,
273 evaluations: 0,
274 completed_runs: 0,
275 improvements: Vec::with_capacity(statistic_num),
276 statistic_num,
277 started: Instant::now(),
278 }
279 }
280
281 fn add(&mut self, result: RetryRunResult, limit: f64) -> bool {
282 self.completed_runs += 1;
283 self.evaluations = self.evaluations.saturating_add(result.evaluations);
284 if result.x.len() != self.dim || !result.y.is_finite() || result.y >= limit {
285 return false;
286 }
287
288 let improved = result.y < self.best_y;
289 if improved {
290 self.best_y = result.y;
291 self.best_x.clone_from(&result.x);
292 if self.statistic_num > 0 {
293 let sample = RetryImprovement {
294 elapsed_seconds: self.started.elapsed().as_secs_f64(),
295 evaluations: self.evaluations,
296 value: result.y,
297 };
298 if self.improvements.len() == self.statistic_num {
299 if let Some(last) = self.improvements.last_mut() {
300 *last = sample;
301 }
302 } else {
303 self.improvements.push(sample);
304 }
305 }
306 }
307
308 if self.entries.len() >= self.capacity {
309 self.sort_basic();
310 if self.entries.len() >= self.capacity {
311 self.entries.pop();
312 }
313 }
314 self.entries.push(RetryEntry {
315 x: result.x,
316 y: result.y,
317 });
318 improved
319 }
320
321 fn sort_basic(&mut self) {
322 self.entries.sort_unstable_by(|a, b| a.y.total_cmp(&b.y));
323 let keep = ((self.capacity as f64) * 0.9).floor() as usize;
324 self.entries.truncate(keep.max(1).min(self.capacity));
325 }
326
327 #[cfg(test)]
328 fn normalized_distance(&self, a: &[f64], b: &[f64], bounds: &RetryBounds) -> f64 {
329 let squared = a
330 .iter()
331 .zip(b)
332 .zip(bounds.lower().iter().zip(bounds.upper()))
333 .map(|((&av, &bv), (&lo, &hi))| ((av - bv) / (hi - lo)).powi(2))
334 .sum::<f64>();
335 (squared / self.dim as f64).sqrt()
336 }
337
338 fn sort_diverse(&mut self, bounds: &RetryBounds, threshold: f64) {
339 self.entries.sort_unstable_by(|a, b| a.y.total_cmp(&b.y));
340 let mut diverse = Vec::with_capacity(self.entries.len());
341 for entry in self.entries.drain(..) {
342 let sufficiently_different =
343 diverse.iter().rev().take(2).all(|previous: &RetryEntry| {
344 let squared = previous
345 .x
346 .iter()
347 .zip(&entry.x)
348 .zip(bounds.lower().iter().zip(bounds.upper()))
349 .map(|((&a, &b), (&lo, &hi))| ((a - b) / (hi - lo)).powi(2))
350 .sum::<f64>();
351 (squared / self.dim as f64).sqrt() > threshold
352 });
353 if sufficiently_different {
354 diverse.push(entry);
355 }
356 }
357 let keep = ((self.capacity as f64) * 0.9).floor() as usize;
358 diverse.truncate(keep.max(1).min(self.capacity));
359 self.entries = diverse;
360 }
361
362 fn into_result(mut self) -> RetryResult {
363 self.entries.sort_unstable_by(|a, b| a.y.total_cmp(&b.y));
364 RetryResult {
365 x: self.best_x,
366 y: self.best_y,
367 evaluations: self.evaluations,
368 runs: self.completed_runs,
369 success: self.best_y.is_finite(),
370 entries: self.entries,
371 improvements: self.improvements,
372 }
373 }
374}
375
376fn lock_store(store: &Mutex<RetryStore>) -> MutexGuard<'_, RetryStore> {
377 store
378 .lock()
379 .unwrap_or_else(std::sync::PoisonError::into_inner)
380}
381
382pub(crate) fn worker_count(requested: usize) -> usize {
383 if requested > 0 {
384 requested
385 } else {
386 std::thread::available_parallelism().map_or(1, usize::from)
387 }
388}
389
390#[inline]
392fn splitmix64(mut z: u64) -> u64 {
393 z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
394 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
395 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
396 z ^ (z >> 31)
397}
398
399pub fn retry_run_seed(root_seed: u64, run_id: usize) -> u64 {
405 splitmix64(
406 root_seed ^ 0xA076_1D64_78BD_642F ^ (run_id as u64).wrapping_mul(0xE703_7ED1_A0B4_28DB),
407 )
408}
409
410pub(crate) fn spawned_worker_rng(root_seed: u64, worker_id: usize) -> Rng {
414 let worker = worker_id as u64;
415 let state = ((splitmix64(root_seed ^ 0xD2B7_4407_B1CE_6E93 ^ worker) as u128) << 64)
416 | splitmix64(root_seed ^ 0xCA5A_8263_9512_1157 ^ worker) as u128;
417 let stream = ((splitmix64(root_seed ^ 0x9E37_79B9_7F4A_7C15) as u128) << 64) | worker as u128;
420 Rng::from_state_stream(state, stream)
421}
422
423fn initial_sdev(dim: usize, rng: &mut Rng) -> Vec<f64> {
424 let value = 0.05 + 0.05 * rng.uniform01();
425 vec![value; dim]
426}
427
428pub(crate) fn run_parallel<F>(workers: usize, task: F)
429where
430 F: Fn(usize) + Sync + Send,
431{
432 rayon::ThreadPoolBuilder::new()
433 .num_threads(workers)
434 .thread_name(|index| format!("fcmaes-retry-{index}"))
435 .build()
436 .expect("failed to build retry worker pool")
437 .install(|| {
438 (0..workers).into_par_iter().for_each(task);
439 });
440}
441
442pub fn retry<O, F>(
446 objective: &O,
447 bounds: &RetryBounds,
448 config: &RetryConfig,
449 optimize: F,
450) -> RetryResult
451where
452 O: Fn(&[f64]) -> f64 + Sync,
453 F: Fn(&O, &RetryContext) -> RetryRunResult + Sync + Send,
454{
455 if config.num_retries == 0 {
456 return RetryStore::new(bounds.dim(), config.capacity, config.statistic_num).into_result();
457 }
458 let workers = worker_count(config.workers).min(config.num_retries);
459 let next_run = AtomicUsize::new(0);
460 let stopped = AtomicBool::new(false);
461 let store = Mutex::new(RetryStore::new(
462 bounds.dim(),
463 config.capacity,
464 config.statistic_num,
465 ));
466
467 run_parallel(workers, |worker_id| {
468 let mut worker_rng = spawned_worker_rng(config.seed, worker_id);
469 loop {
470 if stopped.load(AtomicOrdering::Relaxed) {
471 break;
472 }
473 let run_id = next_run.fetch_add(1, AtomicOrdering::Relaxed);
474 if run_id >= config.num_retries {
475 break;
476 }
477 let sdev = initial_sdev(bounds.dim(), &mut worker_rng);
478 let context = RetryContext {
479 run_id,
480 run_seed: retry_run_seed(config.seed, run_id),
481 seed: worker_rng.next_u64(),
482 bounds: bounds.clone(),
483 guess: None,
484 sdev,
485 max_evaluations: config.max_evaluations,
486 value_limit: config.value_limit,
487 crossover: false,
488 };
489 let result = optimize(objective, &context);
490 let mut shared = lock_store(&store);
491 shared.add(result, config.value_limit);
492 if shared.best_y <= config.stop_fitness {
493 stopped.store(true, AtomicOrdering::Relaxed);
494 }
495 }
496 });
497
498 store
499 .into_inner()
500 .unwrap_or_else(std::sync::PoisonError::into_inner)
501 .into_result()
502}
503
504fn advanced_context(
505 run_id: usize,
506 bounds: &RetryBounds,
507 config: &AdvancedRetryConfig,
508 store: &mut RetryStore,
509 worker_rng: &mut Rng,
510) -> RetryContext {
511 if config.check_interval > 0 && run_id > 0 && run_id.is_multiple_of(config.check_interval) {
512 store.sort_diverse(bounds, config.diversity_threshold.max(0.0));
513 }
514
515 let progress = if config.retry.num_retries <= 1 {
516 1.0
517 } else {
518 run_id as f64 / (config.retry.num_retries - 1) as f64
519 };
520 let factor = 1.0 + (config.max_eval_fac.max(1.0) - 1.0) * progress;
521 let max_evaluations = ((config.retry.max_evaluations as f64) * factor)
522 .round()
523 .clamp(1.0, u64::MAX as f64) as u64;
524
525 let try_crossover = worker_rng.uniform01() < config.crossover_probability.clamp(0.0, 1.0);
526 let use_crossover = store.entries.len() >= 2 && try_crossover;
527 if !use_crossover {
528 let sdev = initial_sdev(bounds.dim(), worker_rng);
529 return RetryContext {
530 run_id,
531 run_seed: retry_run_seed(config.retry.seed, run_id),
532 seed: worker_rng.next_u64(),
533 bounds: bounds.clone(),
534 guess: None,
535 sdev,
536 max_evaluations,
537 value_limit: config.retry.value_limit,
538 crossover: false,
539 };
540 }
541
542 let elite = ((store.entries.len() as f64 * 0.2).ceil() as usize)
545 .max(2)
546 .min(store.entries.len());
547 let first = ((worker_rng.uniform01().powi(2) * elite as f64) as usize).min(elite - 1);
548 let mut second = ((worker_rng.uniform01().powi(2) * elite as f64) as usize).min(elite - 1);
549 if first == second {
550 second = (second + 1) % elite;
551 }
552 let parent = &store.entries[first];
553 let donor = &store.entries[second];
554 let diff_fac = 0.5 + 0.5 * worker_rng.uniform01();
555 let limit_fac = (2.0 + 2.0 * worker_rng.uniform01()) * diff_fac;
556 let mut lower = Vec::with_capacity(bounds.dim());
557 let mut upper = Vec::with_capacity(bounds.dim());
558 let mut guess = Vec::with_capacity(bounds.dim());
559 let mut sdev = Vec::with_capacity(bounds.dim());
560 for i in 0..bounds.dim() {
561 let global_delta = bounds.upper()[i] - bounds.lower()[i];
562 let delta = (donor.x[i] - parent.x[i]).abs();
563 let local_delta = (limit_fac * delta).max(0.0001);
564 let lo = bounds.lower()[i].max(parent.x[i] - local_delta);
565 let hi = bounds.upper()[i].min(parent.x[i] + local_delta);
566 lower.push(lo);
567 upper.push(hi);
568 guess.push(donor.x[i].clamp(lo, hi));
569 sdev.push((diff_fac * delta / global_delta).clamp(0.001, 0.5));
570 }
571
572 RetryContext {
573 run_id,
574 run_seed: retry_run_seed(config.retry.seed, run_id),
575 seed: worker_rng.next_u64(),
576 bounds: RetryBounds::new(lower, upper).expect("crossover bounds are valid"),
577 guess: Some(guess),
578 sdev,
579 max_evaluations,
580 value_limit: parent.y.min(config.retry.value_limit),
581 crossover: true,
582 }
583}
584
585pub fn advanced_retry<O, F>(
588 objective: &O,
589 bounds: &RetryBounds,
590 config: &AdvancedRetryConfig,
591 optimize: F,
592) -> RetryResult
593where
594 O: Fn(&[f64]) -> f64 + Sync,
595 F: Fn(&O, &RetryContext) -> RetryRunResult + Sync + Send,
596{
597 if config.retry.num_retries == 0 {
598 return RetryStore::new(
599 bounds.dim(),
600 config.retry.capacity,
601 config.retry.statistic_num,
602 )
603 .into_result();
604 }
605 let workers = worker_count(config.retry.workers).min(config.retry.num_retries);
606 let next_run = AtomicUsize::new(0);
607 let stopped = AtomicBool::new(false);
608 let store = Mutex::new(RetryStore::new(
609 bounds.dim(),
610 config.retry.capacity,
611 config.retry.statistic_num,
612 ));
613
614 run_parallel(workers, |worker_id| {
615 let mut worker_rng = spawned_worker_rng(config.retry.seed, worker_id);
616 loop {
617 if stopped.load(AtomicOrdering::Relaxed) {
618 break;
619 }
620 let run_id = next_run.fetch_add(1, AtomicOrdering::Relaxed);
621 if run_id >= config.retry.num_retries {
622 break;
623 }
624 let context = {
625 let mut shared = lock_store(&store);
626 advanced_context(run_id, bounds, config, &mut shared, &mut worker_rng)
627 };
628 let limit = context.value_limit;
629 let result = optimize(objective, &context);
630 let mut shared = lock_store(&store);
631 shared.add(result, limit);
632 if shared.best_y <= config.retry.stop_fitness {
633 stopped.store(true, AtomicOrdering::Relaxed);
634 }
635 }
636 });
637
638 let mut store = store
639 .into_inner()
640 .unwrap_or_else(std::sync::PoisonError::into_inner);
641 store.sort_diverse(bounds, config.diversity_threshold.max(0.0));
642 store.into_result()
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 fn bounds() -> RetryBounds {
650 RetryBounds::new(vec![-5.0, -5.0], vec![5.0, 5.0]).unwrap()
651 }
652
653 fn sample_run<O: Fn(&[f64]) -> f64>(objective: &O, context: &RetryContext) -> RetryRunResult {
654 let mut rng = Rng::new(context.seed);
655 let x: Vec<f64> = (0..context.bounds.dim())
656 .map(|i| {
657 context.bounds.lower()[i]
658 + rng.uniform01() * (context.bounds.upper()[i] - context.bounds.lower()[i])
659 })
660 .collect();
661 RetryRunResult {
662 y: objective(&x),
663 x,
664 evaluations: 1,
665 }
666 }
667
668 #[test]
669 fn rejects_invalid_bounds() {
670 assert!(RetryBounds::new(vec![], vec![]).is_err());
671 assert!(RetryBounds::new(vec![0.0], vec![1.0, 2.0]).is_err());
672 assert!(RetryBounds::new(vec![1.0], vec![1.0]).is_err());
673 assert!(RetryBounds::new(vec![f64::NAN], vec![1.0]).is_err());
674 }
675
676 #[test]
677 fn single_worker_retry_is_deterministic_and_counts() {
678 let config = RetryConfig {
679 num_retries: 40,
680 workers: 1,
681 capacity: 8,
682 seed: 123,
683 statistic_num: 3,
684 ..Default::default()
685 };
686 let objective = |x: &[f64]| x.iter().map(|v| v * v).sum();
687 let first = retry(&objective, &bounds(), &config, sample_run);
688 let second = retry(&objective, &bounds(), &config, sample_run);
689 assert!(first.success);
690 assert_eq!(first.y, second.y);
691 assert_eq!(first.x, second.x);
692 assert_eq!(first.runs, 40);
693 assert_eq!(first.evaluations, 40);
694 assert!(first.entries.len() <= config.capacity);
695 assert!(first.improvements.len() <= 3);
696 }
697
698 #[test]
699 fn spawned_worker_streams_are_independent_and_reproducible() {
700 let sample = |root_seed| {
701 (0..8)
702 .map(|worker_id| {
703 let mut rng = spawned_worker_rng(root_seed, worker_id);
704 (0..16).map(|_| rng.next_u64()).collect::<Vec<_>>()
705 })
706 .collect::<Vec<_>>()
707 };
708 let first = sample(123);
709 assert_eq!(first, sample(123));
710 assert_ne!(first, sample(124));
711 for left in 0..first.len() {
712 for right in left + 1..first.len() {
713 assert_ne!(first[left], first[right]);
714 }
715 }
716 }
717
718 #[test]
719 fn run_seeds_are_independent_of_worker_count_and_scheduling() {
720 let collect = |workers| {
721 let observed = Mutex::new(Vec::new());
722 let config = RetryConfig {
723 num_retries: 32,
724 workers,
725 seed: 0x1234_5678,
726 value_limit: f64::INFINITY,
727 ..Default::default()
728 };
729 retry(&|_: &[f64]| 0.0, &bounds(), &config, |_, context| {
730 observed
731 .lock()
732 .unwrap()
733 .push((context.run_id, context.run_seed));
734 RetryRunResult {
735 x: vec![0.0; context.bounds.dim()],
736 y: context.run_id as f64,
737 evaluations: 1,
738 }
739 });
740 let mut values = observed.into_inner().unwrap();
741 values.sort_unstable();
742 values
743 };
744 let serial = collect(1);
745 assert_eq!(serial, collect(8));
746 assert!(
747 serial
748 .iter()
749 .all(|(run_id, seed)| *seed == retry_run_seed(0x1234_5678, *run_id))
750 );
751 let unique: std::collections::HashSet<u64> = serial.iter().map(|(_, seed)| *seed).collect();
752 assert_eq!(unique.len(), serial.len());
753 }
754
755 #[test]
756 fn basic_retry_draws_contexts_from_the_persistent_worker_stream() {
757 let observed = Mutex::new(Vec::new());
758 let config = RetryConfig {
759 num_retries: 3,
760 workers: 1,
761 seed: 321,
762 ..Default::default()
763 };
764 retry(&|_: &[f64]| 0.0, &bounds(), &config, |_, context| {
765 observed
766 .lock()
767 .unwrap()
768 .push((context.sdev[0], context.seed));
769 RetryRunResult {
770 x: vec![0.0; context.bounds.dim()],
771 y: 0.0,
772 evaluations: 1,
773 }
774 });
775
776 let mut worker_rng = spawned_worker_rng(config.seed, 0);
777 let expected: Vec<(f64, u64)> = (0..config.num_retries)
778 .map(|_| {
779 let sdev = initial_sdev(bounds().dim(), &mut worker_rng)[0];
780 (sdev, worker_rng.next_u64())
781 })
782 .collect();
783 assert_eq!(observed.into_inner().unwrap(), expected);
784 }
785
786 #[test]
787 fn advanced_retry_draws_contexts_from_the_persistent_worker_stream() {
788 let observed = Mutex::new(Vec::new());
789 let config = AdvancedRetryConfig {
790 retry: RetryConfig {
791 num_retries: 3,
792 workers: 1,
793 seed: 654,
794 ..Default::default()
795 },
796 crossover_probability: 0.0,
797 ..Default::default()
798 };
799 advanced_retry(&|_: &[f64]| 0.0, &bounds(), &config, |_, context| {
800 observed
801 .lock()
802 .unwrap()
803 .push((context.sdev[0], context.seed));
804 RetryRunResult {
805 x: vec![0.0; context.bounds.dim()],
806 y: 0.0,
807 evaluations: 1,
808 }
809 });
810
811 let mut worker_rng = spawned_worker_rng(config.retry.seed, 0);
812 let expected: Vec<(f64, u64)> = (0..config.retry.num_retries)
813 .map(|_| {
814 let _crossover_draw = worker_rng.uniform01();
815 let sdev = initial_sdev(bounds().dim(), &mut worker_rng)[0];
816 (sdev, worker_rng.next_u64())
817 })
818 .collect();
819 assert_eq!(observed.into_inner().unwrap(), expected);
820 }
821
822 #[test]
823 fn retry_filters_bad_results_and_empty_runs() {
824 let empty = retry(
825 &|_: &[f64]| 0.0,
826 &bounds(),
827 &RetryConfig {
828 num_retries: 0,
829 ..Default::default()
830 },
831 sample_run,
832 );
833 assert!(!empty.success);
834 assert!(empty.y.is_infinite());
835
836 let filtered = retry(
837 &|_: &[f64]| 2.0,
838 &bounds(),
839 &RetryConfig {
840 num_retries: 3,
841 workers: 1,
842 value_limit: 1.0,
843 ..Default::default()
844 },
845 sample_run,
846 );
847 assert!(!filtered.success);
848 assert!(filtered.entries.is_empty());
849 assert_eq!(filtered.runs, 3);
850 }
851
852 #[test]
853 fn stop_fitness_stops_early() {
854 let result = retry(
855 &|_: &[f64]| -1.0,
856 &bounds(),
857 &RetryConfig {
858 num_retries: 100,
859 workers: 1,
860 stop_fitness: 0.0,
861 ..Default::default()
862 },
863 sample_run,
864 );
865 assert_eq!(result.runs, 1);
866 assert_eq!(result.y, -1.0);
867 }
868
869 #[test]
870 fn advanced_retry_increases_budget_and_crosses_over() {
871 let contexts = Mutex::new(Vec::new());
872 let config = AdvancedRetryConfig {
873 retry: RetryConfig {
874 num_retries: 12,
875 workers: 1,
876 capacity: 10,
877 max_evaluations: 100,
878 seed: 99,
879 ..Default::default()
880 },
881 check_interval: 2,
882 max_eval_fac: 4.0,
883 crossover_probability: 1.0,
884 diversity_threshold: 0.0,
885 };
886 let result = advanced_retry(&|x: &[f64]| x[0], &bounds(), &config, |objective, ctx| {
887 contexts.lock().unwrap().push(ctx.clone());
888 sample_run(objective, ctx)
889 });
890 let contexts = contexts.into_inner().unwrap();
891 assert_eq!(contexts.first().unwrap().max_evaluations, 100);
892 assert_eq!(contexts.last().unwrap().max_evaluations, 400);
893 assert!(contexts.iter().skip(2).any(|context| context.crossover));
894 assert!(
895 contexts
896 .iter()
897 .filter(|context| context.crossover)
898 .all(|context| context.guess.is_some())
899 );
900 assert!(result.success);
901 }
902
903 #[test]
904 fn advanced_retry_handles_single_run_and_filters_dimension_mismatch() {
905 let config = AdvancedRetryConfig {
906 retry: RetryConfig {
907 num_retries: 1,
908 workers: 0,
909 max_evaluations: 7,
910 ..Default::default()
911 },
912 check_interval: 0,
913 max_eval_fac: 3.0,
914 ..Default::default()
915 };
916 let result = advanced_retry(&|_: &[f64]| 0.0, &bounds(), &config, |_, context| {
917 assert_eq!(context.max_evaluations, 21);
918 RetryRunResult {
919 x: vec![0.0],
920 y: 0.0,
921 evaluations: 5,
922 }
923 });
924 assert!(!result.success);
925 assert_eq!(result.runs, 1);
926 assert_eq!(result.evaluations, 5);
927
928 let empty = advanced_retry(
929 &|_: &[f64]| 0.0,
930 &bounds(),
931 &AdvancedRetryConfig {
932 retry: RetryConfig {
933 num_retries: 0,
934 ..Default::default()
935 },
936 ..Default::default()
937 },
938 sample_run,
939 );
940 assert!(!empty.success);
941 assert_eq!(empty.runs, 0);
942 }
943
944 #[test]
945 fn store_diversity_and_distance() {
946 let bounds = bounds();
947 let mut store = RetryStore::new(2, 10, 0);
948 for (x, y) in [
949 (vec![0.0, 0.0], 0.0),
950 (vec![0.01, 0.01], 1.0),
951 (vec![4.0, 4.0], 2.0),
952 ] {
953 store.add(
954 RetryRunResult {
955 x,
956 y,
957 evaluations: 1,
958 },
959 f64::INFINITY,
960 );
961 }
962 assert!(store.normalized_distance(&[0.0, 0.0], &[5.0, 5.0], &bounds) > 0.0);
963 store.sort_diverse(&bounds, 0.15);
964 assert_eq!(store.entries.len(), 2);
965 assert_eq!(store.entries[0].y, 0.0);
966
967 let mut tiny = RetryStore::new(2, 1, 0);
968 for value in [3.0, 2.0, 1.0] {
969 tiny.add(
970 RetryRunResult {
971 x: vec![value; 2],
972 y: value,
973 evaluations: 1,
974 },
975 f64::INFINITY,
976 );
977 }
978 assert_eq!(tiny.entries.len(), 1);
979 assert_eq!(tiny.best_y, 1.0);
980 }
981
982 #[test]
983 fn advanced_stop_fitness_stops_early() {
984 let result = advanced_retry(
985 &|_: &[f64]| -2.0,
986 &bounds(),
987 &AdvancedRetryConfig {
988 retry: RetryConfig {
989 num_retries: 20,
990 workers: 1,
991 stop_fitness: -1.0,
992 ..Default::default()
993 },
994 ..Default::default()
995 },
996 sample_run,
997 );
998 assert_eq!(result.runs, 1);
999 assert_eq!(result.y, -2.0);
1000 }
1001}