1#![allow(clippy::needless_range_loop)]
3
4use crate::cmaes::{Cmaes, CmaesParams};
21use crate::fitness::Fitness;
22use crate::rng::Rng;
23use rayon::prelude::*;
24
25pub trait QdFitness {
27 fn eval(&mut self, x: &[f64]) -> (f64, Vec<f64>);
28}
29
30impl<F> QdFitness for F
31where
32 F: FnMut(&[f64]) -> (f64, Vec<f64>),
33{
34 fn eval(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
35 self(x)
36 }
37}
38
39pub trait QdBatchFitness {
42 fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)>;
43}
44
45impl<F> QdBatchFitness for F
46where
47 F: FnMut(&[Vec<f64>]) -> Vec<(f64, Vec<f64>)>,
48{
49 fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)> {
50 self(xs)
51 }
52}
53
54struct SerialQdBatchFitness<'a> {
55 fitness: &'a mut dyn QdFitness,
56}
57
58impl QdBatchFitness for SerialQdBatchFitness<'_> {
59 fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)> {
60 xs.iter().map(|x| self.fitness.eval(x)).collect()
61 }
62}
63
64fn dist2(a: &[f64], b: &[f64]) -> f64 {
69 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
70}
71
72fn grid_centers_2d(k: usize) -> Vec<Vec<f64>> {
75 let rows = (k as f64).sqrt().floor().max(1.0) as usize;
76 let base_columns = k / rows;
77 let extra_columns = k % rows;
78 let mut centers = Vec::with_capacity(k);
79 for row in 0..rows {
80 let columns = base_columns + usize::from(row < extra_columns);
81 for column in 0..columns {
82 centers.push(vec![
83 (column as f64 + 0.5) / columns as f64,
84 (row as f64 + 0.5) / rows as f64,
85 ]);
86 }
87 }
88 centers
89}
90
91fn grid_index_2d(k: usize, descriptor: &[f64]) -> usize {
92 let rows = (k as f64).sqrt().floor().max(1.0) as usize;
93 let base_columns = k / rows;
94 let extra_columns = k % rows;
95 let y = descriptor[1].clamp(0.0, 1.0 - f64::EPSILON);
96 let row = (y * rows as f64) as usize;
97 let columns = base_columns + usize::from(row < extra_columns);
98 let x = descriptor[0].clamp(0.0, 1.0 - f64::EPSILON);
99 let column = (x * columns as f64) as usize;
100 row * base_columns + row.min(extra_columns) + column
101}
102
103fn cvt_centers(k: usize, dim: usize, samples_per_niche: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
106 let n = (k * samples_per_niche).max(k);
107 let samples: Vec<Vec<f64>> = (0..n)
108 .map(|_| (0..dim).map(|_| rng.uniform01()).collect())
109 .collect();
110
111 let mut centers: Vec<Vec<f64>> = Vec::with_capacity(k);
113 centers.push(samples[rng.int_below(n as i64) as usize].clone());
114 let mut d2: Vec<f64> = samples.iter().map(|s| dist2(s, ¢ers[0])).collect();
115 while centers.len() < k {
116 let sum: f64 = d2.iter().sum();
117 let mut target = rng.uniform01() * sum;
118 let mut idx = 0;
119 for (i, &d) in d2.iter().enumerate() {
120 target -= d;
121 if target <= 0.0 {
122 idx = i;
123 break;
124 }
125 }
126 centers.push(samples[idx].clone());
127 let c = centers.last().unwrap();
128 d2.par_iter_mut().zip(&samples).for_each(|(distance, s)| {
129 let candidate = dist2(s, c);
130 if candidate < *distance {
131 *distance = candidate;
132 }
133 });
134 }
135
136 for _ in 0..10 {
139 let (sums, counts) = samples
140 .par_iter()
141 .fold(
142 || (vec![0.0; k * dim], vec![0usize; k]),
143 |(mut sums, mut counts), s| {
144 let mut best = 0;
145 let mut best_distance = f64::MAX;
146 for (index, center) in centers.iter().enumerate() {
147 let distance = dist2(s, center);
148 if distance < best_distance {
149 best_distance = distance;
150 best = index;
151 }
152 }
153 counts[best] += 1;
154 for j in 0..dim {
155 sums[best * dim + j] += s[j];
156 }
157 (sums, counts)
158 },
159 )
160 .reduce(
161 || (vec![0.0; k * dim], vec![0usize; k]),
162 |(mut left_sums, mut left_counts), (right_sums, right_counts)| {
163 for index in 0..k {
164 left_counts[index] += right_counts[index];
165 }
166 for index in 0..k * dim {
167 left_sums[index] += right_sums[index];
168 }
169 (left_sums, left_counts)
170 },
171 );
172 let mut max_shift = 0.0_f64;
173 for ci in 0..k {
174 if counts[ci] > 0 {
175 let old = centers[ci].clone();
176 for j in 0..dim {
177 centers[ci][j] = sums[ci * dim + j] / counts[ci] as f64;
178 }
179 max_shift = max_shift.max(dist2(&old, ¢ers[ci]));
180 }
181 }
182 if max_shift <= 1e-12 {
183 break;
184 }
185 }
186 centers
187}
188
189pub struct Archive {
196 dim: usize,
197 qd_dim: usize,
198 capacity: usize,
199 desc_lb: Vec<f64>,
200 desc_scale: Vec<f64>,
201 centers: Vec<Vec<f64>>, grid_2d: bool,
203 xs: Vec<Vec<f64>>,
204 ds: Vec<Vec<f64>>,
205 ys: Vec<f64>,
206 counts: Vec<u64>,
207 occupied: usize,
208 si: Vec<usize>, }
210
211impl Archive {
212 pub fn try_new(
214 dim: usize,
215 qd_lb: &[f64],
216 qd_ub: &[f64],
217 capacity: usize,
218 samples_per_niche: usize,
219 rng: &mut Rng,
220 ) -> Result<Self, &'static str> {
221 if dim == 0 {
222 return Err("archive decision dimension must be positive");
223 }
224 if qd_lb.is_empty() || qd_lb.len() != qd_ub.len() {
225 return Err("descriptor bounds must be non-empty and have equal lengths");
226 }
227 if qd_lb
228 .iter()
229 .zip(qd_ub)
230 .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
231 {
232 return Err("descriptor bounds must be finite and satisfy lower < upper");
233 }
234 if capacity == 0 {
235 return Err("archive capacity must be positive");
236 }
237 Ok(Self::new_unchecked(
238 dim,
239 qd_lb,
240 qd_ub,
241 capacity,
242 samples_per_niche,
243 rng,
244 ))
245 }
246
247 pub fn new(
250 dim: usize,
251 qd_lb: &[f64],
252 qd_ub: &[f64],
253 capacity: usize,
254 samples_per_niche: usize,
255 rng: &mut Rng,
256 ) -> Self {
257 Self::try_new(dim, qd_lb, qd_ub, capacity, samples_per_niche, rng)
258 .expect("invalid archive configuration")
259 }
260
261 fn new_unchecked(
262 dim: usize,
263 qd_lb: &[f64],
264 qd_ub: &[f64],
265 capacity: usize,
266 samples_per_niche: usize,
267 rng: &mut Rng,
268 ) -> Self {
269 let qd_dim = qd_lb.len();
270 let desc_lb = qd_lb.to_vec();
271 let desc_scale: Vec<f64> = qd_ub.iter().zip(qd_lb).map(|(u, l)| u - l).collect();
272 let grid_2d = samples_per_niche == 0 && qd_dim == 2;
273 let centers = if grid_2d {
274 grid_centers_2d(capacity)
275 } else {
276 cvt_centers(capacity, qd_dim, samples_per_niche.max(1), rng)
277 };
278 Archive {
279 dim,
280 qd_dim,
281 capacity,
282 desc_lb,
283 desc_scale,
284 centers,
285 grid_2d,
286 xs: vec![vec![0.0; dim]; capacity],
287 ds: vec![vec![0.0; qd_dim]; capacity],
288 ys: vec![f64::INFINITY; capacity],
289 counts: vec![0; capacity],
290 occupied: 0,
291 si: (0..capacity).collect(),
292 }
293 }
294
295 pub fn dim(&self) -> usize {
296 self.dim
297 }
298 pub fn qd_dim(&self) -> usize {
299 self.qd_dim
300 }
301 pub fn capacity(&self) -> usize {
302 self.capacity
303 }
304 pub fn occupied(&self) -> usize {
305 self.occupied
306 }
307
308 pub fn seed_uniform(&mut self, lower: &[f64], upper: &[f64], rng: &mut Rng) {
312 assert_eq!(lower.len(), self.dim, "lower bounds length must equal dim");
313 assert_eq!(upper.len(), self.dim, "upper bounds length must equal dim");
314 assert!(
315 lower
316 .iter()
317 .zip(upper)
318 .all(|(&lo, &hi)| lo.is_finite() && hi.is_finite() && lo < hi),
319 "decision bounds must be finite and satisfy lower < upper"
320 );
321 for x in self.xs.iter_mut() {
322 for i in 0..self.dim {
323 x[i] = lower[i] + (upper[i] - lower[i]) * rng.uniform01();
324 }
325 }
326 }
327
328 fn encode_d(&self, d: &[f64]) -> Vec<f64> {
329 (0..self.qd_dim)
330 .map(|i| (d[i] - self.desc_lb[i]) / self.desc_scale[i])
331 .collect()
332 }
333
334 pub fn index_of_niche(&self, d: &[f64]) -> usize {
336 assert_eq!(d.len(), self.qd_dim, "descriptor length must equal qd_dim");
337 let e = self.encode_d(d);
338 if self.grid_2d {
339 return grid_index_2d(self.capacity, &e);
340 }
341 let mut best = 0;
342 let mut bd = f64::MAX;
343 for (i, c) in self.centers.iter().enumerate() {
344 let dist = dist2(&e, c);
345 if dist < bd {
346 bd = dist;
347 best = i;
348 }
349 }
350 best
351 }
352
353 pub fn set(&mut self, i: usize, y: f64, d: &[f64], x: &[f64]) {
355 assert!(i < self.capacity, "niche index out of bounds");
356 assert_eq!(d.len(), self.qd_dim, "descriptor length mismatch");
357 assert_eq!(x.len(), self.dim, "solution length mismatch");
358 self.counts[i] += 1;
359 if y.is_finite() && d.iter().all(|v| v.is_finite()) && y < self.ys[i] {
360 if self.ys[i].is_infinite() {
361 self.occupied += 1;
362 }
363 self.ys[i] = y;
364 self.xs[i].copy_from_slice(x);
365 self.ds[i].copy_from_slice(d);
366 }
367 }
368
369 pub fn update(&mut self, xs: &[Vec<f64>], fitness: &mut dyn QdFitness) -> (Vec<f64>, Vec<f64>) {
373 let evaluations: Vec<(f64, Vec<f64>)> = xs.iter().map(|x| fitness.eval(x)).collect();
374 self.update_evaluated(xs, &evaluations)
375 .expect("serial QD evaluation preserves batch length")
376 }
377
378 pub fn update_evaluated(
382 &mut self,
383 xs: &[Vec<f64>],
384 evaluations: &[(f64, Vec<f64>)],
385 ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
386 if xs.len() != evaluations.len() {
387 return Err("QD evaluation batch length must match candidate batch length");
388 }
389 let mut improvements = Vec::with_capacity(xs.len());
390 let mut real_ys = Vec::with_capacity(xs.len());
391 for (x, (y, desc)) in xs.iter().zip(evaluations) {
392 if x.len() != self.dim
393 || desc.len() != self.qd_dim
394 || !y.is_finite()
395 || desc.iter().any(|value| !value.is_finite())
396 {
397 improvements.push(f64::INFINITY);
398 real_ys.push(f64::INFINITY);
399 continue;
400 }
401 let niche = self.index_of_niche(desc);
402 let oldy = self.ys[niche];
403 let improvement = if oldy.is_infinite() { *y } else { *y - oldy };
404 self.set(niche, *y, desc, x);
405 improvements.push(improvement);
406 real_ys.push(*y);
407 }
408 Ok((improvements, real_ys))
409 }
410
411 pub fn update_batch(
414 &mut self,
415 xs: &[Vec<f64>],
416 fitness: &mut dyn QdBatchFitness,
417 ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
418 let evaluations = fitness.eval_batch(xs);
419 self.update_evaluated(xs, &evaluations)
420 }
421
422 pub fn argsort(&mut self) {
424 let mut si: Vec<usize> = (0..self.capacity).collect();
425 si.sort_by(|&a, &b| self.ys[a].total_cmp(&self.ys[b]));
426 self.si = si;
427 }
428
429 pub fn random_xs(&self, best_n: usize, chunk: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
431 let bn = best_n.max(1).min(self.capacity);
432 (0..chunk)
433 .map(|_| {
434 let sel = rng.int_below(bn as i64) as usize;
435 let niche = if bn < self.capacity {
436 self.si[sel]
437 } else {
438 sel
439 };
440 self.xs[niche].clone()
441 })
442 .collect()
443 }
444
445 pub fn random_x_one(&self, best_n: usize, rng: &mut Rng) -> (Vec<f64>, f64) {
447 let bn = best_n.max(1).min(self.capacity);
448 let selected = rng.int_below(bn as i64) as usize;
449 let niche = if bn < self.capacity {
450 self.si[selected]
451 } else {
452 selected
453 };
454 (self.xs[niche].clone(), self.ys[niche])
455 }
456
457 pub fn best_y(&self) -> f64 {
458 self.ys.iter().cloned().fold(f64::INFINITY, f64::min)
459 }
460
461 pub fn qd_score(&self) -> f64 {
465 let finite: Vec<f64> = self
466 .ys
467 .iter()
468 .copied()
469 .filter(|value| value.is_finite())
470 .collect();
471 if finite.is_empty() {
472 return 0.0;
473 }
474 if finite.iter().copied().fold(f64::INFINITY, f64::min) > 0.0 {
475 finite
476 .iter()
477 .filter(|&&value| value != 0.0)
478 .map(|&value| value.recip())
479 .sum()
480 } else {
481 finite
482 .iter()
483 .filter(|&&value| value < 0.0)
484 .map(|&value| -value)
485 .sum()
486 }
487 }
488
489 pub fn ys(&self) -> &[f64] {
490 &self.ys
491 }
492 pub fn xs(&self) -> &[Vec<f64>] {
493 &self.xs
494 }
495 pub fn descriptors(&self) -> &[Vec<f64>] {
496 &self.ds
497 }
498 pub fn counts(&self) -> &[u64] {
499 &self.counts
500 }
501
502 pub fn occupied_data(&self) -> Vec<(Vec<f64>, f64, Vec<f64>)> {
504 (0..self.capacity)
505 .filter(|&i| self.ys[i].is_finite())
506 .map(|i| (self.xs[i].clone(), self.ys[i], self.ds[i].clone()))
507 .collect()
508 }
509}
510
511pub fn variation(
518 pop: &[Vec<f64>],
519 lower: &[f64],
520 upper: &[f64],
521 rng: &mut Rng,
522 dis_c: f64,
523 dis_m: f64,
524) -> Vec<Vec<f64>> {
525 let dis_c = dis_c * (0.5 + 0.5 * rng.uniform01());
526 let dis_m = dis_m * (0.5 + 0.5 * rng.uniform01());
527 let n = (pop.len() / 2) * 2;
528 let d = lower.len();
529 let half = n / 2;
530 let mut offspring = vec![vec![0.0; d]; n];
531 for p in 0..half {
532 let p1 = &pop[p];
533 let p2 = &pop[half + p];
534 for i in 0..d {
535 let mu = rng.uniform01();
536 let mut beta = if mu <= 0.5 {
537 (2.0 * mu).powf(1.0 / (dis_c + 1.0))
538 } else {
539 (2.0 * mu).powf(-1.0 / (dis_c + 1.0))
540 };
541 if rng.int_below(2) == 1 {
542 beta = -beta;
543 }
544 if rng.uniform01() < 0.5 {
545 beta = 1.0;
546 }
547 let mean = (p1[i] + p2[i]) * 0.5;
548 let diff = (p1[i] - p2[i]) * 0.5;
549 offspring[p][i] = mean + beta * diff;
550 offspring[half + p][i] = mean - beta * diff;
551 }
552 }
553 let site_p = 1.0 / d as f64;
555 for op in offspring.iter_mut() {
556 for i in 0..d {
557 if rng.uniform01() < site_p {
558 let mu = rng.uniform01();
559 let span = upper[i] - lower[i];
560 if mu <= 0.5 {
561 let norm = (op[i] - lower[i]) / span;
562 op[i] += span
563 * ((2.0 * mu + (1.0 - 2.0 * mu) * (1.0 - norm).abs().powf(dis_m + 1.0))
564 .powf(1.0 / (dis_m + 1.0))
565 - 1.0);
566 } else {
567 let norm = (upper[i] - op[i]) / span;
568 op[i] += span
569 * (1.0
570 - (2.0 * (1.0 - mu)
571 + 2.0 * (mu - 0.5) * (1.0 - norm).abs().powf(dis_m + 1.0))
572 .powf(1.0 / (dis_m + 1.0)));
573 }
574 }
575 op[i] = op[i].clamp(lower[i], upper[i]);
576 }
577 }
578 offspring
579}
580
581pub fn iso_dd(
583 x1: &[Vec<f64>],
584 x2: &[Vec<f64>],
585 lower: &[f64],
586 upper: &[f64],
587 rng: &mut Rng,
588 iso_sigma: f64,
589 line_sigma: f64,
590) -> Vec<Vec<f64>> {
591 let d = lower.len();
592 x1.iter()
593 .zip(x2)
594 .map(|(a, b)| {
595 (0..d)
596 .map(|i| {
597 let z = a[i]
598 + rng.normreal(0.0, iso_sigma)
599 + rng.normreal(0.0, line_sigma) * (a[i] - b[i]);
600 z.clamp(lower[i], upper[i])
601 })
602 .collect()
603 })
604 .collect()
605}
606
607#[derive(Clone, Debug)]
613pub struct MapElitesParams {
614 pub generations: usize,
615 pub chunk_size: usize,
616 pub use_sbx: bool,
617 pub dis_c: f64,
618 pub dis_m: f64,
619 pub iso_sigma: f64,
620 pub line_sigma: f64,
621 pub cma_generations: usize,
622}
623
624impl Default for MapElitesParams {
625 fn default() -> Self {
626 Self {
627 generations: 100,
628 chunk_size: 20,
629 use_sbx: true,
630 dis_c: 20.0,
631 dis_m: 20.0,
632 iso_sigma: 0.02,
633 line_sigma: 0.2,
634 cma_generations: 0,
635 }
636 }
637}
638
639pub fn map_elites(
642 archive: &mut Archive,
643 fitness: &mut dyn QdFitness,
644 lower: &[f64],
645 upper: &[f64],
646 p: &MapElitesParams,
647 rng: &mut Rng,
648) {
649 let mut batch_fitness = SerialQdBatchFitness { fitness };
650 map_elites_batch(archive, &mut batch_fitness, lower, upper, p, rng)
651 .expect("serial QD evaluation preserves batch length");
652}
653
654pub fn map_elites_batch(
658 archive: &mut Archive,
659 fitness: &mut dyn QdBatchFitness,
660 lower: &[f64],
661 upper: &[f64],
662 p: &MapElitesParams,
663 rng: &mut Rng,
664) -> Result<(), &'static str> {
665 map_elites_batch_with_progress(archive, fitness, lower, upper, p, rng, &mut |_, _| {})
666}
667
668pub fn map_elites_batch_with_progress(
675 archive: &mut Archive,
676 fitness: &mut dyn QdBatchFitness,
677 lower: &[f64],
678 upper: &[f64],
679 p: &MapElitesParams,
680 rng: &mut Rng,
681 progress: &mut dyn FnMut(usize, &Archive),
682) -> Result<(), &'static str> {
683 let mut select_n = archive.capacity();
684 for generation in 0..p.generations {
685 let xs = if p.use_sbx {
686 let pop = archive.random_xs(select_n, p.chunk_size, rng);
687 variation(&pop, lower, upper, rng, p.dis_c, p.dis_m)
688 } else {
689 let x1 = archive.random_xs(select_n, p.chunk_size, rng);
690 let x2 = archive.random_xs(select_n, p.chunk_size, rng);
691 iso_dd(&x1, &x2, lower, upper, rng, p.iso_sigma, p.line_sigma)
692 };
693 archive.update_batch(&xs, fitness)?;
694 archive.argsort();
695 select_n = archive.occupied().max(1);
696 progress(generation + 1, archive);
697 }
698 for generation in 0..p.cma_generations {
699 cma_emitter_batch(archive, fitness, lower, upper, rng)?;
700 archive.argsort();
701 progress(p.generations + generation + 1, archive);
702 }
703 Ok(())
704}
705
706fn cma_emitter_batch(
709 archive: &mut Archive,
710 fitness: &mut dyn QdBatchFitness,
711 lower: &[f64],
712 upper: &[f64],
713 rng: &mut Rng,
714) -> Result<(), &'static str> {
715 let best_n = 100.min(archive.capacity());
716 let (x0, _) = archive.random_x_one(best_n, rng);
717 let sigma = {
718 let u = 0.03 + rng.uniform01() * 0.27;
719 u * u
720 };
721 let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
722 fit.set_normalize(true);
723 let params = CmaesParams {
724 popsize: 31,
725 max_evaluations: 100_000,
726 seed: rng.int_below(i64::MAX) as u64,
727 ..Default::default()
728 };
729 let mut es = Cmaes::new(fit, &x0, &[sigma], ¶ms);
730 let stall = 5;
731 let mut last_improve = 0i32;
732 let mut old_ys: Option<Vec<f64>> = None;
733 for iter in 0..100 {
734 let xs = es.ask();
735 let (improvement, _real) = archive.update_batch(&xs, fitness)?;
736 let mut sorted = improvement.clone();
737 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
738 if let Some(oy) = &old_ys
739 && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
740 {
741 last_improve = iter;
742 }
743 if last_improve + stall < iter {
744 break;
745 }
746 if es.tell(&improvement) != 0 {
747 break;
748 }
749 old_ys = Some(sorted);
750 }
751 Ok(())
752}
753
754#[derive(Clone, Debug)]
756pub struct DiversifierParams {
757 pub max_evaluations: u64,
758 pub popsize: i32,
759 pub stall_criterion: i32,
760}
761
762impl Default for DiversifierParams {
763 fn default() -> Self {
764 Self {
765 max_evaluations: 100_000,
766 popsize: 31,
767 stall_criterion: 20,
768 }
769 }
770}
771
772pub fn diversify(
776 archive: &mut Archive,
777 fitness: &mut dyn QdFitness,
778 lower: &[f64],
779 upper: &[f64],
780 p: &DiversifierParams,
781 rng: &mut Rng,
782) -> (Vec<f64>, f64) {
783 let mut batch_fitness = SerialQdBatchFitness { fitness };
784 diversify_batch(archive, &mut batch_fitness, lower, upper, p, rng)
785 .expect("serial QD evaluation preserves batch length")
786}
787
788pub fn diversify_batch(
791 archive: &mut Archive,
792 fitness: &mut dyn QdBatchFitness,
793 lower: &[f64],
794 upper: &[f64],
795 p: &DiversifierParams,
796 rng: &mut Rng,
797) -> Result<(Vec<f64>, f64), &'static str> {
798 let mut best_x = vec![0.0; archive.dim()];
799 let mut best_y = f64::INFINITY;
800 let mut evals: u64 = 0;
801 while evals < p.max_evaluations {
802 let (x0, _) = archive.random_x_one(archive.occupied().max(1), rng);
803 let sigma = {
804 let u = 0.03 + rng.uniform01() * 0.27;
805 u * u
806 };
807 let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
808 fit.set_normalize(true);
809 let params = CmaesParams {
810 popsize: p.popsize,
811 max_evaluations: 100_000,
812 seed: rng.int_below(i64::MAX) as u64,
813 ..Default::default()
814 };
815 let mut es = Cmaes::new(fit, &x0, &[sigma], ¶ms);
816 let max_iters = 50_000 / p.popsize.max(1) as usize;
817 let stall = p.stall_criterion;
818 let mut last_improve = 0i32;
819 let mut old_ys: Option<Vec<f64>> = None;
820 for iter in 0..max_iters as i32 {
821 let xs = es.ask();
822 let (improvement, real_ys) = archive.update_batch(&xs, fitness)?;
823 evals += xs.len() as u64;
824 for (x, &ry) in xs.iter().zip(&real_ys) {
826 if ry < best_y {
827 best_y = ry;
828 best_x.copy_from_slice(x);
829 }
830 }
831 let mut sorted = improvement.clone();
832 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
833 if let Some(oy) = &old_ys
834 && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
835 {
836 last_improve = iter;
837 }
838 if last_improve + stall < iter {
839 break;
840 }
841 if es.tell(&improvement) != 0 || evals >= p.max_evaluations {
842 break;
843 }
844 old_ys = Some(sorted);
845 }
846 archive.argsort();
847 }
848 Ok((best_x, best_y))
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854
855 fn qd(x: &[f64]) -> (f64, Vec<f64>) {
857 let f = x.iter().map(|v| v * v).sum();
858 (f, vec![x[0], x[1]])
859 }
860
861 #[test]
862 fn cvt_centers_are_in_unit_box() {
863 let mut rng = Rng::new(1);
864 let c = cvt_centers(20, 2, 10, &mut rng);
865 assert_eq!(c.len(), 20);
866 for center in &c {
867 for &v in center {
868 assert!((0.0..=1.0).contains(&v));
869 }
870 }
871 }
872
873 #[test]
874 fn grid_centers_are_fast_exact_and_cover_the_box() {
875 for capacity in [1, 10, 64, 1_000] {
876 let centers = grid_centers_2d(capacity);
877 assert_eq!(centers.len(), capacity);
878 assert!(
879 centers
880 .iter()
881 .flatten()
882 .all(|value| (0.0..=1.0).contains(value))
883 );
884 }
885 let mut rng = Rng::new(1);
886 let archive = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 100, 0, &mut rng);
887 assert_eq!(archive.centers.len(), 100);
888 assert_eq!(archive.index_of_niche(&[0.0, 0.0]), 0);
889 assert_eq!(archive.index_of_niche(&[1.0, 1.0]), 99);
890 }
891
892 #[test]
893 fn map_elites_fills_niches() {
894 let mut rng = Rng::new(2);
895 let lower = vec![-2.0; 4];
896 let upper = vec![2.0; 4];
897 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
898 let seed_pop: Vec<Vec<f64>> = (0..64)
900 .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
901 .collect();
902 archive.update(&seed_pop, &mut qd);
903 archive.argsort();
904 let params = MapElitesParams {
905 generations: 200,
906 chunk_size: 16,
907 ..Default::default()
908 };
909 map_elites(&mut archive, &mut qd, &lower, &upper, ¶ms, &mut rng);
910 assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
911 assert!(archive.best_y() < 0.5, "best_y={}", archive.best_y());
912 }
913
914 #[test]
915 fn evaluated_batches_match_serial_updates_and_validate_lengths() {
916 let mut rng_a = Rng::new(7);
917 let mut rng_b = Rng::new(7);
918 let mut serial = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_a);
919 let mut batch = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_b);
920 let xs = vec![vec![-1.0, 0.5], vec![0.25, -0.75], vec![1.0, 1.0]];
921 let expected = serial.update(&xs, &mut qd);
922 let evaluations: Vec<_> = xs.iter().map(|x| qd(x)).collect();
923 let actual = batch.update_evaluated(&xs, &evaluations).unwrap();
924 assert_eq!(actual, expected);
925 assert_eq!(batch.ys(), serial.ys());
926 assert_eq!(batch.descriptors(), serial.descriptors());
927 assert!(batch.update_evaluated(&xs, &evaluations[..2]).is_err());
928 }
929
930 #[test]
931 fn batch_map_elites_preserves_serial_results() {
932 let lower = vec![-2.0; 4];
933 let upper = vec![2.0; 4];
934 let mut rng_a = Rng::new(19);
935 let mut rng_b = Rng::new(19);
936 let mut serial = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_a);
937 let mut batch = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_b);
938 serial.seed_uniform(&lower, &upper, &mut rng_a);
939 batch.seed_uniform(&lower, &upper, &mut rng_b);
940 let initial = serial.xs().to_vec();
941 let initial_evaluations: Vec<_> = initial.iter().map(|x| qd(x)).collect();
942 serial.update(&initial, &mut qd);
943 batch
944 .update_evaluated(&initial, &initial_evaluations)
945 .unwrap();
946 serial.argsort();
947 batch.argsort();
948 let params = MapElitesParams {
949 generations: 10,
950 chunk_size: 8,
951 ..Default::default()
952 };
953 map_elites(&mut serial, &mut qd, &lower, &upper, ¶ms, &mut rng_a);
954 let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
955 map_elites_batch(
956 &mut batch,
957 &mut batch_qd,
958 &lower,
959 &upper,
960 ¶ms,
961 &mut rng_b,
962 )
963 .unwrap();
964 assert_eq!(batch.ys(), serial.ys());
965 assert_eq!(batch.xs(), serial.xs());
966 assert_eq!(batch.descriptors(), serial.descriptors());
967 }
968
969 #[test]
970 fn batch_progress_callback_is_ordered_and_observes_committed_updates() {
971 let lower = vec![-2.0; 4];
972 let upper = vec![2.0; 4];
973 let mut rng = Rng::new(23);
974 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng);
975 archive.seed_uniform(&lower, &upper, &mut rng);
976 let params = MapElitesParams {
977 generations: 5,
978 chunk_size: 8,
979 ..Default::default()
980 };
981 let mut observed = Vec::new();
982 let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
983 map_elites_batch_with_progress(
984 &mut archive,
985 &mut batch_qd,
986 &lower,
987 &upper,
988 ¶ms,
989 &mut rng,
990 &mut |generation, committed| {
991 observed.push((generation, committed.occupied(), committed.best_y()));
992 },
993 )
994 .unwrap();
995 assert_eq!(
996 observed.iter().map(|sample| sample.0).collect::<Vec<_>>(),
997 vec![1, 2, 3, 4, 5]
998 );
999 assert!(
1000 observed
1001 .iter()
1002 .all(|(_, occupied, best)| *occupied > 0 && best.is_finite())
1003 );
1004 assert!(observed.windows(2).all(|pair| pair[1].2 <= pair[0].2));
1005 }
1006
1007 #[test]
1008 fn diversify_improves_and_fills() {
1009 let mut rng = Rng::new(3);
1010 let lower = vec![-2.0; 4];
1011 let upper = vec![2.0; 4];
1012 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1013 let seed_pop: Vec<Vec<f64>> = (0..64)
1014 .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1015 .collect();
1016 archive.update(&seed_pop, &mut qd);
1017 let params = DiversifierParams {
1018 max_evaluations: 20_000,
1019 ..Default::default()
1020 };
1021 let (bx, by) = diversify(&mut archive, &mut qd, &lower, &upper, ¶ms, &mut rng);
1022 assert_eq!(bx.len(), 4);
1023 assert!(by < 1e-3, "diversifier best_y={by}");
1024 assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1025 }
1026
1027 #[test]
1028 fn archive_validation_and_bad_evaluations() {
1029 let mut rng = Rng::new(4);
1030 assert!(Archive::try_new(0, &[0.0], &[1.0], 4, 2, &mut rng).is_err());
1031 assert!(Archive::try_new(2, &[0.0], &[0.0], 4, 2, &mut rng).is_err());
1032 assert!(Archive::try_new(2, &[0.0], &[1.0], 0, 2, &mut rng).is_err());
1033
1034 let mut archive = Archive::try_new(2, &[0.0], &[1.0], 4, 2, &mut rng).unwrap();
1035 let (improvements, values) =
1036 archive.update(&[vec![0.0, 0.0]], &mut |_: &[f64]| (f64::NAN, vec![0.5]));
1037 assert!(improvements[0].is_infinite());
1038 assert!(values[0].is_infinite());
1039 assert_eq!(archive.occupied(), 0);
1040 }
1041
1042 #[test]
1043 fn qd_score_matches_positive_and_negative_python_rules() {
1044 let mut rng = Rng::new(5);
1045 let mut positive = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1046 positive.set(0, 2.0, &[0.1], &[0.0]);
1047 positive.set(1, 4.0, &[0.5], &[0.0]);
1048 assert_eq!(positive.qd_score(), 0.75);
1049
1050 let mut mixed = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1051 mixed.set(0, -2.0, &[0.1], &[0.0]);
1052 mixed.set(1, 4.0, &[0.5], &[0.0]);
1053 assert_eq!(mixed.qd_score(), 2.0);
1054 }
1055
1056 #[test]
1057 fn random_x_one_uses_sorted_best_niches() {
1058 let mut rng = Rng::new(6);
1059 let mut archive = Archive::new(1, &[0.0], &[1.0], 4, 2, &mut rng);
1060 for (index, value) in [4.0, 1.0, 3.0, 2.0].into_iter().enumerate() {
1061 archive.set(index, value, &[index as f64 / 4.0], &[index as f64]);
1062 }
1063 archive.argsort();
1064 for _ in 0..10 {
1065 let (x, y) = archive.random_x_one(1, &mut rng);
1066 assert_eq!(x, vec![1.0]);
1067 assert_eq!(y, 1.0);
1068 }
1069 }
1070}