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