1#![allow(clippy::needless_range_loop)]
3
4use crate::cmaes::{Cmaes, CmaesParams};
56use crate::fitness::Fitness;
57use crate::rng::Rng;
58use rayon::prelude::*;
59
60pub trait QdFitness {
62 fn eval(&mut self, x: &[f64]) -> (f64, Vec<f64>);
64}
65
66impl<F> QdFitness for F
67where
68 F: FnMut(&[f64]) -> (f64, Vec<f64>),
69{
70 fn eval(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
71 self(x)
72 }
73}
74
75pub trait QdBatchFitness {
78 fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)>;
80}
81
82impl<F> QdBatchFitness for F
83where
84 F: FnMut(&[Vec<f64>]) -> Vec<(f64, Vec<f64>)>,
85{
86 fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)> {
87 self(xs)
88 }
89}
90
91struct SerialQdBatchFitness<'a> {
92 fitness: &'a mut dyn QdFitness,
93}
94
95impl QdBatchFitness for SerialQdBatchFitness<'_> {
96 fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)> {
97 xs.iter().map(|x| self.fitness.eval(x)).collect()
98 }
99}
100
101fn dist2(a: &[f64], b: &[f64]) -> f64 {
106 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
107}
108
109fn grid_centers_2d(k: usize) -> Vec<Vec<f64>> {
112 let rows = (k as f64).sqrt().floor().max(1.0) as usize;
113 let base_columns = k / rows;
114 let extra_columns = k % rows;
115 let mut centers = Vec::with_capacity(k);
116 for row in 0..rows {
117 let columns = base_columns + usize::from(row < extra_columns);
118 for column in 0..columns {
119 centers.push(vec![
120 (column as f64 + 0.5) / columns as f64,
121 (row as f64 + 0.5) / rows as f64,
122 ]);
123 }
124 }
125 centers
126}
127
128fn grid_index_2d(k: usize, descriptor: &[f64]) -> usize {
129 let rows = (k as f64).sqrt().floor().max(1.0) as usize;
130 let base_columns = k / rows;
131 let extra_columns = k % rows;
132 let y = descriptor[1].clamp(0.0, 1.0 - f64::EPSILON);
133 let row = (y * rows as f64) as usize;
134 let columns = base_columns + usize::from(row < extra_columns);
135 let x = descriptor[0].clamp(0.0, 1.0 - f64::EPSILON);
136 let column = (x * columns as f64) as usize;
137 row * base_columns + row.min(extra_columns) + column
138}
139
140fn cvt_centers(k: usize, dim: usize, samples_per_niche: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
143 let n = (k * samples_per_niche).max(k);
144 let samples: Vec<Vec<f64>> = (0..n)
145 .map(|_| (0..dim).map(|_| rng.uniform01()).collect())
146 .collect();
147
148 let mut centers: Vec<Vec<f64>> = Vec::with_capacity(k);
150 centers.push(samples[rng.int_below(n as i64) as usize].clone());
151 let mut d2: Vec<f64> = samples.iter().map(|s| dist2(s, ¢ers[0])).collect();
152 while centers.len() < k {
153 let sum: f64 = d2.iter().sum();
154 let mut target = rng.uniform01() * sum;
155 let mut idx = 0;
156 for (i, &d) in d2.iter().enumerate() {
157 target -= d;
158 if target <= 0.0 {
159 idx = i;
160 break;
161 }
162 }
163 centers.push(samples[idx].clone());
164 let c = centers.last().unwrap();
165 d2.par_iter_mut().zip(&samples).for_each(|(distance, s)| {
166 let candidate = dist2(s, c);
167 if candidate < *distance {
168 *distance = candidate;
169 }
170 });
171 }
172
173 for _ in 0..10 {
176 let (sums, counts) = samples
177 .par_iter()
178 .fold(
179 || (vec![0.0; k * dim], vec![0usize; k]),
180 |(mut sums, mut counts), s| {
181 let mut best = 0;
182 let mut best_distance = f64::MAX;
183 for (index, center) in centers.iter().enumerate() {
184 let distance = dist2(s, center);
185 if distance < best_distance {
186 best_distance = distance;
187 best = index;
188 }
189 }
190 counts[best] += 1;
191 for j in 0..dim {
192 sums[best * dim + j] += s[j];
193 }
194 (sums, counts)
195 },
196 )
197 .reduce(
198 || (vec![0.0; k * dim], vec![0usize; k]),
199 |(mut left_sums, mut left_counts), (right_sums, right_counts)| {
200 for index in 0..k {
201 left_counts[index] += right_counts[index];
202 }
203 for index in 0..k * dim {
204 left_sums[index] += right_sums[index];
205 }
206 (left_sums, left_counts)
207 },
208 );
209 let mut max_shift = 0.0_f64;
210 for ci in 0..k {
211 if counts[ci] > 0 {
212 let old = centers[ci].clone();
213 for j in 0..dim {
214 centers[ci][j] = sums[ci * dim + j] / counts[ci] as f64;
215 }
216 max_shift = max_shift.max(dist2(&old, ¢ers[ci]));
217 }
218 }
219 if max_shift <= 1e-12 {
220 break;
221 }
222 }
223 centers
224}
225
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub struct GridLayout {
237 pub rows: usize,
239 pub base_columns: usize,
241 pub extra_columns: usize,
243}
244
245impl GridLayout {
246 pub fn cells(&self) -> usize {
248 self.rows * self.base_columns + self.extra_columns
249 }
250
251 pub fn columns_in_row(&self, row: usize) -> Option<usize> {
253 (row < self.rows).then(|| self.base_columns + usize::from(row < self.extra_columns))
254 }
255
256 pub fn max_columns(&self) -> usize {
258 self.base_columns + usize::from(self.extra_columns > 0)
259 }
260}
261
262pub struct Archive {
265 dim: usize,
266 qd_dim: usize,
267 capacity: usize,
268 desc_lb: Vec<f64>,
269 desc_scale: Vec<f64>,
270 centers: Vec<Vec<f64>>, grid_2d: bool,
272 xs: Vec<Vec<f64>>,
273 ds: Vec<Vec<f64>>,
274 ys: Vec<f64>,
275 counts: Vec<u64>,
276 occupied: usize,
277 si: Vec<usize>, }
279
280impl Archive {
281 pub fn try_new(
289 dim: usize,
290 qd_lb: &[f64],
291 qd_ub: &[f64],
292 capacity: usize,
293 samples_per_niche: usize,
294 rng: &mut Rng,
295 ) -> Result<Self, &'static str> {
296 if dim == 0 {
297 return Err("archive decision dimension must be positive");
298 }
299 if qd_lb.is_empty() || qd_lb.len() != qd_ub.len() {
300 return Err("descriptor bounds must be non-empty and have equal lengths");
301 }
302 if qd_lb
303 .iter()
304 .zip(qd_ub)
305 .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
306 {
307 return Err("descriptor bounds must be finite and satisfy lower < upper");
308 }
309 if capacity == 0 {
310 return Err("archive capacity must be positive");
311 }
312 Ok(Self::new_unchecked(
313 dim,
314 qd_lb,
315 qd_ub,
316 capacity,
317 samples_per_niche,
318 rng,
319 ))
320 }
321
322 pub fn new(
329 dim: usize,
330 qd_lb: &[f64],
331 qd_ub: &[f64],
332 capacity: usize,
333 samples_per_niche: usize,
334 rng: &mut Rng,
335 ) -> Self {
336 Self::try_new(dim, qd_lb, qd_ub, capacity, samples_per_niche, rng)
337 .expect("invalid archive configuration")
338 }
339
340 fn new_unchecked(
341 dim: usize,
342 qd_lb: &[f64],
343 qd_ub: &[f64],
344 capacity: usize,
345 samples_per_niche: usize,
346 rng: &mut Rng,
347 ) -> Self {
348 let qd_dim = qd_lb.len();
349 let desc_lb = qd_lb.to_vec();
350 let desc_scale: Vec<f64> = qd_ub.iter().zip(qd_lb).map(|(u, l)| u - l).collect();
351 let grid_2d = samples_per_niche == 0 && qd_dim == 2;
352 let centers = if grid_2d {
353 grid_centers_2d(capacity)
354 } else {
355 cvt_centers(capacity, qd_dim, samples_per_niche.max(1), rng)
356 };
357 Archive {
358 dim,
359 qd_dim,
360 capacity,
361 desc_lb,
362 desc_scale,
363 centers,
364 grid_2d,
365 xs: vec![vec![0.0; dim]; capacity],
366 ds: vec![vec![0.0; qd_dim]; capacity],
367 ys: vec![f64::INFINITY; capacity],
368 counts: vec![0; capacity],
369 occupied: 0,
370 si: (0..capacity).collect(),
371 }
372 }
373
374 pub fn dim(&self) -> usize {
376 self.dim
377 }
378 pub fn qd_dim(&self) -> usize {
380 self.qd_dim
381 }
382 pub fn capacity(&self) -> usize {
384 self.capacity
385 }
386 pub fn occupied(&self) -> usize {
388 self.occupied
389 }
390
391 pub fn grid_layout(&self) -> Option<GridLayout> {
397 if !self.grid_2d {
398 return None;
399 }
400 let rows = (self.capacity as f64).sqrt().floor().max(1.0) as usize;
401 Some(GridLayout {
402 rows,
403 base_columns: self.capacity / rows,
404 extra_columns: self.capacity % rows,
405 })
406 }
407
408 pub fn grid_shape(&self) -> Option<(usize, usize)> {
416 self.grid_layout()
417 .map(|layout| (layout.max_columns(), layout.rows))
418 }
419
420 pub fn seed_uniform(&mut self, lower: &[f64], upper: &[f64], rng: &mut Rng) {
429 assert_eq!(lower.len(), self.dim, "lower bounds length must equal dim");
430 assert_eq!(upper.len(), self.dim, "upper bounds length must equal dim");
431 assert!(
432 lower
433 .iter()
434 .zip(upper)
435 .all(|(&lo, &hi)| lo.is_finite() && hi.is_finite() && lo < hi),
436 "decision bounds must be finite and satisfy lower < upper"
437 );
438 for x in self.xs.iter_mut() {
439 for i in 0..self.dim {
440 x[i] = lower[i] + (upper[i] - lower[i]) * rng.uniform01();
441 }
442 }
443 }
444
445 fn encode_d(&self, d: &[f64]) -> Vec<f64> {
446 (0..self.qd_dim)
447 .map(|i| (d[i] - self.desc_lb[i]) / self.desc_scale[i])
448 .collect()
449 }
450
451 pub fn index_of_niche(&self, d: &[f64]) -> usize {
457 assert_eq!(d.len(), self.qd_dim, "descriptor length must equal qd_dim");
458 let e = self.encode_d(d);
459 if self.grid_2d {
460 return grid_index_2d(self.capacity, &e);
461 }
462 let mut best = 0;
463 let mut bd = f64::MAX;
464 for (i, c) in self.centers.iter().enumerate() {
465 let dist = dist2(&e, c);
466 if dist < bd {
467 bd = dist;
468 best = i;
469 }
470 }
471 best
472 }
473
474 pub fn set(&mut self, i: usize, y: f64, d: &[f64], x: &[f64]) {
481 assert!(i < self.capacity, "niche index out of bounds");
482 assert_eq!(d.len(), self.qd_dim, "descriptor length mismatch");
483 assert_eq!(x.len(), self.dim, "solution length mismatch");
484 self.counts[i] += 1;
485 if y.is_finite() && d.iter().all(|v| v.is_finite()) && y < self.ys[i] {
486 if self.ys[i].is_infinite() {
487 self.occupied += 1;
488 }
489 self.ys[i] = y;
490 self.xs[i].copy_from_slice(x);
491 self.ds[i].copy_from_slice(d);
492 }
493 }
494
495 pub fn update(&mut self, xs: &[Vec<f64>], fitness: &mut dyn QdFitness) -> (Vec<f64>, Vec<f64>) {
504 let evaluations: Vec<(f64, Vec<f64>)> = xs.iter().map(|x| fitness.eval(x)).collect();
505 self.update_evaluated(xs, &evaluations)
506 .expect("serial QD evaluation preserves batch length")
507 }
508
509 pub fn update_evaluated(
517 &mut self,
518 xs: &[Vec<f64>],
519 evaluations: &[(f64, Vec<f64>)],
520 ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
521 if xs.len() != evaluations.len() {
522 return Err("QD evaluation batch length must match candidate batch length");
523 }
524 let mut improvements = Vec::with_capacity(xs.len());
525 let mut real_ys = Vec::with_capacity(xs.len());
526 for (x, (y, desc)) in xs.iter().zip(evaluations) {
527 if x.len() != self.dim
528 || desc.len() != self.qd_dim
529 || !y.is_finite()
530 || desc.iter().any(|value| !value.is_finite())
531 {
532 improvements.push(f64::INFINITY);
533 real_ys.push(f64::INFINITY);
534 continue;
535 }
536 let niche = self.index_of_niche(desc);
537 let oldy = self.ys[niche];
538 let improvement = if oldy.is_infinite() { *y } else { *y - oldy };
539 self.set(niche, *y, desc, x);
540 improvements.push(improvement);
541 real_ys.push(*y);
542 }
543 Ok((improvements, real_ys))
544 }
545
546 pub fn update_batch(
554 &mut self,
555 xs: &[Vec<f64>],
556 fitness: &mut dyn QdBatchFitness,
557 ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
558 let evaluations = fitness.eval_batch(xs);
559 self.update_evaluated(xs, &evaluations)
560 }
561
562 pub fn argsort(&mut self) {
564 let mut si: Vec<usize> = (0..self.capacity).collect();
565 si.sort_by(|&a, &b| self.ys[a].total_cmp(&self.ys[b]));
566 self.si = si;
567 }
568
569 pub fn random_xs(&self, best_n: usize, chunk: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
571 let bn = best_n.max(1).min(self.capacity);
572 (0..chunk)
573 .map(|_| {
574 let sel = rng.int_below(bn as i64) as usize;
575 let niche = if bn < self.capacity {
576 self.si[sel]
577 } else {
578 sel
579 };
580 self.xs[niche].clone()
581 })
582 .collect()
583 }
584
585 pub fn random_x_one(&self, best_n: usize, rng: &mut Rng) -> (Vec<f64>, f64) {
587 let bn = best_n.max(1).min(self.capacity);
588 let selected = rng.int_below(bn as i64) as usize;
589 let niche = if bn < self.capacity {
590 self.si[selected]
591 } else {
592 selected
593 };
594 (self.xs[niche].clone(), self.ys[niche])
595 }
596
597 pub fn best_y(&self) -> f64 {
599 self.ys.iter().cloned().fold(f64::INFINITY, f64::min)
600 }
601
602 pub fn qd_score(&self) -> f64 {
606 let finite: Vec<f64> = self
607 .ys
608 .iter()
609 .copied()
610 .filter(|value| value.is_finite())
611 .collect();
612 if finite.is_empty() {
613 return 0.0;
614 }
615 if finite.iter().copied().fold(f64::INFINITY, f64::min) > 0.0 {
616 finite
617 .iter()
618 .filter(|&&value| value != 0.0)
619 .map(|&value| value.recip())
620 .sum()
621 } else {
622 finite
623 .iter()
624 .filter(|&&value| value < 0.0)
625 .map(|&value| -value)
626 .sum()
627 }
628 }
629
630 pub fn ys(&self) -> &[f64] {
632 &self.ys
633 }
634 pub fn xs(&self) -> &[Vec<f64>] {
636 &self.xs
637 }
638 pub fn descriptors(&self) -> &[Vec<f64>] {
640 &self.ds
641 }
642 pub fn counts(&self) -> &[u64] {
644 &self.counts
645 }
646
647 pub fn occupied_data(&self) -> Vec<(Vec<f64>, f64, Vec<f64>)> {
649 (0..self.capacity)
650 .filter(|&i| self.ys[i].is_finite())
651 .map(|i| (self.xs[i].clone(), self.ys[i], self.ds[i].clone()))
652 .collect()
653 }
654}
655
656pub fn variation(
663 pop: &[Vec<f64>],
664 lower: &[f64],
665 upper: &[f64],
666 rng: &mut Rng,
667 dis_c: f64,
668 dis_m: f64,
669) -> Vec<Vec<f64>> {
670 let dis_c = dis_c * (0.5 + 0.5 * rng.uniform01());
671 let dis_m = dis_m * (0.5 + 0.5 * rng.uniform01());
672 let n = (pop.len() / 2) * 2;
673 let d = lower.len();
674 let half = n / 2;
675 let mut offspring = vec![vec![0.0; d]; n];
676 for p in 0..half {
677 let p1 = &pop[p];
678 let p2 = &pop[half + p];
679 for i in 0..d {
680 let mu = rng.uniform01();
681 let mut beta = if mu <= 0.5 {
682 (2.0 * mu).powf(1.0 / (dis_c + 1.0))
683 } else {
684 (2.0 * mu).powf(-1.0 / (dis_c + 1.0))
685 };
686 if rng.int_below(2) == 1 {
687 beta = -beta;
688 }
689 if rng.uniform01() < 0.5 {
690 beta = 1.0;
691 }
692 let mean = (p1[i] + p2[i]) * 0.5;
693 let diff = (p1[i] - p2[i]) * 0.5;
694 offspring[p][i] = mean + beta * diff;
695 offspring[half + p][i] = mean - beta * diff;
696 }
697 }
698 let site_p = 1.0 / d as f64;
700 for op in offspring.iter_mut() {
701 for i in 0..d {
702 if rng.uniform01() < site_p {
703 let mu = rng.uniform01();
704 let span = upper[i] - lower[i];
705 if mu <= 0.5 {
706 let norm = (op[i] - lower[i]) / span;
707 op[i] += span
708 * ((2.0 * mu + (1.0 - 2.0 * mu) * (1.0 - norm).abs().powf(dis_m + 1.0))
709 .powf(1.0 / (dis_m + 1.0))
710 - 1.0);
711 } else {
712 let norm = (upper[i] - op[i]) / span;
713 op[i] += span
714 * (1.0
715 - (2.0 * (1.0 - mu)
716 + 2.0 * (mu - 0.5) * (1.0 - norm).abs().powf(dis_m + 1.0))
717 .powf(1.0 / (dis_m + 1.0)));
718 }
719 }
720 op[i] = op[i].clamp(lower[i], upper[i]);
721 }
722 }
723 offspring
724}
725
726pub fn iso_dd(
728 x1: &[Vec<f64>],
729 x2: &[Vec<f64>],
730 lower: &[f64],
731 upper: &[f64],
732 rng: &mut Rng,
733 iso_sigma: f64,
734 line_sigma: f64,
735) -> Vec<Vec<f64>> {
736 let d = lower.len();
737 x1.iter()
738 .zip(x2)
739 .map(|(a, b)| {
740 (0..d)
741 .map(|i| {
742 let z = a[i]
743 + rng.normreal(0.0, iso_sigma)
744 + rng.normreal(0.0, line_sigma) * (a[i] - b[i]);
745 z.clamp(lower[i], upper[i])
746 })
747 .collect()
748 })
749 .collect()
750}
751
752#[derive(Clone, Debug)]
758pub struct MapElitesParams {
759 pub generations: usize,
761 pub chunk_size: usize,
763 pub use_sbx: bool,
765 pub dis_c: f64,
767 pub dis_m: f64,
769 pub iso_sigma: f64,
771 pub line_sigma: f64,
773 pub cma_generations: usize,
775}
776
777impl Default for MapElitesParams {
778 fn default() -> Self {
779 Self {
780 generations: 100,
781 chunk_size: 20,
782 use_sbx: true,
783 dis_c: 20.0,
784 dis_m: 20.0,
785 iso_sigma: 0.02,
786 line_sigma: 0.2,
787 cma_generations: 0,
788 }
789 }
790}
791
792pub fn map_elites(
801 archive: &mut Archive,
802 fitness: &mut dyn QdFitness,
803 lower: &[f64],
804 upper: &[f64],
805 p: &MapElitesParams,
806 rng: &mut Rng,
807) {
808 let mut batch_fitness = SerialQdBatchFitness { fitness };
809 map_elites_batch(archive, &mut batch_fitness, lower, upper, p, rng)
810 .expect("serial QD evaluation preserves batch length");
811}
812
813pub fn map_elites_batch(
822 archive: &mut Archive,
823 fitness: &mut dyn QdBatchFitness,
824 lower: &[f64],
825 upper: &[f64],
826 p: &MapElitesParams,
827 rng: &mut Rng,
828) -> Result<(), &'static str> {
829 map_elites_batch_with_progress(archive, fitness, lower, upper, p, rng, &mut |_, _| {})
830}
831
832pub fn map_elites_batch_with_progress(
845 archive: &mut Archive,
846 fitness: &mut dyn QdBatchFitness,
847 lower: &[f64],
848 upper: &[f64],
849 p: &MapElitesParams,
850 rng: &mut Rng,
851 progress: &mut dyn FnMut(usize, &Archive),
852) -> Result<(), &'static str> {
853 let mut select_n = archive.capacity();
854 for generation in 0..p.generations {
855 let xs = if p.use_sbx {
856 let pop = archive.random_xs(select_n, p.chunk_size, rng);
857 variation(&pop, lower, upper, rng, p.dis_c, p.dis_m)
858 } else {
859 let x1 = archive.random_xs(select_n, p.chunk_size, rng);
860 let x2 = archive.random_xs(select_n, p.chunk_size, rng);
861 iso_dd(&x1, &x2, lower, upper, rng, p.iso_sigma, p.line_sigma)
862 };
863 archive.update_batch(&xs, fitness)?;
864 archive.argsort();
865 select_n = archive.occupied().max(1);
866 progress(generation + 1, archive);
867 }
868 for generation in 0..p.cma_generations {
869 cma_emitter_batch(archive, fitness, lower, upper, rng)?;
870 archive.argsort();
871 progress(p.generations + generation + 1, archive);
872 }
873 Ok(())
874}
875
876fn cma_emitter_batch(
879 archive: &mut Archive,
880 fitness: &mut dyn QdBatchFitness,
881 lower: &[f64],
882 upper: &[f64],
883 rng: &mut Rng,
884) -> Result<(), &'static str> {
885 let best_n = 100.min(archive.capacity());
886 let (x0, _) = archive.random_x_one(best_n, rng);
887 let sigma = {
888 let u = 0.03 + rng.uniform01() * 0.27;
889 u * u
890 };
891 let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
892 fit.set_normalize(true);
893 let params = CmaesParams {
894 popsize: 31,
895 max_evaluations: 100_000,
896 seed: rng.int_below(i64::MAX) as u64,
897 ..Default::default()
898 };
899 let mut es = Cmaes::new(fit, &x0, &[sigma], ¶ms);
900 let stall = 5;
901 let mut last_improve = 0i32;
902 let mut old_ys: Option<Vec<f64>> = None;
903 for iter in 0..100 {
904 let xs = es.ask();
905 let (improvement, _real) = archive.update_batch(&xs, fitness)?;
906 let mut sorted = improvement.clone();
907 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
908 if let Some(oy) = &old_ys
909 && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
910 {
911 last_improve = iter;
912 }
913 if last_improve + stall < iter {
914 break;
915 }
916 if es.tell(&improvement) != 0 {
917 break;
918 }
919 old_ys = Some(sorted);
920 }
921 Ok(())
922}
923
924#[derive(Clone, Debug)]
926pub struct DiversifierParams {
927 pub max_evaluations: u64,
929 pub popsize: i32,
931 pub stall_criterion: i32,
933}
934
935impl Default for DiversifierParams {
936 fn default() -> Self {
937 Self {
938 max_evaluations: 100_000,
939 popsize: 31,
940 stall_criterion: 20,
941 }
942 }
943}
944
945pub fn diversify(
955 archive: &mut Archive,
956 fitness: &mut dyn QdFitness,
957 lower: &[f64],
958 upper: &[f64],
959 p: &DiversifierParams,
960 rng: &mut Rng,
961) -> (Vec<f64>, f64) {
962 let mut batch_fitness = SerialQdBatchFitness { fitness };
963 diversify_batch(archive, &mut batch_fitness, lower, upper, p, rng)
964 .expect("serial QD evaluation preserves batch length")
965}
966
967pub fn diversify_batch(
975 archive: &mut Archive,
976 fitness: &mut dyn QdBatchFitness,
977 lower: &[f64],
978 upper: &[f64],
979 p: &DiversifierParams,
980 rng: &mut Rng,
981) -> Result<(Vec<f64>, f64), &'static str> {
982 let mut best_x = vec![0.0; archive.dim()];
983 let mut best_y = f64::INFINITY;
984 let mut evals: u64 = 0;
985 while evals < p.max_evaluations {
986 let (x0, _) = archive.random_x_one(archive.occupied().max(1), rng);
987 let sigma = {
988 let u = 0.03 + rng.uniform01() * 0.27;
989 u * u
990 };
991 let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
992 fit.set_normalize(true);
993 let params = CmaesParams {
994 popsize: p.popsize,
995 max_evaluations: 100_000,
996 seed: rng.int_below(i64::MAX) as u64,
997 ..Default::default()
998 };
999 let mut es = Cmaes::new(fit, &x0, &[sigma], ¶ms);
1000 let max_iters = 50_000 / p.popsize.max(1) as usize;
1001 let stall = p.stall_criterion;
1002 let mut last_improve = 0i32;
1003 let mut old_ys: Option<Vec<f64>> = None;
1004 for iter in 0..max_iters as i32 {
1005 let xs = es.ask();
1006 let (improvement, real_ys) = archive.update_batch(&xs, fitness)?;
1007 evals += xs.len() as u64;
1008 for (x, &ry) in xs.iter().zip(&real_ys) {
1010 if ry < best_y {
1011 best_y = ry;
1012 best_x.copy_from_slice(x);
1013 }
1014 }
1015 let mut sorted = improvement.clone();
1016 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1017 if let Some(oy) = &old_ys
1018 && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
1019 {
1020 last_improve = iter;
1021 }
1022 if last_improve + stall < iter {
1023 break;
1024 }
1025 if es.tell(&improvement) != 0 || evals >= p.max_evaluations {
1026 break;
1027 }
1028 old_ys = Some(sorted);
1029 }
1030 archive.argsort();
1031 }
1032 Ok((best_x, best_y))
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038
1039 fn qd(x: &[f64]) -> (f64, Vec<f64>) {
1041 let f = x.iter().map(|v| v * v).sum();
1042 (f, vec![x[0], x[1]])
1043 }
1044
1045 #[test]
1046 fn cvt_centers_are_in_unit_box() {
1047 let mut rng = Rng::new(1);
1048 let c = cvt_centers(20, 2, 10, &mut rng);
1049 assert_eq!(c.len(), 20);
1050 for center in &c {
1051 for &v in center {
1052 assert!((0.0..=1.0).contains(&v));
1053 }
1054 }
1055 }
1056
1057 #[test]
1058 fn grid_centers_are_fast_exact_and_cover_the_box() {
1059 for capacity in [1, 10, 64, 1_000] {
1060 let centers = grid_centers_2d(capacity);
1061 assert_eq!(centers.len(), capacity);
1062 assert!(
1063 centers
1064 .iter()
1065 .flatten()
1066 .all(|value| (0.0..=1.0).contains(value))
1067 );
1068 }
1069 let mut rng = Rng::new(1);
1070 let archive = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 100, 0, &mut rng);
1071 assert_eq!(archive.centers.len(), 100);
1072 assert_eq!(archive.index_of_niche(&[0.0, 0.0]), 0);
1073 assert_eq!(archive.index_of_niche(&[1.0, 1.0]), 99);
1074 }
1075
1076 #[test]
1077 fn grid_layout_represents_rectangular_and_ragged_capacities_exactly() {
1078 let mut rng = Rng::new(11);
1079 let rectangular = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 120, 0, &mut rng);
1080 let layout = rectangular.grid_layout().unwrap();
1081 assert_eq!(
1082 layout,
1083 GridLayout {
1084 rows: 10,
1085 base_columns: 12,
1086 extra_columns: 0,
1087 }
1088 );
1089 assert_eq!(layout.cells(), rectangular.capacity());
1090 assert_eq!(layout.columns_in_row(9), Some(12));
1091 assert_eq!(layout.columns_in_row(10), None);
1092 assert_eq!(rectangular.grid_shape(), Some((12, 10)));
1093
1094 let ragged = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 60, 0, &mut rng);
1095 let layout = ragged.grid_layout().unwrap();
1096 assert_eq!(
1097 layout,
1098 GridLayout {
1099 rows: 7,
1100 base_columns: 8,
1101 extra_columns: 4,
1102 }
1103 );
1104 assert_eq!(layout.cells(), ragged.capacity());
1105 assert_eq!(layout.columns_in_row(0), Some(9));
1106 assert_eq!(layout.columns_in_row(3), Some(9));
1107 assert_eq!(layout.columns_in_row(4), Some(8));
1108 assert_eq!(ragged.grid_shape(), Some((9, 7)));
1109 }
1110
1111 #[test]
1112 fn map_elites_fills_niches() {
1113 let mut rng = Rng::new(2);
1114 let lower = vec![-2.0; 4];
1115 let upper = vec![2.0; 4];
1116 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1117 let seed_pop: Vec<Vec<f64>> = (0..64)
1119 .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1120 .collect();
1121 archive.update(&seed_pop, &mut qd);
1122 archive.argsort();
1123 let params = MapElitesParams {
1124 generations: 200,
1125 chunk_size: 16,
1126 ..Default::default()
1127 };
1128 map_elites(&mut archive, &mut qd, &lower, &upper, ¶ms, &mut rng);
1129 assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1130 assert!(archive.best_y() < 0.5, "best_y={}", archive.best_y());
1131 }
1132
1133 #[test]
1134 fn evaluated_batches_match_serial_updates_and_validate_lengths() {
1135 let mut rng_a = Rng::new(7);
1136 let mut rng_b = Rng::new(7);
1137 let mut serial = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_a);
1138 let mut batch = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_b);
1139 let xs = vec![vec![-1.0, 0.5], vec![0.25, -0.75], vec![1.0, 1.0]];
1140 let expected = serial.update(&xs, &mut qd);
1141 let evaluations: Vec<_> = xs.iter().map(|x| qd(x)).collect();
1142 let actual = batch.update_evaluated(&xs, &evaluations).unwrap();
1143 assert_eq!(actual, expected);
1144 assert_eq!(batch.ys(), serial.ys());
1145 assert_eq!(batch.descriptors(), serial.descriptors());
1146 assert!(batch.update_evaluated(&xs, &evaluations[..2]).is_err());
1147 }
1148
1149 #[test]
1150 fn batch_map_elites_preserves_serial_results() {
1151 let lower = vec![-2.0; 4];
1152 let upper = vec![2.0; 4];
1153 let mut rng_a = Rng::new(19);
1154 let mut rng_b = Rng::new(19);
1155 let mut serial = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_a);
1156 let mut batch = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_b);
1157 serial.seed_uniform(&lower, &upper, &mut rng_a);
1158 batch.seed_uniform(&lower, &upper, &mut rng_b);
1159 let initial = serial.xs().to_vec();
1160 let initial_evaluations: Vec<_> = initial.iter().map(|x| qd(x)).collect();
1161 serial.update(&initial, &mut qd);
1162 batch
1163 .update_evaluated(&initial, &initial_evaluations)
1164 .unwrap();
1165 serial.argsort();
1166 batch.argsort();
1167 let params = MapElitesParams {
1168 generations: 10,
1169 chunk_size: 8,
1170 ..Default::default()
1171 };
1172 map_elites(&mut serial, &mut qd, &lower, &upper, ¶ms, &mut rng_a);
1173 let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
1174 map_elites_batch(
1175 &mut batch,
1176 &mut batch_qd,
1177 &lower,
1178 &upper,
1179 ¶ms,
1180 &mut rng_b,
1181 )
1182 .unwrap();
1183 assert_eq!(batch.ys(), serial.ys());
1184 assert_eq!(batch.xs(), serial.xs());
1185 assert_eq!(batch.descriptors(), serial.descriptors());
1186 }
1187
1188 #[test]
1189 fn batch_progress_callback_is_ordered_and_observes_committed_updates() {
1190 let lower = vec![-2.0; 4];
1191 let upper = vec![2.0; 4];
1192 let mut rng = Rng::new(23);
1193 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng);
1194 archive.seed_uniform(&lower, &upper, &mut rng);
1195 let params = MapElitesParams {
1196 generations: 5,
1197 chunk_size: 8,
1198 ..Default::default()
1199 };
1200 let mut observed = Vec::new();
1201 let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
1202 map_elites_batch_with_progress(
1203 &mut archive,
1204 &mut batch_qd,
1205 &lower,
1206 &upper,
1207 ¶ms,
1208 &mut rng,
1209 &mut |generation, committed| {
1210 observed.push((generation, committed.occupied(), committed.best_y()));
1211 },
1212 )
1213 .unwrap();
1214 assert_eq!(
1215 observed.iter().map(|sample| sample.0).collect::<Vec<_>>(),
1216 vec![1, 2, 3, 4, 5]
1217 );
1218 assert!(
1219 observed
1220 .iter()
1221 .all(|(_, occupied, best)| *occupied > 0 && best.is_finite())
1222 );
1223 assert!(observed.windows(2).all(|pair| pair[1].2 <= pair[0].2));
1224 }
1225
1226 #[test]
1227 fn diversify_improves_and_fills() {
1228 let mut rng = Rng::new(3);
1229 let lower = vec![-2.0; 4];
1230 let upper = vec![2.0; 4];
1231 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1232 let seed_pop: Vec<Vec<f64>> = (0..64)
1233 .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1234 .collect();
1235 archive.update(&seed_pop, &mut qd);
1236 let params = DiversifierParams {
1237 max_evaluations: 20_000,
1238 ..Default::default()
1239 };
1240 let (bx, by) = diversify(&mut archive, &mut qd, &lower, &upper, ¶ms, &mut rng);
1241 assert_eq!(bx.len(), 4);
1242 assert!(by < 1e-3, "diversifier best_y={by}");
1243 assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1244 }
1245
1246 #[test]
1247 fn archive_validation_and_bad_evaluations() {
1248 let mut rng = Rng::new(4);
1249 assert!(Archive::try_new(0, &[0.0], &[1.0], 4, 2, &mut rng).is_err());
1250 assert!(Archive::try_new(2, &[0.0], &[0.0], 4, 2, &mut rng).is_err());
1251 assert!(Archive::try_new(2, &[0.0], &[1.0], 0, 2, &mut rng).is_err());
1252
1253 let mut archive = Archive::try_new(2, &[0.0], &[1.0], 4, 2, &mut rng).unwrap();
1254 let (improvements, values) =
1255 archive.update(&[vec![0.0, 0.0]], &mut |_: &[f64]| (f64::NAN, vec![0.5]));
1256 assert!(improvements[0].is_infinite());
1257 assert!(values[0].is_infinite());
1258 assert_eq!(archive.occupied(), 0);
1259 }
1260
1261 #[test]
1262 fn qd_score_matches_positive_and_negative_python_rules() {
1263 let mut rng = Rng::new(5);
1264 let mut positive = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1265 positive.set(0, 2.0, &[0.1], &[0.0]);
1266 positive.set(1, 4.0, &[0.5], &[0.0]);
1267 assert_eq!(positive.qd_score(), 0.75);
1268
1269 let mut mixed = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1270 mixed.set(0, -2.0, &[0.1], &[0.0]);
1271 mixed.set(1, 4.0, &[0.5], &[0.0]);
1272 assert_eq!(mixed.qd_score(), 2.0);
1273 }
1274
1275 #[test]
1276 fn random_x_one_uses_sorted_best_niches() {
1277 let mut rng = Rng::new(6);
1278 let mut archive = Archive::new(1, &[0.0], &[1.0], 4, 2, &mut rng);
1279 for (index, value) in [4.0, 1.0, 3.0, 2.0].into_iter().enumerate() {
1280 archive.set(index, value, &[index as f64 / 4.0], &[index as f64]);
1281 }
1282 archive.argsort();
1283 for _ in 0..10 {
1284 let (x, y) = archive.random_x_one(1, &mut rng);
1285 assert_eq!(x, vec![1.0]);
1286 assert_eq!(y, 1.0);
1287 }
1288 }
1289}