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